Skip to content

Commit

Permalink
commands: add support for tree-sitter textobjects
Browse files Browse the repository at this point in the history
  • Loading branch information
71 committed Dec 2, 2023
1 parent 1a0baff commit 21a8b2e
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 26 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"search.exclude": {
".direnv": true,
"out": true
},

Expand Down
16 changes: 8 additions & 8 deletions src/commands/README.md

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

16 changes: 8 additions & 8 deletions src/commands/layouts/azerty.fr.md

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

16 changes: 8 additions & 8 deletions src/commands/layouts/qwerty.md

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

6 changes: 5 additions & 1 deletion src/commands/load-all.build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ function determineFunctionExpression(f: Builder.ParsedFunction) {
break;

case "treeSitter":
givenParameters.push("_.extension.treeSitterOrThrow()");
if (type === "TreeSitter | undefined") {
givenParameters.push("_.extension.treeSitter");
} else {
givenParameters.push("_.extension.treeSitterOrThrow()");
}
break;

case "documentTree":
Expand Down
2 changes: 1 addition & 1 deletion src/commands/load-all.ts

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

66 changes: 66 additions & 0 deletions src/commands/seek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ export async function object(
inner: Argument<boolean> = false,
where?: Argument<"start" | "end">,
shift = Shift.Select,

treeSitter?: TreeSitter,
) {
const input = await inputOr(() => prompt({
prompt: "Object description",
Expand Down Expand Up @@ -492,6 +494,70 @@ export async function object(
return Selections.set(newSelections);
}

// Helix text objects:
// https://github.com/helix-editor/helix/blob/master/book/src/guides/textobject.md#L1
if (match = /^\(\?#textobject=(\w+)\)$/.exec(input)) {
if (treeSitter === undefined) {
throw new Error("tree-sitter is not available");
}

const query = await treeSitter.textObjectQueryFor(_.document);

if (query === undefined) {
throw new Error("no textobject query available for current document");
}

// Languages with queries available are a subset of supported languages, so
// given that we have a `query` `withDocumentTree()` will not fail.
const newSelections = await treeSitter.withDocumentTree(_.document, async (documentTree) => {
const textObjectName = match![1] + (inner ? ".inside" : ".around");

if (!query.captureNames.includes(textObjectName)) {
const existingValues = query.captureNames.map((name) =>
`"${name.replace(".inside", "").replace(".around", "")}"`
).join(", ");

throw new Error(
`unknown textobject ${JSON.stringify(textObjectName)}, valid values are ${existingValues}`,
);
}

// TODO(71): add helpers for checking if a VS Code Position is within a Tree
// Sitter range without creating temporary objects
const captures = query.captures(documentTree.rootNode)
.filter((capture) => capture.name === textObjectName)
.map(({ node }) => [node, treeSitter.toRange(node)] as const);

return Selections.mapByIndex((_i, selection) => {
const active = selection.active;

let smallestNode: SyntaxNode | undefined;
let smallestNodeLength: number = Number.MAX_SAFE_INTEGER;

for (const [node, nodeRange] of captures) {
if (!nodeRange.contains(active)) {
continue;
}

const nodeLength = node.endIndex - node.startIndex;

if (nodeLength < smallestNodeLength && !nodeRange.isEqual(selection)) {
smallestNode = node;
}
}

return smallestNode === undefined
? selection
: Selections.fromStartEnd(
treeSitter.toPosition(smallestNode.startPosition),
treeSitter.toPosition(smallestNode.endPosition),
Selections.isStrictlyReversed(selection, _));
});
});

return Selections.set(newSelections);
}

throw new Error("unknown object " + JSON.stringify(input));
}

Expand Down
4 changes: 4 additions & 0 deletions src/state/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export class Extension implements vscode.Disposable {
}
}

public get treeSitter(): TreeSitter | undefined {
return this._treeSitter;
}

public treeSitterOrThrow(): TreeSitter {
if (this._treeSitter === undefined) {
throw new Error("TreeSitter is not available");
Expand Down
10 changes: 10 additions & 0 deletions src/utils/tree-sitter-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ export declare const withQuerySync: <T>(language: HasLanguage, source: string, k
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.
*
Expand Down

0 comments on commit 21a8b2e

Please sign in to comment.