Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion images/curved-hat.svg

This file was deleted.

1 change: 0 additions & 1 deletion images/diamond-hat.svg

This file was deleted.

1 change: 1 addition & 0 deletions images/hats/chevron.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
1 change: 1 addition & 0 deletions images/hats/star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 57 additions & 17 deletions src/core/Decorations.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
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<HatShape, ShapeMeasurements> = {
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;
}

export default class Decorations {
decorations!: NamedDecoration[];
decorationMap!: DecorationMap;

constructor(fontMeasurements: FontMeasurements) {
constructor(
fontMeasurements: FontMeasurements,
private extensionPath: string
) {
this.constructDecorations(fontMeasurements);
}

Expand All @@ -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<DecorationColorSetting>(color)!;

return {
name: color,
name: styleName,
decoration: vscode.window.createTextEditorDecorationType({
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
light: {
Expand Down Expand Up @@ -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 } =
Expand Down
26 changes: 13 additions & 13 deletions src/core/NavigationMap.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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) {
edit.contentChanges.forEach((editComponent) => {
// 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;
}
Expand All @@ -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;
}

Expand All @@ -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() {
Expand Down
30 changes: 28 additions & 2 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<HatStyleName, HatStyle>;
export const hatStyleNames = Object.keys(hatStyleMap);
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
9 changes: 6 additions & 3 deletions src/test/suite/recorded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
11 changes: 4 additions & 7 deletions src/testUtil/extractTargetedMarks.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions src/testUtil/toPlainObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]) =>
Expand Down
4 changes: 2 additions & 2 deletions src/typings/Types.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -39,7 +39,7 @@ export interface LastCursorPosition {

export interface DecoratedSymbol {
type: "decoratedSymbol";
symbolColor: SymbolColor;
symbolColor: HatStyleName;
character: string;
}

Expand Down
17 changes: 10 additions & 7 deletions src/util/addDecorationsToEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) => [
Expand Down Expand Up @@ -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]!
);
});
});
}