Skip to content

Commit 917fccc

Browse files
Read custom graphemes from state file (#3376)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent fe31380 commit 917fccc

27 files changed

Lines changed: 569 additions & 288 deletions

File tree

‎packages/app-vscode/package.json‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -334,27 +334,6 @@
334334
"order": 2,
335335
"description": "Keep a local, sanitized command history. This history is never sent to our servers, and any commands that may contain text will be sanitized. These statistics can be used in the future for doing local analyses to determine ways you can improve your Cursorless efficiency. We may also support a way for you to send your statistics to us for analysis in the future, but this will be opt-in only."
336336
},
337-
"cursorless.tokenHatSplittingMode.preserveCase": {
338-
"type": "boolean",
339-
"default": false,
340-
"markdownDescription": "Whether to distinguish between uppercase and lower case letters for hats. Set this to `true` if you have separate terms for uppercase letters in your `<user.any_alphanumeric_key>` capture."
341-
},
342-
"cursorless.tokenHatSplittingMode.lettersToPreserve": {
343-
"type": "array",
344-
"items": {
345-
"type": "string"
346-
},
347-
"default": [],
348-
"description": "A list of characters whose accents should not be stripped. This can be used, for example, if you would like to strip all accents except for those of a few characters, which you would add to this list."
349-
},
350-
"cursorless.tokenHatSplittingMode.symbolsToPreserve": {
351-
"type": "array",
352-
"items": {
353-
"type": "string"
354-
},
355-
"default": [],
356-
"markdownDescription": "A list of symbols that shouldn't be normalized by the token hat splitter. Add any extra symbols here that you have added to your `<user.any_alphanumeric_key>` capture. Unlike the Accents To Preserve setting, these symbols won't even undergo case normalisation, so you would need separate terms for the lowercase and uppercase versions (if the symbol has a notion of upper and lower case)."
357-
},
358337
"cursorless.decorationDebounceDelayMs": {
359338
"type": "number",
360339
"default": 50,

‎packages/app-vscode/src/extension.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
FakeCommandServerApi,
55
FakeIDE,
66
NormalizedIDE,
7+
showWarning,
78
} from "@cursorless/lib-common";
89
import type { EngineProps } from "@cursorless/lib-engine";
910
import { CommandHistory, createCursorlessEngine } from "@cursorless/lib-engine";
@@ -26,6 +27,7 @@ import { createScopeVisualizer } from "./createScopeVisualizer";
2627
import { createTreeSitter } from "./createTreeSitter";
2728
import { createTutorial } from "./createTutorial";
2829
import { createVscodeIde } from "./createVscodeIde";
30+
import type { VscodeIDE } from "./ide/vscode/VscodeIDE";
2931
import { InstallationDependencies } from "./InstallationDependencies";
3032
import { KeyboardCommands } from "./keyboard/KeyboardCommands";
3133
import { registerCommands } from "./registerCommands";
@@ -153,6 +155,8 @@ export async function activate(
153155
hats,
154156
);
155157

158+
deprecatedSettings(vscodeIDE);
159+
156160
registerCommands(
157161
context,
158162
vscodeIDE,
@@ -190,3 +194,17 @@ export async function activate(
190194
: undefined,
191195
};
192196
}
197+
198+
function deprecatedSettings(ide: VscodeIDE) {
199+
// DEPRECATED @ 2026-10-05
200+
const value = vscodeApi.workspace
201+
.getConfiguration("cursorless")
202+
.get("tokenHatSplittingMode");
203+
if (value != null) {
204+
void showWarning(
205+
ide.messages,
206+
"tokenHatSplittingModeDeprecated",
207+
"The 'cursorless.tokenHatSplittingMode' setting is deprecated and not needed when using an up to date Cursorless Talon",
208+
);
209+
}
210+
}

