diff --git a/images/curved-hat.svg b/images/curved-hat.svg deleted file mode 100644 index e38e5d5cd8..0000000000 --- a/images/curved-hat.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/diamond-hat.svg b/images/diamond-hat.svg deleted file mode 100644 index 57009204e8..0000000000 --- a/images/diamond-hat.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/hats/chevron.svg b/images/hats/chevron.svg new file mode 100644 index 0000000000..42239e8c9c --- /dev/null +++ b/images/hats/chevron.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/round-hat.svg b/images/hats/default.svg similarity index 100% rename from images/round-hat.svg rename to images/hats/default.svg diff --git a/images/hats/star.svg b/images/hats/star.svg new file mode 100644 index 0000000000..3a08a22c93 --- /dev/null +++ b/images/hats/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/core/Decorations.ts b/src/core/Decorations.ts index 4f17dd9831..4d5ec1545f 100644 --- a/src/core/Decorations.ts +++ b/src/core/Decorations.ts @@ -1,20 +1,42 @@ import * as vscode from "vscode"; import { join } from "path"; -import { COLORS } from "./constants"; -import { SymbolColor } from "./constants"; +import { + HatStyleName, + HatShape, + hatStyleMap, + hatStyleNames, + HAT_SHAPES, +} from "./constants"; import { readFileSync } from "fs"; import { DecorationColorSetting } from "../typings/Types"; import FontMeasurements from "./FontMeasurements"; -const DEFAULT_HAT_WIDTH_TO_CHARACTER_WITH_RATIO = 0.39; -const DEFAULT_HAT_VERTICAL_OFFSET_EM = -0.05; +interface ShapeMeasurements { + hatWidthToCharacterWidthRatio: number; + verticalOffsetEm: number; +} + +const defaultShapeMeasurements: Record = { + default: { + hatWidthToCharacterWidthRatio: 0.507, + verticalOffsetEm: -0.05, + }, + star: { + hatWidthToCharacterWidthRatio: 0.6825, + verticalOffsetEm: -0.105, + }, + chevron: { + hatWidthToCharacterWidthRatio: 0.6825, + verticalOffsetEm: -0.12, + }, +}; export type DecorationMap = { - [k in SymbolColor]?: vscode.TextEditorDecorationType; + [k in HatStyleName]?: vscode.TextEditorDecorationType; }; export interface NamedDecoration { - name: SymbolColor; + name: HatStyleName; decoration: vscode.TextEditorDecorationType; } @@ -22,7 +44,10 @@ export default class Decorations { decorations!: NamedDecoration[]; decorationMap!: DecorationMap; - constructor(fontMeasurements: FontMeasurements) { + constructor( + fontMeasurements: FontMeasurements, + private extensionPath: string + ) { this.constructDecorations(fontMeasurements); } @@ -43,23 +68,37 @@ export default class Decorations { const hatScaleFactor = 1 + hatSizeAdjustment / 100; - const { svg, svgWidthPx, svgHeightPx } = this.processSvg( - fontMeasurements, - hatScaleFactor * DEFAULT_HAT_WIDTH_TO_CHARACTER_WITH_RATIO, - (DEFAULT_HAT_VERTICAL_OFFSET_EM + userHatVerticalOffsetAdjustment / 100) * - fontMeasurements.fontSize + const hatSvgMap = Object.fromEntries( + HAT_SHAPES.map((shape) => { + const { hatWidthToCharacterWidthRatio, verticalOffsetEm } = + defaultShapeMeasurements[shape]; + + return [ + shape, + this.processSvg( + fontMeasurements, + shape, + hatScaleFactor * hatWidthToCharacterWidthRatio, + (verticalOffsetEm + userHatVerticalOffsetAdjustment / 100) * + fontMeasurements.fontSize + ), + ]; + }) ); - const spanWidthPx = - svgWidthPx + (fontMeasurements.characterWidth - svgWidthPx) / 2; + this.decorations = hatStyleNames.map((styleName) => { + const { color, shape } = hatStyleMap[styleName]; + const { svg, svgWidthPx, svgHeightPx } = hatSvgMap[shape]; + + const spanWidthPx = + svgWidthPx + (fontMeasurements.characterWidth - svgWidthPx) / 2; - this.decorations = COLORS.map((color) => { const colorSetting = vscode.workspace .getConfiguration("cursorless.colors") .get(color)!; return { - name: color, + name: styleName, decoration: vscode.window.createTextEditorDecorationType({ rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, light: { @@ -121,10 +160,11 @@ export default class Decorations { */ private processSvg( fontMeasurements: FontMeasurements, + shape: HatShape, hatWidthToCharacterWidthRatio: number, hatVerticalOffset: number ) { - const iconPath = join(__dirname, "..", "images", "round-hat.svg"); + const iconPath = join(this.extensionPath, "images", "hats", `${shape}.svg`); const rawSvg = readFileSync(iconPath, "utf8"); const { originalViewBoxHeight, originalViewBoxWidth } = diff --git a/src/core/NavigationMap.ts b/src/core/NavigationMap.ts index 3484e80413..0b5688e391 100644 --- a/src/core/NavigationMap.ts +++ b/src/core/NavigationMap.ts @@ -1,9 +1,9 @@ import { TextDocumentChangeEvent, Range } from "vscode"; -import { SymbolColor } from "./constants"; +import { HatStyleName } from "./constants"; import { SelectionWithEditor, Token } from "../typings/Types"; /** - * Maps from (color, character) pairs to tokens + * Maps from (hatStyle, character) pairs to tokens */ export default class NavigationMap { updateTokenRanges(edit: TextDocumentChangeEvent) { @@ -11,7 +11,7 @@ export default class NavigationMap { // Amount by which to shift ranges const shift = editComponent.text.length - editComponent.rangeLength; - Object.entries(this.map).forEach(([coloredSymbol, token]) => { + Object.entries(this.map).forEach(([decoratedCharacter, token]) => { if (token.editor.document !== edit.document) { return; } @@ -22,7 +22,7 @@ export default class NavigationMap { if (editComponent.range.end.isAfter(token.range.start)) { // If there is overlap, we just delete the token - delete this.map[coloredSymbol]; + delete this.map[decoratedCharacter]; return; } @@ -40,24 +40,24 @@ export default class NavigationMap { } private map: { - [coloredSymbol: string]: Token; + [decoratedCharacter: string]: Token; } = {}; - static getKey(color: SymbolColor, character: string) { - return `${color}.${character}`; + static getKey(hatStyle: HatStyleName, character: string) { + return `${hatStyle}.${character}`; } static splitKey(key: string) { - const [color, character] = key.split("."); - return { color: color as SymbolColor, character }; + const [hatStyle, character] = key.split("."); + return { hatStyle: hatStyle as HatStyleName, character }; } - public addToken(color: SymbolColor, character: string, token: Token) { - this.map[NavigationMap.getKey(color, character)] = token; + public addToken(hatStyle: HatStyleName, character: string, token: Token) { + this.map[NavigationMap.getKey(hatStyle, character)] = token; } - public getToken(color: SymbolColor, character: string) { - return this.map[NavigationMap.getKey(color, character)]; + public getToken(hatStyle: HatStyleName, character: string) { + return this.map[NavigationMap.getKey(hatStyle, character)]; } public clear() { diff --git a/src/core/constants.ts b/src/core/constants.ts index 3489e944f1..6f5bc6068b 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -2,7 +2,7 @@ export const SUBWORD_MATCHER = /[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+/g; export const DEBOUNCE_DELAY = 175; -export const COLORS = [ +const HAT_COLORS = [ "default", "blue", "green", @@ -11,4 +11,30 @@ export const COLORS = [ "purple", ] as const; -export type SymbolColor = typeof COLORS[number]; +const HAT_NON_DEFAULT_SHAPES = ["star", "chevron"] as const; +export const HAT_SHAPES = [...HAT_NON_DEFAULT_SHAPES, "default"] as const; + +export type HatColor = typeof HAT_COLORS[number]; +export type HatShape = typeof HAT_SHAPES[number]; +type HatNonDefaultShape = typeof HAT_NON_DEFAULT_SHAPES[number]; +export type HatStyleName = HatColor | `${HatColor}-${HatNonDefaultShape}`; + +export interface HatStyle { + color: HatColor; + shape: HatShape; +} + +export const hatStyleMap = { + ...Object.fromEntries( + HAT_COLORS.map((color) => [color, { color, shape: "default" }]) + ), + ...Object.fromEntries( + HAT_COLORS.flatMap((color) => + HAT_NON_DEFAULT_SHAPES.map((shape) => [ + `${color}-${shape}`, + { color, shape }, + ]) + ) + ), +} as Record; +export const hatStyleNames = Object.keys(hatStyleMap); diff --git a/src/extension.ts b/src/extension.ts index ee5abbcffb..1a71b2176f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -23,7 +23,7 @@ import canonicalizeActionName from "./canonicalizeActionName"; export async function activate(context: vscode.ExtensionContext) { const fontMeasurements = new FontMeasurements(context); await fontMeasurements.calculate(); - const decorations = new Decorations(fontMeasurements); + const decorations = new Decorations(fontMeasurements, context.extensionPath); const { getNodeAtLocation } = await getParseTreeApi(); diff --git a/src/test/suite/recorded.test.ts b/src/test/suite/recorded.test.ts index 87d4774c00..9b7d9ddc13 100644 --- a/src/test/suite/recorded.test.ts +++ b/src/test/suite/recorded.test.ts @@ -93,9 +93,12 @@ async function runTest(file: string) { // Assert that recorded decorations are present Object.entries(fixture.marks).forEach(([key, token]) => { - const { color, character } = NavigationMap.splitKey(key); - const currentToken = cursorlessApi.navigationMap.getToken(color, character); - assert(currentToken != null, `Mark "${color} ${character}" not found`); + const { hatStyle, character } = NavigationMap.splitKey(key); + const currentToken = cursorlessApi.navigationMap.getToken( + hatStyle, + character + ); + assert(currentToken != null, `Mark "${hatStyle} ${character}" not found`); assert.deepStrictEqual(rangeToPlainObject(currentToken.range), token); }); diff --git a/src/testUtil/extractTargetedMarks.ts b/src/testUtil/extractTargetedMarks.ts index 9743585f1d..83a9d81c17 100644 --- a/src/testUtil/extractTargetedMarks.ts +++ b/src/testUtil/extractTargetedMarks.ts @@ -1,4 +1,4 @@ -import { SymbolColor } from "../core/constants"; +import { HatStyleName } from "../core/constants"; import NavigationMap from "../core/NavigationMap"; import { PrimitiveTarget, Target, Token } from "../typings/Types"; @@ -33,14 +33,11 @@ export function extractTargetedMarks( targets: Target[], navigationMap: NavigationMap ) { - const targetedMarks: { [coloredSymbol: string]: Token } = {}; + const targetedMarks: { [decoratedCharacter: string]: Token } = {}; const targetKeys = targets.map(extractTargetKeys).flat(); targetKeys.forEach((key) => { - const { color, character } = NavigationMap.splitKey(key); - targetedMarks[key] = navigationMap.getToken( - color as SymbolColor, - character - ); + const { hatStyle, character } = NavigationMap.splitKey(key); + targetedMarks[key] = navigationMap.getToken(hatStyle, character); }); return targetedMarks; } diff --git a/src/testUtil/toPlainObject.ts b/src/testUtil/toPlainObject.ts index 23485b17b3..b5bc745e9b 100644 --- a/src/testUtil/toPlainObject.ts +++ b/src/testUtil/toPlainObject.ts @@ -16,7 +16,7 @@ export type SelectionPlainObject = { active: PositionPlainObject; }; -export type SerializedMarks = { [coloredSymbol: string]: RangePlainObject }; +export type SerializedMarks = { [decoratedCharacter: string]: RangePlainObject }; export function rangeToPlainObject(range: Range): RangePlainObject { return { @@ -38,7 +38,7 @@ export function positionToPlainObject(position: Position): PositionPlainObject { return { line: position.line, character: position.character }; } -export function marksToPlainObject(marks: { [coloredSymbol: string]: Token }) { +export function marksToPlainObject(marks: { [decoratedCharacter: string]: Token }) { const serializedMarks: SerializedMarks = {}; Object.entries(marks).forEach( ([key, value]: [string, Token]) => diff --git a/src/typings/Types.ts b/src/typings/Types.ts index ec2779ab37..d4880b4a79 100644 --- a/src/typings/Types.ts +++ b/src/typings/Types.ts @@ -1,7 +1,7 @@ import { SyntaxNode } from "web-tree-sitter"; import * as vscode from "vscode"; import { Location } from "vscode"; -import { SymbolColor } from "../core/constants"; +import { HatStyleName } from "../core/constants"; import { EditStyles } from "../core/editStyles"; import NavigationMap from "../core/NavigationMap"; @@ -39,7 +39,7 @@ export interface LastCursorPosition { export interface DecoratedSymbol { type: "decoratedSymbol"; - symbolColor: SymbolColor; + symbolColor: HatStyleName; character: string; } diff --git a/src/util/addDecorationsToEditor.ts b/src/util/addDecorationsToEditor.ts index 17328c388b..a38f5a10ab 100644 --- a/src/util/addDecorationsToEditor.ts +++ b/src/util/addDecorationsToEditor.ts @@ -5,7 +5,7 @@ import { getTokenComparator as getTokenComparator } from "./getTokenComparator"; import { getTokensInRange } from "./getTokensInRange"; import { Token } from "../typings/Types"; import Decorations from "../core/Decorations"; -import { COLORS, SymbolColor } from "../core/constants"; +import { hatStyleNames, HatStyleName } from "../core/constants"; import NavigationMap from "../core/NavigationMap"; interface CharacterTokenInfo { @@ -79,7 +79,7 @@ export function addDecorationsToEditors( const decorationRanges: Map< vscode.TextEditor, { - [decorationName in SymbolColor]?: vscode.Range[]; + [decorationName in HatStyleName]?: vscode.Range[]; } > = new Map( editors.map((editor) => [ @@ -132,26 +132,29 @@ export function addDecorationsToEditors( const currentDecorationIndex = bestCharacter.decorationIndex; - const colorName = decorations.decorations[currentDecorationIndex].name; + const hatStyleName = decorations.decorations[currentDecorationIndex].name; decorationRanges .get(token.editor)! - [colorName]!.push( + [hatStyleName]!.push( new vscode.Range( token.range.start.translate(undefined, bestCharacter.characterIdx), token.range.start.translate(undefined, bestCharacter.characterIdx + 1) ) ); - navigationMap.addToken(colorName, bestCharacter.character, token); + navigationMap.addToken(hatStyleName, bestCharacter.character, token); characterDecorationIndices[bestCharacter.character] = currentDecorationIndex + 1; }); decorationRanges.forEach((ranges, editor) => { - COLORS.forEach((color) => { - editor.setDecorations(decorations.decorationMap[color]!, ranges[color]!); + hatStyleNames.forEach((hatStyleName) => { + editor.setDecorations( + decorations.decorationMap[hatStyleName]!, + ranges[hatStyleName]! + ); }); }); }