Skip to content

Commit 00651ad

Browse files
Pass snippets as argument to engine (#2478)
Makes it so that Cursorless everywhere editors doesn't have to provide a snippet implementation ## Checklist - [/] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] 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: Pokey Rule <[email protected]>
1 parent 80172d9 commit 00651ad

File tree

11 files changed

+322
-261
lines changed

11 files changed

+322
-261
lines changed

packages/cursorless-engine/src/actions/Actions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class Actions implements ActionRecord {
9696
foldRegion = new Fold(this.rangeUpdater);
9797
followLink = new FollowLink({ openAside: false });
9898
followLinkAside = new FollowLink({ openAside: true });
99-
generateSnippet = new GenerateSnippet();
99+
generateSnippet = new GenerateSnippet(this.snippets);
100100
getText = new GetText();
101101
highlight = new Highlight();
102102
increment = new Increment(this);

packages/cursorless-engine/src/actions/GenerateSnippet/GenerateSnippet.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { FlashStyle, isTesting, Range } from "@cursorless/common";
2+
import type { Snippets } from "../../core/Snippets";
23
import { Offsets } from "../../processTargets/modifiers/surroundingPair/types";
34
import { ide } from "../../singletons/ide.singleton";
4-
import { Target } from "../../typings/target.types";
5+
import type { Target } from "../../typings/target.types";
56
import { matchAll } from "../../util/regex";
67
import { ensureSingleTarget, flashTargets } from "../../util/targetUtils";
7-
import { ActionReturnValue } from "../actions.types";
8+
import type { ActionReturnValue } from "../actions.types";
89
import { constructSnippetBody } from "./constructSnippetBody";
910
import { editText } from "./editText";
10-
import { openNewSnippetFile } from "./openNewSnippetFile";
1111
import Substituter from "./Substituter";
1212

1313
/**
@@ -46,7 +46,7 @@ import Substituter from "./Substituter";
4646
* confusing escaping.
4747
*/
4848
export default class GenerateSnippet {
49-
constructor() {
49+
constructor(private snippets: Snippets) {
5050
this.run = this.run.bind(this);
5151
}
5252

@@ -228,7 +228,7 @@ export default class GenerateSnippet {
228228
} else {
229229
// Otherwise, we create and open a new document for the snippet in the
230230
// user snippets dir
231-
await openNewSnippetFile(snippetName);
231+
await this.snippets.openNewSnippetFile(snippetName);
232232
}
233233

234234
// Insert the meta-snippet

packages/cursorless-engine/src/actions/GenerateSnippet/openNewSnippetFile.ts

-27
This file was deleted.

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: (
+10-215
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-es";
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,11 @@ 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}. `;
29+
getSnippetStrict(snippetName: string): Snippet;
22730

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);
31+
/**
32+
* Opens a new snippet file in the users snippet directory.
33+
* @param snippetName The name of the snippet
34+
*/
35+
openNewSnippetFile(snippetName: string): Promise<void>;
24136
}

packages/cursorless-engine/src/cursorlessEngine.ts

+5-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,22 @@ 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";
34+
import { DisabledSnippets } from "./disabledComponents/DisabledSnippets";
3435

3536
export async function createCursorlessEngine(
3637
treeSitter: TreeSitter,
3738
ide: IDE,
3839
hats: Hats,
3940
commandServerApi: CommandServerApi | null,
4041
fileSystem: FileSystem,
42+
snippets: Snippets = new DisabledSnippets(),
4143
): Promise<CursorlessEngine> {
4244
injectIde(ide);
4345

4446
const debug = new Debug(treeSitter);
4547

4648
const rangeUpdater = new RangeUpdater();
4749

48-
const snippets = new Snippets();
49-
snippets.init();
50-
5150
const hatTokenMap = new HatTokenMapImpl(
5251
rangeUpdater,
5352
debug,
@@ -123,7 +122,6 @@ export async function createCursorlessEngine(
123122
customSpokenFormGenerator,
124123
storedTargets,
125124
hatTokenMap,
126-
snippets,
127125
injectIde,
128126
runIntegrationTests: () =>
129127
runIntegrationTests(treeSitter, languageDefinitions),

0 commit comments

Comments
 (0)