From e62c2333eb3398ef835ba4cb561f8f168a8b3a75 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 10 Apr 2019 14:23:25 -0700 Subject: [PATCH] Add support for string literals --- src/server/session.ts | 46 +++++--- .../unittests/tsserver/selectionRange.ts | 107 ++++++++++++++++++ 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 307ee37cc1c..9f4fb2b24d6 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2080,8 +2080,9 @@ namespace ts.server { return map(locations, location => { const pos = this.getPosition(location, scriptInfo); let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan }; - const pushSelectionRange = (textSpan: protocol.TextSpan, syntaxKind?: SyntaxKind): void => { + const pushSelectionRange = (start: number, end: number, syntaxKind?: SyntaxKind): void => { // Skip ranges that are identical to the parent + const textSpan = this.toLocationTextSpan(createTextSpanFromBounds(start, end), scriptInfo); if (!this.locationTextSpansAreEqual(textSpan, selectionRange.textSpan)) { selectionRange = { textSpan, parent: selectionRange }; if (syntaxKind) { @@ -2092,6 +2093,7 @@ namespace ts.server { // Skip top-level SyntaxList let current: Node | undefined = sourceFile.getChildAt(0); + let isInTemplateSpan = false; while (true) { const children = current && current.getChildren(sourceFile); if (!children || !children.length) break; @@ -2103,20 +2105,32 @@ namespace ts.server { current = undefined; break; } - // Blocks are effectively redundant with SyntaxLists; dive in without adding to the list - if (isBlock(node)) { + // Blocks are effectively redundant with SyntaxLists. + // TemplateSpans are an unintuitive grouping of two things which + // should be considered independently. + // Dive in without pushing a selection range. + const nodeIsTemplateSpan = isTemplateSpan(node); + const nodeIsTemplateSpanList = prevNode && isTemplateHead(prevNode); + if (isBlock(node) || nodeIsTemplateSpan || nodeIsTemplateSpanList) { + isInTemplateSpan = nodeIsTemplateSpan; current = node; break; } if (positionBelongsToNode(node, pos, sourceFile)) { + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (isInTemplateSpan && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { + const start = node.getFullStart() - "${".length; + const end = nextNode.getStart() + "}".length; + pushSelectionRange(start, end, node.kind); + } + // Blocks with braces should be selected from brace to brace, non-inclusive const isBetweenBraces = isSyntaxList(node) && prevNode && prevNode.kind === SyntaxKind.OpenBraceToken && nextNode && nextNode.kind === SyntaxKind.CloseBraceToken; const start = isBetweenBraces ? prevNode.getEnd() : node.getStart(); const end = isBetweenBraces ? nextNode.getStart() : node.getEnd(); - const textSpan = this.toLocationTextSpan(createTextSpanFromBounds(start, end), scriptInfo); - pushSelectionRange(textSpan, node.kind); + pushSelectionRange(start, end, node.kind); // Mapped types _look_ like ObjectTypes with a single member, // but in fact don’t contain a SyntaxList or a node containing @@ -2134,17 +2148,19 @@ namespace ts.server { const closeBraceToken = Debug.assertDefined(node.getLastToken()); Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); - const spanWithoutBraces = this.toLocationTextSpan(createTextSpanFromBounds( - openBraceToken.getEnd(), - closeBraceToken.getStart(), - ), scriptInfo); - const spanWithoutBracesOrTrivia = this.toLocationTextSpan(createTextSpanFromBounds( - firstNonBraceToken.getStart(), - closeBraceToken.getFullStart(), - ), scriptInfo); - pushSelectionRange(spanWithoutBraces); - pushSelectionRange(spanWithoutBracesOrTrivia); + const spanWithoutBraces = [openBraceToken.getEnd(), closeBraceToken.getStart()] as const; + const spanWithoutBracesOrTrivia = [firstNonBraceToken.getStart(), closeBraceToken.getFullStart()] as const; + pushSelectionRange(...spanWithoutBraces); + pushSelectionRange(...spanWithoutBracesOrTrivia); } + + // String literals should have a stop both inside and outside their quotes. + if (isStringLiteral(node) || isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + + // If we’ve made it here, we’ve already used `isInTemplateSpan` as much as we need + isInTemplateSpan = false; current = node; break; } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 80b8b1971c0..9166acecc40 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -194,5 +194,112 @@ type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAn end: { line: 2, offset: 184 } } } } } } } } } } } } } }, ]); }); + + it.skip("works for object types", () => { + const getSelectionRange = setup("/file.js", ` +type X = { + foo?: string; + readonly bar: number; +}`); + const locations = getSelectionRange([ + { + line: 3, + offset: 5, + }, + { + line: 4, + offset: 5, + }, + { + line: 4, + offset: 14, + }, + ]); + + const allMembersUp: protocol.SelectionRange = { + textSpan: { // all members + whitespace (just inside braces) + start: { line: 2, offset: 11 }, + end: { line: 5, offset: 1 } }, + parent: { + textSpan: { // add braces + start: { line: 2, offset: 10 }, + end: { line: 5, offset: 2 } }, + parent: { + textSpan: { // whole TypeAliasDeclaration + start: { line: 2, offset: 1 }, + end: { line: 5, offset: 2 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 5, offset: 2 } } } } } }; + + const readonlyBarUp: protocol.SelectionRange = { + textSpan: { // readonly bar + start: { line: 4, offset: 5 }, + end: { line: 4, offset: 17 } }, + parent: { + textSpan: { // readonly bar: number; + start: { line: 4, offset: 5 }, + end: { line: 4, offset: 26 } }, + parent: allMembersUp } }; + + assert.deepEqual(locations, [ + { + textSpan: { // foo + start: { line: 3, offset: 5 }, + end: { line: 3, offset: 8 } }, + parent: { + textSpan: { // foo? + start: { line: 3, offset: 5 }, + end: { line: 3, offset: 9 } }, + parent: { + textSpan: { // foo?: string; + start: { line: 3, offset: 5 }, + end: { line: 3, offset: 18 } }, + parent: allMembersUp } } }, + { + textSpan: { // readonly + start: { line: 4, offset: 5 }, + end: { line: 4, offset: 13 } }, + parent: readonlyBarUp }, + { + textSpan: { // bar + start: { line: 4, offset: 14 }, + end: { line: 4, offset: 17 } }, + parent: readonlyBarUp }, + ]); + }); + + it("works for string literals and template strings", () => { + // tslint:disable-next-line:no-invalid-template-strings + const getSelectionRange = setup("/file.ts", "`a b ${\n 'c'\n} d`"); + const locations = getSelectionRange([{ line: 2, offset: 4 }]); + assert.deepEqual(locations, [ + { + textSpan: { // c + start: { line: 2, offset: 4 }, + end: { line: 2, offset: 5 } }, + parent: { + textSpan: { // 'c' + start: { line: 2, offset: 3 }, + end: { line: 2, offset: 6 } }, + // parent: { + // textSpan: { // just inside braces + // start: { line: 1, offset: 8 }, + // end: { line: 3, offset: 1 } }, + parent: { + textSpan: { // whole TemplateSpan: ${ ... } + start: { line: 1, offset: 6 }, + end: { line: 3, offset: 2 } }, + parent: { + textSpan: { // whole template string without backticks + start: { line: 1, offset: 2 }, + end: { line: 3, offset: 4 } }, + parent: { + textSpan: { // whole template string + start: { line: 1, offset: 1 }, + end: { line: 3, offset: 5 } } } } } } } + ]); + }); }); }