Skip to content

Commit 9406cd3

Browse files
Stable hats (#1225)
- Fixes #432 ## Checklist - [ ] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] Add test for case where enabled hat styles change, both adding or removing an enabled hat style - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent db58044 commit 9406cd3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1059
-371
lines changed

.vscode/launch.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@
127127
"!**/node_modules/**"
128128
]
129129
},
130+
{
131+
"name": "Docusaurus Build (Debug)",
132+
"type": "node",
133+
"request": "launch",
134+
"cwd": "${workspaceFolder}/docs-site",
135+
"runtimeExecutable": "npm",
136+
"runtimeArgs": ["run", "build"],
137+
"resolveSourceMapLocations": [
138+
"${workspaceFolder}/**",
139+
"!**/node_modules/**"
140+
]
141+
},
130142
{
131143
"name": "cursorless.org client-side",
132144
"type": "chrome",

cursorless-talon/src/cheatsheet/get_list.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ def get_list(name, type, descriptions=None):
1616

1717

1818
def get_lists(names: list[str], type: str, descriptions=None):
19-
2019
return [item for name in names for item in get_list(name, type, descriptions)]
2120

2221

cursorless-talon/src/vendor/jstyleson.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ def dispose(json_str):
2828
former_index = None
2929

3030
for index, char in enumerate(json_str):
31-
3231
if escaped: # We have just met a '\'
3332
escaped = False
3433
continue

docs-site/docusaurus.config.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ function remarkPluginFixLinksToRepositoryArtifacts() {
3838
return;
3939
}
4040

41+
// Hack; see https://github.com/cursorless-dev/cursorless/issues/1243
42+
let match = link.match(/^(\.\.\/)+docs\//);
43+
if (match != null) {
44+
link = "/docs/" + link.substring(match[0].length);
45+
if (link.endsWith(".md")) {
46+
link = link.substring(0, link.length - 3);
47+
}
48+
node.url = link;
49+
return;
50+
}
51+
4152
let repoRoot = path.resolve(__dirname, "..");
4253
let artifact = path.resolve(file.dirname, link);
4354
let artifactRelative = path.relative(repoRoot, artifact);
@@ -65,7 +76,7 @@ const config = {
6576
baseUrl: "/docs/",
6677
favicon: "/docs/favicon.ico",
6778
onBrokenLinks: "throw",
68-
onBrokenMarkdownLinks: "throw",
79+
onBrokenMarkdownLinks: "warn",
6980
trailingSlash: true,
7081

7182
plugins: [

docs/user/hatAssignment.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Hat assignment
2+
3+
Every time you move your cursor, edit a document, or scroll, Cursorless assigns hats to the tokens in each visible editor. When selecting hats, Cursorless attempts to give "good" hats to tokens near your cursor, while at the same time keeping hats from moving around too much.
4+
5+
Hats are considered "good" that require fewer syllables to say, so for example, a gray dot is the best kind of hat, because you just say the letter it sits on, whereas a colored shape (eg `"blue fox"`) is the worst kind of hat, because you need to say both color and shape. Each hat style has a penalty associated with it that indicates how many syllables it requires to say:
6+
7+
| Hat Style | Example spoken form | Penalty |
8+
| ------------- | ------------------- | ------- |
9+
| gray dot | `"air"` | 0 |
10+
| colored dot | `"blue air"` | 1 |
11+
| gray shape | `"fox air"` | 1 |
12+
| colored shape | `"blue fox air"` | 2 |
13+
14+
## The algorithm
15+
16+
Every time you move your cursor, edit the document, or scroll, Cursorless does a pass through all visible tokens, assigning them hats. It does so by walking through the tokens one by one in order of their "rank". A token's rank is determined by how close it is to your cursor: tokens near your cursor are considered higher rank and get to pick their hats first. For each token, Cursorless proceeds as follows:
17+
18+
### 1. Discard any hats that are considered unacceptable
19+
20+
When a token is picking its hat, it will have a pool of available hats based on
21+
22+
- Which hats are enabled,
23+
- What characters make up the token,
24+
- Which hats have already been taken by a higher ranked hat.
25+
26+
One of these candidate hats might be the hat the token was already wearing before this pass started (if it had one), and some of them will be hats that lower ranked tokens were wearing before the pass started.
27+
28+
The token will start by figuring out the penalty of the best candidate hat (eg is there a gray dot? Are they all colored shapes? etc). Based on that penalty, it will consider only the candidate hats whose penalty is sufficiently close to the best hat. The definition of "sufficiently close" is determined by the user setting `cursorless.experimental.hatStability`:
29+
30+
| Setting value | Behaviour |
31+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32+
| `greedy` | Only consider hats whose penalty is just as good as the best candidate hat. This setting will result in less stable hats, but ensure that tokens near the cursor always get the best hats. |
33+
| `balanced` _(default)_ | If the best candidate hat has a penalty below 2 (eg it is a gray shape or colored dot), then discard all hats whose penalty is 2 or greater. This setting results in fairly stable hats, while ensuring that all tokens near the cursor have a penalty less than 2. |
34+
| `stable` | Don't discard any hats. Always keep existing hat if it wasn't stolen, and don't steal hats unless there are no free hats left to this token. Note that if you have no shapes enabled, then this setting is equivalent to `balanced`. |
35+
36+
### 2. Select a hat from the remaining hat candidates
37+
38+
Once the "unacceptable" hats are discarded, then if the token's existing hat is amongst the remaining acceptable hats, it will keep it. If not, it will pick a hat that won't require it to steal from a lower ranked token if any such hats are left. If it can't keep its own hat or avoid stealing a hat, it will steal a hat from the lowest ranked token.

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,21 @@
687687
"url"
688688
]
689689
}
690+
},
691+
"cursorless.experimental.hatStability": {
692+
"markdownDescription": "As you scroll, edit, and move your cursor, this setting determines how much Cursorless will move hats around to ensure that the best hats are near the cursor. See https://www.cursorless.org/docs/user/hatAssignment/",
693+
"type": "string",
694+
"default": "balanced",
695+
"enum": [
696+
"greedy",
697+
"balanced",
698+
"stable"
699+
],
700+
"markdownEnumDescriptions": [
701+
"Always put the best hats near the cursor",
702+
"Only move hats to avoid having colored shapes near the cursor (eg `\"blue fox\"`); otherwise leave hats where they are",
703+
"Only move hats to ensure that the tokens near the cursor have a hat at all, no matter how bad the hat is. Note that if you have no shapes enabled, then this setting is the same as `balanced`"
704+
]
690705
}
691706
}
692707
},

