Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,21 @@ In addition to the above aspects, you can also use the following inline operator
- `@foo.start` and `@foo.end` to construct the scope using a range between two nodes (inclusive).
- `@foo.startOf` and `@foo.endOf` to refer to the start and end positions of a node. For example, you could use `@foo.start.endOf` to indicate that the scope should start at the end of the node.

## Iteration capture aliases
## Capture groups

Use these aliases when several scopes share an iteration range:
Use `@G_` followed by at least two underscore-separated scope names to give the same capture to multiple scopes. For example, `@G_value_name_namedFunction` expands to `@value`, `@name`, and `@namedFunction`. The `G_` prefix is discarded. Every scope name in the group must be valid.

- `@statementNameValue.iteration` expands to `@statement.iteration`,
`@name.iteration`, and `@value.iteration`.
- `@statementNameValueType.iteration` expands to the same captures plus
`@type.iteration`, for languages and contexts that need type iteration.

Both aliases support `.domain`, `.start.endOf`, `.end.startOf`, and `.end.endOf`. For example, a Java block can share iteration boundaries without repeating the pattern for each scope:
A dot suffix applies to every scope in the group. For example, `@G_value_name_namedFunction.start` expands to `@value.start`, `@name.start`, and `@namedFunction.start`. This also works with relationships and inline operators. A Java block can share iteration boundaries without repeating the capture for each scope:

```scm
(_
"{" @statementNameValueType.iteration.start.endOf
"}" @statementNameValueType.iteration.end.startOf
"{" @G_statement_name_value_type.iteration.start.endOf
"}" @G_statement_name_value_type.iteration.end.startOf
)
```

Predicates refer to the group capture and apply their changes to every expanded capture.

## Query predicate operators

