Skip to content

Commit 82822fc

Browse files
authored
Use a binary search when looking for the token at a given position (#46250)
1 parent bbd9ff5 commit 82822fc

File tree

1 file changed

+70
-13
lines changed

1 file changed

+70
-13
lines changed

src/services/utilities.ts

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,30 +1124,87 @@ namespace ts {
11241124
/** Get the token whose text contains the position */
11251125
function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: ((n: Node) => boolean) | undefined, includeEndPosition: boolean): Node {
11261126
let current: Node = sourceFile;
1127+
let foundToken: Node | undefined;
11271128
outer: while (true) {
11281129
// find the child that contains 'position'
1129-
for (const child of current.getChildren(sourceFile)) {
1130-
const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true);
1130+
1131+
const children = current.getChildren(sourceFile);
1132+
const i = binarySearchKey(children, position, (_, i) => i, (middle, _) => {
1133+
// This last callback is more of a selector than a comparator -
1134+
// `EqualTo` causes the `middle` result to be returned
1135+
// `GreaterThan` causes recursion on the left of the middle
1136+
// `LessThan` causes recursion on the right of the middle
1137+
1138+
// Let's say you have 3 nodes, spanning positons
1139+
// pos: 1, end: 3
1140+
// pos: 3, end: 3
1141+
// pos: 3, end: 5
1142+
// and you're looking for the token at positon 3 - all 3 of these nodes are overlapping with position 3.
1143+
// In fact, there's a _good argument_ that node 2 shouldn't even be allowed to exist - depending on if
1144+
// the start or end of the ranges are considered inclusive, it's either wholly subsumed by the first or the last node.
1145+
// Unfortunately, such nodes do exist. :( - See fourslash/completionsImport_tsx.tsx - empty jsx attributes create
1146+
// a zero-length node.
1147+
// What also you may not expect is that which node we return depends on the includePrecedingTokenAtEndPosition flag.
1148+
// Specifically, if includePrecedingTokenAtEndPosition is set, we return the 1-3 node, while if it's unset, we
1149+
// return the 3-5 node. (The zero length node is never correct.) This is because the includePrecedingTokenAtEndPosition
1150+
// flag causes us to return the first node whose end position matches the position and which produces and acceptable token
1151+
// kind. Meanwhile, if includePrecedingTokenAtEndPosition is unset, we look for the first node whose start is <= the
1152+
// position and whose end is greater than the position.
1153+
1154+
1155+
const start = allowPositionInLeadingTrivia ? children[middle].getFullStart() : children[middle].getStart(sourceFile, /*includeJsDoc*/ true);
11311156
if (start > position) {
1132-
// If this child begins after position, then all subsequent children will as well.
1133-
break;
1157+
return Comparison.GreaterThan;
11341158
}
11351159

1136-
const end = child.getEnd();
1137-
if (position < end || (position === end && (child.kind === SyntaxKind.EndOfFileToken || includeEndPosition))) {
1138-
current = child;
1139-
continue outer;
1140-
}
1141-
else if (includePrecedingTokenAtEndPosition && end === position) {
1142-
const previousToken = findPrecedingToken(position, sourceFile, child);
1143-
if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) {
1144-
return previousToken;
1160+
// first element whose start position is before the input and whose end position is after or equal to the input
1161+
if (nodeContainsPosition(children[middle])) {
1162+
if (children[middle - 1]) {
1163+
// we want the _first_ element that contains the position, so left-recur if the prior node also contains the position
1164+
if (nodeContainsPosition(children[middle - 1])) {
1165+
return Comparison.GreaterThan;
1166+
}
11451167
}
1168+
return Comparison.EqualTo;
11461169
}
1170+
1171+
// this complex condition makes us left-recur around a zero-length node when includePrecedingTokenAtEndPosition is set, rather than right-recur on it
1172+
if (includePrecedingTokenAtEndPosition && start === position && children[middle - 1] && children[middle - 1].getEnd() === position && nodeContainsPosition(children[middle - 1])) {
1173+
return Comparison.GreaterThan;
1174+
}
1175+
return Comparison.LessThan;
1176+
});
1177+
1178+
if (foundToken) {
1179+
return foundToken;
1180+
}
1181+
if (i >= 0 && children[i]) {
1182+
current = children[i];
1183+
continue outer;
11471184
}
11481185

11491186
return current;
11501187
}
1188+
1189+
function nodeContainsPosition(node: Node) {
1190+
const start = allowPositionInLeadingTrivia ? node.getFullStart() : node.getStart(sourceFile, /*includeJsDoc*/ true);
1191+
if (start > position) {
1192+
// If this child begins after position, then all subsequent children will as well.
1193+
return false;
1194+
}
1195+
const end = node.getEnd();
1196+
if (position < end || (position === end && (node.kind === SyntaxKind.EndOfFileToken || includeEndPosition))) {
1197+
return true;
1198+
}
1199+
else if (includePrecedingTokenAtEndPosition && end === position) {
1200+
const previousToken = findPrecedingToken(position, sourceFile, node);
1201+
if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) {
1202+
foundToken = previousToken;
1203+
return true;
1204+
}
1205+
}
1206+
return false;
1207+
}
11511208
}
11521209

11531210
/**

0 commit comments

Comments
 (0)