src/apps/cursorless-vscode-e2e/suite/backwardCompatibility.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async function runTest() {
1717

1818
editor.selections = [new vscode.Selection(0, 0, 0, 0)];
1919

20-
await graph.hatTokenMap.addDecorations();
20+
await graph.hatTokenMap.allocateHats();
2121

2222
await vscode.commands.executeCommand(
2323
CURSORLESS_COMMAND_ID,

src/apps/cursorless-vscode-e2e/suite/breakpoints.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ suite("breakpoints", async function () {
2424
async function breakpointHarpAdd() {
2525
const { graph } = (await getCursorlessApi()).testHelpers!;
2626
await openNewEditor(" hello");
27-
await graph.hatTokenMap.addDecorations();
27+
await graph.hatTokenMap.allocateHats();
2828

2929
await runCursorlessCommand({
3030
version: 1,
@@ -51,7 +51,7 @@ async function breakpointHarpAdd() {
5151
async function breakpointTokenHarpAdd() {
5252
const { graph } = (await getCursorlessApi()).testHelpers!;
5353
await openNewEditor(" hello");
54-
await graph.hatTokenMap.addDecorations();
54+
await graph.hatTokenMap.allocateHats();
5555

5656
await runCursorlessCommand({
5757
version: 1,
@@ -79,7 +79,7 @@ async function breakpointTokenHarpAdd() {
7979
async function breakpointHarpRemove() {
8080
const { graph } = (await getCursorlessApi()).testHelpers!;
8181
const editor = await openNewEditor(" hello");
82-
await graph.hatTokenMap.addDecorations();
82+
await graph.hatTokenMap.allocateHats();
8383

8484
vscode.debug.addBreakpoints([
8585
new vscode.SourceBreakpoint(
@@ -110,7 +110,7 @@ async function breakpointHarpRemove() {
110110
async function breakpointTokenHarpRemove() {
111111
const { graph } = (await getCursorlessApi()).testHelpers!;
112112
const editor = await openNewEditor(" hello");
113-
await graph.hatTokenMap.addDecorations();
113+
await graph.hatTokenMap.allocateHats();
114114

115115
vscode.debug.addBreakpoints([
116116
new vscode.SourceBreakpoint(

src/apps/cursorless-vscode-e2e/suite/containingTokenTwice.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ suite("Take token twice", async function () {
1616
async function runTest() {
1717
const { graph } = (await getCursorlessApi()).testHelpers!;
1818
const editor = await openNewEditor("a)");
19-
await graph.hatTokenMap.addDecorations();
19+
await graph.hatTokenMap.allocateHats();
2020

2121
for (let i = 0; i < 2; ++i) {
2222
editor.selection = new vscode.Selection(0, 1, 0, 1);

src/apps/cursorless-vscode-e2e/suite/crossCellsSetSelection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async function runTest() {
2323
// editor
2424
await sleepWithBackoff(1000);
2525

26-
await graph.hatTokenMap.addDecorations();
26+
await graph.hatTokenMap.allocateHats();
2727

2828
await runCursorlessCommand({
2929
version: 1,

0 commit comments

Comments
 (0)