@@ -1124,30 +1124,87 @@ namespace ts {
1124
1124
/** Get the token whose text contains the position */
1125
1125
function getTokenAtPositionWorker ( sourceFile : SourceFile , position : number , allowPositionInLeadingTrivia : boolean , includePrecedingTokenAtEndPosition : ( ( n : Node ) => boolean ) | undefined , includeEndPosition : boolean ) : Node {
1126
1126
let current : Node = sourceFile ;
1127
+ let foundToken : Node | undefined ;
1127
1128
outer: while ( true ) {
1128
1129
// 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 ) ;
1131
1156
if ( start > position ) {
1132
- // If this child begins after position, then all subsequent children will as well.
1133
- break ;
1157
+ return Comparison . GreaterThan ;
1134
1158
}
1135
1159
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
+ }
1145
1167
}
1168
+ return Comparison . EqualTo ;
1146
1169
}
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;
1147
1184
}
1148
1185
1149
1186
return current ;
1150
1187
}
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
+ }
1151
1208
}
1152
1209
1153
1210
/**
0 commit comments