Skip to content

Commit d71e8ca

Browse files
committed
Support specifying text fragments via queries
1 parent 2c1ef98 commit d71e8ca

File tree

11 files changed

+247
-61
lines changed

11 files changed

+247
-61
lines changed

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function createCursorlessEngine(
4141
const scopeHandlerFactory = new ScopeHandlerFactoryImpl(languageDefinitions);
4242
const markStageFactory = new MarkStageFactoryImpl();
4343
const modifierStageFactory = new ModifierStageFactoryImpl(
44+
languageDefinitions,
4445
scopeHandlerFactory,
4546
);
4647
const actions = new Actions(snippets, rangeUpdater, modifierStageFactory);

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import { ScopeType, SimpleScopeType } from "@cursorless/common";
22
import { existsSync, readFileSync } from "fs";
33
import { join } from "path";
44
import { TreeSitterScopeHandler } from "../processTargets/modifiers/scopeHandlers";
5+
import { TreeSitterTextFragmentScopeHandler } from "../processTargets/modifiers/scopeHandlers/TreeSitterScopeHandler/TreeSitterTextFragmentScopeHandler";
6+
import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types";
57
import { ide } from "../singletons/ide.singleton";
68
import { TreeSitter } from "../typings/TreeSitter";
7-
import { LanguageId } from "./constants";
89
import { TreeSitterQuery } from "./TreeSitterQuery";
10+
import { TEXT_FRAGMENT_CAPTURE_NAME } from "./captureNames";
11+
import { LanguageId } from "./constants";
912

1013
/**
1114
* Represents a language definition for a single language, including the
@@ -63,4 +66,12 @@ export class LanguageDefinition {
6366

6467
return new TreeSitterScopeHandler(this.query, scopeType as SimpleScopeType);
6568
}
69+
70+
getTextFragmentScopeHandler(): ScopeHandler | undefined {
71+
if (!this.query.captureNames.includes(TEXT_FRAGMENT_CAPTURE_NAME)) {
72+
return undefined;
73+
}
74+
75+
return new TreeSitterTextFragmentScopeHandler(this.query);
76+
}
6677
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* The name of the capture group that captures text fragments, for use with
3+
* surrounding pairs.
4+
*/
5+
export const TEXT_FRAGMENT_CAPTURE_NAME = "textFragment";

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const fullDocumentTextFragmentExtractor = null;
122122

123123
const textFragmentExtractors: Record<
124124
SupportedLanguageId,
125-
TextFragmentExtractor | FullDocumentTextFragmentExtractor
125+
TextFragmentExtractor | FullDocumentTextFragmentExtractor | null
126126
> = {
127127
c: constructDefaultTextFragmentExtractor("c"),
128128
clojure: constructDefaultTextFragmentExtractor(

packages/cursorless-engine/src/processTargets/ModifierStageFactoryImpl.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Modifier,
55
SurroundingPairModifier,
66
} from "@cursorless/common";
7+
import { LanguageDefinitions } from "../languages/LanguageDefinitions";
78
import { ModifierStageFactory } from "./ModifierStageFactory";
89
import { ModifierStage } from "./PipelineStages.types";
910
import CascadingStage from "./modifiers/CascadingStage";
@@ -42,7 +43,10 @@ import {
4243
} from "./modifiers/scopeTypeStages/RegexStage";
4344

4445
export class ModifierStageFactoryImpl implements ModifierStageFactory {
45-
constructor(private scopeHandlerFactory: ScopeHandlerFactory) {
46+
constructor(
47+
private languageDefinitions: LanguageDefinitions,
48+
private scopeHandlerFactory: ScopeHandlerFactory,
49+
) {
4650
this.create = this.create.bind(this);
4751
}
4852

@@ -115,15 +119,22 @@ export class ModifierStageFactoryImpl implements ModifierStageFactory {
115119
case "nonWhitespaceSequence":
116120
return new NonWhitespaceSequenceStage(modifier);
117121
case "boundedNonWhitespaceSequence":
118-
return new BoundedNonWhitespaceSequenceStage(this, modifier);
122+
return new BoundedNonWhitespaceSequenceStage(
123+
this.languageDefinitions,
124+
this,
125+
modifier,
126+
);
119127
case "url":
120128
return new UrlStage(modifier);
121129
case "collectionItem":
122-
return new ItemStage(modifier);
130+
return new ItemStage(this.languageDefinitions, modifier);
123131
case "customRegex":
124132
return new CustomRegexStage(modifier as CustomRegexModifier);
125133
case "surroundingPair":
126-
return new SurroundingPairStage(modifier as SurroundingPairModifier);
134+
return new SurroundingPairStage(
135+
this.languageDefinitions,
136+
modifier as SurroundingPairModifier,
137+
);
127138
default:
128139
// Default to containing syntax scope using tree sitter
129140
return new ContainingSyntaxScopeStage(

packages/cursorless-engine/src/processTargets/modifiers/ItemStage/ItemStage.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { NoContainingScopeError, Range, TextEditor } from "@cursorless/common";
21
import {
32
ContainingScopeModifier,
43
EveryScopeModifier,
4+
NoContainingScopeError,
5+
Range,
56
SimpleScopeTypeType,
7+
TextEditor,
68
} from "@cursorless/common";
7-
import { Target } from "../../../typings/target.types";
9+
import { LanguageDefinition } from "../../../languages/LanguageDefinition";
10+
import { LanguageDefinitions } from "../../../languages/LanguageDefinitions";
811
import { ProcessedTargetsContext } from "../../../typings/Types";
12+
import { Target } from "../../../typings/target.types";
913
import { getInsertionDelimiter } from "../../../util/nodeSelectors";
1014
import { getRangeLength } from "../../../util/rangeUtils";
1115
import { ModifierStage } from "../../PipelineStages.types";
@@ -17,7 +21,10 @@ import { getIterationScope } from "./getIterationScope";
1721
import { tokenizeRange } from "./tokenizeRange";
1822

1923
export default class ItemStage implements ModifierStage {
20-
constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {}
24+
constructor(
25+
private languageDefinitions: LanguageDefinitions,
26+
private modifier: ContainingScopeModifier | EveryScopeModifier,
27+
) {}
2128

2229
run(context: ProcessedTargetsContext, target: Target): Target[] {
2330
// First try the language specific implementation of item
@@ -29,15 +36,27 @@ export default class ItemStage implements ModifierStage {
2936
// do nothing
3037
}
3138

39+
const languageDefinition = this.languageDefinitions.get(
40+
target.editor.document.languageId,
41+
);
42+
3243
// Then try the textual implementation
3344
if (this.modifier.type === "everyScope") {
34-
return this.getEveryTarget(context, target);
45+
return this.getEveryTarget(languageDefinition, context, target);
3546
}
36-
return [this.getSingleTarget(context, target)];
47+
return [this.getSingleTarget(languageDefinition, context, target)];
3748
}
3849

39-
private getEveryTarget(context: ProcessedTargetsContext, target: Target) {
40-
const itemInfos = getItemInfosForIterationScope(context, target);
50+
private getEveryTarget(
51+
languageDefinition: LanguageDefinition | undefined,
52+
context: ProcessedTargetsContext,
53+
target: Target,
54+
) {
55+
const itemInfos = getItemInfosForIterationScope(
56+
languageDefinition,
57+
context,
58+
target,
59+
);
4160

4261
// If target has explicit range filter to items in that range. Otherwise expand to all items in iteration scope.
4362
const filteredItemInfos = target.hasExplicitRange
@@ -53,8 +72,16 @@ export default class ItemStage implements ModifierStage {
5372
);
5473
}
5574

56-
private getSingleTarget(context: ProcessedTargetsContext, target: Target) {
57-
const itemInfos = getItemInfosForIterationScope(context, target);
75+
private getSingleTarget(
76+
languageDefinition: LanguageDefinition | undefined,
77+
context: ProcessedTargetsContext,
78+
target: Target,
79+
) {
80+
const itemInfos = getItemInfosForIterationScope(
81+
languageDefinition,
82+
context,
83+
target,
84+
);
5885

5986
const filteredItemInfos = filterItemInfos(target, itemInfos);
6087

@@ -117,10 +144,15 @@ function filterItemInfos(target: Target, itemInfos: ItemInfo[]): ItemInfo[] {
117144
}
118145

119146
function getItemInfosForIterationScope(
147+
languageDefinition: LanguageDefinition | undefined,
120148
context: ProcessedTargetsContext,
121149
target: Target,
122150
) {
123-
const { range, boundary } = getIterationScope(context, target);
151+
const { range, boundary } = getIterationScope(
152+
languageDefinition,
153+
context,
154+
target,
155+
);
124156
return getItemsInRange(target.editor, range, boundary);
125157
}
126158

packages/cursorless-engine/src/processTargets/modifiers/ItemStage/getIterationScope.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Range, TextEditor } from "@cursorless/common";
2-
import { Target } from "../../../typings/target.types";
2+
import { LanguageDefinition } from "../../../languages/LanguageDefinition";
33
import { ProcessedTargetsContext } from "../../../typings/Types";
4+
import { Target } from "../../../typings/target.types";
5+
import { PlainTarget, SurroundingPairTarget } from "../../targets";
46
import { fitRangeToLineContent } from "../scopeHandlers";
57
import { processSurroundingPair } from "../surroundingPair";
6-
import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurroundingPairOffsets";
78

89
/**
910
* Get the iteration scope range for item scope.
@@ -13,21 +14,18 @@ import { SurroundingPairInfo } from "../surroundingPair/extractSelectionFromSurr
1314
* @returns The stage iteration scope and optional surrounding pair boundaries
1415
*/
1516
export function getIterationScope(
17+
languageDefinition: LanguageDefinition | undefined,
1618
context: ProcessedTargetsContext,
1719
target: Target,
1820
): { range: Range; boundary?: [Range, Range] } {
19-
let pairInfo = getSurroundingPair(
20-
context,
21-
target.editor,
22-
target.contentRange,
23-
);
21+
let pairInfo = getSurroundingPair(languageDefinition, context, target);
2422

2523
// Iteration is necessary in case of nested strings
2624
while (pairInfo != null) {
2725
const stringPairInfo = getStringSurroundingPair(
26+
languageDefinition,
2827
context,
29-
target.editor,
30-
pairInfo.contentRange,
28+
pairInfo,
3129
);
3230

3331
// We don't look for items inside strings.
@@ -38,12 +36,20 @@ export function getIterationScope(
3836
stringPairInfo.contentRange.start.isBefore(pairInfo.contentRange.start)
3937
) {
4038
return {
41-
range: pairInfo.interiorRange,
42-
boundary: pairInfo.boundary,
39+
range: pairInfo.getInteriorStrict()[0].contentRange,
40+
boundary: pairInfo.getBoundaryStrict().map((t) => t.contentRange) as [
41+
Range,
42+
Range,
43+
],
4344
};
4445
}
4546

46-
pairInfo = getParentSurroundingPair(context, target.editor, pairInfo);
47+
pairInfo = getParentSurroundingPair(
48+
languageDefinition,
49+
context,
50+
target.editor,
51+
pairInfo,
52+
);
4753
}
4854

4955
// We have not found a surrounding pair. Use the line.
@@ -53,38 +59,47 @@ export function getIterationScope(
5359
}
5460

5561
function getParentSurroundingPair(
62+
languageDefinition: LanguageDefinition | undefined,
5663
context: ProcessedTargetsContext,
5764
editor: TextEditor,
58-
pairInfo: SurroundingPairInfo,
65+
target: SurroundingPairTarget,
5966
) {
60-
const startOffset = editor.document.offsetAt(pairInfo.contentRange.start);
67+
const startOffset = editor.document.offsetAt(target.contentRange.start);
6168
// Can't have a parent; already at start of document
6269
if (startOffset === 0) {
6370
return null;
6471
}
6572
// Step out of this pair and see if we have a parent
6673
const position = editor.document.positionAt(startOffset - 1);
67-
return getSurroundingPair(context, editor, new Range(position, position));
74+
return getSurroundingPair(
75+
languageDefinition,
76+
context,
77+
new PlainTarget({
78+
editor,
79+
contentRange: new Range(position, position),
80+
isReversed: false,
81+
}),
82+
);
6883
}
6984

7085
function getSurroundingPair(
86+
languageDefinition: LanguageDefinition | undefined,
7187
context: ProcessedTargetsContext,
72-
editor: TextEditor,
73-
contentRange: Range,
88+
target: Target,
7489
) {
75-
return processSurroundingPair(context, editor, contentRange, {
90+
return processSurroundingPair(languageDefinition, context, target, {
7691
type: "surroundingPair",
7792
delimiter: "collectionBoundary",
7893
requireStrongContainment: true,
7994
});
8095
}
8196

8297
function getStringSurroundingPair(
98+
languageDefinition: LanguageDefinition | undefined,
8399
context: ProcessedTargetsContext,
84-
editor: TextEditor,
85-
contentRange: Range,
100+
target: Target,
86101
) {
87-
return processSurroundingPair(context, editor, contentRange, {
102+
return processSurroundingPair(languageDefinition, context, target, {
88103
type: "surroundingPair",
89104
delimiter: "string",
90105
requireStrongContainment: true,
Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import type { Target } from "../../typings/target.types";
21
import type {
32
ContainingSurroundingPairModifier,
43
SurroundingPairModifier,
54
} from "@cursorless/common";
5+
import { LanguageDefinition } from "../../languages/LanguageDefinition";
6+
import { LanguageDefinitions } from "../../languages/LanguageDefinitions";
67
import type { ProcessedTargetsContext } from "../../typings/Types";
8+
import type { Target } from "../../typings/target.types";
79
import type { ModifierStage } from "../PipelineStages.types";
810
import { SurroundingPairTarget } from "../targets";
911
import { processSurroundingPair } from "./surroundingPair";
@@ -22,7 +24,10 @@ import { processSurroundingPair } from "./surroundingPair";
2224
* `null` if none was found
2325
*/
2426
export default class SurroundingPairStage implements ModifierStage {
25-
constructor(private modifier: SurroundingPairModifier) {}
27+
constructor(
28+
private languageDefinitions: LanguageDefinitions,
29+
private modifier: SurroundingPairModifier,
30+
) {}
2631

2732
run(
2833
context: ProcessedTargetsContext,
@@ -32,31 +37,31 @@ export default class SurroundingPairStage implements ModifierStage {
3237
throw Error(`Unsupported every scope ${this.modifier.scopeType.type}`);
3338
}
3439

35-
return processedSurroundingPairTarget(this.modifier, context, target);
40+
return processedSurroundingPairTarget(
41+
this.languageDefinitions.get(target.editor.document.languageId),
42+
this.modifier,
43+
context,
44+
target,
45+
);
3646
}
3747
}
3848

3949
function processedSurroundingPairTarget(
50+
languageDefinition: LanguageDefinition | undefined,
4051
modifier: ContainingSurroundingPairModifier,
4152
context: ProcessedTargetsContext,
4253
target: Target,
4354
): SurroundingPairTarget[] {
44-
const pairInfo = processSurroundingPair(
55+
const outputTarget = processSurroundingPair(
56+
languageDefinition,
4557
context,
46-
target.editor,
47-
target.contentRange,
58+
target,
4859
modifier.scopeType,
4960
);
5061

51-
if (pairInfo == null) {
62+
if (outputTarget == null) {
5263
throw new Error("Couldn't find containing pair");
5364
}
5465

55-
return [
56-
new SurroundingPairTarget({
57-
...pairInfo,
58-
editor: target.editor,
59-
isReversed: target.isReversed,
60-
}),
61-
];
66+
return [outputTarget];
6267
}

0 commit comments

Comments
 (0)