Skip to content

Commit 7f03ae6

Browse files
authored
Merge branch 'main' into interiorScope
2 parents bd096f8 + 2c19566 commit 7f03ae6

File tree

9 files changed

+170
-60
lines changed

9 files changed

+170
-60
lines changed

cursorless-talon/src/actions/generate_snippet.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import glob
22
from pathlib import Path
33

4-
from talon import Context, Module, actions, settings
4+
from talon import Context, Module, actions, registry, settings
55

66
from ..targets.target_types import CursorlessExplicitTarget
77

@@ -20,6 +20,15 @@ def private_cursorless_migrate_snippets():
2020
actions.user.private_cursorless_run_rpc_command_no_wait(
2121
"cursorless.migrateSnippets",
2222
str(get_directory_path()),
23+
{
24+
"insertion": registry.lists[
25+
"user.cursorless_insertion_snippet_no_phrase"
26+
][-1],
27+
"insertionWithPhrase": registry.lists[
28+
"user.cursorless_insertion_snippet_single_phrase"
29+
][-1],
30+
"wrapper": registry.lists["user.cursorless_wrapper_snippet"][-1],
31+
},
2332
)
2433

2534
def private_cursorless_generate_snippet_action(target: CursorlessExplicitTarget): # pyright: ignore [reportGeneralTypeIssues]

data/fixtures/recorded/fallback/takeThis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,4 @@ finalState:
2222
active: {line: 0, character: 0}
2323
fallback:
2424
action: setSelection
25-
modifiers:
26-
- {type: containingTokenIfEmpty}
25+
modifiers: []

packages/cursorless-engine/src/core/getCommandFallback.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,11 @@ function getModifiersFromTarget(
142142
return target.modifiers;
143143
}
144144

145-
if (target.mark?.type === "cursor") {
146-
return [{ type: "containingTokenIfEmpty" }];
147-
}
145+
// FIXME: Trying to select a word in the file explorer will create weird behavior.
146+
// https://github.com/cursorless-dev/cursorless/issues/2800
147+
// if (target.mark?.type === "cursor") {
148+
// return [{ type: "containingTokenIfEmpty" }];
149+
// }
148150
}
149151
return [];
150152
}

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/CollectionItemScopeHandler/CollectionItemScopeHandler.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,8 @@ export class CollectionItemScopeHandler extends BaseScopeHandler {
4848

4949
return OneOfScopeHandler.createFromScopeHandlers(
5050
scopeHandlerFactory,
51-
{
52-
type: "oneOf",
53-
scopeTypes: [
54-
languageScopeHandler.scopeType,
55-
textualScopeHandler.scopeType,
56-
],
57-
},
58-
[languageScopeHandler, textualScopeHandler],
5951
languageId,
52+
[languageScopeHandler, textualScopeHandler],
6053
);
6154
})();
6255
}

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/NotebookCellScopeHandler.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,8 @@ export class NotebookCellScopeHandler extends BaseScopeHandler {
4646

4747
return OneOfScopeHandler.createFromScopeHandlers(
4848
scopeHandlerFactory,
49-
{
50-
type: "oneOf",
51-
scopeTypes: [
52-
languageScopeHandler.scopeType,
53-
apiScopeHandler.scopeType,
54-
],
55-
},
56-
[languageScopeHandler, apiScopeHandler],
5749
languageId,
50+
[languageScopeHandler, apiScopeHandler],
5851
);
5952
})();
6053
}

packages/cursorless-engine/src/processTargets/modifiers/scopeHandlers/OneOfScopeHandler.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717

