Skip to content

Commit 7a10719

Browse files
Bugfixes: past token, past end of, subtoken out of range, sort tokens (#229)
* Support take token past * Support take past token * Added more tests * Better error message for sub token index out of range * No delimiters on start and end of positions * Sort multiple match tokens * Sort tokens on length * Sort tokens on length * Sort on alphanumeric as well * Added additional tests * Change directory * Add directory to a message * Changed inference on end * Added tests * find tokens by range * Renamed attributes * Add docstring * Fix multi-editor bug Co-authored-by: Pokey Rule <[email protected]>
1 parent 0ad7888 commit 7a10719

28 files changed

+659
-57
lines changed

src/NavigationMap.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { TextDocumentChangeEvent, Range } from "vscode";
1+
import { TextDocumentChangeEvent, Range, TextDocument } from "vscode";
22
import { SymbolColor } from "./constants";
3-
import { Token } from "./Types";
3+
import { selectionWithEditorFromPositions } from "./selectionUtils";
4+
import { SelectionWithEditor, Token } from "./Types";
45

56
/**
67
* Maps from (color, character) pairs to tokens
@@ -64,9 +65,75 @@ export default class NavigationMap {
6465
this.map = {};
6566
}
6667

67-
public getTokenForRange(range: Range) {
68-
return Object.values(this.map).find(
69-
(token) => token.range.intersection(range) != null
68+
/**
69+
* Given a selection returns a new selection which contains the tokens
70+
* intersecting the given selection. Uses heuristics to tie break when the
71+
* given selection is empty and abuts 2 adjacent tokens
72+
* @param selection Selection to operate on
73+
* @returns Modified selection
74+
*/
75+
public getTokenSelectionForSelection(
76+
selection: SelectionWithEditor
77+
): SelectionWithEditor | null {
78+
const range = selection.selection;
79+
const tokens = range.isEmpty
80+
? this.getTokensForEmptyRange(selection.editor.document, range)
81+
: this.getTokensForRange(selection.editor.document, range);
82+
if (tokens.length < 1) {
83+
return null;
84+
}
85+
const start = tokens[0].range.start;
86+
const end = tokens[tokens.length - 1].range.end;
87+
return selectionWithEditorFromPositions(selection, start, end);
88+
}
89+
90+
// Return tokens for overlapping ranges
91+
private getTokensForRange(document: TextDocument, range: Range) {
92+
const tokens = Object.values(this.map).filter((token) => {
93+
if (token.editor.document !== document) {
94+
return false;
95+
}
96+
const intersection = token.range.intersection(range);
97+
return intersection != null && !intersection.isEmpty;
98+
});
99+
tokens.sort((a, b) => a.startOffset - b.startOffset);
100+
return tokens;
101+
}
102+
103+
// Returned single token for overlapping or adjacent range
104+
private getTokensForEmptyRange(document: TextDocument, range: Range) {
105+
const tokens = Object.values(this.map).filter(
106+
(token) =>
107+
token.editor.document === document &&
108+
token.range.intersection(range) != null
70109
);
110+
111+
// If multiple matches sort and take the first
112+
tokens.sort((a, b) => {
113+
// First sort on alphanumeric
114+
const aIsAlphaNum = isAlphaNum(a.text);
115+
const bIsAlphaNum = isAlphaNum(b.text);
116+
if (aIsAlphaNum && !bIsAlphaNum) {
117+
return -1;
118+
}
119+
if (bIsAlphaNum && !aIsAlphaNum) {
120+
return 1;
121+
}
122+
123+
// Second sort on length
124+
const lengthDiff = b.text.length - a.text.length;
125+
if (lengthDiff !== 0) {
126+
return lengthDiff;
127+
}
128+
129+
// Lastly sort on start position. ie leftmost
130+
return a.startOffset - b.startOffset;
131+
});
132+
133+
return tokens.slice(0, 1);
71134
}
72135
}
136+
137+
function isAlphaNum(text: string) {
138+
return /^\w+$/.test(text);
139+
}

src/Types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export type Delimiter =
7171
| "backtickQuotes";
7272

7373
export type ScopeType =
74+
| "attribute"
7475
| "argumentOrParameter"
7576
| "arrowFunction"
7677
| "class"
@@ -90,7 +91,6 @@ export type ScopeType =
9091
| "string"
9192
| "type"
9293
| "value"
93-
| "xmlAttribute"
9494
| "xmlElement"
9595
| "xmlBothTags"
9696
| "xmlEndTag"

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export async function activate(context: vscode.ExtensionContext) {
9090
} else {
9191
if (await testCaseRecorder.start()) {
9292
vscode.window.showInformationMessage(
93-
"Recording test cases for following commands"
93+
`Recording test cases for following commands in:\n${testCaseRecorder.fixtureSubdirectory}`
9494
);
9595
}
9696
}

