Skip to content

Commit 2be775a

Browse files
committed
Added text based item scope
1 parent 2ee7ca1 commit 2be775a

File tree

6 files changed

+510
-0
lines changed

6 files changed

+510
-0
lines changed

src/processTargets/getModifierStage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ContainingSyntaxScopeStage, {
1919
SimpleContainingScopeModifier,
2020
} from "./modifiers/scopeTypeStages/ContainingSyntaxScopeStage";
2121
import DocumentStage from "./modifiers/scopeTypeStages/DocumentStage";
22+
import ItemStage from "./modifiers/scopeTypeStages/ItemStage";
2223
import LineStage from "./modifiers/scopeTypeStages/LineStage";
2324
import NotebookCellStage from "./modifiers/scopeTypeStages/NotebookCellStage";
2425
import ParagraphStage from "./modifiers/scopeTypeStages/ParagraphStage";
@@ -85,6 +86,8 @@ const getContainingScopeStage = (
8586
);
8687
case "url":
8788
return new UrlStage(modifier as UrlModifier);
89+
case "collectionItem":
90+
return new ItemStage(modifier);
8891
case "surroundingPair":
8992
return new SurroundingPairStage(
9093
modifier as ContainingSurroundingPairModifier
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { Range, TextEditor } from "vscode";
2+
import {
3+
ContainingScopeModifier,
4+
EveryScopeModifier,
5+
SimpleScopeTypeType,
6+
Target,
7+
} from "../../../typings/target.types";
8+
import { ProcessedTargetsContext } from "../../../typings/Types";
9+
import { ModifierStage } from "../../PipelineStages.types";
10+
import ScopeTypeTarget from "../../targets/ScopeTypeTarget";
11+
import { processSurroundingPair } from "../surroundingPair";
12+
import { fitRangeToLineContent } from "./LineStage";
13+
14+
export default class ItemStage implements ModifierStage {
15+
constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {}
16+
17+
run(context: ProcessedTargetsContext, target: Target): Target[] {
18+
if (this.modifier.type === "everyScope") {
19+
return this.getEveryTarget(context, target);
20+
}
21+
return [this.getSingleTarget(context, target)];
22+
}
23+
24+
private getEveryTarget(context: ProcessedTargetsContext, target: Target) {
25+
const itemInfos = getItemInfos(context, target);
26+
27+
if (itemInfos.length < 1) {
28+
throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`);
29+
}
30+
31+
return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo));
32+
}
33+
34+
private getSingleTarget(context: ProcessedTargetsContext, target: Target) {
35+
const itemInfos = getItemInfos(context, target);
36+
37+
const itemInfo =
38+
itemInfos.find((itemInfo) =>
39+
itemInfo.range.intersection(target.contentRange)
40+
) ??
41+
itemInfos.find((itemInfo) =>
42+
itemInfo.delimiterRange?.intersection(target.contentRange)
43+
);
44+
45+
if (itemInfo == null) {
46+
throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`);
47+
}
48+
49+
return this.itemInfoToTarget(target, itemInfo);
50+
}
51+
52+
private itemInfoToTarget(target: Target, itemInfo: ItemInfo) {
53+
return new ScopeTypeTarget({
54+
scopeTypeType: <SimpleScopeTypeType>this.modifier.scopeType.type,
55+
editor: target.editor,
56+
isReversed: target.isReversed,
57+
contentRange: itemInfo.range,
58+
delimiter,
59+
leadingDelimiterRange: itemInfo.leadingDelimiterRange,
60+
trailingDelimiterRange: itemInfo.trailingDelimiterRange,
61+
});
62+
}
63+
}
64+
65+
function getItemInfos(context: ProcessedTargetsContext, target: Target) {
66+
const collectionRange = getCollectionRange(context, target);
67+
return tokensToItemInfos(target.editor, collectionRange);
68+
}
69+
70+
function getCollectionRange(context: ProcessedTargetsContext, target: Target) {
71+
let pairInfo = getSurroundingPair(
72+
context,
73+
target.editor,
74+
target.contentRange
75+
);
76+
77+
while (pairInfo != null) {
78+
// The selection from the beginning was this pair and we should not go into the interior but instead look in the parent.
79+
const isNotInterior =
80+
target.contentRange.isEqual(pairInfo.contentRange) ||
81+
target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) ||
82+
target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end);
83+
if (!isNotInterior) {
84+
return pairInfo.interiorRange;
85+
}
86+
// Step out of this pair and see if we have a parent
87+
const position = target.editor.document.positionAt(
88+
target.editor.document.offsetAt(pairInfo.contentRange.start) - 1
89+
);
90+
pairInfo = getSurroundingPair(
91+
context,
92+
target.editor,
93+
new Range(position, position)
94+
);
95+
}
96+
97+
// We have not found a pair containing the delimiter. Look at the full line.
98+
return fitRangeToLineContent(target.editor, target.contentRange);
99+
}
100+
101+
function tokensToItemInfos(
102+
editor: TextEditor,
103+
collectionRange: Range
104+
): ItemInfo[] {
105+
const tokens = tokenizeRange(editor, collectionRange);
106+
const itemInfos: ItemInfo[] = [];
107+
108+
tokens.forEach((token, i) => {
109+
if (token.type === "delimiter") {
110+
return;
111+
}
112+
const leadingDelimiterRange = (() => {
113+
if (tokens[i - 2]?.type === "item") {
114+
return new Range(tokens[i - 2].range.end, token.range.start);
115+
}
116+
if (tokens[i - 1]?.type === "delimiter") {
117+
return new Range(tokens[i - 1].range.start, token.range.start);
118+
}
119+
return undefined;
120+
})();
121+
const trailingDelimiterRange = (() => {
122+
if (tokens[i + 2]?.type === "item") {
123+
return new Range(token.range.end, tokens[i + 2].range.start);
124+
}
125+
if (tokens[i + 1]?.type === "delimiter") {
126+
return new Range(token.range.end, tokens[i + 1].range.end);
127+
}
128+
return undefined;
129+
})();
130+
const delimiterRange =
131+
tokens[i + 1]?.type === "delimiter" ? tokens[i + 1].range : undefined;
132+
itemInfos.push({
133+
range: token.range,
134+
leadingDelimiterRange,
135+
trailingDelimiterRange,
136+
delimiterRange,
137+
});
138+
});
139+
140+
return itemInfos;
141+
}
142+
143+
function tokenizeRange(editor: TextEditor, collectionRange: Range) {
144+
const { document } = editor;
145+
const text = document.getText(collectionRange);
146+
const parts = text.split(/([,(){}<>[\]"'])/g).filter(Boolean);
147+
const tokens: Token[] = [];
148+
let offset = document.offsetAt(collectionRange.start);
149+
let waitingForDelimiter: string | null = null;
150+
let offsetStart = 0;
151+
152+
parts.forEach((text) => {
153+
// Whitespace found. Just skip
154+
if (text.trim().length === 0) {
155+
offset += text.length;
156+
return;
157+
}
158+
159+
// We are waiting for a closing delimiter
160+
if (waitingForDelimiter != null) {
161+
// Closing delimiter found
162+
if (waitingForDelimiter === text) {
163+
waitingForDelimiter = null;
164+
tokens.push({
165+
type: "item",
166+
range: new Range(
167+
document.positionAt(offsetStart),
168+
document.positionAt(offset + text.length)
169+
),
170+
});
171+
}
172+
}
173+
// Separator delimiter found.
174+
else if (text === delimiter) {
175+
tokens.push({
176+
type: "delimiter",
177+
range: new Range(
178+
document.positionAt(offset),
179+
document.positionAt(offset + text.length)
180+
),
181+
});
182+
}
183+
// Starting delimiter found
184+
else if (delimiters[text] != null) {
185+
waitingForDelimiter = delimiters[text];
186+
offsetStart = offset;
187+
}
188+
// Text/item content found
189+
else {
190+
const offsetStart = offset + (text.length - text.trimStart().length);
191+
tokens.push({
192+
type: "item",
193+
range: new Range(
194+
document.positionAt(offsetStart),
195+
document.positionAt(offsetStart + text.trim().length)
196+
),
197+
});
198+
}
199+
200+
offset += text.length;
201+
});
202+
203+
return tokens;
204+
}
205+
206+
function getSurroundingPair(
207+
context: ProcessedTargetsContext,
208+
editor: TextEditor,
209+
contentRange: Range
210+
) {
211+
return processSurroundingPair(context, editor, contentRange, {
212+
type: "surroundingPair",
213+
delimiter: "any",
214+
});
215+
}
216+
217+
interface ItemInfo {
218+
range: Range;
219+
leadingDelimiterRange?: Range;
220+
trailingDelimiterRange?: Range;
221+
delimiterRange?: Range;
222+
}
223+
224+
interface Token {
225+
range: Range;
226+
type: string;
227+
}
228+
229+
const delimiter = ",";
230+
231+
// Mapping between opening and closing delimiters
232+
const delimiters: { [key: string]: string } = {
233+
"(": ")",
234+
"{": "}",
235+
"<": ">",
236+
"[": "]",
237+
'"': '"',
238+
"'": "'",
239+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
languageId: plaintext
2+
command:
3+
spokenForm: take every item
4+
version: 2
5+
targets:
6+
- type: primitive
7+
modifiers:
8+
- type: everyScope
9+
scopeType: {type: item}
10+
usePrePhraseSnapshot: true
11+
action: {name: setSelection}
12+
initialState:
13+
documentContents: |-
14+
[
15+
a b, (c, d),
16+
e f, {g, h},
17+
i j, [k, l],
18+
m n, <o, p>,
19+
q r, "s, t",
20+
u v, "foo(bar)baz",
21+
]
22+
selections:
23+
- anchor: {line: 1, character: 10}
24+
active: {line: 1, character: 10}
25+
- anchor: {line: 2, character: 10}
26+
active: {line: 2, character: 10}
27+
- anchor: {line: 3, character: 10}
28+
active: {line: 3, character: 10}
29+
- anchor: {line: 4, character: 10}
30+
active: {line: 4, character: 10}
31+
- anchor: {line: 5, character: 10}
32+
active: {line: 5, character: 10}
33+
marks: {}
34+
finalState:
35+
documentContents: |-
36+
[
37+
a b, (c, d),
38+
e f, {g, h},
39+
i j, [k, l],
40+
m n, <o, p>,
41+
q r, "s, t",
42+
u v, "foo(bar)baz",
43+
]
44+
selections:
45+
- anchor: {line: 1, character: 10}
46+
active: {line: 1, character: 11}
47+
- anchor: {line: 1, character: 13}
48+
active: {line: 1, character: 14}
49+
- anchor: {line: 2, character: 10}
50+
active: {line: 2, character: 11}
51+
- anchor: {line: 2, character: 13}
52+
active: {line: 2, character: 14}
53+
- anchor: {line: 3, character: 10}
54+
active: {line: 3, character: 11}
55+
- anchor: {line: 3, character: 13}
56+
active: {line: 3, character: 14}
57+
- anchor: {line: 4, character: 10}
58+
active: {line: 4, character: 11}
59+
- anchor: {line: 4, character: 13}
60+
active: {line: 4, character: 14}
61+
- anchor: {line: 5, character: 10}
62+
active: {line: 5, character: 11}
63+
- anchor: {line: 5, character: 13}
64+
active: {line: 5, character: 14}
65+
thatMark:
66+
- anchor: {line: 1, character: 10}
67+
active: {line: 1, character: 11}
68+
- anchor: {line: 1, character: 13}
69+
active: {line: 1, character: 14}
70+
- anchor: {line: 2, character: 10}
71+
active: {line: 2, character: 11}
72+
- anchor: {line: 2, character: 13}
73+
active: {line: 2, character: 14}
74+
- anchor: {line: 3, character: 10}
75+
active: {line: 3, character: 11}
76+
- anchor: {line: 3, character: 13}
77+
active: {line: 3, character: 14}
78+
- anchor: {line: 4, character: 10}
79+
active: {line: 4, character: 11}
80+
- anchor: {line: 4, character: 13}
81+
active: {line: 4, character: 14}
82+
- anchor: {line: 5, character: 10}
83+
active: {line: 5, character: 11}
84+
- anchor: {line: 5, character: 13}
85+
active: {line: 5, character: 14}
86+
fullTargets: [{type: primitive, mark: {type: cursor}, modifiers: [{type: everyScope, scopeType: {type: item}}]}]

0 commit comments

Comments
 (0)