‎packages/app-vscode/src/registerCommands.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import type {
33
CommandHistoryStorage,
44
CursorlessCommandId,
55
ScopeType,
6+
TalonSpokenForms,
67
} from "@cursorless/lib-common";
78
import { CURSORLESS_COMMAND_ID } from "@cursorless/lib-common";
89
import type { CommandApi, StoredTargetMap } from "@cursorless/lib-engine";
910
import { analyzeCommandHistory } from "@cursorless/lib-engine";
10-
import type {
11-
CheatSheetCommandArg,
12-
FileSystemTalonSpokenForms,
13-
} from "@cursorless/lib-node-common";
11+
import type { CheatSheetCommandArg } from "@cursorless/lib-node-common";
1412
import { showCheatsheet } from "@cursorless/lib-node-common";
1513
import type {
1614
ScopeTestRecorder,
@@ -45,7 +43,7 @@ export function registerCommands(
4543
tutorial: VscodeTutorial,
4644
installationDependencies: InstallationDependencies,
4745
storedTargets: StoredTargetMap,
48-
talonSpokenForms: FileSystemTalonSpokenForms,
46+
talonSpokenForms: TalonSpokenForms,
4947
): void {
5048
const runCommandWrapper = async (run: () => Promise<unknown>) => {
5149
try {

‎packages/app-web-docs/src/docs/user/alphabet-and-symbols.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ sidebar_position: 7
77

88
Cursorless uses the [Talon Community](https://github.com/talonhub/community) alphabet, digits, and symbol spoken forms via the [`user.any_alphanumeric_key`](https://github.com/talonhub/community/blob/607c3415f5f29a5f75db6fe5648e37f514f62ac5/core/keys/keys.py#L71-L74) capture.
99

10+
Additional characters provided by the lists in this capture are automatically preserved when allocating hats. See [Unicode support](unicode-support.md) for case handling, accented letters, and custom symbols.
11+
1012
## Alphabet
1113

1214
| Character | Default spoken form |

‎packages/app-web-docs/src/docs/user/unicode-support.md‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,42 @@ sidebar_position: 2
55

66
# Unicode support
77

8-
Cursorless has first-class support for Unicode. By default, when constructing hats, Cursorless will ignore capitalization and any accents or diacritics over letters. For example, each of the following four tokens could each be selected by saying `"take air"` if there were a gray hat over their first letter (note the accents on the first letter for some of them):
8+
Cursorless has first-class support for Unicode. With the default Talon alphabet, Cursorless ignores capitalization and accents or diacritics when constructing hats. For example, each of the following four tokens could be selected by saying `"take air"` if there were a gray hat over its first letter:
99

1010
- africa
1111
- áfrica
1212
- Africa
1313
- África
1414

15-
For Unicode symbols that are not letters, and that are not speakable by default, for example emoji, Chinese characters, etc, we have a special "character" called `"special"` that can be used. So for example, if there were a blue hat over a '😄' character, you could say `"take blue special"` to select it. As always, the spoken form `"special"` can be [customized](customization.md).
15+
Characters that do not have a known spoken form, even after normalization, can be referred to using `"special"`. For example, if there were a blue hat over a `😄` character, you could say `"take blue special"` to select it. This also works for letters that cannot be normalized to a known character, such as Chinese characters. As always, the spoken form `"special"` can be [customized](customization.md).
1616

1717
## Advanced customization
1818

19-
The above setup will allow you to refer to any Unicode token, and is sufficient for most users. However, if you have overridden your `<user.any_alphanumeric_key>` capture to contain characters other than lowercase letters and the default symbols, you can tell Cursorless to be less aggressive with its normalization, so that it can allocate hats more efficiently. Note that this is not necessary in order to refer to these tokens; it just makes hat allocation slightly more efficient.
19+
With an up-to-date Cursorless Talon installation, Cursorless automatically preserves additional characters provided by the Talon lists used in your `<user.any_alphanumeric_key>` capture. Add your spoken forms there; no editor setting is needed. This lets Cursorless allocate hats separately for these characters instead of grouping them with normalized letters or `"special"`.
20+
21+
The old `cursorless.tokenHatSplittingMode` settings (`preserveCase`, `lettersToPreserve`, and `symbolsToPreserve`) are deprecated and can be removed from your editor settings.
2022

2123
### Preserving case
2224

23-
If you have a separate alphabet for uppercase letters as part of `<user.any_alphanumeric_key>`, you can enable the _Cursorless › Token Hat Splitting Mode: **Preserve Case**_ setting, and Cursorless will distinguish between lower and uppercase letters.
25+
If your `<user.any_alphanumeric_key>` capture provides separate spoken forms for uppercase letters, Cursorless preserves those letters automatically. For example, if `"upper air"` produces `A`, a gray hat on the `A` in `Africa` can be addressed with `"take upper air"`, while a gray hat on lowercase `a` uses `"take air"`.
2426

2527
### Preserving special letters
2628

27-
If you have terms in `<user.any_alphanumeric_key>` for letters with accents, such as `é`, or other letters, such as `ø` or `ꝏ`, you can use the following setting:
29+
If your capture provides terms for accented letters, such as `é`, or other letters, such as `ø` or `ꝏ`, Cursorless preserves them automatically.
30+
31+
For example, if `"a umlaut"` produces `ä` and you have no separate form for `Ä`, a gray hat over the first letter of either `ällo` or `Ällo` can be addressed with `"take a umlaut"`. If you also provide a spoken form for `Ä`, Cursorless treats it separately. Providing only `Ä` does not preserve lowercase `ä`; lowercase `ä` still normalizes to `a`.
32+
33+
### Preserving symbols
34+
35+
Symbols provided by your capture are also preserved automatically. For example, if `"sigma"` produces `σ` and `"upper sigma"` produces `Σ`, a blue hat on those characters can be addressed with `"take blue sigma"` and `"take blue upper sigma"`, respectively.
2836

29-
#### _Cursorless › Token Hat Splitting Mode: **Letters To Preserve**_
37+
## Normalization order
3038

31-
Add any accented letters to this list that you have a spoken form for in `<user.any_alphanumeric_key>`. Cursorless will then preserve their accents during normalization. Note that Cursorless will still do case normalisation for these letters if you have [case preservation](#preserving-case) on. So, for example, if the list contains `ä`, and you'd like to refer to the token `Ällo` with a hat over the first letter (`Ä`), you can use your spoken form for `ä`.
39+
Cursorless first normalizes each character to Unicode NFC so that equivalent representations, such as an accented letter written as one codepoint or with a combining mark, are treated the same. It then:
3240

33-
### _Cursorless › Token Hat Splitting Mode: **Symbols To Preserve**_
41+
1. Preserves the character if it is a default character or is provided by Talon.
42+
2. Otherwise, converts it to lowercase and uses that form if it is known.
43+
3. Otherwise, strips accents and diacritics and uses the resulting form if it is known.
44+
4. Otherwise, assigns it to `"special"`.
3445

35-
Any Unicode symbols in this list will not undergo any normalisation, even case normalisation. Use this list for symbols for which you have spoken forms in `<user.any_alphanumeric_key>` that shouldn't be normalised at all, even by case. For example, if you have spoken forms for `Σ` and `σ`, and would like Cursorless not to treat them the same, you can add them to this list.
46+
If custom characters cannot be loaded from Talon, Cursorless uses the default alphabet, digits, and symbols.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type {
2+
SpokenFormEntry,
3+
TalonSpokenForms,
4+
TalonSpokenFormsPayload,
5+
} from "@cursorless/lib-common";
6+
7+
export class FakeTalonSpokenForms implements TalonSpokenForms {
8+
public static fromGraphemes(graphemes: string[]): FakeTalonSpokenForms {
9+
return new FakeTalonSpokenForms(
10+
graphemes.map((grapheme) => ({
11+
type: "grapheme",
12+
id: grapheme,
13+
spokenForms: [],
14+
})),
15+
);
16+
}
17+
18+
constructor(private spokenForms: SpokenFormEntry[]) {}
19+
20+
getSpokenForms(): Promise<TalonSpokenFormsPayload> {
21+
return Promise.resolve({
22+
version: -1,
23+
spokenForms: this.spokenForms,
24+
});
25+
}
26+
27+
onDidChange() {
28+
return {
29+
dispose: () => {
30+
// No-op
31+
},
32+
};
33+
}
34+
}

‎packages/lib-common/src/ide/types/Configuration.ts‎

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { Disposable } from "./ide.types";
44
import type { GetFieldType, Paths } from "./Paths";
55

66
export type CursorlessConfiguration = {
7-
tokenHatSplittingMode: TokenHatSplittingMode;
87
wordSeparators: string[];
98
experimental: {
109
hatStability: HatStability;
@@ -19,11 +18,6 @@ export type CursorlessConfigKey = keyof CursorlessConfiguration;
1918
export type ConfigurationScope = { languageId: string };
2019

2120
export const CONFIGURATION_DEFAULTS: CursorlessConfiguration = {
22-
tokenHatSplittingMode: {
23-
preserveCase: false,
24-
lettersToPreserve: [],
25-
symbolsToPreserve: [],
26-
},
2721
wordSeparators: ["_"],
2822
decorationDebounceDelayMs: 50,
2923
experimental: {
@@ -51,24 +45,3 @@ export interface Configuration {
5145

5246
onDidChangeConfiguration(listener: Listener): Disposable;
5347
}
54-
55-
export interface TokenHatSplittingMode {
56-
/**
57-
* Whether to distinguished between uppercase and lower case letters for hat
58-
*/
59-
preserveCase: boolean;
60-
61-
/**
62-
* A list of characters whose accents should not be stripped. This can be
63-
* used, for example, if you would like to strip all accents except for those
64-
* of a few characters, which you can add to this string.
65-
*/
66-
lettersToPreserve: string[];
67-
68-
/**
69-
* A list of symbols that shouldn't be normalized by the token hat splitter.
70-
* Add any extra symbols here that you have added to your
71-
* <user.any_alphanumeric_key> capture.
72-
*/
73-
symbolsToPreserve: string[];
74-
}

‎packages/lib-common/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from "./Debouncer";
66
export * from "./errors";
77
export * from "./extensionDependencies";
88
export * from "./FakeCommandServerApi";
9+
export * from "./FakeTalonSpokenForms";
910
export * from "./ide/fake/FakeIDE";
1011
export * from "./ide/inMemoryTextEditor/InMemoryTextDocument";
1112
export * from "./ide/inMemoryTextEditor/InMemoryTextEditor";

‎packages/lib-common/src/types/HatTokenMap.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import type { Token } from "./Token";
77
* Maps from (hatStyle, character) pairs to tokens
88
*/
99
export interface HatTokenMap {
10-
allocateHats(
11-
forceTokenHats?: TokenHat[],
12-
options?: HatAllocationOptions,
13-
): Promise<void>;
10+
allocateHats(options?: HatAllocationOptions): Promise<void>;
1411
getReadableMap(usePrePhraseSnapshot: boolean): Promise<ReadOnlyHatMap>;
1512
}
1613

@@ -20,6 +17,11 @@ export interface HatAllocationOptions {
2017
* Defaults to `false`. Forced hats still apply when starting fresh.
2118
*/
2219
startFresh?: boolean;
20+
21+
/** If supplied, force the allocator to use these hats
22+
* for the given tokens. This is used for the tutorial, and for testing.
23+
*/
24+
forceTokenHats?: TokenHat[];
2325
}
2426

2527
export interface TokenHat {

‎packages/lib-engine/src/core/HatAllocator.test.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Range,
99
tokenHatToPlainObject,
1010
} from "@cursorless/lib-common";
11+
import { DisabledTalonSpokenForms } from "../disabledComponents/DisabledTalonSpokenForms";
1112
import { TokenGraphemeSplitter } from "../tokenGraphemeSplitter";
1213
import { HatAllocator } from "./HatAllocator";
1314
import { IndividualHatMap } from "./IndividualHatMap";
@@ -16,11 +17,12 @@ import { RangeUpdater } from "./updateSelections/RangeUpdater";
1617
suite("HatAllocator", () => {
1718
test("initial allocation is independent of earlier hat assignments", async () => {
1819
const ide = new HatTestIDE();
20+
const talonSpokenForms = new DisabledTalonSpokenForms();
1921
ide.configuration.mockConfiguration("experimental", {
2022
...ide.configuration.getOwnConfiguration("experimental"),
2123
hatStability: HatStability.stable,
2224
});
23-
const splitter = new TokenGraphemeSplitter(ide);
25+
const splitter = new TokenGraphemeSplitter(ide, talonSpokenForms);
2426
const rangeUpdater = new RangeUpdater(ide);
2527
const map = new IndividualHatMap(ide, splitter, rangeUpdater);
2628
const hats: Hats = {
@@ -66,17 +68,18 @@ suite("HatAllocator", () => {
6668

6769
try {
6870
// Establish the expected allocation with no earlier editor events.
69-
await allocator.allocateHats([forcedWorld]);
71+
await allocator.allocateHats({ forceTokenHats: [forcedWorld] });
7072
const expected = snapshot();
7173

7274
// Simulate an allocation that occurred before fixture initialization.
73-
await allocator.allocateHats([previousHello]);
74-
await allocator.allocateHats([forcedWorld]);
75+
await allocator.allocateHats({ forceTokenHats: [previousHello] });
76+
await allocator.allocateHats({ forceTokenHats: [forcedWorld] });
7577
assert.notDeepEqual(snapshot(), expected);
7678
assert.ok(map.getToken("blue", "h"), "Normal allocation preserves hats");
7779

78-
await allocator.allocateHats([forcedWorld], {
80+
await allocator.allocateHats({
7981
startFresh: true,
82+
forceTokenHats: [forcedWorld],
8083
});
8184
assert.deepEqual(snapshot(), expected);
8285
assert.ok(map.getToken("default", "w"), "Forced hats are still applied");

0 commit comments

Comments
 (0)