src/inferFullTargets.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,10 @@ function inferRangeStartTarget(
332332
prototypeTargets: Target[],
333333
actionPreferences: ActionPreferences
334334
): PrimitiveTarget {
335-
const mark = target.mark ?? CURSOR_MARK;
335+
const mark =
336+
target.mark ??
337+
(target.selectionType === "token" ? CURSOR_MARK_TOKEN : CURSOR_MARK);
338+
336339
prototypeTargets = hasContent(target) ? [] : prototypeTargets;
337340

338341
const selectionType =
@@ -375,13 +378,17 @@ export function inferRangeEndTarget(
375378
? []
376379
: possiblePrototypeTargetsIncludingStartTarget;
377380

381+
const startMark = extractAttributeFromList(
382+
possiblePrototypeTargetsIncludingStartTarget,
383+
"mark"
384+
);
385+
378386
const mark =
379387
target.mark ??
380-
extractAttributeFromList(
381-
possiblePrototypeTargetsIncludingStartTarget,
382-
"mark"
383-
) ??
384-
CURSOR_MARK;
388+
(startMark != null && startMark.type !== CURSOR_MARK.type
389+
? startMark
390+
: null) ??
391+
(target.selectionType === "token" ? CURSOR_MARK_TOKEN : CURSOR_MARK);
385392

386393
const selectionType =
387394
target.selectionType ??

src/languages/cpp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
8484
value: valueMatcher("*[declarator][value]", "*[value]", "assignment_expression[right]", "optional_parameter_declaration[default_value]"),
8585
collectionItem: argumentMatcher("initializer_list"),
8686
argumentOrParameter: argumentMatcher("parameter_list", "argument_list"),
87-
xmlAttribute: "attribute"
87+
attribute: "attribute"
8888
};
8989

9090
export default createPatternMatchers(nodeMatchers);

src/languages/typescript.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
154154
),
155155
argumentOrParameter: argumentMatcher("formal_parameters", "arguments"),
156156
// XML, JSX
157-
xmlAttribute: ["jsx_attribute"],
157+
attribute: ["jsx_attribute"],
158158
xmlElement: ["jsx_element", "jsx_self_closing_element"],
159159
xmlBothTags: getTags,
160160
xmlStartTag: getStartTag,

src/processTargets.ts

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
SelectionWithEditor,
2525
Target,
2626
TypedSelection,
27+
Position as TargetPosition,
28+
InsideOutsideType,
2729
} from "./Types";
2830

