Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
haberdashPI committed Sep 25, 2024
1 parent 4bd2bf6 commit 4223b52
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 2 deletions.
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@
"ts-loader": "^9.5.1",
"typescript": "^5.4.3",
"wdio-vscode-service": "^6.0.3",
"web-tree-sitter": "^0.23.0",
"webpack": "^5.92.1",
"webpack-cli": "^5.1.4"
},
Expand Down
197 changes: 197 additions & 0 deletions src/web/tree-sitter-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import * as vscode from 'vscode';
// eslint-disable-next-line n/no-unpublished-import
import TreeSitter, {Query, SyntaxNode, Tree} from 'web-tree-sitter';
export {Query, type SyntaxNode, type Tree, TreeSitter};

/**
* A supported language.
*/
export declare enum Language {
C = 'c',
Cpp = 'cpp',
Go = 'go',
Html = 'html',
JavaScript = 'javascript',
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
JavaScriptReact = 'javascript',
Python = 'python',
Rust = 'rust',
TypeScript = 'typescript',
TypeScriptReact = 'tsx',
}
/**
* Ensures that Tree Sitter is loaded.
*/
export declare function ensureLoaded(): Promise<void>;
/**
* Ensures that the specified language is loaded.
*/
export declare function ensureLoaded(input: HasLanguage): Promise<void>;
/**
* Type from which a {@link Language} can be determined.
*/
export type HasLanguage = Language | string | vscode.Uri | vscode.TextDocument;
/**
* Returns the {@link Language} of the file at the given value if it can be
* reliably determined. Otherwise, returns `undefined`.
*/
export declare function determineLanguage(input: HasLanguage): Language | undefined;
/**
* Same as {@link determineLanguage()}, but throws an error on failure instead
* of returning `undefined`.
*/
export declare function determineLanguageOrFail(input: HasLanguage): Language;
/**
* A cache for trees returned by {@link documentTree()} and
* {@link documentTreeSync()}.
*/
export declare class Cache {
constructor();
}
/**
* Options given to {@link documentTree()} and {@link documentTreeSync()}.
*/
export interface DocumentTreeOptions {
/**
* The language to use; if unspecified, it will be determined using
* {@link determineLanguage()}.
*/
readonly language?: Language;
/**
* The cache used to resolve the tree, or `undefined` if no cache should be
* used.
*/
readonly cache?: Cache;
/**
* The timeout in milliseconds of the operation.
*/
readonly timeoutMs?: number;
}
/**
* Returns the document tree for the specified document,
* {@link ensureLoaded loading} the necessary code first if necessary.
*/
export declare function documentTree(
document: vscode.TextDocument,
options?: DocumentTreeOptions
): Promise<Tree>;
/**
* Returns the document tree for the specified document, failing if the
* relevant language is not already {@link ensureLoaded loaded}.
*/
export declare function documentTreeSync(
document: vscode.TextDocument,
options?: DocumentTreeOptions
): Tree;
/**
* Compiles the given string into a {@link Query} object which can be used to
* perform queries on nodes of the given language.
*
* @see {@link https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax}
*/
export declare function query(
language: HasLanguage
): (strings: TemplateStringsArray, ...args: unknown[]) => Promise<Query>;
export declare function query(language: HasLanguage, source: string): Promise<Query>;
/**
* Compiles the given string into a {@link Query} object which can be used to
* perform queries on nodes of the given language, failing if it is not already
* {@link ensureLoaded loaded}.
*/
export declare function querySync(
language: HasLanguage
): (strings: TemplateStringsArray, ...args: unknown[]) => Query;
export declare function querySync(language: HasLanguage, source: string): Query;
/**
* Executes the specified function with the result of {@link documentTree()},
* {@link Tree.delete() deleting} the tree after the end of the function.
*/
export declare const withDocumentTree: {
<T>(document: vscode.TextDocument, k: (tree: Tree) => T | PromiseLike<T>): Promise<T>;
<T_1>(
document: vscode.TextDocument,
options: DocumentTreeOptions | undefined,
k: (tree: Tree) => T_1 | PromiseLike<T_1>
): Promise<T_1>;
};
/**
* Executes the specified function with the result of {@link documentTreeSync()},
* {@link Tree.delete() deleting} the tree after the end of the function.
*/
export declare const withDocumentTreeSync: {
<T>(document: vscode.TextDocument, k: (tree: Tree) => T): T;
<T_1>(
document: vscode.TextDocument,
options: DocumentTreeOptions | undefined,
k: (tree: Tree) => T_1
): T_1;
};
/**
* Executes the specified function with the result of {@link query()},
* {@link Query.delete() deleting} the query after the end of the function.
*/
export declare const withQuery: <T>(
language: HasLanguage,
source: string,
k: (query: Query) => T | PromiseLike<T>
) => Promise<T>;
/**
* Executes the specified function with the result of {@link querySync()},
* {@link Query.delete() deleting} the query after the end of the function.
*/
export declare const withQuerySync: <T>(
language: HasLanguage,
source: string,
k: (query: Query) => T
) => T;
/**
* Executes the specified function with the given arguments, calling
* `arg.delete()` for each `arg` in `args` after the end of its execution.
*
* The function may return a `Promise`, in which case a promise will be
* returned as well.
*/
export declare function using<
T,
Args extends {
delete(): void;
}[],
>(...args: [...Args, (...args: Args) => T]): T;
/**
* Returns the built-in {@link Query} for textobjects of the given language, or
* `undefined` if there is no such built-in query.
*
* This function automatically memoizes its results; callers should neither
* cache nor {@link Query.delete delete} the returned query.
*
* @see https://docs.helix-editor.com/guides/textobject.html
*/
export declare function textObjectQueryFor(
input: HasLanguage
): Promise<Omit<Query, 'delete'> | undefined>;
/**
* A Tree Sitter point with UTF-16-based offsets.
*
* @see {@link TreeSitter.Point}
*/
export type Point = TreeSitter.Point;
/**
* Converts a Tree Sitter {@link Point} to a {@link vscode.Position}.
*/
export declare function toPosition(point: Point): vscode.Position;
/**
* Converts a {@link vscode.Position} into a Tree Sitter {@link Point}.
*/
export declare function fromPosition(position: vscode.Position): Point;
/**
* Returns the {@link vscode.Position} of a Tree Sitter syntax node.
*/
export declare function toRange(node: SyntaxNode): vscode.Range;
/**
* Returns the start and end Tree Sitter {@link Point} positions of a
* {@link vscode.Range}.
*/
export declare function fromRange(range: vscode.Range): {
startPosition: Point;
endPosition: Point;
};
90 changes: 90 additions & 0 deletions src/web/treeSitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as vscode from 'vscode';
import { fromPosition, toPosition, toRange, type SyntaxNode, type Tree, type Cache as TreeCache } from './tree-sitter-api'
import { TreeCursor } from 'web-tree-sitter';
export type TreeSitter = typeof import('./tree-sitter-api');

