Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Tree sitter provider #2488

Merged
merged 12 commits into from
Jul 12, 2024
13 changes: 13 additions & 0 deletions packages/common/src/ide/types/RawTreeSitterQueryProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Disposable } from "@cursorless/common";

export interface RawTreeSitterQueryProvider {
/**
* Listen for changes to language definitions
*/
onChanges(listener: () => void): Disposable;

/**
* Read a query definition. The query name is the name of one of our `.scm` files.
*/
readQuery(name: string): Promise<string | undefined>;
}
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from "./ide/types/QuickPickOptions";
export * from "./ide/types/events.types";
export * from "./ide/types/Paths";
export * from "./ide/types/CommandHistoryStorage";
export * from "./ide/types/RawTreeSitterQueryProvider";
export * from "./ide/types/FileSystem.types";
export * from "./types/RangeExpansionBehavior";
export * from "./types/InputBoxOptions";
Expand Down
15 changes: 12 additions & 3 deletions packages/cursorless-engine/src/cursorlessEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
IDE,
ScopeProvider,
ensureCommandShape,
type RawTreeSitterQueryProvider,
} from "@cursorless/common";
import { KeyboardTargetUpdater } from "./KeyboardTargetUpdater";
import {
Expand All @@ -17,10 +18,15 @@ import { HatTokenMapImpl } from "./core/HatTokenMapImpl";
import type { Snippets } from "./core/Snippets";
import { StoredTargetMap } from "./core/StoredTargets";
import { RangeUpdater } from "./core/updateSelections/RangeUpdater";
import { DisabledLanguageDefinitions } from "./disabledComponents/DisabledLanguageDefinitions";
import { DisabledSnippets } from "./disabledComponents/DisabledSnippets";
import { DisabledTalonSpokenForms } from "./disabledComponents/DisabledTalonSpokenForms";
import { DisabledTreeSitter } from "./disabledComponents/DisabledTreeSitter";
import { CustomSpokenFormGeneratorImpl } from "./generateSpokenForm/CustomSpokenFormGeneratorImpl";
import { LanguageDefinitions } from "./languages/LanguageDefinitions";
import {
LanguageDefinitionsImpl,
type LanguageDefinitions,
} from "./languages/LanguageDefinitions";
import { ModifierStageFactoryImpl } from "./processTargets/ModifierStageFactoryImpl";
import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandlers";
import { runCommand } from "./runCommand";
Expand All @@ -35,12 +41,13 @@ import { injectIde } from "./singletons/ide.singleton";
import { TreeSitter } from "./typings/TreeSitter";

export async function createCursorlessEngine(
treeSitter: TreeSitter,
treeSitter: TreeSitter = new DisabledTreeSitter(),
ide: IDE,
hats: Hats,
commandServerApi: CommandServerApi | null,
fileSystem: FileSystem,
talonSpokenForms: TalonSpokenForms | undefined,
treeSitterQueryProvider: RawTreeSitterQueryProvider | undefined,
snippets: Snippets = new DisabledSnippets(),
): Promise<CursorlessEngine> {
injectIde(ide);
Expand All @@ -61,7 +68,9 @@ export async function createCursorlessEngine(

const keyboardTargetUpdater = new KeyboardTargetUpdater(storedTargets);

const languageDefinitions = new LanguageDefinitions(fileSystem, treeSitter);
const languageDefinitions = treeSitterQueryProvider
? new LanguageDefinitionsImpl(ide, treeSitter, treeSitterQueryProvider)
: new DisabledLanguageDefinitions();
await languageDefinitions.init();

const customSpokenFormGenerator = new CustomSpokenFormGeneratorImpl(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { TextDocument, Range, Listener } from "@cursorless/common";
import type { SyntaxNode } from "web-tree-sitter";
import type { LanguageDefinition } from "../languages/LanguageDefinition";
import type { LanguageDefinitions } from "../languages/LanguageDefinitions";

export class DisabledLanguageDefinitions implements LanguageDefinitions {
init(): Promise<void> {
return Promise.resolve();
}

onDidChangeDefinition(_listener: Listener) {
return { dispose: () => {} };
}

loadLanguage(_languageId: string): Promise<void> {
return Promise.resolve();
}

get(_languageId: string): LanguageDefinition | undefined {
return undefined;
}

getNodeAtLocation(
_document: TextDocument,
_range: Range,
): SyntaxNode | undefined {
return undefined;
}

dispose(): void {
// Do nothing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { TextDocument, Range } from "@cursorless/common";
import type { SyntaxNode, Tree, Language } from "web-tree-sitter";
import type { TreeSitter } from "../typings/TreeSitter";

export class DisabledTreeSitter implements TreeSitter {
getTree(_document: TextDocument): Tree {
throw new Error("Tree sitter not provided");
}

loadLanguage(_languageId: string): Promise<boolean> {
return Promise.resolve(false);
}

getLanguage(_languageId: string): Language | undefined {
throw new Error("Tree sitter not provided");
}

getNodeAtLocation(_document: TextDocument, _range: Range): SyntaxNode {
throw new Error("Tree sitter not provided");
}
}
53 changes: 26 additions & 27 deletions packages/cursorless-engine/src/languages/LanguageDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
FileSystem,
ScopeType,
SimpleScopeType,
showError,
type IDE,
type RawTreeSitterQueryProvider,
} from "@cursorless/common";
import { basename, dirname, join } from "pathe";
import { dirname, join } from "pathe";
import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers";
import { ide } from "../singletons/ide.singleton";
import { TreeSitter } from "../typings/TreeSitter";
import { matchAll } from "../util/regex";
import { TreeSitterQuery } from "./TreeSitterQuery";
Expand Down Expand Up @@ -36,16 +36,15 @@ export class LanguageDefinition {
* id doesn't have a new-style query definition
*/
static async create(
ide: IDE,
treeSitterQueryProvider: RawTreeSitterQueryProvider,
treeSitter: TreeSitter,
fileSystem: FileSystem,
queryDir: string,
languageId: string,
): Promise<LanguageDefinition | undefined> {
const languageQueryPath = join(queryDir, `${languageId}.scm`);

const rawLanguageQueryString = await readQueryFileAndImports(
fileSystem,
languageQueryPath,
ide,
treeSitterQueryProvider,
`${languageId}.scm`,
);

if (rawLanguageQueryString == null) {
Expand Down Expand Up @@ -91,43 +90,42 @@ export class LanguageDefinition {
* @returns The text of the query file, with all imports inlined
*/
async function readQueryFileAndImports(
fileSystem: FileSystem,
languageQueryPath: string,
ide: IDE,
provider: RawTreeSitterQueryProvider,
languageQueryName: string,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for switching from path to name is that if we have a non file system implementation of the language provider, path is irrelevant. I think every definition name(scm filename) should just be unique and in which folder they are located doesn't really matter.

) {
// Seed the map with the query file itself
const rawQueryStrings: Record<string, string | null> = {
[languageQueryPath]: null,
[languageQueryName]: null,
};

const doValidation = ide().runMode !== "production";
const doValidation = ide.runMode !== "production";

// Keep reading imports until we've read all the imports. Every time we
// encounter an import in a query file, we add it to the map with a value
// of null, so that it will be read on the next iteration
while (Object.values(rawQueryStrings).some((v) => v == null)) {
for (const [queryPath, rawQueryString] of Object.entries(rawQueryStrings)) {
for (const [queryName, rawQueryString] of Object.entries(rawQueryStrings)) {
if (rawQueryString != null) {
continue;
}

const fileName = basename(queryPath);

let rawQuery = await fileSystem.readBundledFile(queryPath);
let rawQuery = await provider.readQuery(queryName);

if (rawQuery == null) {
if (queryPath === languageQueryPath) {
if (queryName === languageQueryName) {
// If this is the main query file, then we know that this language
// just isn't defined using new-style queries
return undefined;
}

showError(
ide().messages,
ide.messages,
"LanguageDefinition.readQueryFileAndImports.queryNotFound",
`Could not find imported query file ${queryPath}`,
`Could not find imported query file ${queryName}`,
);

if (ide().runMode === "test") {
if (ide.runMode === "test") {
throw new Error("Invalid import statement");
}

Expand All @@ -136,10 +134,10 @@ async function readQueryFileAndImports(
}

if (doValidation) {
validateQueryCaptures(fileName, rawQuery);
validateQueryCaptures(queryName, rawQuery);
}

rawQueryStrings[queryPath] = rawQuery;
rawQueryStrings[queryName] = rawQuery;
matchAll(
rawQuery,
// Matches lines like:
Expand All @@ -154,10 +152,10 @@ async function readQueryFileAndImports(
const relativeImportPath = match[1];

if (doValidation) {
validateImportSyntax(fileName, relativeImportPath, match[0]);
validateImportSyntax(ide, queryName, relativeImportPath, match[0]);
}

const importQueryPath = join(dirname(queryPath), relativeImportPath);
const importQueryPath = join(dirname(queryName), relativeImportPath);
rawQueryStrings[importQueryPath] =
rawQueryStrings[importQueryPath] ?? null;
},
Expand All @@ -169,6 +167,7 @@ async function readQueryFileAndImports(
}

function validateImportSyntax(
ide: IDE,
file: string,
relativeImportPath: string,
actual: string,
Expand All @@ -177,12 +176,12 @@ function validateImportSyntax(

if (actual !== canonicalSyntax) {
showError(
ide().messages,
ide.messages,
"LanguageDefinition.readQueryFileAndImports.malformedImport",
`Malformed import statement in ${file}: "${actual}". Import statements must be of the form "${canonicalSyntax}"`,
);

if (ide().runMode === "test") {
if (ide.runMode === "test") {
throw new Error("Invalid import statement");
}
}
Expand Down
Loading
Loading