-
-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
1,050 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** | ||
* A map that uses a composite key to store values. If a value is not found for | ||
* a given key, the default value is returned. | ||
*/ | ||
export class CompositeKeyDefaultMap<K, V> { | ||
private map = new Map<string, V>(); | ||
|
||
constructor( | ||
private getDefaultValue: (key: K) => V, | ||
private hashFunction: (key: K) => unknown[], | ||
) {} | ||
|
||
hash(key: K): string { | ||
return this.hashFunction(key).join("\u0000"); | ||
} | ||
|
||
get(key: K): V { | ||
const stringKey = this.hash(key); | ||
const currentValue = this.map.get(stringKey); | ||
|
||
if (currentValue != null) { | ||
return currentValue; | ||
} | ||
|
||
const value = this.getDefaultValue(key); | ||
this.map.set(stringKey, value); | ||
|
||
return value; | ||
} | ||
|
||
entries(): IterableIterator<[string, V]> { | ||
return this.map.entries(); | ||
} | ||
|
||
values(): IterableIterator<V> { | ||
return this.map.values(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { range as lodashRange } from "lodash"; | ||
import { Range } from "../types/Range"; | ||
import { TextEditor } from "../types/TextEditor"; | ||
|
||
/** | ||
* @param editor The editor containing the range | ||
* @param range The range to get the line ranges for | ||
* @returns A list of ranges, one for each line in the given range, with the | ||
* first and last ranges trimmed to the start and end of the given range. | ||
*/ | ||
export function getLineRanges(editor: TextEditor, range: Range): Range[] { | ||
const { document } = editor; | ||
const lineRanges = lodashRange(range.start.line, range.end.line + 1).map( | ||
(lineNumber) => document.lineAt(lineNumber).range, | ||
); | ||
lineRanges[0] = lineRanges[0].with(range.start); | ||
lineRanges[lineRanges.length - 1] = lineRanges[lineRanges.length - 1].with( | ||
undefined, | ||
range.end, | ||
); | ||
return lineRanges; | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/RangeTypeColors.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** | ||
* The colors used to render a range type, such as "domain", "content", etc. | ||
*/ | ||
export interface RangeTypeColors { | ||
background: ThemeColors; | ||
borderSolid: ThemeColors; | ||
borderPorous: ThemeColors; | ||
} | ||
|
||
interface ThemeColors { | ||
light: string; | ||
dark: string; | ||
} |
72 changes: 72 additions & 0 deletions
72
...e/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { GeneralizedRange, Range } from "@cursorless/common"; | ||
import { flatmap } from "itertools"; | ||
import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; | ||
import { RangeTypeColors } from "../RangeTypeColors"; | ||
import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; | ||
import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; | ||
import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; | ||
import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; | ||
import { DifferentiatedStyledRange } from "./decorationStyle.types"; | ||
import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRanges"; | ||
|
||
/** | ||
* A class for highlighting ranges in a VSCode editor, which does the following: | ||
* | ||
* - Uses a combination of solid lines and dotted lines to make it easier to | ||
* visualize multi-line ranges, while still making directly adjacent ranges | ||
* visually distinct. | ||
* - Works around a bug in VSCode where decorations that are touching get merged | ||
* together. | ||
* - Ensures that nested ranges are rendered after their parents, so that they | ||
* look properly nested. | ||
*/ | ||
export class VscodeFancyRangeHighlighter { | ||
private renderer: VscodeFancyRangeHighlighterRenderer; | ||
|
||
constructor(colors: RangeTypeColors) { | ||
this.renderer = new VscodeFancyRangeHighlighterRenderer(colors); | ||
} | ||
|
||
setRanges(editor: VscodeTextEditorImpl, ranges: GeneralizedRange[]) { | ||
const decoratedRanges: Iterable<DifferentiatedStyledRange> = flatmap( | ||
// We first generate a list of differentiated ranges, which are ranges | ||
// where any ranges that are touching have different differentiation | ||
// indices. This is used to ensure that ranges that are touching are | ||
// rendered with different TextEditorDecorationTypes, so that they don't | ||
// get merged together by VSCode. | ||
generateDifferentiatedRanges(ranges), | ||
|
||
// Then, we generate the actual decorations for each differentiated range. | ||
// A single range will be split into multiple decorations if it spans | ||
// multiple lines, so that we can eg use dashed lines to end lines that | ||
// are part of the same range. | ||
function* ({ range, differentiationIndex }) { | ||
const iterable = | ||
range.type === "line" | ||
? generateDecorationsForLineRange(range.start, range.end) | ||
: generateDecorationsForCharacterRange( | ||
editor, | ||
new Range(range.start, range.end), | ||
); | ||
|
||
for (const { range, style } of iterable) { | ||
yield { | ||
range, | ||
differentiatedStyle: { style, differentiationIndex }, | ||
}; | ||
} | ||
}, | ||
); | ||
|
||
this.renderer.setRanges( | ||
editor, | ||
// Group the decorations so that we have a list of ranges for each | ||
// differentiated style | ||
groupDifferentiatedStyledRanges(decoratedRanges), | ||
); | ||
} | ||
|
||
dispose() { | ||
this.renderer.dispose(); | ||
} | ||
} |
156 changes: 156 additions & 0 deletions
156
.../VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { CompositeKeyDefaultMap } from "@cursorless/common"; | ||
import { toVscodeRange } from "@cursorless/vscode-common"; | ||
import { | ||
DecorationRangeBehavior, | ||
DecorationRenderOptions, | ||
TextEditorDecorationType, | ||
} from "vscode"; | ||
import { vscodeApi } from "../../../../vscodeApi"; | ||
import { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; | ||
import { RangeTypeColors } from "../RangeTypeColors"; | ||
import { | ||
BorderStyle, | ||
DecorationStyle, | ||
DifferentiatedStyle, | ||
DifferentiatedStyledRangeList, | ||
} from "./decorationStyle.types"; | ||
import { getDifferentiatedStyleMapKey } from "./getDifferentiatedStyleMapKey"; | ||
|
||
const BORDER_WIDTH = "1px"; | ||
const BORDER_RADIUS = "2px"; | ||
|
||
/** | ||
* Handles the actual rendering of decorations for | ||
* {@link VscodeFancyRangeHighlighter}. | ||
*/ | ||
export class VscodeFancyRangeHighlighterRenderer { | ||
private decorationTypes: CompositeKeyDefaultMap< | ||
DifferentiatedStyle, | ||
TextEditorDecorationType | ||
>; | ||
|
||
constructor(colors: RangeTypeColors) { | ||
this.decorationTypes = new CompositeKeyDefaultMap( | ||
({ style }) => getDecorationStyle(colors, style), | ||
getDifferentiatedStyleMapKey, | ||
); | ||
} | ||
|
||
/** | ||
* Renders the given ranges in the given editor. | ||
* | ||
* @param editor The editor to render the decorations in. | ||
* @param decoratedRanges A list with one element per differentiated style, | ||
* each of which contains a list of ranges to render for that style. We render | ||
* the ranges in order of increasing differentiation index. | ||
* {@link VscodeFancyRangeHighlighter} uses this to ensure that nested ranges | ||
* are rendered after their parents. Otherwise they partially interleave, | ||
* which looks bad. | ||
*/ | ||
setRanges( | ||
editor: VscodeTextEditorImpl, | ||
decoratedRanges: DifferentiatedStyledRangeList[], | ||
): void { | ||
/** | ||
* Keep track of which styles have no ranges, so that we can set their | ||
* range list to `[]` | ||
*/ | ||
const untouchedDecorationTypes = new Set(this.decorationTypes.values()); | ||
|
||
decoratedRanges.sort( | ||
(a, b) => | ||
a.differentiatedStyle.differentiationIndex - | ||
b.differentiatedStyle.differentiationIndex, | ||
); | ||
|
||
decoratedRanges.forEach( | ||
({ differentiatedStyle: styleParameters, ranges }) => { | ||
const decorationType = this.decorationTypes.get(styleParameters); | ||
|
||
vscodeApi.editor.setDecorations( | ||
editor.vscodeEditor, | ||
decorationType, | ||
ranges.map(toVscodeRange), | ||
); | ||
|
||
untouchedDecorationTypes.delete(decorationType); | ||
}, | ||
); | ||
|
||
untouchedDecorationTypes.forEach((decorationType) => { | ||
editor.vscodeEditor.setDecorations(decorationType, []); | ||
}); | ||
} | ||
|
||
dispose() { | ||
Array.from(this.decorationTypes.values()).forEach((decorationType) => { | ||
decorationType.dispose(); | ||
}); | ||
} | ||
} | ||
|
||
function getDecorationStyle( | ||
colors: RangeTypeColors, | ||
borders: DecorationStyle, | ||
): TextEditorDecorationType { | ||
const options: DecorationRenderOptions = { | ||
light: { | ||
backgroundColor: colors.background.light, | ||
borderColor: getBorderColor( | ||
colors.borderSolid.light, | ||
colors.borderPorous.light, | ||
borders, | ||
), | ||
}, | ||
dark: { | ||
backgroundColor: colors.background.dark, | ||
borderColor: getBorderColor( | ||
colors.borderSolid.dark, | ||
colors.borderPorous.dark, | ||
borders, | ||
), | ||
}, | ||
borderStyle: getBorderStyle(borders), | ||
borderWidth: BORDER_WIDTH, | ||
borderRadius: getBorderRadius(borders), | ||
rangeBehavior: DecorationRangeBehavior.ClosedClosed, | ||
isWholeLine: borders.isWholeLine, | ||
}; | ||
|
||
return vscodeApi.window.createTextEditorDecorationType(options); | ||
} | ||
|
||
function getBorderStyle(borders: DecorationStyle): string { | ||
return [borders.top, borders.right, borders.bottom, borders.left].join(" "); | ||
} | ||
|
||
function getBorderColor( | ||
solidColor: string, | ||
porousColor: string, | ||
borders: DecorationStyle, | ||
): string { | ||
return [ | ||
borders.top === BorderStyle.solid ? solidColor : porousColor, | ||
borders.right === BorderStyle.solid ? solidColor : porousColor, | ||
borders.bottom === BorderStyle.solid ? solidColor : porousColor, | ||
borders.left === BorderStyle.solid ? solidColor : porousColor, | ||
].join(" "); | ||
} | ||
|
||
function getBorderRadius(borders: DecorationStyle): string { | ||
return [ | ||
getSingleCornerBorderRadius(borders.top, borders.left), | ||
getSingleCornerBorderRadius(borders.top, borders.right), | ||
getSingleCornerBorderRadius(borders.bottom, borders.right), | ||
getSingleCornerBorderRadius(borders.bottom, borders.left), | ||
].join(" "); | ||
} | ||
|
||
function getSingleCornerBorderRadius(side1: BorderStyle, side2: BorderStyle) { | ||
// We only round the corners if both sides are solid, as that makes them look | ||
// more finished, whereas we want the dotted borders to look unfinished / cut | ||
// off. | ||
return side1 === BorderStyle.solid && side2 === BorderStyle.solid | ||
? BORDER_RADIUS | ||
: "0px"; | ||
} |
56 changes: 56 additions & 0 deletions
56
...src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { GeneralizedRange, Range } from "@cursorless/common"; | ||
|
||
export enum BorderStyle { | ||
porous = "dashed", | ||
solid = "solid", | ||
none = "none", | ||
} | ||
|
||
export interface DecorationStyle { | ||
top: BorderStyle; | ||
bottom: BorderStyle; | ||
left: BorderStyle; | ||
right: BorderStyle; | ||
isWholeLine?: boolean; | ||
} | ||
|
||
/** | ||
* A decoration style that is differentiated from other styles by a number. We | ||
* use this number to ensure that adjacent ranges are rendered with different | ||
* TextEditorDecorationTypes, so that they don't get merged together due to a | ||
* VSCode bug. | ||
*/ | ||
export interface DifferentiatedStyle { | ||
style: DecorationStyle; | ||
|
||
/** | ||
* A number that is different from the differentiation indices of any other | ||
* ranges that are touching this range. | ||
*/ | ||
differentiationIndex: number; | ||
} | ||
|
||
export interface StyledRange { | ||
style: DecorationStyle; | ||
range: Range; | ||
} | ||
|
||
export interface DifferentiatedStyledRange { | ||
differentiatedStyle: DifferentiatedStyle; | ||
range: Range; | ||
} | ||
|
||
export interface DifferentiatedStyledRangeList { | ||
differentiatedStyle: DifferentiatedStyle; | ||
ranges: Range[]; | ||
} | ||
|
||
export interface DifferentiatedGeneralizedRange { | ||
range: GeneralizedRange; | ||
|
||
/** | ||
* A number that is different from the differentiation indices of any other | ||
* ranges that are touching this range. | ||
*/ | ||
differentiationIndex: number; | ||
} |
Oops, something went wrong.