// TODO: how to import API
let treeSitter: TreeSitter | undefined;
let treeCache: TreeCache | undefined;


function smallestSurroundingFilteredChild(filter: (x: SyntaxNode) => boolean, range: vscode.Range, cursor: TreeCursor): SyntaxNode | undefined {
let node = cursor.currentNode
let boundaries = toRange(node);
if (!boundaries.contains(range)) {
return undefined;
}

if (cursor.gotoFirstChildForPosition(fromPosition(range.start))) {
node = cursor.currentNode;
boundaries = toRange(node);
let match = boundaries.contains(range) && filter(node)

while (!match) {
cursor.gotoNextSibling();
node = cursor.currentNode;
if (toPosition(node.startPosition) > range.start) {
break;
}
boundaries = toRange(node);
match = boundaries.contains(range) && filter(node)
}

if (match) {
const maybeChildNode = smallestSurroundingFilteredChild(filter, range, cursor);
if (maybeChildNode) {
return maybeChildNode;
} else {
return node;
}
}
}

cursor.gotoParent();
return undefined;
}

async function smallestSurroundingNode(filter: (x: SyntaxNode) => boolean, range: vscode.Range): Promise<SyntaxNode | undefined> {
let editor = vscode.window.activeTextEditor;
if(editor && treeSitter && treeCache) {
// TODO: configure timeout??
let tree = await treeSitter?.documentTree(editor.document, {cache: treeCache, timeoutMs: 2000});
let node = smallestSurroundingFilteredChild(filter, range, tree.walk());
if (node) {
return node;
} else {
return tree.rootNode;
}
}
return undefined;
}

// filters to be used for `smallestSurroundingNode`
function isNamed(x: SyntaxNode) {
return x.isNamed;
}

function anyNode(x: SyntaxNode) {
return true
}

// STEPS:
// 1. find node using functions above
// 2. (lazy?) computation of boundary sequence
// 3. do we leverage untiMotions over this boundary sequence? (how do we deal with statefulness of cursors?)
// 3.b - implement expansions separately

export function registerTreeSitter(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('selection-utilities.nextNamedSibling', () =>
smallestSurroundingFilteredChild(isNamed, sel)
);
);

treeSitter = await vscode.extensions.getExtension<TreeSitter>(
"gregoire.tree-sitter",
)?.activate();
if(treeSitter) {
treeCache = new treeSitter.Cache();
}
}

0 comments on commit 4223b52

Please sign in to comment.