Skip to content

Commit 9ee7771

Browse files
authored
Reintroduce stable hats (#1252)
1 parent 926b503 commit 9ee7771

Some content is hidden

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

49 files changed

+1008
-354
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",

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
@@ -26,7 +26,7 @@ async function runTest() {
2626
// editor
2727
await sleepWithBackoff(1000);
2828

29-
await graph.hatTokenMap.addDecorations();
29+
await graph.hatTokenMap.allocateHats();
3030

3131
await runCursorlessCommand({
3232
version: 1,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async function runTest(
3535
// editor
3636
await sleepWithBackoff(100);
3737

38-
await graph.hatTokenMap.addDecorations();
38+
await graph.hatTokenMap.allocateHats();
3939

4040
assert.equal(notebook.cellCount, 1);
4141

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async function runTest() {
2727
vscode.ViewColumn.Beside,
2828
);
2929

30-
await graph.hatTokenMap.addDecorations();
30+
await graph.hatTokenMap.allocateHats();
3131
const hatMap = await graph.hatTokenMap.getReadableMap(false);
3232

3333
const hat1 = hatMap

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async function runTest() {
2626
// editor
2727
await sleepWithBackoff(1000);
2828

29-
await graph.hatTokenMap.addDecorations();
29+
await graph.hatTokenMap.allocateHats();
3030

3131
await runCursorlessCommand({
3232
version: 1,

0 commit comments

Comments
 (0)