Skip to content

Commit bc83546

Browse files
committed
Add surrounding pair modifier (#3)
1 parent 7b3db14 commit bc83546

27 files changed

+724
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
node_modules
44
.vscode-test/
55
*.vsix
6+
package-lock.json

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,4 @@
221221
"immutability-helper": "^3.1.1",
222222
"lodash": "^4.17.21"
223223
}
224-
}
224+
}

src/Types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export type Delimiter =
6868
| "parentheses"
6969
| "singleQuotes"
7070
| "doubleQuotes";
71+
7172
export type ScopeType =
7273
| "argumentOrParameter"
7374
| "arrowFunction"
@@ -95,9 +96,11 @@ export type ScopeType =
9596
| "xmlStartTag";
9697
export type PieceType = "word" | "character";
9798

99+
export type SurroundingPairModifierSubtype = "matchingSubtype" | "boundSubtype";
98100
export interface SurroundingPairModifier {
99101
type: "surroundingPair";
100-
delimiter: Delimiter;
102+
subtype: SurroundingPairModifierSubtype;
103+
delimiter: Delimiter | null;
101104
}
102105
export interface ContainingScopeModifier {
103106
type: "containingScope";

src/extension.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export async function activate(context: vscode.ExtensionContext) {
125125
console.debug(JSON.stringify(partialTargets, null, 3));
126126
console.debug(`extraArgs:`);
127127
console.debug(JSON.stringify(extraArgs, null, 3));
128-
129128
const action = graph.actions[actionName];
130129

131130
const selectionContents =

src/languages/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1+
import { SyntaxNode } from "web-tree-sitter";
2+
import { notSupported } from "../nodeMatchers";
3+
import { selectionWithEditorFromRange } from "../selectionUtils";
14
import {
25
NodeMatcher,
36
NodeMatcherValue,
47
ScopeType,
58
SelectionWithEditor,
69
} from "../Types";
10+
import csharp from "./csharp";
11+
import java from "./java";
712
import json from "./json";
813
import python from "./python";
914
import typescript from "./typescript";
10-
import csharp from "./csharp";
11-
import java from "./java";
12-
import { notSupported } from "../nodeMatchers";
13-
import { SyntaxNode } from "web-tree-sitter";
14-
import { selectionWithEditorFromRange } from "../selectionUtils";
1515

1616
const languageMatchers: Record<string, Record<ScopeType, NodeMatcher>> = {
1717
csharp: csharp,

src/languages/surroundingPair.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { Position, TextEditor, Selection } from "vscode";
2+
import { Point, SyntaxNode } from "web-tree-sitter";
3+
import {
4+
Delimiter,
5+
NodeMatcher,
6+
NodeMatcherValue,
7+
SelectionWithContext,
8+
SelectionWithEditor,
9+
SurroundingPairModifierSubtype,
10+
} from "../Types";
11+
12+
function positionFromPoint(point: Point): Position {
13+
return new Position(point.row, point.column);
14+
}
15+
16+
const delimiterToText: Record<Delimiter, String[]> = {
17+
squareBrackets: ["[", "]"],
18+
curlyBrackets: ["{", "}"],
19+
angleBrackets: ["<", ">"],
20+
parentheses: ["(", ")"],
21+
singleQuotes: ["'", "'"],
22+
doubleQuotes: ['"', '"'],
23+
};
24+
function isSyntaxNodeLeftPartOfMatching(
25+
node: SyntaxNode,
26+
delimiter: Delimiter
27+
): boolean {
28+
return node.type === delimiterToText[delimiter][0];
29+
}
30+
function isSyntaxNodeRightPartOfMatching(
31+
node: SyntaxNode,
32+
delimiter: Delimiter
33+
): boolean {
34+
return node.type === delimiterToText[delimiter][1];
35+
}
36+
37+
export function createSurroundingPairMatcher(
38+
delimiter: Delimiter | null,
39+
matchingSubtype: SurroundingPairModifierSubtype
40+
): NodeMatcher {
41+
return function nodeMatcher(
42+
selection: SelectionWithEditor,
43+
node: SyntaxNode
44+
) {
45+
let delimetersToCheck: Delimiter[];
46+
if (delimiter != null) {
47+
delimetersToCheck = [delimiter];
48+
} else {
49+
delimetersToCheck = [
50+
"squareBrackets",
51+
"curlyBrackets",
52+
"angleBrackets",
53+
"parentheses",
54+
"singleQuotes",
55+
"doubleQuotes",
56+
];
57+
}
58+
59+
// This is a special case.
60+
let nodeLeftOfSelection: SyntaxNode | null = null;
61+
let nodeRightOfSelection: SyntaxNode | null = null;
62+
for (const child of node.children) {
63+
// We iterate from the so we take the **last** node that is good
64+
if (
65+
positionFromPoint(child.endPosition).isBeforeOrEqual(
66+
selection.selection.start
67+
)
68+
) {
69+
nodeLeftOfSelection = child;
70+
}
71+
// We iterate from the so we take the **first** node that is good
72+
if (
73+
nodeRightOfSelection == null &&
74+
selection.selection.start.isBeforeOrEqual(
75+
positionFromPoint(child.startPosition)
76+
)
77+
) {
78+
nodeRightOfSelection = child;
79+
}
80+
}
81+
if (nodeLeftOfSelection != null && nodeRightOfSelection != null) {
82+
let result = doOutwardScan(
83+
nodeLeftOfSelection,
84+
nodeRightOfSelection,
85+
delimetersToCheck,
86+
matchingSubtype
87+
);
88+
if (result != null) {
89+
return result;
90+
}
91+
}
92+
93+
if (node.parent == null) {
94+
return null;
95+
}
96+
// We don't take the next sibling here, because if current node is a
97+
// closing element of the pair we want to take it.
98+
return doOutwardScan(
99+
node.previousSibling,
100+
node,
101+
delimetersToCheck,
102+
matchingSubtype
103+
);
104+
};
105+
}
106+
107+
function doOutwardScan(
108+
scanLeftStartNode: SyntaxNode | null,
109+
scanRightStartNode: SyntaxNode | null,
110+
delimetersToCheck: Delimiter[],
111+
matchingSubtype: SurroundingPairModifierSubtype
112+
): NodeMatcherValue[] | null {
113+
for (const delimiter of delimetersToCheck) {
114+
let left = scanLeftStartNode;
115+
while (left != null) {
116+
if (isSyntaxNodeLeftPartOfMatching(left, delimiter)) {
117+
break;
118+
}
119+
left = left.previousSibling;
120+
}
121+
let right = scanRightStartNode;
122+
while (right != null) {
123+
if (isSyntaxNodeRightPartOfMatching(right, delimiter)) {
124+
break;
125+
}
126+
right = right.nextSibling;
127+
}
128+
if (left != null && right != null) {
129+
// We have found the matching pair
130+
if (matchingSubtype === "matchingSubtype") {
131+
return [
132+
{
133+
node: left,
134+
selection: {
135+
selection: new Selection(
136+
positionFromPoint(left.endPosition),
137+
positionFromPoint(right.startPosition)
138+
),
139+
context: {
140+
outerSelection: new Selection(
141+
positionFromPoint(left.startPosition),
142+
positionFromPoint(right.endPosition)
143+
),
144+
},
145+
},
146+
},
147+
];
148+
} else {
149+
throw new Error("Not implemented");
150+
}
151+
}
152+
}
153+
return null;
154+
}

src/processTargets.ts

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1-
import { concat, range, zip } from "lodash";
21
import update from "immutability-helper";
2+
import { concat, range, zip } from "lodash";
3+
import { start } from "repl";
4+
import * as vscode from "vscode";
5+
import { Location, Position, Range, Selection, TextDocument } from "vscode";
36
import { SyntaxNode } from "web-tree-sitter";
7+
import { SUBWORD_MATCHER } from "./constants";
48
import { getNodeMatcher } from "./languages";
5-
import { Selection, Range, Position, Location, TextDocument } from "vscode";
9+
import { createSurroundingPairMatcher } from "./languages/surroundingPair";
10+
import { performInsideOutsideAdjustment } from "./performInsideOutsideAdjustment";
11+
import {
12+
selectionWithEditorFromPositions,
13+
selectionWithEditorFromRange,
14+
} from "./selectionUtils";
615
import {
16+
LineNumberPosition,
717
Mark,
18+
Modifier,
19+
NodeMatcher,
820
PrimitiveTarget,
921
ProcessedTargetsContext,
1022
RangeTarget,
1123
SelectionContext,
1224
SelectionWithEditor,
1325
Target,
1426
TypedSelection,
15-
Modifier,
16-
LineNumberPosition,
1727
} from "./Types";
18-
import { performInsideOutsideAdjustment } from "./performInsideOutsideAdjustment";
19-
import { SUBWORD_MATCHER } from "./constants";
20-
import {
21-
selectionWithEditorFromPositions,
22-
selectionWithEditorFromRange,
23-
} from "./selectionUtils";
2428

2529
export default function processTargets(
2630
context: ProcessedTargetsContext,
@@ -250,6 +254,31 @@ function getSelectionsFromMark(
250254
}
251255
}
252256

257+
function findFirstMatchingNode(
258+
startNode: SyntaxNode,
259+
nodeMatcher: NodeMatcher,
260+
selection: SelectionWithEditor
261+
) {
262+
let node: SyntaxNode | null = startNode;
263+
while (node != null) {
264+
const matches = nodeMatcher(selection, node);
265+
if (matches != null) {
266+
return matches
267+
.map((match) => match.selection)
268+
.map((matchedSelection) => ({
269+
selection: selectionWithEditorFromRange(
270+
selection,
271+
matchedSelection.selection
272+
),
273+
context: matchedSelection.context,
274+
}));
275+
}
276+
node = node.parent;
277+
}
278+
279+
return null;
280+
}
281+
253282
function transformSelection(
254283
context: ProcessedTargetsContext,
255284
target: PrimitiveTarget,
@@ -271,21 +300,10 @@ function transformSelection(
271300
modifier.scopeType,
272301
modifier.includeSiblings ?? false
273302
);
303+
let result = findFirstMatchingNode(node, nodeMatcher, selection);
274304

275-
while (node != null) {
276-
const matches = nodeMatcher(selection, node);
277-
if (matches != null) {
278-
return matches
279-
.map((match) => match.selection)
280-
.map((matchedSelection) => ({
281-
selection: selectionWithEditorFromRange(
282-
selection,
283-
matchedSelection.selection
284-
),
285-
context: matchedSelection.context,
286-
}));
287-
}
288-
node = node.parent;
305+
if (result != null) {
306+
return result;
289307
}
290308

291309
throw new Error(`Couldn't find containing ${modifier.scopeType}`);
@@ -391,8 +409,31 @@ function transformSelection(
391409
}
392410

393411
case "matchingPairSymbol":
394-
case "surroundingPair":
395412
throw new Error("Not implemented");
413+
414+
case "surroundingPair":
415+
{
416+
if (modifier.subtype === "boundSubtype") {
417+
throw new Error("Not implemented");
418+
}
419+
let node: SyntaxNode | null = context.getNodeAtLocation(
420+
new vscode.Location(
421+
selection.editor.document.uri,
422+
selection.selection
423+
)
424+
);
425+
426+
const nodeMatcher = createSurroundingPairMatcher(
427+
modifier.delimiter,
428+
modifier.subtype
429+
);
430+
let result = findFirstMatchingNode(node, nodeMatcher, selection);
431+
if (result != null) {
432+
return result;
433+
}
434+
}
435+
436+
throw new Error(`Couldn't find containing `);
396437
}
397438
}
398439

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
spokenForm: clear match
2+
languageId: python
3+
command:
4+
actionName: clear
5+
partialTargets:
6+
- type: primitive
7+
modifier: {type: surroundingPair, subtype: matchingSubtype, delimiter: null}
8+
extraArgs: []
9+
marks: {}
10+
initialState:
11+
documentContents: |
12+
"fdsfads"
13+
selections:
14+
- anchor: {line: 0, character: 5}
15+
active: {line: 0, character: 5}
16+
finalState:
17+
documentContents: |
18+
""
19+
selections:
20+
- anchor: {line: 0, character: 1}
21+
active: {line: 0, character: 1}
22+
thatMark:
23+
- anchor: {line: 0, character: 1}
24+
active: {line: 0, character: 1}
25+
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: surroundingPair, subtype: matchingSubtype, delimiter: null}, insideOutsideType: inside}]

0 commit comments

Comments
 (0)