We also support a number of query predicate operators for modifying the scope. See [`queryPredicateOperators.ts`](../../../../../packages/lib-engine/src/languages/TreeSitterQuery/queryPredicateOperators.ts) for a list of available operators.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
TextDocument,
TreeSitter,
} from "@cursorless/lib-common";
import { expandCaptureName, expandCaptures } from "./captureAliases";
import { expandCaptureName, expandCaptures } from "./captureGroup";
import type { ScopeCaptureName } from "./captureNames";
import {
getNormalizedCaptureIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import {
Position,
} from "@cursorless/lib-common";
import { TestTreeSitter } from "../../testUtil/TestTreeSitter";
import { expandCaptureName } from "./captureAliases";
import { expandCaptureName } from "./captureGroup";
import { TreeSitterQuery } from "./TreeSitterQuery";
import { treeSitterQueryCache } from "./TreeSitterQueryCache";

for (const alias of [
"statementNameValue.iteration",
"statementNameValueType.iteration",
"G_statement_name_value.iteration",
"G_statement_name_value_type.iteration",
]) {
suite(`${alias} capture alias`, () => {
suite(`${alias} capture group`, () => {
const ide = new FakeIDE();
const treeSitter = new TestTreeSitter();
const names = [
"statement.iteration",
"name.iteration",
"value.iteration",
...(alias === "statementNameValueType.iteration"
...(alias === "G_statement_name_value_type.iteration"
? ["type.iteration"]
: []),
];
Expand All @@ -48,7 +48,19 @@ for (const alias of [
);
}
assert.deepEqual(expandCaptureName("interior"), ["interior"]);
assert.deepEqual(expandCaptureName(`${alias}Other`), [`${alias}Other`]);
assert.deepEqual(expandCaptureName("Group_value_name"), [
"Group_value_name",
]);
assert.deepEqual(expandCaptureName("G_value_name_namedFunction.start"), [
"value.start",
"name.start",
"namedFunction.start",
]);
assert.deepEqual(expandCaptureName("G_value_name_namedFunction"), [
"value",
"name",
"namedFunction",
]);
});

for (const withPredicates of [false, true]) {
Expand Down Expand Up @@ -87,7 +99,7 @@ for (const alias of [
"statement",
"name",
"value",
...(alias === "statementNameValueType.iteration"
...(alias === "G_statement_name_value_type.iteration"
? ["type" as const]
: []),
] as const) {
Expand Down Expand Up @@ -126,6 +138,27 @@ for (const alias of [
}
});

test("expands an arbitrary group", () => {
const { query, document } = createQuery(`
(compound_statement) @G_value_name_namedFunction
`);
const matches = query.matches(
document,
new Position(0, 0),
new Position(0, 15),
);
assert.deepEqual(
matches[0].captures.map((capture) => capture.name),
["value", "name", "namedFunction"],
);
assert.deepEqual(
query
.matchesForScopeTypes(document, ["name"])[0]
.captures.map((capture) => capture.name),
["name"],
);
});

test("shares capture slots with ordinary captures", () => {
const { query, document } = createQuery(`
(compound_statement
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** Expand shared captures after Tree-sitter has matched the query. */
export function expandCaptureName(name: string): string[] {
if (!captureNameIsGroup(name)) {
return [name];
}

const dotIndex = name.indexOf(".");
const group = dotIndex === -1 ? name.slice(2) : name.slice(2, dotIndex);
const suffix = dotIndex === -1 ? "" : name.slice(dotIndex);
Comment thread
AndreasArvidsson marked this conversation as resolved.
return group.split("_").map((member) => `${member}${suffix}`);
}

/** Preserve predicate changes while giving each expanded capture its own name. */
export function expandCaptures<T extends { name: string }>(
captures: readonly T[],
aliases: ReadonlyMap<string, readonly string[]>,
): readonly T[] {
let result: T[] | undefined;

for (let i = 0; i < captures.length; i++) {
const capture = captures[i];
const names = aliases.get(capture.name);

if (names == null) {
result?.push(capture);
} else {
result ??= captures.slice(0, i);
for (const name of names) {
result.push({ ...capture, name });
}
}
}

return result ?? captures;
}

export function captureNameIsGroup(name: string): boolean {
return name.startsWith("G_");
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
pseudoScopeTypeTypes,
simpleScopeTypeTypes,
} from "@cursorless/lib-common";
import { expandCaptureName } from "./captureAliases";
import { captureNameIsGroup, expandCaptureName } from "./captureGroup";

const scopeCaptureNames = simpleScopeTypeTypes.filter(
(s) => !pseudoScopeTypeTypes.has(s),
Expand Down Expand Up @@ -98,8 +98,10 @@ function getScopeName(captureName: string): string {
}

export function isCaptureAllowed(captureName: string): boolean {
return expandCaptureName(captureName).some((name) =>
allowedCaptures.has(name),
const names = expandCaptureName(captureName);
return (
(!captureNameIsGroup(captureName) || names.length > 1) &&
names.every((name) => allowedCaptures.has(name))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,92 @@ import { validateQueryCaptures } from "./validateQueryCaptures";

const testCases: { name: string; isOk: boolean; content: string }[] = [
{
name: "Statement/name/value iteration alias",
name: "Statement/name/value iteration group",
isOk: true,
content: "(compound_statement) @statementNameValue.iteration",
content: "(compound_statement) @G_statement_name_value.iteration",
},
{
name: "Statement/name/value iteration boundaries",
isOk: true,
content:
'(compound_statement "{" @statementNameValue.iteration.start.endOf "}" @statementNameValue.iteration.end.startOf)',
'(compound_statement "{" @G_statement_name_value.iteration.start.endOf "}" @G_statement_name_value.iteration.end.startOf)',
},
{
name: "Statement/name/value iteration domain",
isOk: true,
content: "(compound_statement) @statementNameValue.iteration.domain",
content: "(compound_statement) @G_statement_name_value.iteration.domain",
},
{
name: "Unknown iteration relationship",
isOk: false,
content: "(compound_statement) @statementNameValue.iteration.unknown",
content: "(compound_statement) @G_statement_name_value.iteration.unknown",
},
{
name: "Statement/name/value/type iteration boundaries",
isOk: true,
content:
'(compound_statement "{" @statementNameValueType.iteration.start.endOf "}" @statementNameValueType.iteration.end.startOf)',
'(compound_statement "{" @G_statement_name_value_type.iteration.start.endOf "}" @G_statement_name_value_type.iteration.end.startOf)',
},
{
name: "Unknown type iteration relationship",
isOk: false,
content: "(compound_statement) @statementNameValueType.iteration.unknown",
content:
"(compound_statement) @G_statement_name_value_type.iteration.unknown",
},
{
name: "Old alias",
isOk: false,
content: "(compound_statement) @statementNameValue.iteration",
},
{
name: "Old type alias",
isOk: false,
content: "(compound_statement) @statementNameValueType.iteration",
},
{
name: "Group without suffix",
isOk: true,
content: "(compound_statement) @G_value_name_namedFunction",
},
{
name: "Group with start suffix",
isOk: true,
content: "(compound_statement) @G_value_name_namedFunction.start",
},
{
name: "Single member group",
isOk: false,
content: "(compound_statement) @G_value",
},
{
name: "Single member group with suffix",
isOk: false,
content: "(compound_statement) @G_value.start",
},
{
name: "Unknown first group member",
isOk: false,
content: "(compound_statement) @G_typo_name.iteration",
},
{
name: "Unknown middle group member",
isOk: false,
content: "(compound_statement) @G_statement_typo_value.iteration",
},
{
name: "Unknown last group member",
isOk: false,
content: "(compound_statement) @G_statement_name_typo.iteration",
},
{
name: "Empty group",
isOk: false,
content: "(compound_statement) @G_",
},
{
name: "Alias without iteration",
name: "Empty group member",
isOk: false,
content: "(compound_statement) @statementNameValue",
content: "(compound_statement) @G_statement__value.iteration",
},
{
name: "Scope captures",
Expand Down
Loading
Loading