• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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