1/* @internal */ 2namespace ts.SmartSelectionRange { 3 export function getSmartSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { 4 let selectionRange: SelectionRange = { 5 textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) 6 }; 7 8 let parentNode: Node = sourceFile; 9 outer: while (true) { 10 const children = getSelectionChildren(parentNode); 11 if (!children.length) break; 12 for (let i = 0; i < children.length; i++) { 13 const prevNode: Node | undefined = children[i - 1]; 14 const node: Node = children[i]; 15 const nextNode: Node | undefined = children[i + 1]; 16 if (getTokenPosOfNode(node, sourceFile, /*includeJsDoc*/ true) > pos) { 17 break outer; 18 } 19 20 if (positionShouldSnapToNode(sourceFile, pos, node)) { 21 // 1. Blocks are effectively redundant with SyntaxLists. 22 // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping 23 // of things that should be considered independently. 24 // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. 25 // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. 26 // Dive in without pushing a selection range. 27 if (isBlock(node) 28 || isTemplateSpan(node) || isTemplateHead(node) || isTemplateTail(node) 29 || prevNode && isTemplateHead(prevNode) 30 || isVariableDeclarationList(node) && isVariableStatement(parentNode) 31 || isSyntaxList(node) && isVariableDeclarationList(parentNode) 32 || isVariableDeclaration(node) && isSyntaxList(parentNode) && children.length === 1 33 || isJSDocTypeExpression(node) || isJSDocSignature(node) || isJSDocTypeLiteral(node)) { 34 parentNode = node; 35 break; 36 } 37 38 // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. 39 if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { 40 const start = node.getFullStart() - "${".length; 41 const end = nextNode.getStart() + "}".length; 42 pushSelectionRange(start, end); 43 } 44 45 // Blocks with braces, brackets, parens, or JSX tags on separate lines should be 46 // selected from open to close, including whitespace but not including the braces/etc. themselves. 47 const isBetweenMultiLineBookends = isSyntaxList(node) && isListOpener(prevNode) && isListCloser(nextNode) 48 && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); 49 const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); 50 const end = isBetweenMultiLineBookends ? nextNode.getStart() : getEndPos(sourceFile, node); 51 52 if (hasJSDocNodes(node) && node.jsDoc?.length) { 53 pushSelectionRange(first(node.jsDoc).getStart(), end); 54 } 55 56 pushSelectionRange(start, end); 57 58 // String literals should have a stop both inside and outside their quotes. 59 if (isStringLiteral(node) || isTemplateLiteral(node)) { 60 pushSelectionRange(start + 1, end - 1); 61 } 62 63 parentNode = node; 64 break; 65 } 66 67 // If we made it to the end of the for loop, we’re done. 68 // In practice, I’ve only seen this happen at the very end 69 // of a SourceFile. 70 if (i === children.length - 1) { 71 break outer; 72 } 73 } 74 } 75 76 return selectionRange; 77 78 function pushSelectionRange(start: number, end: number): void { 79 // Skip empty ranges 80 if (start !== end) { 81 const textSpan = createTextSpanFromBounds(start, end); 82 if (!selectionRange || ( 83 // Skip ranges that are identical to the parent 84 !textSpansEqual(textSpan, selectionRange.textSpan) && 85 // Skip ranges that don’t contain the original position 86 textSpanIntersectsWithPosition(textSpan, pos) 87 )) { 88 selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; 89 } 90 } 91 } 92 } 93 94 /** 95 * Like `ts.positionBelongsToNode`, except positions immediately after nodes 96 * count too, unless that position belongs to the next node. In effect, makes 97 * selections able to snap to preceding tokens when the cursor is on the tail 98 * end of them with only whitespace ahead. 99 * @param sourceFile The source file containing the nodes. 100 * @param pos The position to check. 101 * @param node The candidate node to snap to. 102 */ 103 function positionShouldSnapToNode(sourceFile: SourceFile, pos: number, node: Node) { 104 // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts 105 // for missing nodes, which can’t really be considered when deciding what 106 // to select. 107 Debug.assert(node.pos <= pos); 108 if (pos < node.end) { 109 return true; 110 } 111 const nodeEnd = node.getEnd(); 112 if (nodeEnd === pos) { 113 return getTouchingPropertyName(sourceFile, pos).pos < node.end; 114 } 115 return false; 116 } 117 118 const isImport = or(isImportDeclaration, isImportEqualsDeclaration); 119 120 /** 121 * Gets the children of a node to be considered for selection ranging, 122 * transforming them into an artificial tree according to their intuitive 123 * grouping where no grouping actually exists in the parse tree. For example, 124 * top-level imports are grouped into their own SyntaxList so they can be 125 * selected all together, even though in the AST they’re just siblings of each 126 * other as well as of other top-level statements and declarations. 127 */ 128 function getSelectionChildren(node: Node): readonly Node[] { 129 // Group top-level imports 130 if (isSourceFile(node)) { 131 return groupChildren(node.getChildAt(0).getChildren(), isImport); 132 } 133 134 // Mapped types _look_ like ObjectTypes with a single member, 135 // but in fact don’t contain a SyntaxList or a node containing 136 // the “key/value” pair like ObjectTypes do, but it seems intuitive 137 // that the selection would snap to those points. The philosophy 138 // of choosing a selection range is not so much about what the 139 // syntax currently _is_ as what the syntax might easily become 140 // if the user is making a selection; e.g., we synthesize a selection 141 // around the “key/value” pair not because there’s a node there, but 142 // because it allows the mapped type to become an object type with a 143 // few keystrokes. 144 if (isMappedTypeNode(node)) { 145 const [openBraceToken, ...children] = node.getChildren(); 146 const closeBraceToken = Debug.checkDefined(children.pop()); 147 Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); 148 Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); 149 // Group `-/+readonly` and `-/+?` 150 const groupedWithPlusMinusTokens = groupChildren(children, child => 151 child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword || 152 child === node.questionToken || child.kind === SyntaxKind.QuestionToken); 153 // Group type parameter with surrounding brackets 154 const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => 155 kind === SyntaxKind.OpenBracketToken || 156 kind === SyntaxKind.TypeParameter || 157 kind === SyntaxKind.CloseBracketToken 158 ); 159 return [ 160 openBraceToken, 161 // Pivot on `:` 162 createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === SyntaxKind.ColonToken)), 163 closeBraceToken, 164 ]; 165 } 166 167 // Group modifiers and property name, then pivot on `:`. 168 if (isPropertySignature(node)) { 169 const children = groupChildren(node.getChildren(), child => 170 child === node.name || contains(node.modifiers, child)); 171 return splitChildren(children, ({ kind }) => kind === SyntaxKind.ColonToken); 172 } 173 174 // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. 175 if (isParameter(node)) { 176 const groupedDotDotDotAndName = groupChildren(node.getChildren(), child => 177 child === node.dotDotDotToken || child === node.name); 178 const groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName, child => 179 child === groupedDotDotDotAndName[0] || child === node.questionToken); 180 return splitChildren(groupedWithQuestionToken, ({ kind }) => kind === SyntaxKind.EqualsToken); 181 } 182 183 // Pivot on '=' 184 if (isBindingElement(node)) { 185 return splitChildren(node.getChildren(), ({ kind }) => kind === SyntaxKind.EqualsToken); 186 } 187 188 return node.getChildren(); 189 } 190 191 /** 192 * Groups sibling nodes together into their own SyntaxList if they 193 * a) are adjacent, AND b) match a predicate function. 194 */ 195 function groupChildren(children: Node[], groupOn: (child: Node) => boolean): Node[] { 196 const result: Node[] = []; 197 let group: Node[] | undefined; 198 for (const child of children) { 199 if (groupOn(child)) { 200 group = group || []; 201 group.push(child); 202 } 203 else { 204 if (group) { 205 result.push(createSyntaxList(group)); 206 group = undefined; 207 } 208 result.push(child); 209 } 210 } 211 if (group) { 212 result.push(createSyntaxList(group)); 213 } 214 215 return result; 216 } 217 218 /** 219 * Splits sibling nodes into up to four partitions: 220 * 1) everything left of the first node matched by `pivotOn`, 221 * 2) the first node matched by `pivotOn`, 222 * 3) everything right of the first node matched by `pivotOn`, 223 * 4) a trailing semicolon, if `separateTrailingSemicolon` is enabled. 224 * The left and right groups, if not empty, will each be grouped into their own containing SyntaxList. 225 * @param children The sibling nodes to split. 226 * @param pivotOn The predicate function to match the node to be the pivot. The first node that matches 227 * the predicate will be used; any others that may match will be included into the right-hand group. 228 * @param separateTrailingSemicolon If the last token is a semicolon, it will be returned as a separate 229 * child rather than be included in the right-hand group. 230 */ 231 function splitChildren(children: Node[], pivotOn: (child: Node) => boolean, separateTrailingSemicolon = true): Node[] { 232 if (children.length < 2) { 233 return children; 234 } 235 const splitTokenIndex = findIndex(children, pivotOn); 236 if (splitTokenIndex === -1) { 237 return children; 238 } 239 const leftChildren = children.slice(0, splitTokenIndex); 240 const splitToken = children[splitTokenIndex]; 241 const lastToken = last(children); 242 const separateLastToken = separateTrailingSemicolon && lastToken.kind === SyntaxKind.SemicolonToken; 243 const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); 244 const result = compact([ 245 leftChildren.length ? createSyntaxList(leftChildren) : undefined, 246 splitToken, 247 rightChildren.length ? createSyntaxList(rightChildren) : undefined, 248 ]); 249 return separateLastToken ? result.concat(lastToken) : result; 250 } 251 252 function createSyntaxList(children: Node[]): SyntaxList { 253 Debug.assertGreaterThanOrEqual(children.length, 1); 254 return setTextRangePosEnd(parseNodeFactory.createSyntaxList(children), children[0].pos, last(children).end); 255 } 256 257 function isListOpener(token: Node | undefined): token is Node { 258 const kind = token && token.kind; 259 return kind === SyntaxKind.OpenBraceToken 260 || kind === SyntaxKind.OpenBracketToken 261 || kind === SyntaxKind.OpenParenToken 262 || kind === SyntaxKind.JsxOpeningElement; 263 } 264 265 function isListCloser(token: Node | undefined): token is Node { 266 const kind = token && token.kind; 267 return kind === SyntaxKind.CloseBraceToken 268 || kind === SyntaxKind.CloseBracketToken 269 || kind === SyntaxKind.CloseParenToken 270 || kind === SyntaxKind.JsxClosingElement; 271 } 272 273 function getEndPos(sourceFile: SourceFile, node: Node): number { 274 switch (node.kind) { 275 case SyntaxKind.JSDocParameterTag: 276 case SyntaxKind.JSDocCallbackTag: 277 case SyntaxKind.JSDocPropertyTag: 278 case SyntaxKind.JSDocTypedefTag: 279 case SyntaxKind.JSDocThisTag: 280 return sourceFile.getLineEndOfPosition(node.getStart()); 281 default: 282 return node.getEnd(); 283 } 284 } 285} 286