Skip to content

Commit 9e8b4fc

Browse files
bjaspanBarry Jaspanpre-commit-ci-lite[bot]pokey
authored
Change LanguageDefinitions to use VscodeFilesystem. (#2199)
Remove some uses of `fs`, `fs/promises`, and `path` for the web port. --------- Co-authored-by: Barry Jaspan <[email protected]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Pokey Rule <[email protected]>
1 parent cdbd302 commit 9e8b4fc

File tree

6 files changed

+182
-43
lines changed

6 files changed

+182
-43
lines changed

packages/common/src/ide/types/FileSystem.types.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import { Disposable } from "./ide.types";
33
export type PathChangeListener = () => void;
44

55
export interface FileSystem {
6+
/**
7+
* Reads a file that comes bundled with Cursorless, with the utf-8 encoding.
8+
* {@link path} is expected to be relative to the root of the extension
9+
* bundle. If the file doesn't exist, returns `undefined`.
10+
*
11+
* Note that in development mode, it is possible to supply an absolute path to
12+
* a file on the local filesystem, for things like hot-reloading.
13+
*
14+
* @param path The path of the file to read
15+
* @returns The contents of path, decoded as UTF-8
16+
*/
17+
readBundledFile(path: string): Promise<string | undefined>;
18+
619
/**
720
* Recursively watch a directory for changes.
821
* @param path The path of the directory to watch
@@ -11,12 +24,6 @@ export interface FileSystem {
1124
*/
1225
watchDir(path: string, onDidChange: PathChangeListener): Disposable;
1326

14-
/**
15-
* The path to the directory that Cursorless talon uses to share its state
16-
* with the Cursorless engine.
17-
*/
18-
readonly cursorlessDir: string;
19-
2027
/**
2128
* The path to the Cursorless talon state JSON file.
2229
*/

packages/cursorless-engine/src/languages/LanguageDefinition.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { ScopeType, SimpleScopeType, showError } from "@cursorless/common";
2-
import { existsSync, readFileSync } from "fs";
1+
import {
2+
FileSystem,
3+
ScopeType,
4+
SimpleScopeType,
5+
showError,
6+
} from "@cursorless/common";
37
import { dirname, join } from "path";
48
import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers";
59
import { TreeSitterTextFragmentScopeHandler } from "../processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler";
@@ -33,19 +37,23 @@ export class LanguageDefinition {
3337
* @returns A language definition for the given language id, or undefined if the given language
3438
* id doesn't have a new-style query definition
3539
*/
36-
static create(
40+
static async create(
3741
treeSitter: TreeSitter,
42+
fileSystem: FileSystem,
3843
queryDir: string,
3944
languageId: string,
40-
): LanguageDefinition | undefined {
45+
): Promise<LanguageDefinition | undefined> {
4146
const languageQueryPath = join(queryDir, `${languageId}.scm`);
4247

43-
if (!existsSync(languageQueryPath)) {
48+
const rawLanguageQueryString = await readQueryFileAndImports(
49+
fileSystem,
50+
languageQueryPath,
51+
);
52+
53+
if (rawLanguageQueryString == null) {
4454
return undefined;
4555
}
4656

47-
const rawLanguageQueryString = readQueryFileAndImports(languageQueryPath);
48-
4957
const rawQuery = treeSitter
5058
.getLanguage(languageId)!
5159
.query(rawLanguageQueryString);
@@ -88,7 +96,10 @@ export class LanguageDefinition {
8896
* @param languageQueryPath The path to the query file to read
8997
* @returns The text of the query file, with all imports inlined
9098
*/
91-
function readQueryFileAndImports(languageQueryPath: string) {
99+
async function readQueryFileAndImports(
100+
fileSystem: FileSystem,
101+
languageQueryPath: string,
102+
) {
92103
// Seed the map with the query file itself
93104
const rawQueryStrings: Record<string, string | null> = {
94105
[languageQueryPath]: null,
@@ -103,7 +114,29 @@ function readQueryFileAndImports(languageQueryPath: string) {
103114
continue;
104115
}
105116

106-
const rawQuery = readFileSync(queryPath, "utf8");
117+
let rawQuery = await fileSystem.readBundledFile(queryPath);
118+
119+
if (rawQuery == null) {
120+
if (queryPath === languageQueryPath) {
121+
// If this is the main query file, then we know that this language
122+
// just isn't defined using new-style queries
123+
return undefined;
124+
}
125+
126+
showError(
127+
ide().messages,
128+
"LanguageDefinition.readQueryFileAndImports.queryNotFound",
129+
`Could not find imported query file ${queryPath}`,
130+
);
131+
132+
if (ide().runMode === "test") {
133+
throw new Error("Invalid import statement");
134+
}
135+
136+
// If we're not in test mode, we just ignore the import and continue
137+
rawQuery = "";
138+
}
139+
107140
rawQueryStrings[queryPath] = rawQuery;
108141
matchAll(
109142
rawQuery,

packages/cursorless-engine/src/languages/LanguageDefinitions.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,61 @@ export class LanguageDefinitions {
4343
private disposables: Disposable[] = [];
4444

4545
constructor(
46-
fileSystem: FileSystem,
46+
private fileSystem: FileSystem,
4747
private treeSitter: TreeSitter,
4848
) {
49+
ide().onDidOpenTextDocument((document) => {
50+
this.loadLanguage(document.languageId);
51+
});
52+
ide().onDidChangeVisibleTextEditors((editors) => {
53+
editors.forEach(({ document }) => this.loadLanguage(document.languageId));
54+
});
55+
4956
// Use the repo root as the root for development mode, so that we can
5057
// we can make hot-reloading work for the queries
51-
this.queryDir = join(
58+
this.queryDir =
5259
ide().runMode === "development"
53-
? getCursorlessRepoRoot()
54-
: ide().assetsRoot,
55-
"queries",
60+
? join(getCursorlessRepoRoot(), "queries")
61+
: "queries";
62+
63+
ide().visibleTextEditors.forEach(({ document }) =>
64+
this.loadLanguage(document.languageId),
5665
);
5766

5867
if (ide().runMode === "development") {
5968
this.disposables.push(
6069
fileSystem.watchDir(this.queryDir, () => {
61-
this.languageDefinitions.clear();
62-
this.notifier.notifyListeners();
70+
this.reloadLanguageDefinitions();
6371
}),
6472
);
6573
}
6674
}
6775

76+
public async loadLanguage(languageId: string): Promise<void> {
77+
if (this.languageDefinitions.has(languageId)) {
78+
return;
79+
}
80+
81+
const definition =
82+
(await LanguageDefinition.create(
83+
this.treeSitter,
84+
this.fileSystem,
85+
this.queryDir,
86+
languageId,
87+
)) ?? LANGUAGE_UNDEFINED;
88+
89+
this.languageDefinitions.set(languageId, definition);
90+
}
91+
92+
private async reloadLanguageDefinitions(): Promise<void> {
93+
const languageIds = Array.from(this.languageDefinitions.keys());
94+
this.languageDefinitions.clear();
95+
await Promise.all(
96+
languageIds.map((languageId) => this.loadLanguage(languageId)),
97+
);
98+
this.notifier.notifyListeners();
99+
}
100+
68101
/**
69102
* Get a language definition for the given language id, if the language
70103
* has a new-style query definition, or return undefined if the language doesn't
@@ -74,14 +107,13 @@ export class LanguageDefinitions {
74107
* the given language id doesn't have a new-style query definition
75108
*/
76109
get(languageId: string): LanguageDefinition | undefined {
77-
let definition = this.languageDefinitions.get(languageId);
110+
const definition = this.languageDefinitions.get(languageId);
78111

79112
if (definition == null) {
80-
definition =
81-
LanguageDefinition.create(this.treeSitter, this.queryDir, languageId) ??
82-
LANGUAGE_UNDEFINED;
83-
84-
this.languageDefinitions.set(languageId, definition);
113+
throw new Error(
114+
"Expected language definition entry missing for languageId " +
115+
languageId,
116+
);
85117
}
86118

87119
return definition === LANGUAGE_UNDEFINED ? undefined : definition;

packages/cursorless-engine/src/runIntegrationTests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function assertNoScopesBothLegacyAndNew(
2727
const errors: string[] = [];
2828
for (const languageId of legacyLanguageIds) {
2929
await treeSitter.loadLanguage(languageId);
30+
await languageDefinitions.loadLanguage(languageId);
3031

3132
unsafeKeys(languageMatchers[languageId] ?? {}).map((scopeTypeType) => {
3233
if (

packages/cursorless-vscode/src/extension.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
toVscodeRange,
2525
} from "@cursorless/vscode-common";
2626
import * as crypto from "crypto";
27-
import { mkdir } from "fs/promises";
2827
import * as os from "os";
2928
import * as path from "path";
3029
import * as vscode from "vscode";
@@ -183,9 +182,15 @@ async function createVscodeIde(context: vscode.ExtensionContext) {
183182
const cursorlessDir = isTesting()
184183
? path.join(os.tmpdir(), crypto.randomBytes(16).toString("hex"))
185184
: path.join(os.homedir(), ".cursorless");
186-
await mkdir(cursorlessDir, { recursive: true });
187185

188-
return { vscodeIDE, hats, fileSystem: new VscodeFileSystem(cursorlessDir) };
186+
const fileSystem = new VscodeFileSystem(
187+
context,
188+
vscodeIDE.runMode,
189+
cursorlessDir,
190+
);
191+
await fileSystem.initialize();
192+
193+
return { vscodeIDE, hats, fileSystem };
189194
}
190195

191196
function createTreeSitter(parseTreeApi: ParseTreeApi): TreeSitter {

packages/cursorless-vscode/src/ide/vscode/VscodeFileSystem.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,87 @@
1-
import { Disposable, FileSystem, PathChangeListener } from "@cursorless/common";
2-
import { RelativePattern, workspace } from "vscode";
3-
import * as path from "path";
1+
import {
2+
Disposable,
3+
FileSystem,
4+
PathChangeListener,
5+
RunMode,
6+
} from "@cursorless/common";
7+
import { isAbsolute, join } from "path";
8+
import * as vscode from "vscode";
49

510
export class VscodeFileSystem implements FileSystem {
611
public readonly cursorlessTalonStateJsonPath: string;
712
public readonly cursorlessCommandHistoryDirPath: string;
813

9-
constructor(public readonly cursorlessDir: string) {
10-
this.cursorlessTalonStateJsonPath = path.join(
11-
this.cursorlessDir,
12-
"state.json",
13-
);
14-
this.cursorlessCommandHistoryDirPath = path.join(
14+
private decoder = new TextDecoder("utf-8");
15+
16+
constructor(
17+
private readonly extensionContext: vscode.ExtensionContext,
18+
private readonly runMode: RunMode,
19+
private readonly cursorlessDir: string,
20+
) {
21+
this.cursorlessTalonStateJsonPath = join(this.cursorlessDir, "state.json");
22+
this.cursorlessCommandHistoryDirPath = join(
1523
this.cursorlessDir,
1624
"commandHistory",
1725
);
1826
}
1927

20-
watchDir(path: string, onDidChange: PathChangeListener): Disposable {
28+
public async initialize(): Promise<void> {
29+
try {
30+
await vscode.workspace.fs.createDirectory(
31+
vscode.Uri.file(this.cursorlessDir),
32+
);
33+
} catch (err) {
34+
console.log("Cannot create cursorlessDir", this.cursorlessDir, err);
35+
}
36+
}
37+
38+
/**
39+
* Reads a file that comes bundled with Cursorless, with the utf-8 encoding.
40+
* {@link path} is expected to be relative to the root of the extension
41+
* bundle. If the file doesn't exist, returns `undefined`.
42+
*
43+
* Note that in development mode, it is possible to supply an absolute path to
44+
* a file on the local filesystem, for things like hot-reloading.
45+
*
46+
* @param path The path of the file to read
47+
* @returns The contents of path, decoded as UTF-8
48+
*/
49+
public async readBundledFile(path: string): Promise<string | undefined> {
50+
try {
51+
return this.decoder.decode(
52+
await vscode.workspace.fs.readFile(this.resolveBundledPath(path)),
53+
);
54+
} catch (err) {
55+
if (
56+
err instanceof Error &&
57+
"code" in err &&
58+
err.code === "FileNotFound"
59+
) {
60+
return undefined;
61+
}
62+
throw err;
63+
}
64+
}
65+
66+
private resolveBundledPath(path: string) {
67+
if (isAbsolute(path)) {
68+
if (this.runMode !== "development") {
69+
throw new Error(
70+
"Absolute paths are not supported outside of development mode",
71+
);
72+
}
73+
74+
return vscode.Uri.file(path);
75+
}
76+
77+
return vscode.Uri.joinPath(this.extensionContext.extensionUri, path);
78+
}
79+
80+
public watchDir(path: string, onDidChange: PathChangeListener): Disposable {
81+
// return { dispose: () => {} };
2182
// FIXME: Support globs?
22-
const watcher = workspace.createFileSystemWatcher(
23-
new RelativePattern(path, "**"),
83+
const watcher = vscode.workspace.createFileSystemWatcher(
84+
new vscode.RelativePattern(path, "**"),
2485
);
2586

2687
watcher.onDidChange(onDidChange);

0 commit comments

Comments
 (0)