Skip to content

Commit 2e69df8

Browse files
Pass snippets as argument to engine
1 parent 0a70357 commit 2e69df8

File tree

10 files changed

+288
-233
lines changed

10 files changed

+288
-233
lines changed

Diff for: packages/cursorless-engine/src/actions/Actions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import { Decrement, Increment } from "./incrementDecrement";
6969
export class Actions implements ActionRecord {
7070
constructor(
7171
private treeSitter: TreeSitter,
72-
private snippets: Snippets,
72+
private snippets: Snippets | undefined,
7373
private rangeUpdater: RangeUpdater,
7474
private modifierStageFactory: ModifierStageFactory,
7575
) {}

Diff for: packages/cursorless-engine/src/actions/InsertSnippet.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default class InsertSnippet {
3131

3232
constructor(
3333
private rangeUpdater: RangeUpdater,
34-
private snippets: Snippets,
34+
private snippets: Snippets | undefined,
3535
private actions: Actions,
3636
private modifierStageFactory: ModifierStageFactory,
3737
) {
@@ -56,6 +56,10 @@ export default class InsertSnippet {
5656

5757
private getScopeTypes(snippetDescription: InsertSnippetArg): ScopeType[] {
5858
if (snippetDescription.type === "named") {
59+
if (this.snippets == null) {
60+
return [];
61+
}
62+
5963
const { name } = snippetDescription;
6064

6165
const snippet = this.snippets.getSnippetStrict(name);
@@ -76,6 +80,10 @@ export default class InsertSnippet {
7680
targets: Target[],
7781
) {
7882
if (snippetDescription.type === "named") {
83+
if (this.snippets == null) {
84+
throw Error("Named snippets are not enabled");
85+
}
86+
7987
const { name } = snippetDescription;
8088

8189
const snippet = this.snippets.getSnippetStrict(name);

Diff for: packages/cursorless-engine/src/actions/WrapWithSnippet.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class WrapWithSnippet {
1919

2020
constructor(
2121
private rangeUpdater: RangeUpdater,
22-
private snippets: Snippets,
22+
private snippets: Snippets | undefined,
2323
private modifierStageFactory: ModifierStageFactory,
2424
) {
2525
this.run = this.run.bind(this);
@@ -47,6 +47,10 @@ export default class WrapWithSnippet {
4747
snippetDescription: WrapWithSnippetArg,
4848
): ScopeType | undefined {
4949
if (snippetDescription.type === "named") {
50+
if (this.snippets == null) {
51+
return undefined;
52+
}
53+
5054
const { name, variableName } = snippetDescription;
5155

5256
const snippet = this.snippets.getSnippetStrict(name);
@@ -68,6 +72,10 @@ export default class WrapWithSnippet {
6872
targets: Target[],
6973
): string {
7074
if (snippetDescription.type === "named") {
75+
if (this.snippets == null) {
76+
throw Error("Named snippets are not enabled");
77+
}
78+
7179
const { name } = snippetDescription;
7280

7381
const snippet = this.snippets.getSnippetStrict(name);

Diff for: packages/cursorless-engine/src/api/CursorlessEngineApi.ts

-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
ScopeProvider,
88
} from "@cursorless/common";
99
import type { CommandRunner } from "../CommandRunner";
10-
import type { Snippets } from "../core/Snippets";
1110
import type { StoredTargetMap } from "../core/StoredTargets";
1211

1312
export interface CursorlessEngine {
@@ -16,7 +15,6 @@ export interface CursorlessEngine {
1615
customSpokenFormGenerator: CustomSpokenFormGenerator;
1716
storedTargets: StoredTargetMap;
1817
hatTokenMap: HatTokenMap;
19-
snippets: Snippets;
2018
injectIde: (ide: IDE | undefined) => void;
2119
runIntegrationTests: () => Promise<void>;
2220
addCommandRunnerDecorator: (

Diff for: packages/cursorless-engine/src/core/Snippets.ts

+5-216
Original file line numberDiff line numberDiff line change
@@ -1,190 +1,12 @@
1-
import { showError, Snippet, SnippetMap, walkFiles } from "@cursorless/common";
2-
import { readFile, stat } from "fs/promises";
3-
import { max } from "lodash";
4-
import { join } from "path";
5-
import { ide } from "../singletons/ide.singleton";
6-
import { mergeStrict } from "../util/object";
7-
import { mergeSnippets } from "./mergeSnippets";
8-
9-
const CURSORLESS_SNIPPETS_SUFFIX = ".cursorless-snippets";
10-
const SNIPPET_DIR_REFRESH_INTERVAL_MS = 1000;
11-
12-
interface DirectoryErrorMessage {
13-
directory: string;
14-
errorMessage: string;
15-
}
1+
import { Snippet, SnippetMap } from "@cursorless/common";
162

173
/**
184
* Handles all cursorless snippets, including core, third-party and
195
* user-defined. Merges these collections and allows looking up snippets by
206
* name.
217
*/
22-
export class Snippets {
23-
private coreSnippets!: SnippetMap;
24-
private thirdPartySnippets: Record<string, SnippetMap> = {};
25-
private userSnippets!: SnippetMap[];
26-
27-
private mergedSnippets!: SnippetMap;
28-
29-
private userSnippetsDir?: string;
30-
31-
/**
32-
* The maximum modification time of any snippet in user snippets dir.
33-
*
34-
* This variable will be set to -1 if no user snippets have yet been read or
35-
* if the user snippets path has changed.
36-
*
37-
* This variable will be set to 0 if the user has no snippets dir configured and
38-
* we've already set userSnippets to {}.
39-
*/
40-
private maxSnippetMtimeMs: number = -1;
41-
42-
/**
43-
* If the user has misconfigured their snippet dir, then we keep track of it
44-
* so that we can show them the error message if we can't find a snippet
45-
* later, and so that we don't show them the same error message every time
46-
* we try to poll the directory.
47-
*/
48-
private directoryErrorMessage: DirectoryErrorMessage | null | undefined =
49-
null;
50-
51-
constructor() {
52-
this.updateUserSnippetsPath();
53-
54-
this.updateUserSnippets = this.updateUserSnippets.bind(this);
55-
this.registerThirdPartySnippets =
56-
this.registerThirdPartySnippets.bind(this);
57-
58-
const timer = setInterval(
59-
this.updateUserSnippets,
60-
SNIPPET_DIR_REFRESH_INTERVAL_MS,
61-
);
62-
63-
ide().disposeOnExit(
64-
ide().configuration.onDidChangeConfiguration(() => {
65-
if (this.updateUserSnippetsPath()) {
66-
this.updateUserSnippets();
67-
}
68-
}),
69-
{
70-
dispose() {
71-
clearInterval(timer);
72-
},
73-
},
74-
);
75-
}
76-
77-
async init() {
78-
const extensionPath = ide().assetsRoot;
79-
const snippetsDir = join(extensionPath, "cursorless-snippets");
80-
const snippetFiles = await getSnippetPaths(snippetsDir);
81-
this.coreSnippets = mergeStrict(
82-
...(await Promise.all(
83-
snippetFiles.map(async (path) =>
84-
JSON.parse(await readFile(path, "utf8")),
85-
),
86-
)),
87-
);
88-
await this.updateUserSnippets();
89-
}
90-
91-
/**
92-
* Updates the userSnippetsDir field if it has change, returning a boolean
93-
* indicating whether there was an update. If there was an update, resets the
94-
* maxSnippetMtime to -1 to ensure snippet update.
95-
* @returns Boolean indicating whether path has changed
96-
*/
97-
private updateUserSnippetsPath(): boolean {
98-
const newUserSnippetsDir = ide().configuration.getOwnConfiguration(
99-
"experimental.snippetsDir",
100-
);
101-
102-
if (newUserSnippetsDir === this.userSnippetsDir) {
103-
return false;
104-
}
105-
106-
// Reset mtime to -1 so that next time we'll update the snippets
107-
this.maxSnippetMtimeMs = -1;
108-
109-
this.userSnippetsDir = newUserSnippetsDir;
110-
111-
return true;
112-
}
113-
114-
async updateUserSnippets() {
115-
let snippetFiles: string[];
116-
try {
117-
snippetFiles = this.userSnippetsDir
118-
? await getSnippetPaths(this.userSnippetsDir)
119-
: [];
120-
} catch (err) {
121-
if (this.directoryErrorMessage?.directory !== this.userSnippetsDir) {
122-
// NB: We suppress error messages once we've shown it the first time
123-
// because we poll the directory every second and want to make sure we
124-
// don't show the same error message repeatedly
125-
const errorMessage = `Error with cursorless snippets dir "${
126-
this.userSnippetsDir
127-
}": ${(err as Error).message}`;
128-
129-
showError(ide().messages, "snippetsDirError", errorMessage);
130-
131-
this.directoryErrorMessage = {
132-
directory: this.userSnippetsDir!,
133-
errorMessage,
134-
};
135-
}
136-
137-
this.userSnippets = [];
138-
this.mergeSnippets();
139-
140-
return;
141-
}
142-
143-
this.directoryErrorMessage = null;
144-
145-
const maxSnippetMtime =
146-
max(
147-
(await Promise.all(snippetFiles.map((file) => stat(file)))).map(
148-
(stat) => stat.mtimeMs,
149-
),
150-
) ?? 0;
151-
152-
if (maxSnippetMtime <= this.maxSnippetMtimeMs) {
153-
return;
154-
}
155-
156-
this.maxSnippetMtimeMs = maxSnippetMtime;
157-
158-
this.userSnippets = await Promise.all(
159-
snippetFiles.map(async (path) => {
160-
try {
161-
const content = await readFile(path, "utf8");
162-
163-
if (content.length === 0) {
164-
// Gracefully handle an empty file
165-
return {};
166-
}
167-
168-
return JSON.parse(content);
169-
} catch (err) {
170-
showError(
171-
ide().messages,
172-
"snippetsFileError",
173-
`Error with cursorless snippets file "${path}": ${
174-
(err as Error).message
175-
}`,
176-
);
177-
178-
// We don't want snippets from all files to stop working if there is
179-
// a parse error in one file, so we just effectively ignore this file
180-
// once we've shown an error message
181-
return {};
182-
}
183-
}),
184-
);
185-
186-
this.mergeSnippets();
187-
}
8+
export interface Snippets {
9+
updateUserSnippets(): Promise<void>;
18810

18911
/**
19012
* Allows extensions to register third-party snippets. Calling this function
@@ -195,22 +17,7 @@ export class Snippets {
19517
* @param extensionId The id of the extension registering the snippets.
19618
* @param snippets The snippets to be registered.
19719
*/
198-
registerThirdPartySnippets(extensionId: string, snippets: SnippetMap) {
199-
this.thirdPartySnippets[extensionId] = snippets;
200-
this.mergeSnippets();
201-
}
202-
203-
/**
204-
* Merge core, third-party, and user snippets, with precedence user > third
205-
* party > core.
206-
*/
207-
private mergeSnippets() {
208-
this.mergedSnippets = mergeSnippets(
209-
this.coreSnippets,
210-
this.thirdPartySnippets,
211-
this.userSnippets,
212-
);
213-
}
20+
registerThirdPartySnippets(extensionId: string, snippets: SnippetMap): void;
21421

21522
/**
21623
* Looks in merged collection of snippets for a snippet with key
@@ -219,23 +26,5 @@ export class Snippets {
21926
* @param snippetName The name of the snippet to look up
22027
* @returns The named snippet
22128
*/
222-
getSnippetStrict(snippetName: string): Snippet {
223-
const snippet = this.mergedSnippets[snippetName];
224-
225-
if (snippet == null) {
226-
let errorMessage = `Couldn't find snippet ${snippetName}. `;
227-
228-
if (this.directoryErrorMessage != null) {
229-
errorMessage += `This could be due to: ${this.directoryErrorMessage.errorMessage}.`;
230-
}
231-
232-
throw Error(errorMessage);
233-
}
234-
235-
return snippet;
236-
}
237-
}
238-
239-
function getSnippetPaths(snippetsDir: string) {
240-
return walkFiles(snippetsDir, CURSORLESS_SNIPPETS_SUFFIX);
29+
getSnippetStrict(snippetName: string): Snippet;
24130
}

Diff for: packages/cursorless-engine/src/cursorlessEngine.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
Command,
33
CommandServerApi,
4+
ensureCommandShape,
45
FileSystem,
56
Hats,
67
IDE,
7-
ensureCommandShape,
88
ScopeProvider,
99
} from "@cursorless/common";
1010
import {
@@ -13,7 +13,8 @@ import {
1313
} from "./api/CursorlessEngineApi";
1414
import { Debug } from "./core/Debug";
1515
import { HatTokenMapImpl } from "./core/HatTokenMapImpl";
16-
import { Snippets } from "./core/Snippets";
16+
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
17+
import type { Snippets } from "./core/Snippets";
1718
import { StoredTargetMap } from "./core/StoredTargets";
1819
import { RangeUpdater } from "./core/updateSelections/RangeUpdater";
1920
import { CustomSpokenFormGeneratorImpl } from "./generateSpokenForm/CustomSpokenFormGeneratorImpl";
@@ -30,24 +31,21 @@ import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
3031
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
3132
import { injectIde } from "./singletons/ide.singleton";
3233
import { TreeSitter } from "./typings/TreeSitter";
33-
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
3434

3535
export async function createCursorlessEngine(
3636
treeSitter: TreeSitter,
3737
ide: IDE,
3838
hats: Hats,
3939
commandServerApi: CommandServerApi | null,
4040
fileSystem: FileSystem,
41+
snippets: Snippets | undefined,
4142
): Promise<CursorlessEngine> {
4243
injectIde(ide);
4344

4445
const debug = new Debug(treeSitter);
4546

4647
const rangeUpdater = new RangeUpdater();
4748

48-
const snippets = new Snippets();
49-
snippets.init();
50-
5149
const hatTokenMap = new HatTokenMapImpl(
5250
rangeUpdater,
5351
debug,
@@ -123,7 +121,6 @@ export async function createCursorlessEngine(
123121
customSpokenFormGenerator,
124122
storedTargets,
125123
hatTokenMap,
126-
snippets,
127124
injectIde,
128125
runIntegrationTests: () =>
129126
runIntegrationTests(treeSitter, languageDefinitions),

Diff for: packages/cursorless-engine/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ export * from "./CommandHistory";
1212
export * from "./CommandHistoryAnalyzer";
1313
export * from "./util/grammarHelpers";
1414
export * from "./ScopeTestRecorder";
15+
export * from "./core/Snippets";
16+
export * from "./core/mergeSnippets";
17+
export * from "./util/object";

0 commit comments

Comments
 (0)