1818
export class OneOfScopeHandler extends BaseScopeHandler {
1919
protected isHierarchical = true;
20+
public scopeType = undefined;
2021
private iterationScopeHandler: OneOfScopeHandler | undefined;
2122
private lastYieldedIndex: number | undefined;
2223

@@ -31,21 +32,18 @@ export class OneOfScopeHandler extends BaseScopeHandler {
3132

3233
return this.createFromScopeHandlers(
3334
scopeHandlerFactory,
34-
scopeType,
35-
scopeHandlers,
3635
languageId,
36+
scopeHandlers,
3737
);
3838
}
3939

4040
static createFromScopeHandlers(
4141
scopeHandlerFactory: ScopeHandlerFactory,
42-
scopeType: OneOfScopeType,
43-
scopeHandlers: ScopeHandler[],
4442
languageId: string,
43+
scopeHandlers: ScopeHandler[],
4544
): ScopeHandler {
4645
const getIterationScopeHandler = () =>
4746
new OneOfScopeHandler(
48-
undefined,
4947
scopeHandlers.map((scopeHandler) =>
5048
scopeHandlerFactory.create(
5149
scopeHandler.iterationScopeType,
@@ -57,11 +55,14 @@ export class OneOfScopeHandler extends BaseScopeHandler {
5755
},
5856
);
5957

60-
return new OneOfScopeHandler(
61-
scopeType,
62-
scopeHandlers,
63-
getIterationScopeHandler,
64-
);
58+
return new OneOfScopeHandler(scopeHandlers, getIterationScopeHandler);
59+
}
60+
61+
private constructor(
62+
private scopeHandlers: ScopeHandler[],
63+
private getIterationScopeHandler: () => OneOfScopeHandler,
64+
) {
65+
super();
6566
}
6667

6768
get iterationScopeType(): CustomScopeType {
@@ -74,21 +75,13 @@ export class OneOfScopeHandler extends BaseScopeHandler {
7475
};
7576
}
7677

77-
private constructor(
78-
public readonly scopeType: OneOfScopeType | undefined,
79-
private scopeHandlers: ScopeHandler[],
80-
private getIterationScopeHandler: () => OneOfScopeHandler,
81-
) {
82-
super();
83-
}
84-
8578
*generateScopeCandidates(
8679
editor: TextEditor,
8780
position: Position,
8881
direction: Direction,
8982
hints: ScopeIteratorRequirements,
9083
): Iterable<TargetScope> {
91-
// If we have used the iteration scope handler, we only want to yield from its handler.
84+
// If we have used an iteration scope handler, we only want to yield additional scopes from its handler.
9285
if (this.iterationScopeHandler?.lastYieldedIndex != null) {
9386
const handlerIndex = this.iterationScopeHandler.lastYieldedIndex;
9487
const handler = this.scopeHandlers[handlerIndex];

packages/cursorless-vscode/src/migrateSnippets.ts

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,71 +15,130 @@ import {
1515
type VscodeSnippets,
1616
} from "./VscodeSnippets";
1717

18+
interface Result {
19+
migrated: Record<string, string>;
20+
migratedPartially: Record<string, string>;
21+
skipped: string[];
22+
}
23+
24+
interface SpokenForms {
25+
insertion: Record<string, string>;
26+
insertionWithPhrase: Record<string, string>;
27+
wrapper: Record<string, string>;
28+
}
29+
1830
export async function migrateSnippets(
1931
snippets: VscodeSnippets,
2032
targetDirectory: string,
33+
spokenForms: SpokenForms,
2134
) {
22-
const userSnippetsDir = snippets.getUserDirectoryStrict();
23-
const files = await snippets.getSnippetPaths(userSnippetsDir);
35+
const sourceDirectory = snippets.getUserDirectoryStrict();
36+
const files = await snippets.getSnippetPaths(sourceDirectory);
37+
38+
const spokenFormsInverted: SpokenForms = {
39+
insertion: swapKeyValue(spokenForms.insertion),
40+
insertionWithPhrase: swapKeyValue(
41+
spokenForms.insertionWithPhrase,
42+
(name) => name.split(".")[0],
43+
),
44+
wrapper: swapKeyValue(spokenForms.wrapper),
45+
};
46+
47+
const result: Result = {
48+
migrated: {},
49+
migratedPartially: {},
50+
skipped: [],
51+
};
2452

2553
for (const file of files) {
26-
await migrateFile(targetDirectory, file);
54+
await migrateFile(result, spokenFormsInverted, targetDirectory, file);
2755
}
2856

29-
await vscode.window.showInformationMessage(
30-
`${files.length} snippet files migrated successfully!`,
31-
);
57+
await openResultDocument(result, sourceDirectory, targetDirectory);
3258
}
3359

34-
async function migrateFile(targetDirectory: string, filePath: string) {
60+
async function migrateFile(
61+
result: Result,
62+
spokenForms: SpokenForms,
63+
targetDirectory: string,
64+
filePath: string,
65+
) {
3566
const fileName = path.basename(filePath, CURSORLESS_SNIPPETS_SUFFIX);
3667
const snippetFile = await readLegacyFile(filePath);
3768
const communitySnippetFile: SnippetFile = { snippets: [] };
69+
let hasSkippedSnippet = false;
3870

3971
for (const snippetName in snippetFile) {
4072
const snippet = snippetFile[snippetName];
73+
const phrase =
74+
spokenForms.insertion[snippetName] ??
75+
spokenForms.insertionWithPhrase[snippetName];
4176

4277
communitySnippetFile.header = {
4378
name: snippetName,
4479
description: snippet.description,
45-
variables: parseVariables(snippet.variables),
80+
phrases: phrase ? [phrase] : undefined,
81+
variables: parseVariables(spokenForms, snippetName, snippet.variables),
4682
insertionScopes: snippet.insertionScopeTypes,
4783
};
4884

4985
for (const def of snippet.definitions) {
86+
if (
87+
def.scope?.scopeTypes?.length ||
88+
def.scope?.excludeDescendantScopeTypes?.length
89+
) {
90+
hasSkippedSnippet = true;
91+
continue;
92+
}
5093
communitySnippetFile.snippets.push({
5194
body: def.body.map((line) => line.replaceAll("\t", " ")),
5295
languages: def.scope?.langIds,
53-
variables: parseVariables(def.variables),
96+
variables: parseVariables(spokenForms, snippetName, def.variables),
5497
// SKIP: def.scope?.scopeTypes
5598
// SKIP: def.scope?.excludeDescendantScopeTypes
5699
});
57100
}
58101
}
59102

103+
if (communitySnippetFile.snippets.length === 0) {
104+
result.skipped.push(fileName);
105+
return;
106+
}
107+
108+
let destinationName: string;
109+
60110
try {
61-
const destinationPath = path.join(targetDirectory, `${fileName}.snippet`);
62-
await writeCommunityFile(communitySnippetFile, destinationPath);
111+
destinationName = `${fileName}.snippet`;
112+
const destinationPath = path.join(targetDirectory, destinationName);
113+
await writeCommunityFile(communitySnippetFile, destinationPath, "wx");
63114
} catch (error: any) {
64115
if (error.code === "EEXIST") {
65-
const destinationPath = path.join(
66-
targetDirectory,
67-
`${fileName}_CONFLICT.snippet`,
68-
);
69-
await writeCommunityFile(communitySnippetFile, destinationPath);
116+
destinationName = `${fileName}_CONFLICT.snippet`;
117+
const destinationPath = path.join(targetDirectory, destinationName);
118+
await writeCommunityFile(communitySnippetFile, destinationPath, "w");
70119
} else {
71120
throw error;
72121
}
73122
}
123+
124+
if (hasSkippedSnippet) {
125+
result.migratedPartially[fileName] = destinationName;
126+
} else {
127+
result.migrated[fileName] = destinationName;
128+
}
74129
}
75130

76131
function parseVariables(
132+
spokenForms: SpokenForms,
133+
snippetName: string,
77134
variables?: Record<string, SnippetVariableLegacy>,
78135
): SnippetVariable[] {
79136
return Object.entries(variables ?? {}).map(
80137
([name, variable]): SnippetVariable => {
138+
const phrase = spokenForms.wrapper[`${snippetName}.${name}`];
81139
return {
82140
name,
141+
wrapperPhrases: phrase ? [phrase] : undefined,
83142
wrapperScope: variable.wrapperScopeType,
84143
insertionFormatters: variable.formatter
85144
? [variable.formatter]
@@ -90,6 +149,52 @@ function parseVariables(
90149
);
91150
}
92151

152+
async function openResultDocument(
153+
result: Result,
154+
sourceDirectory: string,
155+
targetDirectory: string,
156+
) {
157+
const migratedKeys = Object.keys(result.migrated).sort();
158+
const migratedPartiallyKeys = Object.keys(result.migratedPartially).sort();
159+
const skipMessage =
160+
"(Snippets containing `scopeTypes` and/or `excludeDescendantScopeTypes` attributes are not supported by community snippets.)";
161+
162+
const content: string[] = [
163+
`# Snippets migrated from Cursorless`,
164+
"",
165+
`From: ${sourceDirectory}`,
166+
`To: ${targetDirectory}`,
167+
"",
168+
`## Migrated ${migratedKeys.length} snippet files:`,
169+
...migratedKeys.map((key) => `- ${key} -> ${result.migrated[key]}`),
170+
"",
171+
];
172+
173+
if (migratedPartiallyKeys.length > 0) {
174+
content.push(
175+
`## Migrated ${migratedPartiallyKeys.length} snippet files partially:`,
176+
...migratedPartiallyKeys.map(
177+
(key) => `- ${key} -> ${result.migratedPartially[key]}`,
178+
),
179+
skipMessage,
180+
);
181+
}
182+
183+
if (result.skipped.length > 0) {
184+
content.push(
185+
`## Skipped ${result.skipped.length} snippet files:`,
186+
...result.skipped.map((key) => `- ${key}`),
187+
skipMessage,
188+
);
189+
}
190+
191+
const textDocument = await vscode.workspace.openTextDocument({
192+
content: content.join("\n"),
193+
language: "markdown",
194+
});
195+
await vscode.window.showTextDocument(textDocument);
196+
}
197+
93198
async function readLegacyFile(filePath: string): Promise<SnippetMap> {
94199
const content = await fs.readFile(filePath, "utf8");
95200
if (content.length === 0) {
@@ -98,12 +203,25 @@ async function readLegacyFile(filePath: string): Promise<SnippetMap> {
98203
return JSON.parse(content);
99204
}
100205

101-
async function writeCommunityFile(snippetFile: SnippetFile, filePath: string) {
206+
async function writeCommunityFile(
207+
snippetFile: SnippetFile,
208+
filePath: string,
209+
flags: string,
210+
) {
102211
const snippetText = serializeSnippetFile(snippetFile);
103-
const file = await fs.open(filePath, "wx");
212+
const file = await fs.open(filePath, flags);
104213
try {
105214
await file.write(snippetText);
106215
} finally {
107216
await file.close();
108217
}
109218
}
219+
220+
function swapKeyValue(
221+
obj: Record<string, string>,
222+
map?: (value: string) => string,
223+
): Record<string, string> {
224+
return Object.fromEntries(
225+
Object.entries(obj).map(([key, value]) => [map?.(value) ?? value, key]),
226+
);
227+
}

packages/cursorless-vscode/src/registerCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function registerCommands(
8989
["cursorless.showDocumentation"]: showDocumentation,
9090
["cursorless.showInstallationDependencies"]: installationDependencies.show,
9191

92-
["cursorless.migrateSnippets"]: (dir) => migrateSnippets(snippets, dir),
92+
["cursorless.migrateSnippets"]: migrateSnippets.bind(null, snippets),
9393

9494
["cursorless.private.logQuickActions"]: logQuickActions,
9595

0 commit comments

Comments
 (0)