2931
export default function processTargets(
@@ -199,19 +201,15 @@ function getSelectionsFromMark(
199201
return context.sourceMark;
200202

201203
case "cursorToken": {
202-
const tokens = context.currentSelections.map((selection) => {
203-
const token = context.navigationMap.getTokenForRange(
204-
selection.selection
205-
);
206-
if (token == null) {
207-
throw new Error("Couldn't find mark under cursor");
204+
const tokenSelections = context.currentSelections.map((selection) => {
205+
const tokenSelection =
206+
context.navigationMap.getTokenSelectionForSelection(selection);
207+
if (tokenSelection == null) {
208+
throw new Error("Couldn't find token in selection");
208209
}
209-
return token;
210+
return tokenSelection;
210211
});
211-
return tokens.map((token) => ({
212-
selection: new Selection(token.range.start, token.range.end),
213-
editor: token.editor,
214-
}));
212+
return tokenSelections;
215213
}
216214

217215
case "decoratedSymbol":
@@ -334,6 +332,15 @@ function transformSelection(
334332
const activeIndex =
335333
modifier.active < 0 ? modifier.active + pieces.length : modifier.active;
336334

335+
if (
336+
anchorIndex < 0 ||
337+
activeIndex < 0 ||
338+
anchorIndex >= pieces.length ||
339+
activeIndex >= pieces.length
340+
) {
341+
throw new Error("Subtoken index out of range");
342+
}
343+
337344
const isReversed = activeIndex < anchorIndex;
338345

339346
const anchor = selection.selection.start.translate(
@@ -461,6 +468,8 @@ function createTypedSelection(
461468
selectionContext: getTokenSelectionContext(
462469
selection,
463470
modifier,
471+
position,
472+
insideOutsideType,
464473
selectionContext
465474
),
466475
};
@@ -600,6 +609,8 @@ function performPositionAdjustment(
600609
function getTokenSelectionContext(
601610
selection: SelectionWithEditor,
602611
modifier: Modifier,
612+
position: TargetPosition,
613+
insideOutsideType: InsideOutsideType,
603614
selectionContext: SelectionContext
604615
): SelectionContext {
605616
if (!isSelectionContextEmpty(selectionContext)) {
@@ -611,32 +622,38 @@ function getTokenSelectionContext(
611622

612623
const document = selection.editor.document;
613624
const { start, end } = selection.selection;
614-
615-
const startLine = document.lineAt(start);
616-
const leadingText = startLine.text.slice(0, start.character);
617-
const leadingDelimiters = leadingText.match(/\s+$/);
618-
const leadingDelimiterRange =
619-
leadingDelimiters != null
620-
? new Range(
621-
start.line,
622-
start.character - leadingDelimiters[0].length,
623-
start.line,
624-
start.character
625-
)
626-
: null;
627-
628625
const endLine = document.lineAt(end);
629-
const trailingText = endLine.text.slice(end.character);
630-
const trailingDelimiters = trailingText.match(/^\s+/);
631-
const trailingDelimiterRange =
632-
trailingDelimiters != null
633-
? new Range(
634-
end.line,
635-
end.character,
636-
end.line,
637-
end.character + trailingDelimiters[0].length
638-
)
639-
: null;
626+
let leadingDelimiterRange, trailingDelimiterRange;
627+
628+
// Position start/end of has no delimiter
629+
if (position !== "before" || insideOutsideType !== "inside") {
630+
const startLine = document.lineAt(start);
631+
const leadingText = startLine.text.slice(0, start.character);
632+
const leadingDelimiters = leadingText.match(/\s+$/);
633+
leadingDelimiterRange =
634+
leadingDelimiters != null
635+
? new Range(
636+
start.line,
637+
start.character - leadingDelimiters[0].length,
638+
start.line,
639+
start.character
640+
)
641+
: null;
642+
}
643+
644+
if (position !== "after" || insideOutsideType !== "inside") {
645+
const trailingText = endLine.text.slice(end.character);
646+
const trailingDelimiters = trailingText.match(/^\s+/);
647+
trailingDelimiterRange =
648+
trailingDelimiters != null
649+
? new Range(
650+
end.line,
651+
end.character,
652+
end.line,
653+
end.character + trailingDelimiters[0].length
654+
)
655+
: null;
656+
}
640657

641658
const isInDelimitedList =
642659
(leadingDelimiterRange != null || trailingDelimiterRange != null) &&
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
spokenForm: take past end of token
2+
languageId: typescript
3+
command:
4+
actionName: setSelection
5+
partialTargets:
6+
- type: range
7+
start: {type: primitive}
8+
end: {type: primitive, position: after, insideOutsideType: inside, selectionType: token}
9+
excludeStart: false
10+
excludeEnd: false
11+
extraArgs: []
12+
marks: {}
13+
initialState:
14+
documentContents: hello there
15+
selections:
16+
- anchor: {line: 0, character: 8}
17+
active: {line: 0, character: 8}
18+
finalState:
19+
documentContents: hello there
20+
selections:
21+
- anchor: {line: 0, character: 8}
22+
active: {line: 0, character: 11}
23+
thatMark:
24+
- anchor: {line: 0, character: 8}
25+
active: {line: 0, character: 11}
26+
fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: cursorToken}, selectionType: token, position: after, modifier: {type: identity}, insideOutsideType: inside}}]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
spokenForm: take past start of token
2+
languageId: typescript
3+
command:
4+
actionName: setSelection
5+
partialTargets:
6+
- type: range
7+
start: {type: primitive}
8+
end: {type: primitive, position: before, insideOutsideType: inside, selectionType: token}
9+
excludeStart: false
10+
excludeEnd: false
11+
extraArgs: []
12+
marks: {}
13+
initialState:
14+
documentContents: hello there
15+
selections:
16+
- anchor: {line: 0, character: 8}
17+
active: {line: 0, character: 8}
18+
finalState:
19+
documentContents: hello there
20+
selections:
21+
- anchor: {line: 0, character: 8}
22+
active: {line: 0, character: 6}
23+
thatMark:
24+
- anchor: {line: 0, character: 8}
25+
active: {line: 0, character: 6}
26+
fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: cursorToken}, selectionType: token, position: before, modifier: {type: identity}, insideOutsideType: inside}}]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
spokenForm: take past trap
2+
languageId: typescript
3+
command:
4+
actionName: setSelection
5+
partialTargets:
6+
- type: range
7+
start: {type: primitive}
8+
end:
9+
type: primitive
10+
mark: {type: decoratedSymbol, symbolColor: default, character: t}
11+
excludeStart: false
12+
excludeEnd: false
13+
extraArgs: []
14+
marks:
15+
default.t:
16+
start: {line: 0, character: 6}
17+
end: {line: 0, character: 11}
18+
initialState:
19+
documentContents: hello there
20+
selections:
21+
- anchor: {line: 0, character: 3}
22+
active: {line: 0, character: 3}
23+
finalState:
24+
documentContents: hello there
25+
selections:
26+
- anchor: {line: 0, character: 3}
27+
active: {line: 0, character: 11}
28+
thatMark:
29+
- anchor: {line: 0, character: 3}
30+
active: {line: 0, character: 11}
31+
fullTargets: [{type: range, excludeStart: false, excludeEnd: false, start: {type: primitive, mark: {type: cursor}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}, end: {type: primitive, mark: {type: decoratedSymbol, symbolColor: default, character: t}, selectionType: token, position: contents, modifier: {type: identity}, insideOutsideType: inside}}]

0 commit comments

Comments
 (0)