From af3d0f0248cbc20366fd5591063958cd9f7ea391 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 9 Apr 2019 11:31:17 -0700 Subject: [PATCH 01/26] Start smart select API --- src/server/protocol.ts | 21 ++++- src/server/session.ts | 63 +++++++++++++- src/testRunner/tsconfig.json | 1 + .../unittests/tsserver/selectionRange.ts | 84 +++++++++++++++++++ src/testRunner/unittests/tsserver/session.ts | 1 + 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/testRunner/unittests/tsserver/selectionRange.ts diff --git a/src/server/protocol.ts b/src/server/protocol.ts index e5580874369e6..1709c6196ef83 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -130,7 +130,8 @@ namespace ts.server.protocol { GetEditsForFileRename = "getEditsForFileRename", /* @internal */ GetEditsForFileRenameFull = "getEditsForFileRename-full", - ConfigurePlugin = "configurePlugin" + ConfigurePlugin = "configurePlugin", + SelectionRange = "selectionRange", // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. } @@ -1395,6 +1396,24 @@ namespace ts.server.protocol { export interface ConfigurePluginResponse extends Response { } + export interface SelectionRangeRequest extends FileRequest { + command: CommandTypes.SelectionRange; + arguments: SelectionRangeRequestArgs; + } + + export interface SelectionRangeRequestArgs extends FileRequestArgs { + locations: Location[]; + } + + export interface SelectionRangeResponse extends Response { + body?: SelectionRange[]; + } + + export interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } + /** * Information found in an "open" request. */ diff --git a/src/server/session.ts b/src/server/session.ts index 3c202c9aecd02..f566340cb287e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1318,11 +1318,11 @@ namespace ts.server { this.projectService.openClientFileWithNormalizedPath(fileName, fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath); } - private getPosition(args: protocol.FileLocationRequestArgs, scriptInfo: ScriptInfo): number { + private getPosition(args: protocol.Location & { position?: number }, scriptInfo: ScriptInfo): number { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getPositionInFile(args: protocol.FileLocationRequestArgs, file: NormalizedPath): number { + private getPositionInFile(args: protocol.Location & { position?: number }, file: NormalizedPath): number { const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; return this.getPosition(args, scriptInfo); } @@ -2059,6 +2059,62 @@ namespace ts.server { this.projectService.configurePlugin(args); } + private getSelectionRange(args: protocol.SelectionRangeRequestArgs): protocol.SelectionRange[] { + const { locations } = args; + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + + const sourceFile = languageService.getNonBoundSourceFile(file); + const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); + const fullTextSpan = this.toLocationTextSpan( + createTextSpan(sourceFile.getFullStart(), sourceFile.getEnd() - sourceFile.getFullStart()), + scriptInfo); + + return map(locations, location => { + const pos = this.getPosition(location, scriptInfo); + let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan }; + // Skip top-level SyntaxList + let current: Node | undefined = sourceFile.getChildAt(0); + while (true) { + const children = current && current.getChildren(sourceFile); + if (!children || !children.length) break; + for (let i = 0; i < children.length; i++) { + const prevNode: Node | undefined = children[i - 1]; + const node: Node = children[i]; + const nextNode: Node | undefined = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + current = undefined; + break; + } + // Blocks are effectively redundant with SyntaxLists; dive in without adding to the list + if (isBlock(node)) { + current = node; + break; + } + if (positionBelongsToNode(node, pos, sourceFile)) { + // 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(createTextSpan(start, end - start), scriptInfo); + current = node; + // Skip ranges that are identical to the parent + if (selectionRange.textSpan.start !== textSpan.start || selectionRange.textSpan.end !== textSpan.end) { + selectionRange = { + textSpan, + parent: selectionRange, + }; + Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(node.kind) }); + } + break; + } + } + } + return selectionRange; + }); + } + getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return normalizePath(name); @@ -2414,6 +2470,9 @@ namespace ts.server { this.configurePlugin(request.arguments); this.doOutput(/*info*/ undefined, CommandNames.ConfigurePlugin, request.seq, /*success*/ true); return this.notRequired(); + }, + [CommandNames.SelectionRange]: (request: protocol.SelectionRangeRequest) => { + return this.requiredResponse(this.getSelectionRange(request.arguments)); } }); diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 6b73324e2f737..5ae94c80c2316 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -141,6 +141,7 @@ "unittests/tsserver/reload.ts", "unittests/tsserver/rename.ts", "unittests/tsserver/resolutionCache.ts", + "unittests/tsserver/selectionRange.ts", "unittests/tsserver/session.ts", "unittests/tsserver/skipLibCheck.ts", "unittests/tsserver/symLinks.ts", diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts new file mode 100644 index 0000000000000..ae67ea5de0a0f --- /dev/null +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -0,0 +1,84 @@ +namespace ts.projectSystem { + function setup(fileName: string, content: string) { + const file: File = { path: fileName, content }; + const host = createServerHost([file, libFile]); + const session = createSession(host); + openFilesForSession([file], session); + return function getSelectionRange(locations: protocol.SelectionRangeRequestArgs["locations"]) { + return executeSessionRequest( + session, + CommandNames.SelectionRange, + { file: fileName, locations }); + }; + } + + describe("unittests:: tsserver:: selectionRange", () => { + it("works for simple JavaScript", () => { + const getSelectionRange = setup("/file.js", ` +class Foo { + bar(a, b) { + if (a === b) { + return true; + } + return false; + } +}`); + + const locations = getSelectionRange([{ + line: 4, + offset: 13 + }]); + + assert.deepEqual(locations, [ + { + textSpan: { // a + start: { line: 4, offset: 13 }, + end: { line: 4, offset: 14 }, + }, + parent: { + textSpan: { // a === b + start: { line: 4, offset: 13 }, + end: { line: 4, offset: 20 }, + }, + parent: { + textSpan: { // IfStatement + start: { line: 4, offset: 9 }, + end: { line: 6, offset: 10 }, + }, + parent: { + textSpan: { // SyntaxList + whitespace (body of method) + start: { line: 3, offset: 16 }, + end: { line: 8, offset: 5 }, + }, + parent: { + textSpan: { // MethodDeclaration + start: { line: 3, offset: 5 }, + end: { line: 8, offset: 6 }, + }, + parent: { + textSpan: { // SyntaxList + whitespace (body of class) + start: { line: 2, offset: 12 }, + end: { line: 9, offset: 1 }, + }, + parent: { + textSpan: { // ClassDeclaration + start: { line: 2, offset: 1 }, + end: { line: 9, offset: 2 }, + }, + parent: { + textSpan: { // SourceFile (all text) + start: { line: 1, offset: 1 }, + end: { line: 9, offset: 2 }, + } + } + } + } + }, + }, + }, + }, + }, + ]); + }); + }); +} diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 715c0ab332408..cf84ffce6e8bc 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -264,6 +264,7 @@ namespace ts.server { CommandNames.OrganizeImportsFull, CommandNames.GetEditsForFileRename, CommandNames.GetEditsForFileRenameFull, + CommandNames.SelectionRange, ]; it("should not throw when commands are executed with invalid arguments", () => { From f98c00ab9dbf9b37fbadb9a15b6a41f65b06d383 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 9 Apr 2019 15:32:38 -0700 Subject: [PATCH 02/26] Add more tests, special handling for mapped types --- src/server/session.ts | 59 +++++- .../unittests/tsserver/selectionRange.ts | 194 ++++++++++++++---- 2 files changed, 203 insertions(+), 50 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index f566340cb287e..307ee37cc1c45 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1744,6 +1744,14 @@ namespace ts.server { }; } + private locationsAreEqual(a: protocol.Location, b: protocol.Location): boolean { + return a.line === b.line && a.offset === b.offset; + } + + private locationTextSpansAreEqual(a: protocol.TextSpan, b: protocol.TextSpan): boolean { + return this.locationsAreEqual(a.start, b.start) && this.locationsAreEqual(a.end, b.end); + } + private getNavigationTree(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationTree | NavigationTree | undefined { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const tree = languageService.getNavigationTree(file); @@ -2066,12 +2074,22 @@ namespace ts.server { const sourceFile = languageService.getNonBoundSourceFile(file); const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); const fullTextSpan = this.toLocationTextSpan( - createTextSpan(sourceFile.getFullStart(), sourceFile.getEnd() - sourceFile.getFullStart()), + createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()), scriptInfo); return map(locations, location => { const pos = this.getPosition(location, scriptInfo); let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan }; + const pushSelectionRange = (textSpan: protocol.TextSpan, syntaxKind?: SyntaxKind): void => { + // Skip ranges that are identical to the parent + if (!this.locationTextSpansAreEqual(textSpan, selectionRange.textSpan)) { + selectionRange = { textSpan, parent: selectionRange }; + if (syntaxKind) { + Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) }); + } + } + }; + // Skip top-level SyntaxList let current: Node | undefined = sourceFile.getChildAt(0); while (true) { @@ -2097,16 +2115,37 @@ namespace ts.server { && nextNode && nextNode.kind === SyntaxKind.CloseBraceToken; const start = isBetweenBraces ? prevNode.getEnd() : node.getStart(); const end = isBetweenBraces ? nextNode.getStart() : node.getEnd(); - const textSpan = this.toLocationTextSpan(createTextSpan(start, end - start), scriptInfo); - current = node; - // Skip ranges that are identical to the parent - if (selectionRange.textSpan.start !== textSpan.start || selectionRange.textSpan.end !== textSpan.end) { - selectionRange = { - textSpan, - parent: selectionRange, - }; - Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(node.kind) }); + const textSpan = this.toLocationTextSpan(createTextSpanFromBounds(start, end), scriptInfo); + pushSelectionRange(textSpan, node.kind); + + // Mapped types _look_ like ObjectTypes with a single member, + // but in fact don’t contain a SyntaxList or a node containing + // the “key/value” pair like ObjectTypes do, but it seems intuitive + // that the selection would snap to those points. The philosophy + // of choosing a selection range is not so much about what the + // syntax currently _is_ as what the syntax might easily become + // if the user is making a selection; e.g., we synthesize a selection + // around the “key/value” pair not because there’s a node there, but + // because it allows the mapped type to become an object type with a + // few keystrokes. + if (isMappedTypeNode(node)) { + const openBraceToken = Debug.assertDefined(node.getFirstToken()); + const firstNonBraceToken = Debug.assertDefined(node.getChildAt(1)); + 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); } + current = node; break; } } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index ae67ea5de0a0f..80b8b1971c078 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -24,61 +24,175 @@ class Foo { } }`); - const locations = getSelectionRange([{ - line: 4, - offset: 13 - }]); + const locations = getSelectionRange([ + { + line: 4, + offset: 13, + }, { + line: 5, + offset: 22, + }, + ]); + + // Common to results for both locations + const ifStatementUp: protocol.SelectionRange = { + textSpan: { // IfStatement + start: { line: 4, offset: 9 }, + end: { line: 6, offset: 10 } }, + parent: { + textSpan: { // SyntaxList + whitespace (body of method) + start: { line: 3, offset: 16 }, + end: { line: 8, offset: 5 } }, + parent: { + textSpan: { // MethodDeclaration + start: { line: 3, offset: 5 }, + end: { line: 8, offset: 6 } }, + parent: { + textSpan: { // SyntaxList + whitespace (body of class) + start: { line: 2, offset: 12 }, + end: { line: 9, offset: 1 } }, + parent: { + textSpan: { // ClassDeclaration + start: { line: 2, offset: 1 }, + end: { line: 9, offset: 2 } }, + parent: { + textSpan: { // SourceFile (all text) + start: { line: 1, offset: 1 }, + end: { line: 9, offset: 2 }, } } } } } } }; assert.deepEqual(locations, [ { textSpan: { // a start: { line: 4, offset: 13 }, - end: { line: 4, offset: 14 }, - }, + end: { line: 4, offset: 14 } }, parent: { textSpan: { // a === b start: { line: 4, offset: 13 }, - end: { line: 4, offset: 20 }, - }, + end: { line: 4, offset: 20 } }, + parent: ifStatementUp } }, + { + textSpan: { // true + start: { line: 5, offset: 20 }, + end: { line: 5, offset: 24 } }, + parent: { + textSpan: { // return true; + start: { line: 5, offset: 13 }, + end: { line: 5, offset: 25 } }, + parent: { + textSpan: { // SyntaxList + whitespace (body of IfStatement) + start: { line: 4, offset: 23 }, + end: { line: 6, offset: 9 } }, + parent: ifStatementUp } } } + ]); + }); + + it("works for simple TypeScript", () => { + const getSelectionRange = setup("/file.ts", ` +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; +}`); + const locations = getSelectionRange([ + { + line: 5, + offset: 12, + }, + ]); + + assert.deepEqual(locations, [ + { + textSpan: { // host + start: { line: 5, offset: 10 }, + end: { line: 5, offset: 14 } }, + parent: { + textSpan: { // host: number + start: { line: 5, offset: 10 }, + end: { line: 5, offset: 22 } }, parent: { - textSpan: { // IfStatement - start: { line: 4, offset: 9 }, - end: { line: 6, offset: 10 }, - }, + textSpan: { // host: number, data: any + start: { line: 5, offset: 10 }, + end: { line: 5, offset: 33 } }, parent: { - textSpan: { // SyntaxList + whitespace (body of method) - start: { line: 3, offset: 16 }, - end: { line: 8, offset: 5 }, - }, + textSpan: { // open(host: number, data: any): Promise; + start: { line: 5, offset: 5 }, + end: { line: 5, offset: 49 } }, parent: { - textSpan: { // MethodDeclaration - start: { line: 3, offset: 5 }, - end: { line: 8, offset: 6 }, - }, + textSpan: { // SyntaxList + whitespace (body of interface) + start: { line: 2, offset: 28 }, + end: { line: 6, offset: 1 } }, parent: { - textSpan: { // SyntaxList + whitespace (body of class) - start: { line: 2, offset: 12 }, - end: { line: 9, offset: 1 }, - }, + textSpan: { // InterfaceDeclaration + start: { line: 2, offset: 1 }, + end: { line: 6, offset: 2 } }, parent: { - textSpan: { // ClassDeclaration - start: { line: 2, offset: 1 }, - end: { line: 9, offset: 2 }, - }, - parent: { - textSpan: { // SourceFile (all text) - start: { line: 1, offset: 1 }, - end: { line: 9, offset: 2 }, - } - } - } - } - }, - }, - }, - }, + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 6, offset: 2 } } } } } } } } }, + ]); + }); + + it("works for complex TypeScript", () => { + const getSelectionRange = setup("/file.ts", ` +type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) +`); + const locations = getSelectionRange([ + { + line: 2, + offset: 133, }, ]); + + assert.deepEqual(locations, [ + { + textSpan: { // K + start: { line: 2, offset: 133 }, + end: { line: 2, offset: 134 } }, + parent: { + textSpan: { // P[K] + start: { line: 2, offset: 131 }, + end: { line: 2, offset: 135 } }, + parent: { + textSpan: { // K extends keyof T ? T[K] : P[K] + start: { line: 2, offset: 104 }, + end: { line: 2, offset: 135 } }, + parent: { + textSpan: { // IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K] + start: { line: 2, offset: 70 }, + end: { line: 2, offset: 142 } }, + parent: { + textSpan: { // [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; + start: { line: 2, offset: 54 }, + end: { line: 2, offset: 143 } }, + parent: { // same as above + whitespace + textSpan: { + start: { line: 2, offset: 53 }, + end: { line: 2, offset: 144 } }, + parent: { + textSpan: { // MappedType: same as above + braces + start: { line: 2, offset: 52 }, + end: { line: 2, offset: 145 } }, + parent: { + textSpan: { // IntersectionType: { [K in keyof P]: ... } & Pick> + start: { line: 2, offset: 52 }, + end: { line: 2, offset: 182 } }, + parent: { + textSpan: { // same as above + parens + start: { line: 2, offset: 51 }, + end: { line: 2, offset: 183 } }, + parent: { + textSpan: { // Whole TypeNode of TypeAliasDeclaration + start: { line: 2, offset: 16 }, + end: { line: 2, offset: 183 } }, + parent: { + textSpan: { // Whole TypeAliasDeclaration + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 183 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 184 } } } } } } } } } } } } } }, + ]); }); }); } From e62c2333eb3398ef835ba4cb561f8f168a8b3a75 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 10 Apr 2019 14:23:25 -0700 Subject: [PATCH 03/26] 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 307ee37cc1c45..9f4fb2b24d65b 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 80b8b1971c078..9166acecc40ca 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 } } } } } } } + ]); + }); }); } From fd88e5225270121e949d4cff67b2a664f21dac07 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Apr 2019 11:38:05 -0700 Subject: [PATCH 04/26] Start imports --- src/server/session.ts | 75 +++++++++++++------ .../unittests/tsserver/selectionRange.ts | 40 ++++++++++ 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 9f4fb2b24d65b..90a8aa2f00fff 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -112,6 +112,32 @@ namespace ts.server { return edits.every(edit => textSpanEnd(edit.span) < pos); } + function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { + let first = index; + let last = index; + let i = index; + while (i > 0) { + const element = array[--i]; + if (predicate(element)) { + first = i; + } + else { + break; + } + } + i = index; + while (i < array.length - 1) { + const element = array[++i]; + if (predicate(element)) { + last = i; + } + else { + break; + } + } + return [first, last]; + } + // CommandNames used to be exposed before TS 2.4 as a namespace // In TS 2.4 we switched to an enum, keep this for backward compatibility // The var assignment ensures that even though CommandTypes are a const enum @@ -2071,6 +2097,7 @@ namespace ts.server { const { locations } = args; const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const isImport = or(isImportDeclaration, isImportEqualsDeclaration); const sourceFile = languageService.getNonBoundSourceFile(file); const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); const fullTextSpan = this.toLocationTextSpan( @@ -2092,37 +2119,41 @@ 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; + let parentNode = sourceFile.getChildAt(0); + outer: while (true) { + const children = parentNode.getChildren(sourceFile); + if (!children.length) break; for (let i = 0; i < children.length; i++) { const prevNode: Node | undefined = children[i - 1]; const node: Node = children[i]; const nextNode: Node | undefined = children[i + 1]; if (node.getStart(sourceFile) > pos) { - current = undefined; - break; - } - // 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; + break outer; } + if (positionBelongsToNode(node, pos, sourceFile)) { + // Blocks are effectively redundant with SyntaxLists. + // TemplateSpans, along with the SyntaxLists containing them, + // are a somewhat unintuitive grouping of things that should be + // considered independently. Dive in without pushing a selection range. + if (isBlock(node) || isTemplateSpan(node) || prevNode && isTemplateHead(prevNode)) { + parentNode = node; + break; + } + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. - if (isInTemplateSpan && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { + if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { const start = node.getFullStart() - "${".length; const end = nextNode.getStart() + "}".length; pushSelectionRange(start, end, node.kind); } + // Synthesize a stop for group of adjacent imports + else if (isImport(node)) { + const [firstImportIndex, lastImportIndex] = getGroupBounds(children, i, isImport); + pushSelectionRange( + children[firstImportIndex].getStart(), + children[lastImportIndex].getEnd()); + } // Blocks with braces should be selected from brace to brace, non-inclusive const isBetweenBraces = isSyntaxList(node) @@ -2155,13 +2186,11 @@ namespace ts.server { } // String literals should have a stop both inside and outside their quotes. - if (isStringLiteral(node) || isTemplateLiteral(node)) { + else 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; + parentNode = node; break; } } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 9166acecc40ca..4b95f929bf982 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -301,5 +301,45 @@ type X = { end: { line: 3, offset: 5 } } } } } } } ]); }); + + it.skip("works for ES2015 import lists", () => { + const getSelectionRange = setup("/file.ts", ` +import { x as y, z } from './z'; +import { b } from './'; + +console.log(1);`); + + const locations = getSelectionRange([{ line: 2, offset: 10 }]); + assert.deepEqual(locations, [ + { + textSpan: { // x + start: { line: 2, offset: 10 }, + end: { line: 2, offset: 11 } }, + parent: { + textSpan: { // x as y + start: { line: 2, offset: 10 }, + end: { line: 2, offset: 16 } }, + parent: { + textSpan: { // x as y, z + start: { line: 2, offset: 10 }, + end: { line: 2, offset: 19 } }, + parent: { + textSpan: { // { x as y, z } + start: { line: 2, offset: 8 }, + end: { line: 2, offset: 21 } }, + parent: { + textSpan: { // import { x as y, z } from './z'; + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 33 } }, + parent: { + textSpan: { // all imports + start: { line: 2, offset: 1 }, + end: { line: 3, offset: 24 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 5, offset: 16 } } } } } } } } } + ]); + }); }); } From 039487c84e518cb6de798d1011dca7e9db624315 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Apr 2019 12:47:10 -0700 Subject: [PATCH 05/26] Also skip TemplateHeads --- src/server/session.ts | 2 +- .../unittests/tsserver/selectionRange.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 90a8aa2f00fff..f64674b129a77 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2136,7 +2136,7 @@ namespace ts.server { // TemplateSpans, along with the SyntaxLists containing them, // are a somewhat unintuitive grouping of things that should be // considered independently. Dive in without pushing a selection range. - if (isBlock(node) || isTemplateSpan(node) || prevNode && isTemplateHead(prevNode)) { + if (isBlock(node) || isTemplateSpan(node) || isTemplateHead(node) || prevNode && isTemplateHead(prevNode)) { parentNode = node; break; } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 4b95f929bf982..ce88b4072cc74 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -273,7 +273,10 @@ type X = { 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 }]); + const locations = getSelectionRange([ + { line: 2, offset: 4 }, + { line: 1, offset: 4 }, + ]); assert.deepEqual(locations, [ { textSpan: { // c @@ -298,7 +301,15 @@ type X = { parent: { textSpan: { // whole template string start: { line: 1, offset: 1 }, - end: { line: 3, offset: 5 } } } } } } } + end: { line: 3, offset: 5 } } } } } } }, + { + 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 } } } }, ]); }); From 0a4ef0f63069f29beac7689d4b9a579689d732dc Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Apr 2019 13:02:49 -0700 Subject: [PATCH 06/26] Distinguish between same-line and different-line braces --- src/server/session.ts | 12 +++--- .../unittests/tsserver/selectionRange.ts | 37 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index f64674b129a77..ed9f4d8990a8c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2155,12 +2155,14 @@ namespace ts.server { children[lastImportIndex].getEnd()); } - // Blocks with braces should be selected from brace to brace, non-inclusive - const isBetweenBraces = isSyntaxList(node) + // Blocks with braces on separate lines should be selected from brace to brace, + // including whitespace but not including the braces themselves. + const isBetweenMultiLineBraces = 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(); + && nextNode && nextNode.kind === SyntaxKind.CloseBraceToken + && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + const start = isBetweenMultiLineBraces ? prevNode.getEnd() : node.getStart(); + const end = isBetweenMultiLineBraces ? nextNode.getStart() : node.getEnd(); pushSelectionRange(start, end, node.kind); // Mapped types _look_ like ObjectTypes with a single member, diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index ce88b4072cc74..b4858407ee8eb 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -199,21 +199,13 @@ type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAn const getSelectionRange = setup("/file.js", ` type X = { foo?: string; - readonly bar: number; + readonly bar: { x: number }; }`); const locations = getSelectionRange([ - { - line: 3, - offset: 5, - }, - { - line: 4, - offset: 5, - }, - { - line: 4, - offset: 14, - }, + { line: 3, offset: 5 }, + { line: 4, offset: 5 }, + { line: 4, offset: 14 }, + { line: 4, offset: 27 }, ]); const allMembersUp: protocol.SelectionRange = { @@ -238,9 +230,9 @@ type X = { start: { line: 4, offset: 5 }, end: { line: 4, offset: 17 } }, parent: { - textSpan: { // readonly bar: number; + textSpan: { // readonly bar: { x: number }; start: { line: 4, offset: 5 }, - end: { line: 4, offset: 26 } }, + end: { line: 4, offset: 33 } }, parent: allMembersUp } }; assert.deepEqual(locations, [ @@ -267,6 +259,19 @@ type X = { start: { line: 4, offset: 14 }, end: { line: 4, offset: 17 } }, parent: readonlyBarUp }, + { + textSpan: { // number + start: { line: 4, offset: 24 }, + end: { line: 4, offset: 30 } }, + parent: { + textSpan: { // x: number + start: { line: 4, offset: 21 }, + end: { line: 4, offset: 30 } }, + parent: { + textSpan: { // { x: number } + start: { line: 4, offset: 19 }, + end: { line: 4, offset: 32 } }, + parent: readonlyBarUp } } }, ]); }); @@ -313,7 +318,7 @@ type X = { ]); }); - it.skip("works for ES2015 import lists", () => { + it("works for ES2015 import lists", () => { const getSelectionRange = setup("/file.ts", ` import { x as y, z } from './z'; import { b } from './'; From 61425cb3047bf7144d910dc710be10e7eb0fe45c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Apr 2019 15:15:17 -0700 Subject: [PATCH 07/26] Move most logic to separate file --- src/server/selectionRange.ts | 116 +++++++++++++++++ src/server/session.ts | 119 +----------------- src/server/tsconfig.json | 55 ++++---- .../unittests/tsserver/selectionRange.ts | 38 +++--- 4 files changed, 167 insertions(+), 161 deletions(-) create mode 100644 src/server/selectionRange.ts diff --git a/src/server/selectionRange.ts b/src/server/selectionRange.ts new file mode 100644 index 0000000000000..60027e330dc55 --- /dev/null +++ b/src/server/selectionRange.ts @@ -0,0 +1,116 @@ +/* @internal */ +namespace ts.server { + const isImport = or(isImportDeclaration, isImportEqualsDeclaration); + + export function getSelectionRange(pos: number, sourceFile: SourceFile, pushSelectionRange: (start: number, end: number, kind?: SyntaxKind) => void) { + pushSelectionRange(sourceFile.getFullStart(), sourceFile.getEnd(), SyntaxKind.SourceFile); + + // Skip top-level SyntaxList + let parentNode = sourceFile.getChildAt(0); + outer: while (true) { + const children = parentNode.getChildren(sourceFile); + if (!children.length) break; + for (let i = 0; i < children.length; i++) { + const prevNode: Node | undefined = children[i - 1]; + const node: Node = children[i]; + const nextNode: Node | undefined = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + break outer; + } + + if (positionBelongsToNode(node, pos, sourceFile)) { + // Blocks are effectively redundant with SyntaxLists. + // TemplateSpans, along with the SyntaxLists containing them, + // are a somewhat unintuitive grouping of things that should be + // considered independently. Dive in without pushing a selection range. + if (isBlock(node) || isTemplateSpan(node) || isTemplateHead(node) || prevNode && isTemplateHead(prevNode)) { + parentNode = node; + break; + } + + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { + const start = node.getFullStart() - "${".length; + const end = nextNode.getStart() + "}".length; + pushSelectionRange(start, end, node.kind); + } + // Synthesize a stop for group of adjacent imports + else if (isImport(node)) { + const [firstImportIndex, lastImportIndex] = getGroupBounds(children, i, isImport); + pushSelectionRange( + children[firstImportIndex].getStart(), + children[lastImportIndex].getEnd()); + } + + // Blocks with braces on separate lines should be selected from brace to brace, + // including whitespace but not including the braces themselves. + const isBetweenMultiLineBraces = isSyntaxList(node) + && prevNode && prevNode.kind === SyntaxKind.OpenBraceToken + && nextNode && nextNode.kind === SyntaxKind.CloseBraceToken + && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + const start = isBetweenMultiLineBraces ? prevNode.getEnd() : node.getStart(); + const end = isBetweenMultiLineBraces ? nextNode.getStart() : node.getEnd(); + 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 + // the “key/value” pair like ObjectTypes do, but it seems intuitive + // that the selection would snap to those points. The philosophy + // of choosing a selection range is not so much about what the + // syntax currently _is_ as what the syntax might easily become + // if the user is making a selection; e.g., we synthesize a selection + // around the “key/value” pair not because there’s a node there, but + // because it allows the mapped type to become an object type with a + // few keystrokes. + if (isMappedTypeNode(node)) { + const openBraceToken = Debug.assertDefined(node.getFirstToken()); + const firstNonBraceToken = Debug.assertDefined(node.getChildAt(1)); + const closeBraceToken = Debug.assertDefined(node.getLastToken()); + Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); + Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); + const spanWithoutBraces = [openBraceToken.getEnd(), closeBraceToken.getStart()] as const; + const spanWithoutBracesOrTrivia = [firstNonBraceToken.getStart(), closeBraceToken.getFullStart()] as const; + if (!positionsAreOnSameLine(openBraceToken.getStart(), closeBraceToken.getEnd(), sourceFile)) { + pushSelectionRange(...spanWithoutBraces); + } + pushSelectionRange(...spanWithoutBracesOrTrivia); + } + + // String literals should have a stop both inside and outside their quotes. + else if (isStringLiteral(node) || isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + + parentNode = node; + break; + } + } + } + } + + function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { + let first = index; + let last = index; + let i = index; + while (i > 0) { + const element = array[--i]; + if (predicate(element)) { + first = i; + } + else { + break; + } + } + i = index; + while (i < array.length - 1) { + const element = array[++i]; + if (predicate(element)) { + last = i; + } + else { + break; + } + } + return [first, last]; + } +} diff --git a/src/server/session.ts b/src/server/session.ts index ed9f4d8990a8c..68c34d27b2666 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -112,32 +112,6 @@ namespace ts.server { return edits.every(edit => textSpanEnd(edit.span) < pos); } - function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { - let first = index; - let last = index; - let i = index; - while (i > 0) { - const element = array[--i]; - if (predicate(element)) { - first = i; - } - else { - break; - } - } - i = index; - while (i < array.length - 1) { - const element = array[++i]; - if (predicate(element)) { - last = i; - } - else { - break; - } - } - return [first, last]; - } - // CommandNames used to be exposed before TS 2.4 as a namespace // In TS 2.4 we switched to an enum, keep this for backward compatibility // The var assignment ensures that even though CommandTypes are a const enum @@ -2097,107 +2071,26 @@ namespace ts.server { const { locations } = args; const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); - const isImport = or(isImportDeclaration, isImportEqualsDeclaration); + const sourceFile = languageService.getNonBoundSourceFile(file); const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); - const fullTextSpan = this.toLocationTextSpan( - createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()), - scriptInfo); return map(locations, location => { const pos = this.getPosition(location, scriptInfo); - let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan }; + let selectionRange: protocol.SelectionRange | undefined; 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 (!selectionRange || !this.locationTextSpansAreEqual(textSpan, selectionRange.textSpan)) { + selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; if (syntaxKind) { Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) }); } } }; - // Skip top-level SyntaxList - let parentNode = sourceFile.getChildAt(0); - outer: while (true) { - const children = parentNode.getChildren(sourceFile); - if (!children.length) break; - for (let i = 0; i < children.length; i++) { - const prevNode: Node | undefined = children[i - 1]; - const node: Node = children[i]; - const nextNode: Node | undefined = children[i + 1]; - if (node.getStart(sourceFile) > pos) { - break outer; - } - - if (positionBelongsToNode(node, pos, sourceFile)) { - // Blocks are effectively redundant with SyntaxLists. - // TemplateSpans, along with the SyntaxLists containing them, - // are a somewhat unintuitive grouping of things that should be - // considered independently. Dive in without pushing a selection range. - if (isBlock(node) || isTemplateSpan(node) || isTemplateHead(node) || prevNode && isTemplateHead(prevNode)) { - parentNode = node; - break; - } - - // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. - if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { - const start = node.getFullStart() - "${".length; - const end = nextNode.getStart() + "}".length; - pushSelectionRange(start, end, node.kind); - } - // Synthesize a stop for group of adjacent imports - else if (isImport(node)) { - const [firstImportIndex, lastImportIndex] = getGroupBounds(children, i, isImport); - pushSelectionRange( - children[firstImportIndex].getStart(), - children[lastImportIndex].getEnd()); - } - - // Blocks with braces on separate lines should be selected from brace to brace, - // including whitespace but not including the braces themselves. - const isBetweenMultiLineBraces = isSyntaxList(node) - && prevNode && prevNode.kind === SyntaxKind.OpenBraceToken - && nextNode && nextNode.kind === SyntaxKind.CloseBraceToken - && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); - const start = isBetweenMultiLineBraces ? prevNode.getEnd() : node.getStart(); - const end = isBetweenMultiLineBraces ? nextNode.getStart() : node.getEnd(); - 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 - // the “key/value” pair like ObjectTypes do, but it seems intuitive - // that the selection would snap to those points. The philosophy - // of choosing a selection range is not so much about what the - // syntax currently _is_ as what the syntax might easily become - // if the user is making a selection; e.g., we synthesize a selection - // around the “key/value” pair not because there’s a node there, but - // because it allows the mapped type to become an object type with a - // few keystrokes. - if (isMappedTypeNode(node)) { - const openBraceToken = Debug.assertDefined(node.getFirstToken()); - const firstNonBraceToken = Debug.assertDefined(node.getChildAt(1)); - const closeBraceToken = Debug.assertDefined(node.getLastToken()); - Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); - Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); - 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. - else if (isStringLiteral(node) || isTemplateLiteral(node)) { - pushSelectionRange(start + 1, end - 1); - } - - parentNode = node; - break; - } - } - } - return selectionRange; + getSelectionRange(pos, sourceFile, pushSelectionRange); + return selectionRange!; }); } diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 3cf28ab40eebc..d4f2edb39bea8 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,27 +1,28 @@ -{ - "extends": "../tsconfig-base", - "compilerOptions": { - "removeComments": false, - "outFile": "../../built/local/server.js", - "preserveConstEnums": true, - "types": [ - "node" - ] - }, - "references": [ - { "path": "../compiler" }, - { "path": "../jsTyping" }, - { "path": "../services" } - ], - "files": [ - "types.ts", - "utilities.ts", - "protocol.ts", - "scriptInfo.ts", - "typingsCache.ts", - "project.ts", - "editorServices.ts", - "session.ts", - "scriptVersionCache.ts" - ] -} +{ + "extends": "../tsconfig-base", + "compilerOptions": { + "removeComments": false, + "outFile": "../../built/local/server.js", + "preserveConstEnums": true, + "types": [ + "node" + ] + }, + "references": [ + { "path": "../compiler" }, + { "path": "../jsTyping" }, + { "path": "../services" } + ], + "files": [ + "types.ts", + "utilities.ts", + "protocol.ts", + "scriptInfo.ts", + "typingsCache.ts", + "project.ts", + "editorServices.ts", + "selectionRange.ts", + "session.ts", + "scriptVersionCache.ts" + ] +} diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index b4858407ee8eb..c2261e041e0f3 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -164,38 +164,34 @@ type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAn textSpan: { // [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; start: { line: 2, offset: 54 }, end: { line: 2, offset: 143 } }, - parent: { // same as above + whitespace - textSpan: { - start: { line: 2, offset: 53 }, - end: { line: 2, offset: 144 } }, + parent: { + textSpan: { // MappedType: same as above + braces + start: { line: 2, offset: 52 }, + end: { line: 2, offset: 145 } }, parent: { - textSpan: { // MappedType: same as above + braces + textSpan: { // IntersectionType: { [K in keyof P]: ... } & Pick> start: { line: 2, offset: 52 }, - end: { line: 2, offset: 145 } }, + end: { line: 2, offset: 182 } }, parent: { - textSpan: { // IntersectionType: { [K in keyof P]: ... } & Pick> - start: { line: 2, offset: 52 }, - end: { line: 2, offset: 182 } }, + textSpan: { // same as above + parens + start: { line: 2, offset: 51 }, + end: { line: 2, offset: 183 } }, parent: { - textSpan: { // same as above + parens - start: { line: 2, offset: 51 }, + textSpan: { // Whole TypeNode of TypeAliasDeclaration + start: { line: 2, offset: 16 }, end: { line: 2, offset: 183 } }, parent: { - textSpan: { // Whole TypeNode of TypeAliasDeclaration - start: { line: 2, offset: 16 }, + textSpan: { // Whole TypeAliasDeclaration + start: { line: 2, offset: 1 }, end: { line: 2, offset: 183 } }, parent: { - textSpan: { // Whole TypeAliasDeclaration - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 183 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 184 } } } } } } } } } } } } } }, + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 184 } } } } } } } } } } } } }, ]); }); - it.skip("works for object types", () => { + it("works for object types", () => { const getSelectionRange = setup("/file.js", ` type X = { foo?: string; From 0f7bc0289230a78a4ebc7b21005761034c87b665 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Apr 2019 17:32:40 -0700 Subject: [PATCH 08/26] Move to language service --- src/harness/client.ts | 4 ++ src/harness/harnessLanguageService.ts | 3 ++ src/server/protocol.ts | 2 + src/server/session.ts | 46 +++++++++------------- src/server/tsconfig.json | 1 - src/{server => services}/selectionRange.ts | 31 +++++++++++++-- src/services/services.ts | 5 +++ src/services/shims.ts | 8 ++++ src/services/tsconfig.json | 1 + src/services/types.ts | 7 ++++ 10 files changed, 76 insertions(+), 32 deletions(-) rename src/{server => services}/selectionRange.ts (77%) diff --git a/src/harness/client.ts b/src/harness/client.ts index 03d5334461688..bb9e674bbc666 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -424,6 +424,10 @@ namespace ts.server { return renameInfo; } + getSelectionRange() { + return notImplemented(); + } + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { if (!this.lastRenameEntry || this.lastRenameEntry.inputs.fileName !== fileName || diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index b70afecb7916d..fff02e5b94a71 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -472,6 +472,9 @@ namespace Harness.LanguageService { getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); } + getSelectionRange(fileName: string, position: number): ts.SelectionRange { + return unwrapJSONCallResult(this.shim.getSelectionRange(fileName, position)); + } findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1709c6196ef83..02ebee1efc57b 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -132,6 +132,8 @@ namespace ts.server.protocol { GetEditsForFileRenameFull = "getEditsForFileRename-full", ConfigurePlugin = "configurePlugin", SelectionRange = "selectionRange", + /* @internal */ + SelectionRangeFull = "selectionRange-full", // NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`. } diff --git a/src/server/session.ts b/src/server/session.ts index 68c34d27b2666..261356f2cd06f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1744,14 +1744,6 @@ namespace ts.server { }; } - private locationsAreEqual(a: protocol.Location, b: protocol.Location): boolean { - return a.line === b.line && a.offset === b.offset; - } - - private locationTextSpansAreEqual(a: protocol.TextSpan, b: protocol.TextSpan): boolean { - return this.locationsAreEqual(a.start, b.start) && this.locationsAreEqual(a.end, b.end); - } - private getNavigationTree(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationTree | NavigationTree | undefined { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const tree = languageService.getNavigationTree(file); @@ -2067,33 +2059,28 @@ namespace ts.server { this.projectService.configurePlugin(args); } - private getSelectionRange(args: protocol.SelectionRangeRequestArgs): protocol.SelectionRange[] { + private getSelectionRange(args: protocol.SelectionRangeRequestArgs, simplifiedResult: boolean) { const { locations } = args; const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); - - - const sourceFile = languageService.getNonBoundSourceFile(file); const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); return map(locations, location => { const pos = this.getPosition(location, scriptInfo); - let selectionRange: protocol.SelectionRange | undefined; - 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 (!selectionRange || !this.locationTextSpansAreEqual(textSpan, selectionRange.textSpan)) { - selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; - if (syntaxKind) { - Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) }); - } - } - }; - - getSelectionRange(pos, sourceFile, pushSelectionRange); - return selectionRange!; + const selectionRange = languageService.getSelectionRange(file, pos); + return simplifiedResult ? this.mapSelectionRange(selectionRange, scriptInfo) : selectionRange; }); } + private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { + const result: protocol.SelectionRange = { + textSpan: this.toLocationTextSpan(selectionRange.textSpan, scriptInfo), + }; + if (selectionRange.parent) { + result.parent = this.mapSelectionRange(selectionRange.parent, scriptInfo); + } + return result; + } + getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); return normalizePath(name); @@ -2451,8 +2438,11 @@ namespace ts.server { return this.notRequired(); }, [CommandNames.SelectionRange]: (request: protocol.SelectionRangeRequest) => { - return this.requiredResponse(this.getSelectionRange(request.arguments)); - } + return this.requiredResponse(this.getSelectionRange(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.SelectionRangeFull]: (request: protocol.SelectionRangeRequest) => { + return this.requiredResponse(this.getSelectionRange(request.arguments, /*simplifiedResult*/ false)); + }, }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index d4f2edb39bea8..1410440512b49 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -21,7 +21,6 @@ "typingsCache.ts", "project.ts", "editorServices.ts", - "selectionRange.ts", "session.ts", "scriptVersionCache.ts" ] diff --git a/src/server/selectionRange.ts b/src/services/selectionRange.ts similarity index 77% rename from src/server/selectionRange.ts rename to src/services/selectionRange.ts index 60027e330dc55..7abff8cda8837 100644 --- a/src/server/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -1,9 +1,11 @@ /* @internal */ -namespace ts.server { +namespace ts.SelectionRange { const isImport = or(isImportDeclaration, isImportEqualsDeclaration); - export function getSelectionRange(pos: number, sourceFile: SourceFile, pushSelectionRange: (start: number, end: number, kind?: SyntaxKind) => void) { - pushSelectionRange(sourceFile.getFullStart(), sourceFile.getEnd(), SyntaxKind.SourceFile); + export function getSelectionRange(pos: number, sourceFile: SourceFile): ts.SelectionRange { + let selectionRange: SelectionRange = { + textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; // Skip top-level SyntaxList let parentNode = sourceFile.getChildAt(0); @@ -86,8 +88,31 @@ namespace ts.server { } } } + + return selectionRange; + + function pushSelectionRange(start: number, end: number, syntaxKind?: SyntaxKind): void { + // Skip ranges that are identical to the parent + const textSpan = createTextSpanFromBounds(start, end); + if (!selectionRange || !textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + if (syntaxKind) { + Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) }); + } + } + } } + // function getSiblingExpansionRule(parentNode: T): (SyntaxKind | SyntaxKind[])[] | undefined { + // switch (parentNode.kind) { + // case SyntaxKind.BindingElement: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken]; + // case SyntaxKind.Parameter: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken, SyntaxKind.QuestionToken]; + // case SyntaxKind.PropertySignature: return [SyntaxKind.Identifier, SyntaxKind.QuestionToken, SyntaxKind.SyntaxList]; + // case SyntaxKind.ElementAccessExpression: return [[SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken]]; + // case SyntaxKind.IndexedAccessType: return [[SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken]]; + // } + // } + function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { let first = index; let last = index; diff --git a/src/services/services.ts b/src/services/services.ts index 5f0d3472a3e93..721777c2bad44 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2087,6 +2087,10 @@ namespace ts { }; } + function getSelectionRange(fileName: string, position: number): SelectionRange { + return SelectionRange.getSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + } + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] { synchronizeHostData(); const file = getValidSourceFile(fileName); @@ -2134,6 +2138,7 @@ namespace ts { getBreakpointStatementAtPosition, getNavigateToItems, getRenameInfo, + getSelectionRange, findRenameLocations, getNavigationBarItems, getNavigationTree, diff --git a/src/services/shims.ts b/src/services/shims.ts index 208cf3dbc3c01..3f3b3c2bef652 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -165,6 +165,7 @@ namespace ts { * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } */ getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string; + getSelectionRange(fileName: string, position: number): string; /** * Returns a JSON-encoded value of the type: @@ -838,6 +839,13 @@ namespace ts { ); } + public getSelectionRange(fileName: string, position: number): string { + return this.forwardJSONCall( + `getSelectionRange('${fileName}', ${position})`, + () => this.languageService.getSelectionRange(fileName, position) + ); + } + public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string { return this.forwardJSONCall( `findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments}, ${providePrefixAndSuffixTextForRename})`, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index e3f2358be1059..a456fbb957f16 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -28,6 +28,7 @@ "patternMatcher.ts", "preProcess.ts", "rename.ts", + "selectionRange.ts", "signatureHelp.ts", "sourcemaps.ts", "suggestionDiagnostics.ts", diff --git a/src/services/types.ts b/src/services/types.ts index 3502e3e06712f..d3c9de0f332ca 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -297,6 +297,8 @@ namespace ts { getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; + getSelectionRange(fileName: string, position: number): SelectionRange; + getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -859,6 +861,11 @@ namespace ts { isOptional: boolean; } + export interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } + /** * Represents a single signature to show in signature help. * The id is used for subsequent calls into the language service to ask questions about the From 70e2672ab3b765672de00502ae8f6347669423ea Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 12 Apr 2019 10:53:20 -0700 Subject: [PATCH 09/26] Add rules for expanding selection to sibling nodes --- src/services/selectionRange.ts | 51 +++++++++++--- .../unittests/tsserver/selectionRange.ts | 67 ++++++++++--------- 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 7abff8cda8837..3a3c0cba08662 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -13,9 +13,9 @@ namespace ts.SelectionRange { const children = parentNode.getChildren(sourceFile); if (!children.length) break; for (let i = 0; i < children.length; i++) { - const prevNode: Node | undefined = children[i - 1]; + let prevNode: Node | undefined = children[i - 1]; const node: Node = children[i]; - const nextNode: Node | undefined = children[i + 1]; + let nextNode: Node | undefined = children[i + 1]; if (node.getStart(sourceFile) > pos) { break outer; } @@ -30,6 +30,26 @@ namespace ts.SelectionRange { break; } + const siblingExpansionRule = getSiblingExpansionRule(parentNode); + let expansionCandidate: SyntaxKind | [SyntaxKind, SyntaxKind] | undefined; + while ((prevNode || nextNode) && (expansionCandidate = siblingExpansionRule.shift())) { + if (isArray(expansionCandidate) + && prevNode && prevNode.kind === expansionCandidate[0] + && nextNode && nextNode.kind === expansionCandidate[1]) { + pushSelectionRange(prevNode.getStart(), nextNode.getEnd()); + prevNode = children[children.indexOf(prevNode) - 1]; + nextNode = children[children.indexOf(nextNode) + 1]; + } + else if (prevNode && prevNode.kind === expansionCandidate) { + pushSelectionRange(prevNode.getStart(), node.getEnd()); + prevNode = children[children.indexOf(prevNode) - 1]; + } + else if (nextNode && nextNode.kind === expansionCandidate) { + pushSelectionRange(node.getStart(), nextNode.getEnd()); + nextNode = children[children.indexOf(nextNode) + 1]; + } + } + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { const start = node.getFullStart() - "${".length; @@ -103,15 +123,24 @@ namespace ts.SelectionRange { } } - // function getSiblingExpansionRule(parentNode: T): (SyntaxKind | SyntaxKind[])[] | undefined { - // switch (parentNode.kind) { - // case SyntaxKind.BindingElement: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken]; - // case SyntaxKind.Parameter: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken, SyntaxKind.QuestionToken]; - // case SyntaxKind.PropertySignature: return [SyntaxKind.Identifier, SyntaxKind.QuestionToken, SyntaxKind.SyntaxList]; - // case SyntaxKind.ElementAccessExpression: return [[SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken]]; - // case SyntaxKind.IndexedAccessType: return [[SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken]]; - // } - // } + function getSiblingExpansionRule(parentNode: T): (SyntaxKind | [SyntaxKind, SyntaxKind])[] { + switch (parentNode.kind) { + case SyntaxKind.BindingElement: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken]; + case SyntaxKind.Parameter: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken, SyntaxKind.QuestionToken]; + case SyntaxKind.PropertySignature: return [SyntaxKind.Identifier, SyntaxKind.QuestionToken, SyntaxKind.SyntaxList]; + case SyntaxKind.ElementAccessExpression: return [[SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken]]; + case SyntaxKind.MappedType: return [ + SyntaxKind.TypeParameter, + [SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken], + SyntaxKind.MinusToken, + SyntaxKind.PlusToken, + SyntaxKind.ReadonlyKeyword, + SyntaxKind.MinusToken, + SyntaxKind.PlusToken, + ]; + default: return []; + } + } function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { let first = index; diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index c2261e041e0f3..88bc0a6ad10ca 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -231,44 +231,45 @@ type X = { end: { line: 4, offset: 33 } }, parent: allMembersUp } }; - assert.deepEqual(locations, [ - { - textSpan: { // foo + assert.deepEqual(locations![0], { + textSpan: { // foo + start: { line: 3, offset: 5 }, + end: { line: 3, offset: 8 } }, + parent: { + textSpan: { // foo? start: { line: 3, offset: 5 }, - end: { line: 3, offset: 8 } }, + end: { line: 3, offset: 9 } }, parent: { - textSpan: { // foo? + textSpan: { // foo?: string; 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 }, - { - textSpan: { // number - start: { line: 4, offset: 24 }, + end: { line: 3, offset: 18 } }, + parent: allMembersUp } } }); + + assert.deepEqual(locations![1], { + textSpan: { // readonly + start: { line: 4, offset: 5 }, + end: { line: 4, offset: 13 } }, + parent: readonlyBarUp }); + + assert.deepEqual(locations![2], { + textSpan: { // bar + start: { line: 4, offset: 14 }, + end: { line: 4, offset: 17 } }, + parent: readonlyBarUp }); + + assert.deepEqual(locations![3], { + textSpan: { // number + start: { line: 4, offset: 24 }, + end: { line: 4, offset: 30 } }, + parent: { + textSpan: { // x: number + start: { line: 4, offset: 21 }, end: { line: 4, offset: 30 } }, parent: { - textSpan: { // x: number - start: { line: 4, offset: 21 }, - end: { line: 4, offset: 30 } }, - parent: { - textSpan: { // { x: number } - start: { line: 4, offset: 19 }, - end: { line: 4, offset: 32 } }, - parent: readonlyBarUp } } }, - ]); + textSpan: { // { x: number } + start: { line: 4, offset: 19 }, + end: { line: 4, offset: 32 } }, + parent: readonlyBarUp.parent } } }); }); it("works for string literals and template strings", () => { From fcb7f0152fc8d430510b6b94e52c49e341f0920d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 14 Apr 2019 13:58:04 -0700 Subject: [PATCH 10/26] Rethink sibling expansion by creating fake subtrees --- src/compiler/utilities.ts | 1 + src/services/selectionRange.ts | 110 ++++++++---------- .../unittests/tsserver/selectionRange.ts | 50 +++++++- 3 files changed, 96 insertions(+), 65 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 2e6c3ae4f866e..91f6e3b421865 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7624,6 +7624,7 @@ namespace ts { return root + pathComponents.slice(1).join(directorySeparator); } + } /* @internal */ diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 3a3c0cba08662..631ec25fc0930 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -2,7 +2,7 @@ namespace ts.SelectionRange { const isImport = or(isImportDeclaration, isImportEqualsDeclaration); - export function getSelectionRange(pos: number, sourceFile: SourceFile): ts.SelectionRange { + export function getSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { let selectionRange: SelectionRange = { textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) }; @@ -10,12 +10,12 @@ namespace ts.SelectionRange { // Skip top-level SyntaxList let parentNode = sourceFile.getChildAt(0); outer: while (true) { - const children = parentNode.getChildren(sourceFile); + const children = getSelectionChildren(parentNode); if (!children.length) break; for (let i = 0; i < children.length; i++) { - let prevNode: Node | undefined = children[i - 1]; + const prevNode: Node | undefined = children[i - 1]; const node: Node = children[i]; - let nextNode: Node | undefined = children[i + 1]; + const nextNode: Node | undefined = children[i + 1]; if (node.getStart(sourceFile) > pos) { break outer; } @@ -30,26 +30,6 @@ namespace ts.SelectionRange { break; } - const siblingExpansionRule = getSiblingExpansionRule(parentNode); - let expansionCandidate: SyntaxKind | [SyntaxKind, SyntaxKind] | undefined; - while ((prevNode || nextNode) && (expansionCandidate = siblingExpansionRule.shift())) { - if (isArray(expansionCandidate) - && prevNode && prevNode.kind === expansionCandidate[0] - && nextNode && nextNode.kind === expansionCandidate[1]) { - pushSelectionRange(prevNode.getStart(), nextNode.getEnd()); - prevNode = children[children.indexOf(prevNode) - 1]; - nextNode = children[children.indexOf(nextNode) + 1]; - } - else if (prevNode && prevNode.kind === expansionCandidate) { - pushSelectionRange(prevNode.getStart(), node.getEnd()); - prevNode = children[children.indexOf(prevNode) - 1]; - } - else if (nextNode && nextNode.kind === expansionCandidate) { - pushSelectionRange(node.getStart(), nextNode.getEnd()); - nextNode = children[children.indexOf(nextNode) + 1]; - } - } - // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { const start = node.getFullStart() - "${".length; @@ -74,32 +54,8 @@ namespace ts.SelectionRange { const end = isBetweenMultiLineBraces ? nextNode.getStart() : node.getEnd(); 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 - // the “key/value” pair like ObjectTypes do, but it seems intuitive - // that the selection would snap to those points. The philosophy - // of choosing a selection range is not so much about what the - // syntax currently _is_ as what the syntax might easily become - // if the user is making a selection; e.g., we synthesize a selection - // around the “key/value” pair not because there’s a node there, but - // because it allows the mapped type to become an object type with a - // few keystrokes. - if (isMappedTypeNode(node)) { - const openBraceToken = Debug.assertDefined(node.getFirstToken()); - const firstNonBraceToken = Debug.assertDefined(node.getChildAt(1)); - const closeBraceToken = Debug.assertDefined(node.getLastToken()); - Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); - Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); - const spanWithoutBraces = [openBraceToken.getEnd(), closeBraceToken.getStart()] as const; - const spanWithoutBracesOrTrivia = [firstNonBraceToken.getStart(), closeBraceToken.getFullStart()] as const; - if (!positionsAreOnSameLine(openBraceToken.getStart(), closeBraceToken.getEnd(), sourceFile)) { - pushSelectionRange(...spanWithoutBraces); - } - pushSelectionRange(...spanWithoutBracesOrTrivia); - } - // String literals should have a stop both inside and outside their quotes. - else if (isStringLiteral(node) || isTemplateLiteral(node)) { + if (isStringLiteral(node) || isTemplateLiteral(node)) { pushSelectionRange(start + 1, end - 1); } @@ -123,23 +79,49 @@ namespace ts.SelectionRange { } } - function getSiblingExpansionRule(parentNode: T): (SyntaxKind | [SyntaxKind, SyntaxKind])[] { - switch (parentNode.kind) { - case SyntaxKind.BindingElement: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken]; - case SyntaxKind.Parameter: return [SyntaxKind.Identifier, SyntaxKind.DotDotDotToken, SyntaxKind.QuestionToken]; - case SyntaxKind.PropertySignature: return [SyntaxKind.Identifier, SyntaxKind.QuestionToken, SyntaxKind.SyntaxList]; - case SyntaxKind.ElementAccessExpression: return [[SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken]]; - case SyntaxKind.MappedType: return [ - SyntaxKind.TypeParameter, - [SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken], - SyntaxKind.MinusToken, - SyntaxKind.PlusToken, - SyntaxKind.ReadonlyKeyword, - SyntaxKind.MinusToken, - SyntaxKind.PlusToken, + function getSelectionChildren(node: Node): ReadonlyArray { + // Mapped types _look_ like ObjectTypes with a single member, + // but in fact don’t contain a SyntaxList or a node containing + // the “key/value” pair like ObjectTypes do, but it seems intuitive + // that the selection would snap to those points. The philosophy + // of choosing a selection range is not so much about what the + // syntax currently _is_ as what the syntax might easily become + // if the user is making a selection; e.g., we synthesize a selection + // around the “key/value” pair not because there’s a node there, but + // because it allows the mapped type to become an object type with a + // few keystrokes. + if (isMappedTypeNode(node)) { + const [openBraceToken, ...children] = node.getChildren(); + const closeBraceToken = Debug.assertDefined(children.pop()); + Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); + Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); + const colonTokenIndex = findIndex(children, child => child.kind === SyntaxKind.ColonToken); + const typeNodeIndex = node.type && children.indexOf(node.type); + const leftChildren = children.slice(0, colonTokenIndex); + const colonToken = Debug.assertDefined(children[colonTokenIndex]); + const rightChildren = children.slice(colonTokenIndex + 1, typeNodeIndex && (typeNodeIndex + 1)); + // Possible semicolon + const extraChildren = typeNodeIndex && typeNodeIndex > -1 ? children.slice(typeNodeIndex + 1) : []; + const syntaxList = createSyntaxList([ + createSyntaxList(leftChildren), + colonToken, + createSyntaxList(rightChildren), + createSyntaxList(extraChildren), + ]); + return [ + openBraceToken, + syntaxList, + closeBraceToken, ]; - default: return []; } + return node.getChildren(); + } + + function createSyntaxList(children: Node[]): SyntaxList { + Debug.assertGreaterThanOrEqual(children.length, 1); + const syntaxList = createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList; + syntaxList._children = children; + return syntaxList; } function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 88bc0a6ad10ca..837607a58f222 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -191,7 +191,7 @@ type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAn ]); }); - it("works for object types", () => { + it.skip("works for object types", () => { const getSelectionRange = setup("/file.js", ` type X = { foo?: string; @@ -354,5 +354,53 @@ console.log(1);`); end: { line: 5, offset: 16 } } } } } } } } } ]); }); + + it.skip("works for complex mapped types", () => { + const getSelectionRange = setup("/file.ts", ` +type M = { -readonly [K in keyof any]-?: any };`); + + const locations = getSelectionRange([ + { line: 2, offset: 12 }, // -readonly + { line: 2, offset: 14 }, // eadonly + { line: 2, offset: 22 }, // [ + { line: 2, offset: 30 }, // yof any + { line: 2, offset: 38 }, // -? + { line: 2, offset: 39 }, // ? + ]); + + assert.deepEqual(locations![0], { + textSpan: { // - + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 13 } }, + parent: { + textSpan: { // -readonly + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 21 } }, + parent: { + textSpan: { // -readonly [K in keyof any] + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 38 } }, + parent: { + textSpan: { // -readonly [K in keyof any]-? + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 40 } }, + parent: { + textSpan: { // -readonly [K in keyof any]-?: any + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 45 } }, + parent: { + textSpan: { // { -readonly [K in keyof any]-?: any } + start: { line: 2, offset: 10 }, + end: { line: 2, offset: 47 } }, + parent: { + textSpan: { // whole line + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 48 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 48 } } } } } } } } } + }); + }); }); } From 4ecdc82736ee45c539b6fd0ddaa16d9fc7a68f28 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 15 Apr 2019 14:51:52 -0700 Subject: [PATCH 11/26] Solidify fake tree approach --- src/services/selectionRange.ts | 157 ++++++++++++------ .../unittests/tsserver/selectionRange.ts | 127 ++++++++++---- 2 files changed, 204 insertions(+), 80 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 631ec25fc0930..5cf92ec3d6921 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -1,14 +1,11 @@ /* @internal */ namespace ts.SelectionRange { - const isImport = or(isImportDeclaration, isImportEqualsDeclaration); - export function getSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { let selectionRange: SelectionRange = { textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) }; - // Skip top-level SyntaxList - let parentNode = sourceFile.getChildAt(0); + let parentNode: Node = sourceFile; outer: while (true) { const children = getSelectionChildren(parentNode); if (!children.length) break; @@ -36,13 +33,6 @@ namespace ts.SelectionRange { const end = nextNode.getStart() + "}".length; pushSelectionRange(start, end, node.kind); } - // Synthesize a stop for group of adjacent imports - else if (isImport(node)) { - const [firstImportIndex, lastImportIndex] = getGroupBounds(children, i, isImport); - pushSelectionRange( - children[firstImportIndex].getStart(), - children[lastImportIndex].getEnd()); - } // Blocks with braces on separate lines should be selected from brace to brace, // including whitespace but not including the braces themselves. @@ -79,7 +69,22 @@ namespace ts.SelectionRange { } } + const isImport = or(isImportDeclaration, isImportEqualsDeclaration); + + /** + * Gets the children of a node to be considered for selection ranging, + * transforming them into an artificial tree according to their intuitive + * grouping where no grouping actually exists in the parse tree. For example, + * top-level imports are grouped into their own SyntaxList so they can be + * selected all together, even though in the AST they’re just siblings of each + * other as well as of other top-level statements and declarations. + */ function getSelectionChildren(node: Node): ReadonlyArray { + // Group top-level imports + if (isSourceFile(node)) { + return groupChildren(node.getChildAt(0).getChildren(), isImport); + } + // Mapped types _look_ like ObjectTypes with a single member, // but in fact don’t contain a SyntaxList or a node containing // the “key/value” pair like ObjectTypes do, but it seems intuitive @@ -95,58 +100,108 @@ namespace ts.SelectionRange { const closeBraceToken = Debug.assertDefined(children.pop()); Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); - const colonTokenIndex = findIndex(children, child => child.kind === SyntaxKind.ColonToken); - const typeNodeIndex = node.type && children.indexOf(node.type); - const leftChildren = children.slice(0, colonTokenIndex); - const colonToken = Debug.assertDefined(children[colonTokenIndex]); - const rightChildren = children.slice(colonTokenIndex + 1, typeNodeIndex && (typeNodeIndex + 1)); - // Possible semicolon - const extraChildren = typeNodeIndex && typeNodeIndex > -1 ? children.slice(typeNodeIndex + 1) : []; - const syntaxList = createSyntaxList([ - createSyntaxList(leftChildren), - colonToken, - createSyntaxList(rightChildren), - createSyntaxList(extraChildren), - ]); + const [leftOfColon, ...rest] = splitChildren(children, child => child.kind === SyntaxKind.ColonToken); + // Group `-/+readonly` and `-/+?` + const leftChildren = groupChildren(getChildrenOrSingleNode(leftOfColon), child => + child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword || + child === node.questionToken || child.kind === SyntaxKind.QuestionToken); return [ openBraceToken, - syntaxList, + createSyntaxList([ + // Group type parameter with surrounding brackets + createSyntaxList(groupChildren(leftChildren, ({ kind }) => + kind === SyntaxKind.OpenBracketToken || + kind === SyntaxKind.TypeParameter || + kind === SyntaxKind.CloseBracketToken + )), + ...rest, + ]), closeBraceToken, ]; } - return node.getChildren(); - } - function createSyntaxList(children: Node[]): SyntaxList { - Debug.assertGreaterThanOrEqual(children.length, 1); - const syntaxList = createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList; - syntaxList._children = children; - return syntaxList; + // Split e.g. `readonly foo?: string` into left and right sides of the colon, + // the group `readonly foo` without the QuestionToken. + if (isPropertySignature(node)) { + const [leftOfColon, ...rest] = splitChildren(node.getChildren(), child => child.kind === SyntaxKind.ColonToken); + return [ + createSyntaxList(groupChildren(getChildrenOrSingleNode(leftOfColon), child => child !== node.questionToken)), + ...rest, + ]; + } + + return node.getChildren(); } - function getGroupBounds(array: ArrayLike, index: number, predicate: (element: T) => boolean): [number, number] { - let first = index; - let last = index; - let i = index; - while (i > 0) { - const element = array[--i]; - if (predicate(element)) { - first = i; + /** + * Groups sibling nodes together into their own SyntaxList if they + * a) are adjacent, AND b) match a predicate function. + */ + function groupChildren(children: Node[], groupOn: (child: Node) => boolean): Node[] { + const result: Node[] = []; + let group: Node[] | undefined; + for (const child of children) { + if (groupOn(child)) { + group = group || []; + group.push(child); } else { - break; + if (group) { + result.push(createSyntaxList(group)); + group = undefined; + } + result.push(child); } } - i = index; - while (i < array.length - 1) { - const element = array[++i]; - if (predicate(element)) { - last = i; - } - else { - break; - } + if (group) { + result.push(createSyntaxList(group)); + } + + return result; + } + + /** + * Splits sibling nodes into up to four partitions: + * 1) everything left of the first node matched by `pivotOn`, + * 2) the first node matched by `pivotOn`, + * 3) everything right of the first node matched by `pivotOn`, + * 4) a trailing semicolon, if `separateTrailingSemicolon` is enabled. + * The left and right groups, if not empty, will each be grouped into their own containing SyntaxList. + * @param children The sibling nodes to split. + * @param pivotOn The predicate function to match the node to be the pivot. The first node that matches + * the predicate will be used; any others that may match will be included into the right-hand group. + * @param separateTrailingSemicolon If the last token is a semicolon, it will be returned as a separate + * child rather than be included in the right-hand group. + */ + function splitChildren(children: Node[], pivotOn: (child: Node) => boolean, separateTrailingSemicolon = true): Node[] { + if (children.length < 2) { + return children; + } + const splitTokenIndex = findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; } - return [first, last]; + const leftChildren = children.slice(0, splitTokenIndex); + const splitToken = children[splitTokenIndex]; + const lastToken = last(children); + const separateLastToken = separateTrailingSemicolon && lastToken.kind === SyntaxKind.SemicolonToken; + const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + const result = compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; + } + + function getChildrenOrSingleNode(node: Node): Node[] { + return isSyntaxList(node) ? node.getChildren() : [node]; + } + + function createSyntaxList(children: Node[]): SyntaxList { + Debug.assertGreaterThanOrEqual(children.length, 1); + const syntaxList = createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList; + syntaxList._children = children; + return syntaxList; } } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 837607a58f222..0d36690713d79 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -191,35 +191,37 @@ type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAn ]); }); - it.skip("works for object types", () => { + it("works for object types", () => { const getSelectionRange = setup("/file.js", ` type X = { foo?: string; readonly bar: { x: number }; + meh }`); const locations = getSelectionRange([ { line: 3, offset: 5 }, { line: 4, offset: 5 }, { line: 4, offset: 14 }, { line: 4, offset: 27 }, + { line: 5, offset: 5 }, ]); const allMembersUp: protocol.SelectionRange = { textSpan: { // all members + whitespace (just inside braces) start: { line: 2, offset: 11 }, - end: { line: 5, offset: 1 } }, + end: { line: 6, offset: 1 } }, parent: { textSpan: { // add braces start: { line: 2, offset: 10 }, - end: { line: 5, offset: 2 } }, + end: { line: 6, offset: 2 } }, parent: { textSpan: { // whole TypeAliasDeclaration start: { line: 2, offset: 1 }, - end: { line: 5, offset: 2 } }, + end: { line: 6, offset: 2 } }, parent: { textSpan: { // SourceFile start: { line: 1, offset: 1 }, - end: { line: 5, offset: 2 } } } } } }; + end: { line: 6, offset: 2 } } } } } }; const readonlyBarUp: protocol.SelectionRange = { textSpan: { // readonly bar @@ -270,6 +272,12 @@ type X = { start: { line: 4, offset: 19 }, end: { line: 4, offset: 32 } }, parent: readonlyBarUp.parent } } }); + + assert.deepEqual(locations![4], { + textSpan: { // meh + start: { line: 5, offset: 5 }, + end: { line: 5, offset: 8 } }, + parent: allMembersUp }); }); it("works for string literals and template strings", () => { @@ -355,7 +363,7 @@ console.log(1);`); ]); }); - it.skip("works for complex mapped types", () => { + it("works for complex mapped types", () => { const getSelectionRange = setup("/file.ts", ` type M = { -readonly [K in keyof any]-?: any };`); @@ -368,38 +376,99 @@ type M = { -readonly [K in keyof any]-?: any };`); { line: 2, offset: 39 }, // ? ]); + const leftOfColonUp: protocol.SelectionRange = { + textSpan: { // -readonly [K in keyof any]-? + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 40 } }, + parent: { + textSpan: { // -readonly [K in keyof any]-?: any + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 45 } }, + parent: { + textSpan: { // { -readonly [K in keyof any]-?: any } + start: { line: 2, offset: 10 }, + end: { line: 2, offset: 47 } }, + parent: { + textSpan: { // whole line + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 48 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 48 } } } } } } }; + assert.deepEqual(locations![0], { - textSpan: { // - + textSpan: { // - (in -readonly) start: { line: 2, offset: 12 }, end: { line: 2, offset: 13 } }, parent: { textSpan: { // -readonly start: { line: 2, offset: 12 }, end: { line: 2, offset: 21 } }, + parent: leftOfColonUp }, + }); + + assert.deepEqual(locations![1], { + textSpan: { // readonly + start: { line: 2, offset: 13 }, + end: { line: 2, offset: 21 } }, + parent: { + textSpan: { // -readonly + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 21 } }, + parent: leftOfColonUp }, + }); + + assert.deepEqual(locations![2], { + textSpan: { // [ + start: { line: 2, offset: 22 }, + end: { line: 2, offset: 23 } }, + parent: { + textSpan: { // [K in keyof any] + start: { line: 2, offset: 22 }, + end: { line: 2, offset: 38 } }, + parent: leftOfColonUp } + }); + + assert.deepEqual(locations![3], { + textSpan: { // keyof + start: { line: 2, offset: 28 }, + end: { line: 2, offset: 33 } }, + parent: { + textSpan: { // keyof any + start: { line: 2, offset: 28 }, + end: { line: 2, offset: 37 } }, parent: { - textSpan: { // -readonly [K in keyof any] - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 38 } }, + textSpan: { // K in keyof any + start: { line: 2, offset: 23 }, + end: { line: 2, offset: 37 } }, parent: { - textSpan: { // -readonly [K in keyof any]-? - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 40 } }, - parent: { - textSpan: { // -readonly [K in keyof any]-?: any - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 45 } }, - parent: { - textSpan: { // { -readonly [K in keyof any]-?: any } - start: { line: 2, offset: 10 }, - end: { line: 2, offset: 47 } }, - parent: { - textSpan: { // whole line - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 48 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 48 } } } } } } } } } + textSpan: { // [K in keyof any] + start: { line: 2, offset: 22 }, + end: { line: 2, offset: 38 } }, + parent: leftOfColonUp } } }, + }); + + assert.deepEqual(locations![4], { + textSpan: { // - (in -?) + start: { line: 2, offset: 38 }, + end: { line: 2, offset: 39 } }, + parent: { + textSpan: { // -? + start: { line: 2, offset: 38 }, + end: { line: 2, offset: 40 } }, + parent: leftOfColonUp }, + }); + + assert.deepEqual(locations![5], { + textSpan: { // ? + start: { line: 2, offset: 39 }, + end: { line: 2, offset: 40 } }, + parent: { + textSpan: { // -? + start: { line: 2, offset: 38 }, + end: { line: 2, offset: 40 } }, + parent: leftOfColonUp }, }); }); }); From 74fc84ff844c8e4b961b3b9e5dca1086e663b129 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 15 Apr 2019 17:07:38 -0700 Subject: [PATCH 12/26] Snap to nodes directly behind the cursor, create special rules for ParameterNodes --- src/services/selectionRange.ts | 68 ++++++++++++------ .../unittests/tsserver/selectionRange.ts | 70 +++++++++++++++++++ 2 files changed, 115 insertions(+), 23 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 5cf92ec3d6921..07a7abd7d26d9 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -17,7 +17,7 @@ namespace ts.SelectionRange { break outer; } - if (positionBelongsToNode(node, pos, sourceFile)) { + if (positionShouldSnapToNode(pos, node, nextNode, sourceFile)) { // Blocks are effectively redundant with SyntaxLists. // TemplateSpans, along with the SyntaxLists containing them, // are a somewhat unintuitive grouping of things that should be @@ -69,6 +69,28 @@ namespace ts.SelectionRange { } } + /** + * Like `ts.positionBelongsToNode`, except positions immediately after nodes + * count too, unless that position belongs to the next node. In effect, makes + * selections able to snap to preceding tokens when the cursor is on the tail + * end of them with only whitespace ahead. + * @param pos The position to check. + * @param node The candidate node to snap to. + * @param nextNode The next sibling node in the tree. + * @param sourceFile The source file containing the nodes. + */ + function positionShouldSnapToNode(pos: number, node: Node, nextNode: Node | undefined, sourceFile: SourceFile) { + if (positionBelongsToNode(node, pos, sourceFile)) { + return true; + } + const nodeEnd = node.getEnd(); + const nextNodeStart = nextNode && nextNode.getStart(); + if (nodeEnd === pos) { + return pos !== nextNodeStart; + } + return false; + } + const isImport = or(isImportDeclaration, isImportEqualsDeclaration); /** @@ -100,34 +122,38 @@ namespace ts.SelectionRange { const closeBraceToken = Debug.assertDefined(children.pop()); Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); - const [leftOfColon, ...rest] = splitChildren(children, child => child.kind === SyntaxKind.ColonToken); // Group `-/+readonly` and `-/+?` - const leftChildren = groupChildren(getChildrenOrSingleNode(leftOfColon), child => + const groupedWithPlusMinusTokens = groupChildren(children, child => child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword || child === node.questionToken || child.kind === SyntaxKind.QuestionToken); + // Group type parameter with surrounding brackets + const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => + kind === SyntaxKind.OpenBracketToken || + kind === SyntaxKind.TypeParameter || + kind === SyntaxKind.CloseBracketToken + ); return [ openBraceToken, - createSyntaxList([ - // Group type parameter with surrounding brackets - createSyntaxList(groupChildren(leftChildren, ({ kind }) => - kind === SyntaxKind.OpenBracketToken || - kind === SyntaxKind.TypeParameter || - kind === SyntaxKind.CloseBracketToken - )), - ...rest, - ]), + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === SyntaxKind.ColonToken)), closeBraceToken, ]; } - // Split e.g. `readonly foo?: string` into left and right sides of the colon, - // the group `readonly foo` without the QuestionToken. + // Group modifiers and property name, then pivot on `:`. if (isPropertySignature(node)) { - const [leftOfColon, ...rest] = splitChildren(node.getChildren(), child => child.kind === SyntaxKind.ColonToken); - return [ - createSyntaxList(groupChildren(getChildrenOrSingleNode(leftOfColon), child => child !== node.questionToken)), - ...rest, - ]; + const children = groupChildren(node.getChildren(), child => + child === node.name || contains(node.modifiers, child)); + return splitChildren(children, ({ kind }) => kind === SyntaxKind.ColonToken); + } + + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (isParameter(node)) { + const groupedDotDotDotAndName = groupChildren(node.getChildren(), child => + child === node.dotDotDotToken || child === node.name); + const groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName, child => + child === groupedDotDotDotAndName[0] || child === node.questionToken); + return splitChildren(groupedWithQuestionToken, ({ kind }) => kind === SyntaxKind.EqualsToken); } return node.getChildren(); @@ -194,10 +220,6 @@ namespace ts.SelectionRange { return separateLastToken ? result.concat(lastToken) : result; } - function getChildrenOrSingleNode(node: Node): Node[] { - return isSyntaxList(node) ? node.getChildren() : [node]; - } - function createSyntaxList(children: Node[]): SyntaxList { Debug.assertGreaterThanOrEqual(children.length, 1); const syntaxList = createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList; diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 0d36690713d79..5a4bbb41f19ea 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -471,5 +471,75 @@ type M = { -readonly [K in keyof any]-?: any };`); parent: leftOfColonUp }, }); }); + + it("works for parameters", () => { + const getSelectionRange = setup("/file.ts", ` +function f(p, q?, ...r: any[] = []) {}`); + + const locations = getSelectionRange([ + { line: 2, offset: 12 }, // p + { line: 2, offset: 15 }, // q + { line: 2, offset: 19 }, // ... + ]); + + const allParamsUp: protocol.SelectionRange = { + textSpan: { // just inside parens + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 35 } }, + parent: { + textSpan: { + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 39 } }, + parent: { + textSpan: { + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 39 } } } } }; + + assert.deepEqual(locations![0], { + textSpan: { // p + start: { line: 2, offset: 12 }, + end: { line: 2, offset: 13 } }, + parent: allParamsUp, + }); + + assert.deepEqual(locations![1], { + textSpan: { // q + start: { line: 2, offset: 15 }, + end: { line: 2, offset: 16 } }, + parent: { + textSpan: { // q? + start: { line: 2, offset: 15 }, + end: { line: 2, offset: 17 } }, + parent: allParamsUp }, + }); + + assert.deepEqual(locations![2], { + textSpan: { // ... + start: { line: 2, offset: 19 }, + end: { line: 2, offset: 22 } }, + parent: { + textSpan: { // ...r + start: { line: 2, offset: 19 }, + end: { line: 2, offset: 23 } }, + parent: { + textSpan: { // ...r: any[] + start: { line: 2, offset: 19 }, + end: { line: 2, offset: 30 } }, + parent: { + textSpan: { // ...r: any[] = [] + start: { line: 2, offset: 19 }, + end: { line: 2, offset: 35 } }, + parent: allParamsUp } } }, + }); + }); + + it("snaps to nodes directly behind the cursor instead of trivia ahead of the cursor", () => { + const getSelectionRange = setup("/file.ts", `let x: string`); + const locations = getSelectionRange([{ line: 1, offset: 4 }]); + assert.deepEqual(locations![0].textSpan, { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 4 }, + }); + }); }); } From d73eabd35ac852a7fd7532cafc8779bd0b128584 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 16 Apr 2019 11:23:57 -0700 Subject: [PATCH 13/26] Special rules for binding elements, extend brace/whitespace logic to other kinds of bookended lists --- src/services/selectionRange.ts | 44 +++++++++--- .../unittests/tsserver/selectionRange.ts | 72 +++++++++++++++++++ 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 07a7abd7d26d9..536efbfb8d17f 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -21,8 +21,13 @@ namespace ts.SelectionRange { // Blocks are effectively redundant with SyntaxLists. // TemplateSpans, along with the SyntaxLists containing them, // are a somewhat unintuitive grouping of things that should be - // considered independently. Dive in without pushing a selection range. - if (isBlock(node) || isTemplateSpan(node) || isTemplateHead(node) || prevNode && isTemplateHead(prevNode)) { + // considered independently. A VariableStatement’s children are just + // a VaraiableDeclarationList and a semicolon. + // Dive in without pushing a selection range. + if (isBlock(node) + || isTemplateSpan(node) || isTemplateHead(node) + || prevNode && isTemplateHead(prevNode) + || isVariableDeclarationList(node) && isVariableStatement(parentNode)) { parentNode = node; break; } @@ -34,14 +39,14 @@ namespace ts.SelectionRange { pushSelectionRange(start, end, node.kind); } - // Blocks with braces on separate lines should be selected from brace to brace, - // including whitespace but not including the braces themselves. - const isBetweenMultiLineBraces = isSyntaxList(node) - && prevNode && prevNode.kind === SyntaxKind.OpenBraceToken - && nextNode && nextNode.kind === SyntaxKind.CloseBraceToken + // Blocks with braces, brackets, parens, or JSX tags on separate lines should be + // selected from open to close, including whitespace but not including the braces/etc. themselves. + const isBetweenMultiLineBookends = isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); - const start = isBetweenMultiLineBraces ? prevNode.getEnd() : node.getStart(); - const end = isBetweenMultiLineBraces ? nextNode.getStart() : node.getEnd(); + const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + const end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); pushSelectionRange(start, end, node.kind); // String literals should have a stop both inside and outside their quotes. @@ -156,6 +161,11 @@ namespace ts.SelectionRange { return splitChildren(groupedWithQuestionToken, ({ kind }) => kind === SyntaxKind.EqualsToken); } + // Pivot on '=' + if (isBindingElement(node)) { + return splitChildren(node.getChildren(), ({ kind }) => kind === SyntaxKind.EqualsToken); + } + return node.getChildren(); } @@ -226,4 +236,20 @@ namespace ts.SelectionRange { syntaxList._children = children; return syntaxList; } + + function isListOpener(token: Node | undefined): token is Node { + const kind = token && token.kind; + return kind === SyntaxKind.OpenBraceToken + || kind === SyntaxKind.OpenBracketToken + || kind === SyntaxKind.OpenParenToken + || kind === SyntaxKind.JsxOpeningElement; + } + + function isListCloser(token: Node | undefined): token is Node { + const kind = token && token.kind; + return kind === SyntaxKind.CloseBraceToken + || kind === SyntaxKind.CloseBracketToken + || kind === SyntaxKind.CloseParenToken + || kind === SyntaxKind.JsxClosingElement; + } } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 5a4bbb41f19ea..759ae0dd254ce 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -533,6 +533,78 @@ function f(p, q?, ...r: any[] = []) {}`); }); }); + it("works for binding elements", () => { + const getSelectionRange = setup("/file.ts", ` +const { x, y: a, ...zs = {} } = {};`); + const locations = getSelectionRange([ + { line: 2, offset: 9 }, // x + { line: 2, offset: 15 }, // a + { line: 2, offset: 21 }, // zs + ]); + + // Don’t care about checking first two locations, because + // they’re pretty boring, just want to make sure they don’t cause a crash + assert.deepEqual(locations![2], { + textSpan: { // zs + start: { line: 2, offset: 21 }, + end: { line: 2, offset: 23 } }, + parent: { + textSpan: { // ...zs + start: { line: 2, offset: 18 }, + end: { line: 2, offset: 23 } }, + parent: { + textSpan: { // ...zs = {} + start: { line: 2, offset: 18 }, + end: { line: 2, offset: 28 } }, + parent: { + textSpan: { // x, y: a, ...zs = {} + start: { line: 2, offset: 9 }, + end: { line: 2, offset: 28 } }, + parent: { + textSpan: { // { x, y: a, ...zs = {} } + start: { line: 2, offset: 7 }, + end: { line: 2, offset: 30 } }, + parent: { + textSpan: { // { x, y: a, ...zs = {} } = {} + start: { line: 2, offset: 7 }, + end: { line: 2, offset: 35 } }, + parent: { + textSpan: { // whole line + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 36 } }, + parent: { + textSpan: { + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 36 } } } } } } } } }, + }); + }); + + it("consumes all whitespace in a multi-line function parameter list", () => { + const getSelectionRange = setup("/file.ts", ` +function f( + a, + b +) {}`); + const locations = getSelectionRange([{ line: 4, offset: 5 }]); // b + assert.deepEqual(locations, [{ + textSpan: { // b + start: { line: 4, offset: 5 }, + end: { line: 4, offset: 6 } }, + parent: { // all params and whitespace inside parens + textSpan: { + start: { line: 2, offset: 12 }, + end: { line: 5, offset: 1 } }, + parent: { + textSpan: { // whole function declaration + start: { line: 2, offset: 1 }, + end: { line: 5, offset: 5 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 5, offset: 5 } } } } } + }]); + }); + it("snaps to nodes directly behind the cursor instead of trivia ahead of the cursor", () => { const getSelectionRange = setup("/file.ts", `let x: string`); const locations = getSelectionRange([{ line: 1, offset: 4 }]); From fed910fd0c9e911319dd492b51d410c1e655a090 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 14:22:19 -0700 Subject: [PATCH 14/26] Add stop for JSDoc comments --- src/services/selectionRange.ts | 14 ++- .../unittests/tsserver/selectionRange.ts | 85 +++++++++++++------ 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 536efbfb8d17f..1c2a525167ae8 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -17,7 +17,7 @@ namespace ts.SelectionRange { break outer; } - if (positionShouldSnapToNode(pos, node, nextNode, sourceFile)) { + if (positionShouldSnapToNode(pos, node, nextNode)) { // Blocks are effectively redundant with SyntaxLists. // TemplateSpans, along with the SyntaxLists containing them, // are a somewhat unintuitive grouping of things that should be @@ -45,8 +45,12 @@ namespace ts.SelectionRange { && isListOpener(prevNode) && isListCloser(nextNode) && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + const jsDocCommentStart = hasJSDocNodes(node) && node.jsDoc![0].getStart(); const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); const end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); + } pushSelectionRange(start, end, node.kind); // String literals should have a stop both inside and outside their quotes. @@ -84,8 +88,12 @@ namespace ts.SelectionRange { * @param nextNode The next sibling node in the tree. * @param sourceFile The source file containing the nodes. */ - function positionShouldSnapToNode(pos: number, node: Node, nextNode: Node | undefined, sourceFile: SourceFile) { - if (positionBelongsToNode(node, pos, sourceFile)) { + function positionShouldSnapToNode(pos: number, node: Node, nextNode: Node | undefined) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + Debug.assert(node.pos <= pos); + if (pos < node.end) { return true; } const nodeEnd = node.getEnd(); diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 759ae0dd254ce..3c0a12a58f797 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -92,44 +92,47 @@ export interface IService { _serviceBrand: any; open(host: number, data: any): Promise; + bar(): void }`); const locations = getSelectionRange([ - { - line: 5, - offset: 12, - }, + { line: 5, offset: 12 }, // ho/**/st + { line: 6, offset: 16 }, // void/**/ ]); - assert.deepEqual(locations, [ - { - textSpan: { // host + assert.deepEqual(locations![0], { + textSpan: { // host + start: { line: 5, offset: 10 }, + end: { line: 5, offset: 14 } }, + parent: { + textSpan: { // host: number start: { line: 5, offset: 10 }, - end: { line: 5, offset: 14 } }, + end: { line: 5, offset: 22 } }, parent: { - textSpan: { // host: number + textSpan: { // host: number, data: any start: { line: 5, offset: 10 }, - end: { line: 5, offset: 22 } }, + end: { line: 5, offset: 33 } }, parent: { - textSpan: { // host: number, data: any - start: { line: 5, offset: 10 }, - end: { line: 5, offset: 33 } }, + textSpan: { // open(host: number, data: any): Promise; + start: { line: 5, offset: 5 }, + end: { line: 5, offset: 49 } }, parent: { - textSpan: { // open(host: number, data: any): Promise; - start: { line: 5, offset: 5 }, - end: { line: 5, offset: 49 } }, + textSpan: { // SyntaxList + whitespace (body of interface) + start: { line: 2, offset: 28 }, + end: { line: 7, offset: 1 } }, parent: { - textSpan: { // SyntaxList + whitespace (body of interface) - start: { line: 2, offset: 28 }, - end: { line: 6, offset: 1 } }, + textSpan: { // InterfaceDeclaration + start: { line: 2, offset: 1 }, + end: { line: 7, offset: 2 } }, parent: { - textSpan: { // InterfaceDeclaration - start: { line: 2, offset: 1 }, - end: { line: 6, offset: 2 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 6, offset: 2 } } } } } } } } }, - ]); + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 7, offset: 2 } } } } } } } } }); + + // Ensures positions after a zero-width node work, because ts.positionBelongsToNode + // treats them strangely. + assert.deepEqual(locations![1].textSpan, { // void + start: { line: 6, offset: 12 }, + end: { line: 6, offset: 16 }}); }); it("works for complex TypeScript", () => { @@ -613,5 +616,33 @@ function f( end: { line: 1, offset: 4 }, }); }); + + it("creates a stop for JSDoc ranges", () => { + const getSelectionRange = setup("/file.js", "" + +`// Not a JSDoc comment +/** + * @param {number} x The number to square + */ +function square(x) { + return x * x; +}`); + const locations = getSelectionRange([{ line: 5, offset: 10 }]); // square(x) + assert.deepEqual(locations, [{ + textSpan: { // square + start: { line: 5 , offset: 10 }, + end: { line: 5, offset: 16 } }, + parent: { // whole function declaration + textSpan: { + start: { line: 5, offset: 1 }, + end: { line: 7, offset: 2 } }, + parent: { + textSpan: { // add JSDoc + start: { line: 2, offset: 1 }, + end: { line: 7, offset: 2 } }, + parent: { + textSpan: { // SourceFile + start: { line: 1, offset: 1 }, + end: { line: 7, offset: 2 } } } } } }]); + }); }); } From 5479893f009bc92af33413170a0e99a175e5414d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 16:03:53 -0700 Subject: [PATCH 15/26] Skip lone variable declarations --- src/services/selectionRange.ts | 15 ++++++---- .../unittests/tsserver/selectionRange.ts | 30 ++++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 1c2a525167ae8..63a65d42e3b51 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -18,16 +18,19 @@ namespace ts.SelectionRange { } if (positionShouldSnapToNode(pos, node, nextNode)) { - // Blocks are effectively redundant with SyntaxLists. - // TemplateSpans, along with the SyntaxLists containing them, - // are a somewhat unintuitive grouping of things that should be - // considered independently. A VariableStatement’s children are just - // a VaraiableDeclarationList and a semicolon. + // 1. Blocks are effectively redundant with SyntaxLists. + // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping + // of things that should be considered independently. + // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. + // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. + // // Dive in without pushing a selection range. if (isBlock(node) || isTemplateSpan(node) || isTemplateHead(node) || prevNode && isTemplateHead(prevNode) - || isVariableDeclarationList(node) && isVariableStatement(parentNode)) { + || isVariableDeclarationList(node) && isVariableStatement(parentNode) + || isSyntaxList(node) && isVariableDeclarationList(parentNode) + || isVariableDeclaration(node) && isSyntaxList(parentNode) && children.length === 1) { parentNode = node; break; } diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 3c0a12a58f797..7940edcdebb49 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -568,18 +568,13 @@ const { x, y: a, ...zs = {} } = {};`); start: { line: 2, offset: 7 }, end: { line: 2, offset: 30 } }, parent: { - textSpan: { // { x, y: a, ...zs = {} } = {} - start: { line: 2, offset: 7 }, - end: { line: 2, offset: 35 } }, + textSpan: { // whole line + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 36 } }, parent: { - textSpan: { // whole line - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 36 } }, - parent: { - textSpan: { - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 36 } } } } } } } } }, - }); + textSpan: { + start: { line: 1, offset: 1 }, + end: { line: 2, offset: 36 } } } } } } } } }); }); it("consumes all whitespace in a multi-line function parameter list", () => { @@ -644,5 +639,18 @@ function square(x) { start: { line: 1, offset: 1 }, end: { line: 7, offset: 2 } } } } } }]); }); + + it("skips lone VariableDeclarations in a declaration list", () => { + const getSelectionRange = setup("/file.ts", `const x = 3;`); + const locations = getSelectionRange([{ line: 1, offset: 7 }]); // x + assert.deepEqual(locations, [{ + textSpan: { + start: { line: 1, offset: 7 }, + end: { line: 1, offset: 8 } }, + parent: { + textSpan: { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 13 } } } }]); + }); }); } From f0f7d82d7ac4f8091ca1ffdb3425a30017f12808 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 16:04:47 -0700 Subject: [PATCH 16/26] Remove debug info --- src/services/selectionRange.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/services/selectionRange.ts b/src/services/selectionRange.ts index 63a65d42e3b51..0ec0641a8a2cf 100644 --- a/src/services/selectionRange.ts +++ b/src/services/selectionRange.ts @@ -39,7 +39,7 @@ namespace ts.SelectionRange { if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { const start = node.getFullStart() - "${".length; const end = nextNode.getStart() + "}".length; - pushSelectionRange(start, end, node.kind); + pushSelectionRange(start, end); } // Blocks with braces, brackets, parens, or JSX tags on separate lines should be @@ -54,7 +54,7 @@ namespace ts.SelectionRange { if (isNumber(jsDocCommentStart)) { pushSelectionRange(jsDocCommentStart, end); } - pushSelectionRange(start, end, node.kind); + pushSelectionRange(start, end); // String literals should have a stop both inside and outside their quotes. if (isStringLiteral(node) || isTemplateLiteral(node)) { @@ -69,14 +69,11 @@ namespace ts.SelectionRange { return selectionRange; - function pushSelectionRange(start: number, end: number, syntaxKind?: SyntaxKind): void { + function pushSelectionRange(start: number, end: number): void { // Skip ranges that are identical to the parent const textSpan = createTextSpanFromBounds(start, end); if (!selectionRange || !textSpansEqual(textSpan, selectionRange.textSpan)) { selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; - if (syntaxKind) { - Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) }); - } } } } From d8936fd2906c66c294420e35f1dce654eeac6a13 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 16:23:06 -0700 Subject: [PATCH 17/26] Rename to be smarter --- src/harness/client.ts | 2 +- src/harness/harnessLanguageService.ts | 4 +- src/server/session.ts | 8 +-- src/services/services.ts | 6 +-- src/services/shims.ts | 8 +-- .../{selectionRange.ts => smartSelection.ts} | 4 +- src/services/tsconfig.json | 2 +- src/services/types.ts | 2 +- .../unittests/tsserver/selectionRange.ts | 54 +++++++++---------- .../reference/api/tsserverlibrary.d.ts | 24 ++++++++- tests/baselines/reference/api/typescript.d.ts | 5 ++ .../TypeScript-Node-Starter | 2 +- .../user/create-react-app/create-react-app | 2 +- tests/cases/user/prettier/prettier | 2 +- 14 files changed, 76 insertions(+), 49 deletions(-) rename src/services/{selectionRange.ts => smartSelection.ts} (97%) diff --git a/src/harness/client.ts b/src/harness/client.ts index bb9e674bbc666..d223f9f5fe3a5 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -424,7 +424,7 @@ namespace ts.server { return renameInfo; } - getSelectionRange() { + getSmartSelectionRange() { return notImplemented(); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index fff02e5b94a71..03ccbb5d6de93 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -472,8 +472,8 @@ namespace Harness.LanguageService { getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); } - getSelectionRange(fileName: string, position: number): ts.SelectionRange { - return unwrapJSONCallResult(this.shim.getSelectionRange(fileName, position)); + getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange { + return unwrapJSONCallResult(this.shim.getSmartSelectionRange(fileName, position)); } findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ts.RenameLocation[] { return unwrapJSONCallResult(this.shim.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); diff --git a/src/server/session.ts b/src/server/session.ts index 261356f2cd06f..2e9d6948b1a5c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2059,14 +2059,14 @@ namespace ts.server { this.projectService.configurePlugin(args); } - private getSelectionRange(args: protocol.SelectionRangeRequestArgs, simplifiedResult: boolean) { + private getSmartSelectionRange(args: protocol.SelectionRangeRequestArgs, simplifiedResult: boolean) { const { locations } = args; const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file)); return map(locations, location => { const pos = this.getPosition(location, scriptInfo); - const selectionRange = languageService.getSelectionRange(file, pos); + const selectionRange = languageService.getSmartSelectionRange(file, pos); return simplifiedResult ? this.mapSelectionRange(selectionRange, scriptInfo) : selectionRange; }); } @@ -2438,10 +2438,10 @@ namespace ts.server { return this.notRequired(); }, [CommandNames.SelectionRange]: (request: protocol.SelectionRangeRequest) => { - return this.requiredResponse(this.getSelectionRange(request.arguments, /*simplifiedResult*/ true)); + return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.SelectionRangeFull]: (request: protocol.SelectionRangeRequest) => { - return this.requiredResponse(this.getSelectionRange(request.arguments, /*simplifiedResult*/ false)); + return this.requiredResponse(this.getSmartSelectionRange(request.arguments, /*simplifiedResult*/ false)); }, }); diff --git a/src/services/services.ts b/src/services/services.ts index 721777c2bad44..64fe3bca664b9 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2087,8 +2087,8 @@ namespace ts { }; } - function getSelectionRange(fileName: string, position: number): SelectionRange { - return SelectionRange.getSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); + function getSmartSelectionRange(fileName: string, position: number): SelectionRange { + return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); } function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] { @@ -2138,7 +2138,7 @@ namespace ts { getBreakpointStatementAtPosition, getNavigateToItems, getRenameInfo, - getSelectionRange, + getSmartSelectionRange, findRenameLocations, getNavigationBarItems, getNavigationTree, diff --git a/src/services/shims.ts b/src/services/shims.ts index 3f3b3c2bef652..e914a4acd424f 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -165,7 +165,7 @@ namespace ts { * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } */ getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string; - getSelectionRange(fileName: string, position: number): string; + getSmartSelectionRange(fileName: string, position: number): string; /** * Returns a JSON-encoded value of the type: @@ -839,10 +839,10 @@ namespace ts { ); } - public getSelectionRange(fileName: string, position: number): string { + public getSmartSelectionRange(fileName: string, position: number): string { return this.forwardJSONCall( - `getSelectionRange('${fileName}', ${position})`, - () => this.languageService.getSelectionRange(fileName, position) + `getSmartSelectionRange('${fileName}', ${position})`, + () => this.languageService.getSmartSelectionRange(fileName, position) ); } diff --git a/src/services/selectionRange.ts b/src/services/smartSelection.ts similarity index 97% rename from src/services/selectionRange.ts rename to src/services/smartSelection.ts index 0ec0641a8a2cf..ba2ec60e9c55b 100644 --- a/src/services/selectionRange.ts +++ b/src/services/smartSelection.ts @@ -1,6 +1,6 @@ /* @internal */ -namespace ts.SelectionRange { - export function getSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { +namespace ts.SmartSelectionRange { + export function getSmartSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { let selectionRange: SelectionRange = { textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) }; diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index a456fbb957f16..d5977ee46bfbf 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -28,7 +28,7 @@ "patternMatcher.ts", "preProcess.ts", "rename.ts", - "selectionRange.ts", + "smartSelection.ts", "signatureHelp.ts", "sourcemaps.ts", "suggestionDiagnostics.ts", diff --git a/src/services/types.ts b/src/services/types.ts index d3c9de0f332ca..4e784e8160b03 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -297,7 +297,7 @@ namespace ts { getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; - getSelectionRange(fileName: string, position: number): SelectionRange; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/selectionRange.ts index 7940edcdebb49..5d4113d3f973e 100644 --- a/src/testRunner/unittests/tsserver/selectionRange.ts +++ b/src/testRunner/unittests/tsserver/selectionRange.ts @@ -4,7 +4,7 @@ namespace ts.projectSystem { const host = createServerHost([file, libFile]); const session = createSession(host); openFilesForSession([file], session); - return function getSelectionRange(locations: protocol.SelectionRangeRequestArgs["locations"]) { + return function getSmartSelectionRange(locations: protocol.SelectionRangeRequestArgs["locations"]) { return executeSessionRequest( session, CommandNames.SelectionRange, @@ -14,7 +14,7 @@ namespace ts.projectSystem { describe("unittests:: tsserver:: selectionRange", () => { it("works for simple JavaScript", () => { - const getSelectionRange = setup("/file.js", ` + const getSmartSelectionRange = setup("/file.js", ` class Foo { bar(a, b) { if (a === b) { @@ -24,7 +24,7 @@ class Foo { } }`); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 4, offset: 13, @@ -87,14 +87,14 @@ class Foo { }); it("works for simple TypeScript", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` export interface IService { _serviceBrand: any; open(host: number, data: any): Promise; bar(): void }`); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 5, offset: 12 }, // ho/**/st { line: 6, offset: 16 }, // void/**/ ]); @@ -136,10 +136,10 @@ export interface IService { }); it("works for complex TypeScript", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) `); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 2, offset: 133, @@ -195,13 +195,13 @@ type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAn }); it("works for object types", () => { - const getSelectionRange = setup("/file.js", ` + const getSmartSelectionRange = setup("/file.js", ` type X = { foo?: string; readonly bar: { x: number }; meh }`); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 3, offset: 5 }, { line: 4, offset: 5 }, { line: 4, offset: 14 }, @@ -285,8 +285,8 @@ type X = { 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([ + const getSmartSelectionRange = setup("/file.ts", "`a b ${\n 'c'\n} d`"); + const locations = getSmartSelectionRange([ { line: 2, offset: 4 }, { line: 1, offset: 4 }, ]); @@ -327,13 +327,13 @@ type X = { }); it("works for ES2015 import lists", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` import { x as y, z } from './z'; import { b } from './'; console.log(1);`); - const locations = getSelectionRange([{ line: 2, offset: 10 }]); + const locations = getSmartSelectionRange([{ line: 2, offset: 10 }]); assert.deepEqual(locations, [ { textSpan: { // x @@ -367,10 +367,10 @@ console.log(1);`); }); it("works for complex mapped types", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` type M = { -readonly [K in keyof any]-?: any };`); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 2, offset: 12 }, // -readonly { line: 2, offset: 14 }, // eadonly { line: 2, offset: 22 }, // [ @@ -476,10 +476,10 @@ type M = { -readonly [K in keyof any]-?: any };`); }); it("works for parameters", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` function f(p, q?, ...r: any[] = []) {}`); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 2, offset: 12 }, // p { line: 2, offset: 15 }, // q { line: 2, offset: 19 }, // ... @@ -537,9 +537,9 @@ function f(p, q?, ...r: any[] = []) {}`); }); it("works for binding elements", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` const { x, y: a, ...zs = {} } = {};`); - const locations = getSelectionRange([ + const locations = getSmartSelectionRange([ { line: 2, offset: 9 }, // x { line: 2, offset: 15 }, // a { line: 2, offset: 21 }, // zs @@ -578,12 +578,12 @@ const { x, y: a, ...zs = {} } = {};`); }); it("consumes all whitespace in a multi-line function parameter list", () => { - const getSelectionRange = setup("/file.ts", ` + const getSmartSelectionRange = setup("/file.ts", ` function f( a, b ) {}`); - const locations = getSelectionRange([{ line: 4, offset: 5 }]); // b + const locations = getSmartSelectionRange([{ line: 4, offset: 5 }]); // b assert.deepEqual(locations, [{ textSpan: { // b start: { line: 4, offset: 5 }, @@ -604,8 +604,8 @@ function f( }); it("snaps to nodes directly behind the cursor instead of trivia ahead of the cursor", () => { - const getSelectionRange = setup("/file.ts", `let x: string`); - const locations = getSelectionRange([{ line: 1, offset: 4 }]); + const getSmartSelectionRange = setup("/file.ts", `let x: string`); + const locations = getSmartSelectionRange([{ line: 1, offset: 4 }]); assert.deepEqual(locations![0].textSpan, { start: { line: 1, offset: 1 }, end: { line: 1, offset: 4 }, @@ -613,7 +613,7 @@ function f( }); it("creates a stop for JSDoc ranges", () => { - const getSelectionRange = setup("/file.js", "" + + const getSmartSelectionRange = setup("/file.js", "" + `// Not a JSDoc comment /** * @param {number} x The number to square @@ -621,7 +621,7 @@ function f( function square(x) { return x * x; }`); - const locations = getSelectionRange([{ line: 5, offset: 10 }]); // square(x) + const locations = getSmartSelectionRange([{ line: 5, offset: 10 }]); // square(x) assert.deepEqual(locations, [{ textSpan: { // square start: { line: 5 , offset: 10 }, @@ -641,8 +641,8 @@ function square(x) { }); it("skips lone VariableDeclarations in a declaration list", () => { - const getSelectionRange = setup("/file.ts", `const x = 3;`); - const locations = getSelectionRange([{ line: 1, offset: 7 }]); // x + const getSmartSelectionRange = setup("/file.ts", `const x = 3;`); + const locations = getSmartSelectionRange([{ line: 1, offset: 7 }]); // x assert.deepEqual(locations, [{ textSpan: { start: { line: 1, offset: 7 }, diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 1b4acd88ebf79..0cf9780593dac 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4788,6 +4788,7 @@ declare namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -5239,6 +5240,10 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; isOptional: boolean; } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Represents a single signature to show in signature help. * The id is used for subsequent calls into the language service to ask questions about the @@ -5790,7 +5795,8 @@ declare namespace ts.server.protocol { GetEditsForRefactor = "getEditsForRefactor", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", - ConfigurePlugin = "configurePlugin" + ConfigurePlugin = "configurePlugin", + SelectionRange = "selectionRange", } /** * A TypeScript Server message @@ -6753,6 +6759,20 @@ declare namespace ts.server.protocol { } interface ConfigurePluginResponse extends Response { } + interface SelectionRangeRequest extends FileRequest { + command: CommandTypes.SelectionRange; + arguments: SelectionRangeRequestArgs; + } + interface SelectionRangeRequestArgs extends FileRequestArgs { + locations: Location[]; + } + interface SelectionRangeResponse extends Response { + body?: SelectionRange[]; + } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Information found in an "open" request. */ @@ -9026,6 +9046,8 @@ declare namespace ts.server { private getBraceMatching; private getDiagnosticsForProject; private configurePlugin; + private getSmartSelectionRange; + private mapSelectionRange; getCanonicalFileName(fileName: string): string; exit(): void; private notRequired; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c25e1d733c0c8..db24e19b3d8fa 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4788,6 +4788,7 @@ declare namespace ts { getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReadonlyArray | undefined; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; getDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; getTypeDefinitionAtPosition(fileName: string, position: number): ReadonlyArray | undefined; @@ -5239,6 +5240,10 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; isOptional: boolean; } + interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; + } /** * Represents a single signature to show in signature help. * The id is used for subsequent calls into the language service to ask questions about the diff --git a/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter b/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter index 40bdb4eadabc9..46971a8454761 160000 --- a/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter +++ b/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter @@ -1 +1 @@ -Subproject commit 40bdb4eadabc9fbed7d83e3f26817a931c0763b6 +Subproject commit 46971a8454761f1a11d8fde4d96ff8d29bc4e754 diff --git a/tests/cases/user/create-react-app/create-react-app b/tests/cases/user/create-react-app/create-react-app index 1a61db58d434d..9514cb88ab92f 160000 --- a/tests/cases/user/create-react-app/create-react-app +++ b/tests/cases/user/create-react-app/create-react-app @@ -1 +1 @@ -Subproject commit 1a61db58d434d33603f20e73ca643ec83c561b73 +Subproject commit 9514cb88ab92fec7f5df2914702ef23a62c0a249 diff --git a/tests/cases/user/prettier/prettier b/tests/cases/user/prettier/prettier index 1e471a007968b..0b07e1083339e 160000 --- a/tests/cases/user/prettier/prettier +++ b/tests/cases/user/prettier/prettier @@ -1 +1 @@ -Subproject commit 1e471a007968b7490563b91ed6909ae6046f3fe8 +Subproject commit 0b07e1083339e28a8239df3f5245f530cc4fd7f8 From 12492a369e7d973cf89fd9301d0c452c12e218d7 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 16:28:12 -0700 Subject: [PATCH 18/26] Rename test to match --- src/testRunner/tsconfig.json | 2 +- .../unittests/tsserver/{selectionRange.ts => smartSelection.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/testRunner/unittests/tsserver/{selectionRange.ts => smartSelection.ts} (100%) diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 5ae94c80c2316..c4fa16ba71520 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -141,7 +141,7 @@ "unittests/tsserver/reload.ts", "unittests/tsserver/rename.ts", "unittests/tsserver/resolutionCache.ts", - "unittests/tsserver/selectionRange.ts", + "unittests/tsserver/smartSelection.ts", "unittests/tsserver/session.ts", "unittests/tsserver/skipLibCheck.ts", "unittests/tsserver/symLinks.ts", diff --git a/src/testRunner/unittests/tsserver/selectionRange.ts b/src/testRunner/unittests/tsserver/smartSelection.ts similarity index 100% rename from src/testRunner/unittests/tsserver/selectionRange.ts rename to src/testRunner/unittests/tsserver/smartSelection.ts From 511cc79642bed5e0a612a227e9458d8b55ff8b45 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 17:21:52 -0700 Subject: [PATCH 19/26] Revert accidental line break added --- src/compiler/utilities.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 91f6e3b421865..2e6c3ae4f866e 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7624,7 +7624,6 @@ namespace ts { return root + pathComponents.slice(1).join(directorySeparator); } - } /* @internal */ From 99ace033bfdcf3155357e420f19281e0e7721abc Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 17:23:54 -0700 Subject: [PATCH 20/26] Revert accidental line ending change --- src/server/tsconfig.json | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 1410440512b49..3cf28ab40eebc 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,27 +1,27 @@ -{ - "extends": "../tsconfig-base", - "compilerOptions": { - "removeComments": false, - "outFile": "../../built/local/server.js", - "preserveConstEnums": true, - "types": [ - "node" - ] - }, - "references": [ - { "path": "../compiler" }, - { "path": "../jsTyping" }, - { "path": "../services" } - ], - "files": [ - "types.ts", - "utilities.ts", - "protocol.ts", - "scriptInfo.ts", - "typingsCache.ts", - "project.ts", - "editorServices.ts", - "session.ts", - "scriptVersionCache.ts" - ] -} +{ + "extends": "../tsconfig-base", + "compilerOptions": { + "removeComments": false, + "outFile": "../../built/local/server.js", + "preserveConstEnums": true, + "types": [ + "node" + ] + }, + "references": [ + { "path": "../compiler" }, + { "path": "../jsTyping" }, + { "path": "../services" } + ], + "files": [ + "types.ts", + "utilities.ts", + "protocol.ts", + "scriptInfo.ts", + "typingsCache.ts", + "project.ts", + "editorServices.ts", + "session.ts", + "scriptVersionCache.ts" + ] +} From 6177596c270274d18949106cf668254cdb5e0c90 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Apr 2019 17:52:09 -0700 Subject: [PATCH 21/26] Revert accidental submodule change I guess --- .../cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter | 2 +- tests/cases/user/create-react-app/create-react-app | 2 +- tests/cases/user/prettier/prettier | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter b/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter index 46971a8454761..40bdb4eadabc9 160000 --- a/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter +++ b/tests/cases/user/TypeScript-Node-Starter/TypeScript-Node-Starter @@ -1 +1 @@ -Subproject commit 46971a8454761f1a11d8fde4d96ff8d29bc4e754 +Subproject commit 40bdb4eadabc9fbed7d83e3f26817a931c0763b6 diff --git a/tests/cases/user/create-react-app/create-react-app b/tests/cases/user/create-react-app/create-react-app index 9514cb88ab92f..1a61db58d434d 160000 --- a/tests/cases/user/create-react-app/create-react-app +++ b/tests/cases/user/create-react-app/create-react-app @@ -1 +1 @@ -Subproject commit 9514cb88ab92fec7f5df2914702ef23a62c0a249 +Subproject commit 1a61db58d434d33603f20e73ca643ec83c561b73 diff --git a/tests/cases/user/prettier/prettier b/tests/cases/user/prettier/prettier index 0b07e1083339e..1e471a007968b 160000 --- a/tests/cases/user/prettier/prettier +++ b/tests/cases/user/prettier/prettier @@ -1 +1 @@ -Subproject commit 0b07e1083339e28a8239df3f5245f530cc4fd7f8 +Subproject commit 1e471a007968b7490563b91ed6909ae6046f3fe8 From f0a3d2bf92cd59d3c068246109c5832f83d3d150 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 19 Apr 2019 09:44:42 -0700 Subject: [PATCH 22/26] Filter out zero-width selections --- src/services/smartSelection.ts | 11 ++++--- .../unittests/tsserver/smartSelection.ts | 33 ++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/services/smartSelection.ts b/src/services/smartSelection.ts index ba2ec60e9c55b..ddd3c75c2c7de 100644 --- a/src/services/smartSelection.ts +++ b/src/services/smartSelection.ts @@ -70,10 +70,13 @@ namespace ts.SmartSelectionRange { return selectionRange; function pushSelectionRange(start: number, end: number): void { - // Skip ranges that are identical to the parent - const textSpan = createTextSpanFromBounds(start, end); - if (!selectionRange || !textSpansEqual(textSpan, selectionRange.textSpan)) { - selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + // Skip empty ranges + if (start !== end) { + // Skip ranges that are identical to the parent + const textSpan = createTextSpanFromBounds(start, end); + if (!selectionRange || !textSpansEqual(textSpan, selectionRange.textSpan)) { + selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + } } } } diff --git a/src/testRunner/unittests/tsserver/smartSelection.ts b/src/testRunner/unittests/tsserver/smartSelection.ts index 5d4113d3f973e..fca96ab848e01 100644 --- a/src/testRunner/unittests/tsserver/smartSelection.ts +++ b/src/testRunner/unittests/tsserver/smartSelection.ts @@ -12,7 +12,7 @@ namespace ts.projectSystem { }; } - describe("unittests:: tsserver:: selectionRange", () => { + describe("unittests:: tsserver:: smartSelection", () => { it("works for simple JavaScript", () => { const getSmartSelectionRange = setup("/file.js", ` class Foo { @@ -652,5 +652,36 @@ function square(x) { start: { line: 1, offset: 1 }, end: { line: 1, offset: 13 } } } }]); }); + + it("never returns empty ranges", () => { + const getSmartSelectionRange = setup("/file.ts", ` +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +}`); + const locations = getSmartSelectionRange([ + { line: 3, offset: 23 }, // componentDidMount(/**/) + { line: 4, offset: 32 }, // username/**/) + { line: 5, offset: 21 }, // return '/**/' + ]); + + assert.deepEqual(locations![0].textSpan, { // this.props.username + start: { line: 3, offset: 23 }, + end: { line: 3, offset: 24 }, + }); + + assert.deepEqual(locations![1].textSpan, { // this.props.username + start: { line: 4, offset: 32 }, + end: { line: 4, offset: 33 }, + }); + + assert.deepEqual(locations![2].textSpan, { // '' + start: { line: 5, offset: 20 }, + end: { line: 5, offset: 22 }, + }); + }); }); } From 6fc2e4a32e9da29bdf4e4b0fbc1616836d31db6a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 23 Apr 2019 15:34:01 -0700 Subject: [PATCH 23/26] Add custom baseline format for smart selection --- src/harness/fourslash.ts | 52 +++++++++++++++++++++++++----- tests/cases/fourslash/fourslash.ts | 3 +- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 9e117cdfd8fe9..b562f908a6c9e 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1417,12 +1417,7 @@ Actual: ${stringify(fullActual)}`); } public baselineCurrentFileBreakpointLocations() { - let baselineFile = this.testData.globalOptions[MetadataOptionNames.baselineFile]; - if (!baselineFile) { - baselineFile = this.activeFile.fileName.replace(this.basePath + "/breakpointValidation", "bpSpan"); - baselineFile = baselineFile.replace(ts.Extension.Ts, ".baseline"); - - } + const baselineFile = this.getBaselineFileName().replace(this.basePath + "/breakpointValidation", "bpSpan"); Harness.Baseline.runBaseline(baselineFile, this.baselineCurrentFileLocations(pos => this.getBreakpointStatementLocation(pos)!)); } @@ -1497,8 +1492,7 @@ Actual: ${stringify(fullActual)}`); } public baselineQuickInfo() { - const baselineFile = this.testData.globalOptions[MetadataOptionNames.baselineFile] || - ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ".baseline"); + const baselineFile = this.getBaselineFileName(); Harness.Baseline.runBaseline( baselineFile, stringify( @@ -1508,6 +1502,39 @@ Actual: ${stringify(fullActual)}`); })))); } + public baselineSmartSelection() { + const n = ts.sys.newLine; + const baselineFile = this.getBaselineFileName(); + const markers = this.getMarkers(); + const fileContent = this.activeFile.content; + const text = markers.map(marker => { + const baselineContent = [fileContent.slice(0, marker.position) + "/**/" + fileContent.slice(marker.position) + n]; + let selectionRange: ts.SelectionRange | undefined = this.languageService.getSmartSelectionRange(this.activeFile.fileName, marker.position); + while (selectionRange) { + const { textSpan } = selectionRange; + let masked = Array.from(fileContent).map((char, index) => { + const charCode = char.charCodeAt(0); + if (index >= textSpan.start && index < ts.textSpanEnd(textSpan)) { + return char === " " ? "•" : ts.isLineBreak(charCode) ? `↲${n}` : char; + } + return ts.isLineBreak(charCode) ? char : " "; + }).join(""); + masked = masked.replace(/^\s*$\r?\n?/gm, ""); // Remove blank lines + const isRealCharacter = (char: string) => char !== "•" && char !== "↲" && !ts.isWhiteSpaceLike(char.charCodeAt(0)); + const leadingWidth = Array.from(masked).findIndex(isRealCharacter); + const trailingWidth = ts.findLastIndex(Array.from(masked), isRealCharacter); + masked = masked.slice(0, leadingWidth) + + masked.slice(leadingWidth, trailingWidth).replace(/•/g, " ").replace(/↲/g, "") + + masked.slice(trailingWidth); + baselineContent.push(masked); + selectionRange = selectionRange.parent; + } + return baselineContent.join(n); + }).join(n.repeat(2) + "=".repeat(80) + n.repeat(2)); + + Harness.Baseline.runBaseline(baselineFile, text); + } + public printBreakpointLocation(pos: number) { Harness.IO.log("\n**Pos: " + pos + " " + this.spanInfoToString(this.getBreakpointStatementLocation(pos)!, " ")); } @@ -1562,6 +1589,11 @@ Actual: ${stringify(fullActual)}`); Harness.IO.log(stringify(help.items[help.selectedItemIndex])); } + private getBaselineFileName() { + return this.testData.globalOptions[MetadataOptionNames.baselineFile] || + ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ".baseline"); + } + private getSignatureHelp({ triggerReason }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined { return this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, { triggerReason @@ -3973,6 +4005,10 @@ namespace FourSlashInterface { this.state.baselineQuickInfo(); } + public baselineSmartSelection() { + this.state.baselineSmartSelection(); + } + public nameOrDottedNameSpanTextIs(text: string) { this.state.verifyCurrentNameOrDottedNameSpanText(text); } diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 6488748b7286e..4252296d5da48 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -244,6 +244,7 @@ declare namespace FourSlashInterface { baselineGetEmitOutput(insertResultsIntoVfs?: boolean): void; getEmitOutput(expectedOutputFiles: ReadonlyArray): void; baselineQuickInfo(): void; + baselineSmartSelection(): void; nameOrDottedNameSpanTextIs(text: string): void; outliningSpansInCurrentFile(spans: Range[]): void; todoCommentsInCurrentFile(descriptors: string[]): void; @@ -508,7 +509,7 @@ declare namespace FourSlashInterface { readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; } interface CompletionsOptions { - readonly marker?: ArrayOrSingle, + readonly marker?: ArrayOrSingle; readonly isNewIdentifierLocation?: boolean; readonly isGlobalCompletion?: boolean; readonly exact?: ArrayOrSingle; From e28b9b2ba2f9b4857006293db5b9d567a2618754 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 23 Apr 2019 16:41:59 -0700 Subject: [PATCH 24/26] Copy smartSelect tests to fourslash --- src/harness/fourslash.ts | 2 +- .../reference/smartSelection_JSDoc.baseline | 30 +++ .../smartSelection_behindCaret.baseline | 4 + .../smartSelection_bindingPatterns.baseline | 27 +++ .../reference/smartSelection_complex.baseline | 12 ++ .../smartSelection_emptyRanges.baseline | 140 ++++++++++++++ .../smartSelection_functionParams1.baseline | 25 +++ .../smartSelection_functionParams2.baseline | 18 ++ .../reference/smartSelection_imports.baseline | 29 +++ ...Selection_loneVariableDeclaration.baseline | 4 + .../smartSelection_mappedTypes.baseline | 65 +++++++ .../smartSelection_objectTypes.baseline | 174 ++++++++++++++++++ .../reference/smartSelection_simple1.baseline | 116 ++++++++++++ .../reference/smartSelection_simple2.baseline | 63 +++++++ .../smartSelection_templateStrings.baseline | 37 ++++ tests/cases/fourslash/smartSelection_JSDoc.ts | 11 ++ .../fourslash/smartSelection_behindCaret.ts | 6 + .../smartSelection_bindingPatterns.ts | 5 + .../cases/fourslash/smartSelection_complex.ts | 5 + .../fourslash/smartSelection_emptyRanges.ts | 11 ++ .../smartSelection_functionParams1.ts | 5 + .../smartSelection_functionParams2.ts | 8 + .../cases/fourslash/smartSelection_imports.ts | 8 + .../smartSelection_loneVariableDeclaration.ts | 5 + .../fourslash/smartSelection_mappedTypes.ts | 5 + .../fourslash/smartSelection_objectTypes.ts | 9 + .../cases/fourslash/smartSelection_simple1.ts | 12 ++ .../cases/fourslash/smartSelection_simple2.ts | 10 + .../smartSelection_templateStrings.ts | 7 + 29 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/smartSelection_JSDoc.baseline create mode 100644 tests/baselines/reference/smartSelection_behindCaret.baseline create mode 100644 tests/baselines/reference/smartSelection_bindingPatterns.baseline create mode 100644 tests/baselines/reference/smartSelection_complex.baseline create mode 100644 tests/baselines/reference/smartSelection_emptyRanges.baseline create mode 100644 tests/baselines/reference/smartSelection_functionParams1.baseline create mode 100644 tests/baselines/reference/smartSelection_functionParams2.baseline create mode 100644 tests/baselines/reference/smartSelection_imports.baseline create mode 100644 tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline create mode 100644 tests/baselines/reference/smartSelection_mappedTypes.baseline create mode 100644 tests/baselines/reference/smartSelection_objectTypes.baseline create mode 100644 tests/baselines/reference/smartSelection_simple1.baseline create mode 100644 tests/baselines/reference/smartSelection_simple2.baseline create mode 100644 tests/baselines/reference/smartSelection_templateStrings.baseline create mode 100644 tests/cases/fourslash/smartSelection_JSDoc.ts create mode 100644 tests/cases/fourslash/smartSelection_behindCaret.ts create mode 100644 tests/cases/fourslash/smartSelection_bindingPatterns.ts create mode 100644 tests/cases/fourslash/smartSelection_complex.ts create mode 100644 tests/cases/fourslash/smartSelection_emptyRanges.ts create mode 100644 tests/cases/fourslash/smartSelection_functionParams1.ts create mode 100644 tests/cases/fourslash/smartSelection_functionParams2.ts create mode 100644 tests/cases/fourslash/smartSelection_imports.ts create mode 100644 tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts create mode 100644 tests/cases/fourslash/smartSelection_mappedTypes.ts create mode 100644 tests/cases/fourslash/smartSelection_objectTypes.ts create mode 100644 tests/cases/fourslash/smartSelection_simple1.ts create mode 100644 tests/cases/fourslash/smartSelection_simple2.ts create mode 100644 tests/cases/fourslash/smartSelection_templateStrings.ts diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index b562f908a6c9e..66ea1ba38c429 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1529,7 +1529,7 @@ Actual: ${stringify(fullActual)}`); baselineContent.push(masked); selectionRange = selectionRange.parent; } - return baselineContent.join(n); + return baselineContent.join(fileContent.includes("\n") ? n + n : n); }).join(n.repeat(2) + "=".repeat(80) + n.repeat(2)); Harness.Baseline.runBaseline(baselineFile, text); diff --git a/tests/baselines/reference/smartSelection_JSDoc.baseline b/tests/baselines/reference/smartSelection_JSDoc.baseline new file mode 100644 index 0000000000000..6f223644e4366 --- /dev/null +++ b/tests/baselines/reference/smartSelection_JSDoc.baseline @@ -0,0 +1,30 @@ +// Not a JSDoc comment +/** + * @param {number} x The number to square + */ +function /**/square(x) { + return x * x; +} + + + square + + +function square(x) { + return x * x; +} + +/** + * @param {number} x The number to square + */ +function square(x) { + return x * x; +} + +// Not a JSDoc comment +/** + * @param {number} x The number to square + */ +function square(x) { + return x * x; +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_behindCaret.baseline b/tests/baselines/reference/smartSelection_behindCaret.baseline new file mode 100644 index 0000000000000..6b7b393f7ea8d --- /dev/null +++ b/tests/baselines/reference/smartSelection_behindCaret.baseline @@ -0,0 +1,4 @@ +let/**/ x: string + +let +let x: string \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_bindingPatterns.baseline b/tests/baselines/reference/smartSelection_bindingPatterns.baseline new file mode 100644 index 0000000000000..78f3e4a10b74e --- /dev/null +++ b/tests/baselines/reference/smartSelection_bindingPatterns.baseline @@ -0,0 +1,27 @@ +const { /**/x, y: a, ...zs = {} } = {}; + + x + x, y: a, ...zs = {} + { x, y: a, ...zs = {} } +const { x, y: a, ...zs = {} } = {}; + +================================================================================ + +const { x, y: /**/a, ...zs = {} } = {}; + + a + y: a + x, y: a, ...zs = {} + { x, y: a, ...zs = {} } +const { x, y: a, ...zs = {} } = {}; + +================================================================================ + +const { x, y: a, .../**/zs = {} } = {}; + + zs + ...zs + ...zs = {} + x, y: a, ...zs = {} + { x, y: a, ...zs = {} } +const { x, y: a, ...zs = {} } = {}; \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_complex.baseline b/tests/baselines/reference/smartSelection_complex.baseline new file mode 100644 index 0000000000000..c16b1912ae78c --- /dev/null +++ b/tests/baselines/reference/smartSelection_complex.baseline @@ -0,0 +1,12 @@ +type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[/**/K] : P[K]; } & Pick>) + + K + P[K] + K extends keyof T ? T[K] : P[K] + IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K] + [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; + { [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } + { [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick> + ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) + IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) +type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_emptyRanges.baseline b/tests/baselines/reference/smartSelection_emptyRanges.baseline new file mode 100644 index 0000000000000..8ec58d839abbb --- /dev/null +++ b/tests/baselines/reference/smartSelection_emptyRanges.baseline @@ -0,0 +1,140 @@ +class HomePage { + componentDidMount(/**/) { + if (this.props.username) { + return ''; + } + } +} + + + ) + + + componentDidMount() { + if (this.props.username) { + return ''; + } + } + + + ↲ +••componentDidMount() { + if (this.props.username) { + return ''; + } + }↲ + + +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +} + +================================================================================ + +class HomePage { + componentDidMount() { + if (this.props.username/**/) { + return ''; + } + } +} + + + ) + + + if (this.props.username) { + return ''; + } + + + ↲ +••••if (this.props.username) { + return ''; + }↲ +•• + + + componentDidMount() { + if (this.props.username) { + return ''; + } + } + + + ↲ +••componentDidMount() { + if (this.props.username) { + return ''; + } + }↲ + + +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +} + +================================================================================ + +class HomePage { + componentDidMount() { + if (this.props.username) { + return '/**/'; + } + } +} + + + '' + + + return ''; + + + ↲ +••••••return '';↲ +•••• + + + if (this.props.username) { + return ''; + } + + + ↲ +••••if (this.props.username) { + return ''; + }↲ +•• + + + componentDidMount() { + if (this.props.username) { + return ''; + } + } + + + ↲ +••componentDidMount() { + if (this.props.username) { + return ''; + } + }↲ + + +class HomePage { + componentDidMount() { + if (this.props.username) { + return ''; + } + } +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_functionParams1.baseline b/tests/baselines/reference/smartSelection_functionParams1.baseline new file mode 100644 index 0000000000000..1162ceb917a40 --- /dev/null +++ b/tests/baselines/reference/smartSelection_functionParams1.baseline @@ -0,0 +1,25 @@ +function f(/**/p, q?, ...r: any[] = []) {} + + p + p, q?, ...r: any[] = [] +function f(p, q?, ...r: any[] = []) {} + +================================================================================ + +function f(p, /**/q?, ...r: any[] = []) {} + + q + q? + p, q?, ...r: any[] = [] +function f(p, q?, ...r: any[] = []) {} + +================================================================================ + +function f(p, q?, /**/...r: any[] = []) {} + + ... + ...r + ...r: any[] + ...r: any[] = [] + p, q?, ...r: any[] = [] +function f(p, q?, ...r: any[] = []) {} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_functionParams2.baseline b/tests/baselines/reference/smartSelection_functionParams2.baseline new file mode 100644 index 0000000000000..5f355ff0f0e88 --- /dev/null +++ b/tests/baselines/reference/smartSelection_functionParams2.baseline @@ -0,0 +1,18 @@ +function f( + a, + /**/b +) {} + + + b + + + ↲ +••a, + b↲ + + +function f( + a, + b +) {} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_imports.baseline b/tests/baselines/reference/smartSelection_imports.baseline new file mode 100644 index 0000000000000..f3a449ee13e58 --- /dev/null +++ b/tests/baselines/reference/smartSelection_imports.baseline @@ -0,0 +1,29 @@ +import { /**/x as y, z } from './z'; +import { b } from './'; + +console.log(1); + + + x + + + x as y + + + x as y, z + + + { x as y, z } + + +import { x as y, z } from './z'; + + +import { x as y, z } from './z'; +import { b } from './'; + + +import { x as y, z } from './z'; +import { b } from './'; + +console.log(1); \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline b/tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline new file mode 100644 index 0000000000000..df2345bd20f12 --- /dev/null +++ b/tests/baselines/reference/smartSelection_loneVariableDeclaration.baseline @@ -0,0 +1,4 @@ +const /**/x = 3; + + x +const x = 3; \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_mappedTypes.baseline b/tests/baselines/reference/smartSelection_mappedTypes.baseline new file mode 100644 index 0000000000000..5dd6b37d7feef --- /dev/null +++ b/tests/baselines/reference/smartSelection_mappedTypes.baseline @@ -0,0 +1,65 @@ +type M = { /**/-readonly [K in keyof any]-?: any }; + + - + -readonly + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -re/**/adonly [K in keyof any]-?: any }; + + readonly + -readonly + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly /**/[K in keyof any]-?: any }; + + [ + [K in keyof any] + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly [K in ke/**/yof any]-?: any }; + + keyof + keyof any + K in keyof any + [K in keyof any] + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly [K in keyof any]/**/-?: any }; + + - + -? + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; + +================================================================================ + +type M = { -readonly [K in keyof any]-/**/?: any }; + + ? + -? + -readonly [K in keyof any]-? + -readonly [K in keyof any]-?: any + { -readonly [K in keyof any]-?: any } +type M = { -readonly [K in keyof any]-?: any }; \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_objectTypes.baseline b/tests/baselines/reference/smartSelection_objectTypes.baseline new file mode 100644 index 0000000000000..612d5df2faae0 --- /dev/null +++ b/tests/baselines/reference/smartSelection_objectTypes.baseline @@ -0,0 +1,174 @@ +type X = { + /**/foo?: string; + readonly bar: { x: number }; + meh +} + + + foo + + + foo? + + + foo?: string; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + /**/readonly bar: { x: number }; + meh +} + + + readonly + + + readonly bar + + + readonly bar: { x: number }; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + readonly /**/bar: { x: number }; + meh +} + + + bar + + + readonly bar + + + readonly bar: { x: number }; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + readonly bar: { x: num/**/ber }; + meh +} + + + number + + + x: number + + + { x: number } + + + readonly bar: { x: number }; + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} + +================================================================================ + +type X = { + foo?: string; + readonly bar: { x: number }; + /**/meh +} + + + meh + + + ↲ +••foo?: string; + readonly bar: { x: number }; + meh↲ + + + { + foo?: string; + readonly bar: { x: number }; + meh +} + +type X = { + foo?: string; + readonly bar: { x: number }; + meh +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_simple1.baseline b/tests/baselines/reference/smartSelection_simple1.baseline new file mode 100644 index 0000000000000..f28c607c524ba --- /dev/null +++ b/tests/baselines/reference/smartSelection_simple1.baseline @@ -0,0 +1,116 @@ +class Foo { + bar(a, b) { + if (/**/a === b) { + return true; + } + return false; + } +} + + + a + + + a === b + + + if (a === b) { + return true; + } + + + ↲ +••••••if (a === b) { + return true; + } + return false;↲ +•• + + + bar(a, b) { + if (a === b) { + return true; + } + return false; + } + + + ↲ +••bar(a, b) { + if (a === b) { + return true; + } + return false; + }↲ + + +class Foo { + bar(a, b) { + if (a === b) { + return true; + } + return false; + } +} + +================================================================================ + +class Foo { + bar(a, b) { + if (a === b) { + return tr/**/ue; + } + return false; + } +} + + + true + + + return true; + + + ↲ +••••••••••return true;↲ +•••••• + + + if (a === b) { + return true; + } + + + ↲ +••••••if (a === b) { + return true; + } + return false;↲ +•• + + + bar(a, b) { + if (a === b) { + return true; + } + return false; + } + + + ↲ +••bar(a, b) { + if (a === b) { + return true; + } + return false; + }↲ + + +class Foo { + bar(a, b) { + if (a === b) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_simple2.baseline b/tests/baselines/reference/smartSelection_simple2.baseline new file mode 100644 index 0000000000000..2c4b520d449f6 --- /dev/null +++ b/tests/baselines/reference/smartSelection_simple2.baseline @@ -0,0 +1,63 @@ +export interface IService { + _serviceBrand: any; + + open(ho/**/st: number, data: any): Promise; + bar(): void +} + + + host + + + host: number + + + host: number, data: any + + + open(host: number, data: any): Promise; + + + ↲ +••_serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void↲ + + +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void +} + +================================================================================ + +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void/**/ +} + + + void + + + bar(): void + + + ↲ +••_serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void↲ + + +export interface IService { + _serviceBrand: any; + + open(host: number, data: any): Promise; + bar(): void +} \ No newline at end of file diff --git a/tests/baselines/reference/smartSelection_templateStrings.baseline b/tests/baselines/reference/smartSelection_templateStrings.baseline new file mode 100644 index 0000000000000..91fec157842a2 --- /dev/null +++ b/tests/baselines/reference/smartSelection_templateStrings.baseline @@ -0,0 +1,37 @@ +`a /**/b ${ + 'c' +} d` + + + a b ${ + 'c' +} d + +`a b ${ + 'c' +} d` + +================================================================================ + +`a b ${ + '/**/c' +} d` + + + c + + + 'c' + + + ${ + 'c' +} + + a b ${ + 'c' +} d + +`a b ${ + 'c' +} d` \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_JSDoc.ts b/tests/cases/fourslash/smartSelection_JSDoc.ts new file mode 100644 index 0000000000000..2d50810a11e8d --- /dev/null +++ b/tests/cases/fourslash/smartSelection_JSDoc.ts @@ -0,0 +1,11 @@ +/// + +////// Not a JSDoc comment +/////** +//// * @param {number} x The number to square +//// */ +////function /**/square(x) { +//// return x * x; +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_behindCaret.ts b/tests/cases/fourslash/smartSelection_behindCaret.ts new file mode 100644 index 0000000000000..f7bb0ec169def --- /dev/null +++ b/tests/cases/fourslash/smartSelection_behindCaret.ts @@ -0,0 +1,6 @@ +/// + +////let/**/ x: string + +// Verifies that the selection goes to 'let' first even though it’s behind the caret +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_bindingPatterns.ts b/tests/cases/fourslash/smartSelection_bindingPatterns.ts new file mode 100644 index 0000000000000..350f72bac255f --- /dev/null +++ b/tests/cases/fourslash/smartSelection_bindingPatterns.ts @@ -0,0 +1,5 @@ +/// + +////const { /*1*/x, y: /*2*/a, .../*3*/zs = {} } = {}; + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_complex.ts b/tests/cases/fourslash/smartSelection_complex.ts new file mode 100644 index 0000000000000..28797e4833ebe --- /dev/null +++ b/tests/cases/fourslash/smartSelection_complex.ts @@ -0,0 +1,5 @@ +/// + +////type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[/**/K] : P[K]; } & Pick>) + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_emptyRanges.ts b/tests/cases/fourslash/smartSelection_emptyRanges.ts new file mode 100644 index 0000000000000..6f2bfe7a2c000 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_emptyRanges.ts @@ -0,0 +1,11 @@ +/// + +////class HomePage { +//// componentDidMount(/*1*/) { +//// if (this.props.username/*2*/) { +//// return '/*3*/'; +//// } +//// } +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_functionParams1.ts b/tests/cases/fourslash/smartSelection_functionParams1.ts new file mode 100644 index 0000000000000..d2e0350d73abd --- /dev/null +++ b/tests/cases/fourslash/smartSelection_functionParams1.ts @@ -0,0 +1,5 @@ +/// + +////function f(/*1*/p, /*2*/q?, /*3*/...r: any[] = []) {} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_functionParams2.ts b/tests/cases/fourslash/smartSelection_functionParams2.ts new file mode 100644 index 0000000000000..0172763a70ec7 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_functionParams2.ts @@ -0,0 +1,8 @@ +/// + +////function f( +//// a, +//// /**/b +////) {} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_imports.ts b/tests/cases/fourslash/smartSelection_imports.ts new file mode 100644 index 0000000000000..4e800f6973b44 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_imports.ts @@ -0,0 +1,8 @@ +/// + +////import { /**/x as y, z } from './z'; +////import { b } from './'; +//// +////console.log(1); + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts b/tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts new file mode 100644 index 0000000000000..ae50d094ee77f --- /dev/null +++ b/tests/cases/fourslash/smartSelection_loneVariableDeclaration.ts @@ -0,0 +1,5 @@ +/// + +////const /**/x = 3; + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_mappedTypes.ts b/tests/cases/fourslash/smartSelection_mappedTypes.ts new file mode 100644 index 0000000000000..0f76491a59212 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_mappedTypes.ts @@ -0,0 +1,5 @@ +/// + +////type M = { /*1*/-re/*2*/adonly /*3*/[K in ke/*4*/yof any]/*5*/-/*6*/?: any }; + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_objectTypes.ts b/tests/cases/fourslash/smartSelection_objectTypes.ts new file mode 100644 index 0000000000000..3d458c43b4cc7 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_objectTypes.ts @@ -0,0 +1,9 @@ +/// + +////type X = { +//// /*1*/foo?: string; +//// /*2*/readonly /*3*/bar: { x: num/*4*/ber }; +//// /*5*/meh +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_simple1.ts b/tests/cases/fourslash/smartSelection_simple1.ts new file mode 100644 index 0000000000000..97d7249138b8e --- /dev/null +++ b/tests/cases/fourslash/smartSelection_simple1.ts @@ -0,0 +1,12 @@ +/// + +////class Foo { +//// bar(a, b) { +//// if (/*1*/a === b) { +//// return tr/*2*/ue; +//// } +//// return false; +//// } +////} + +verify.baselineSmartSelection(); diff --git a/tests/cases/fourslash/smartSelection_simple2.ts b/tests/cases/fourslash/smartSelection_simple2.ts new file mode 100644 index 0000000000000..85d3252bca0fa --- /dev/null +++ b/tests/cases/fourslash/smartSelection_simple2.ts @@ -0,0 +1,10 @@ +/// + +////export interface IService { +//// _serviceBrand: any; +//// +//// open(ho/*1*/st: number, data: any): Promise; +//// bar(): void/*2*/ +////} + +verify.baselineSmartSelection(); \ No newline at end of file diff --git a/tests/cases/fourslash/smartSelection_templateStrings.ts b/tests/cases/fourslash/smartSelection_templateStrings.ts new file mode 100644 index 0000000000000..e3b311d20d992 --- /dev/null +++ b/tests/cases/fourslash/smartSelection_templateStrings.ts @@ -0,0 +1,7 @@ +/// + +////`a /*1*/b ${ +//// '/*2*/c' +////} d` + +verify.baselineSmartSelection(); \ No newline at end of file From 3e30a7c2ad44293111afbb0f86a4008d0848444e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 23 Apr 2019 16:45:08 -0700 Subject: [PATCH 25/26] Remove all but one server unit test --- .../unittests/tsserver/smartSelection.ts | 669 +----------------- 1 file changed, 24 insertions(+), 645 deletions(-) diff --git a/src/testRunner/unittests/tsserver/smartSelection.ts b/src/testRunner/unittests/tsserver/smartSelection.ts index fca96ab848e01..3c903d7da675f 100644 --- a/src/testRunner/unittests/tsserver/smartSelection.ts +++ b/src/testRunner/unittests/tsserver/smartSelection.ts @@ -12,6 +12,7 @@ namespace ts.projectSystem { }; } + // More tests in fourslash/smartSelection_* describe("unittests:: tsserver:: smartSelection", () => { it("works for simple JavaScript", () => { const getSmartSelectionRange = setup("/file.js", ` @@ -25,663 +26,41 @@ class Foo { }`); const locations = getSmartSelectionRange([ - { - line: 4, - offset: 13, - }, { - line: 5, - offset: 22, - }, + { line: 4, offset: 13 }, // a === b ]); - // Common to results for both locations - const ifStatementUp: protocol.SelectionRange = { - textSpan: { // IfStatement - start: { line: 4, offset: 9 }, - end: { line: 6, offset: 10 } }, + assert.deepEqual(locations, [{ + textSpan: { // a + start: { line: 4, offset: 13 }, + end: { line: 4, offset: 14 } }, parent: { - textSpan: { // SyntaxList + whitespace (body of method) - start: { line: 3, offset: 16 }, - end: { line: 8, offset: 5 } }, - parent: { - textSpan: { // MethodDeclaration - start: { line: 3, offset: 5 }, - end: { line: 8, offset: 6 } }, - parent: { - textSpan: { // SyntaxList + whitespace (body of class) - start: { line: 2, offset: 12 }, - end: { line: 9, offset: 1 } }, - parent: { - textSpan: { // ClassDeclaration - start: { line: 2, offset: 1 }, - end: { line: 9, offset: 2 } }, - parent: { - textSpan: { // SourceFile (all text) - start: { line: 1, offset: 1 }, - end: { line: 9, offset: 2 }, } } } } } } }; - - assert.deepEqual(locations, [ - { - textSpan: { // a + textSpan: { // a === b start: { line: 4, offset: 13 }, - end: { line: 4, offset: 14 } }, - parent: { - textSpan: { // a === b - start: { line: 4, offset: 13 }, - end: { line: 4, offset: 20 } }, - parent: ifStatementUp } }, - { - textSpan: { // true - start: { line: 5, offset: 20 }, - end: { line: 5, offset: 24 } }, - parent: { - textSpan: { // return true; - start: { line: 5, offset: 13 }, - end: { line: 5, offset: 25 } }, - parent: { - textSpan: { // SyntaxList + whitespace (body of IfStatement) - start: { line: 4, offset: 23 }, - end: { line: 6, offset: 9 } }, - parent: ifStatementUp } } } - ]); - }); - - it("works for simple TypeScript", () => { - const getSmartSelectionRange = setup("/file.ts", ` -export interface IService { - _serviceBrand: any; - - open(host: number, data: any): Promise; - bar(): void -}`); - const locations = getSmartSelectionRange([ - { line: 5, offset: 12 }, // ho/**/st - { line: 6, offset: 16 }, // void/**/ - ]); - - assert.deepEqual(locations![0], { - textSpan: { // host - start: { line: 5, offset: 10 }, - end: { line: 5, offset: 14 } }, - parent: { - textSpan: { // host: number - start: { line: 5, offset: 10 }, - end: { line: 5, offset: 22 } }, - parent: { - textSpan: { // host: number, data: any - start: { line: 5, offset: 10 }, - end: { line: 5, offset: 33 } }, - parent: { - textSpan: { // open(host: number, data: any): Promise; - start: { line: 5, offset: 5 }, - end: { line: 5, offset: 49 } }, - parent: { - textSpan: { // SyntaxList + whitespace (body of interface) - start: { line: 2, offset: 28 }, - end: { line: 7, offset: 1 } }, - parent: { - textSpan: { // InterfaceDeclaration - start: { line: 2, offset: 1 }, - end: { line: 7, offset: 2 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 7, offset: 2 } } } } } } } } }); - - // Ensures positions after a zero-width node work, because ts.positionBelongsToNode - // treats them strangely. - assert.deepEqual(locations![1].textSpan, { // void - start: { line: 6, offset: 12 }, - end: { line: 6, offset: 16 }}); - }); - - it("works for complex TypeScript", () => { - const getSmartSelectionRange = setup("/file.ts", ` -type X = IsExactlyAny

extends true ? T : ({ [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick>) -`); - const locations = getSmartSelectionRange([ - { - line: 2, - offset: 133, - }, - ]); - - assert.deepEqual(locations, [ - { - textSpan: { // K - start: { line: 2, offset: 133 }, - end: { line: 2, offset: 134 } }, + end: { line: 4, offset: 20 } }, parent: { - textSpan: { // P[K] - start: { line: 2, offset: 131 }, - end: { line: 2, offset: 135 } }, + textSpan: { // IfStatement + start: { line: 4, offset: 9 }, + end: { line: 6, offset: 10 } }, parent: { - textSpan: { // K extends keyof T ? T[K] : P[K] - start: { line: 2, offset: 104 }, - end: { line: 2, offset: 135 } }, + textSpan: { // SyntaxList + whitespace (body of method) + start: { line: 3, offset: 16 }, + end: { line: 8, offset: 5 } }, parent: { - textSpan: { // IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K] - start: { line: 2, offset: 70 }, - end: { line: 2, offset: 142 } }, + textSpan: { // MethodDeclaration + start: { line: 3, offset: 5 }, + end: { line: 8, offset: 6 } }, parent: { - textSpan: { // [K in keyof P]: IsExactlyAny extends true ? K extends keyof T ? T[K] : P[K] : P[K]; - start: { line: 2, offset: 54 }, - end: { line: 2, offset: 143 } }, + textSpan: { // SyntaxList + whitespace (body of class) + start: { line: 2, offset: 12 }, + end: { line: 9, offset: 1 } }, parent: { - textSpan: { // MappedType: same as above + braces - start: { line: 2, offset: 52 }, - end: { line: 2, offset: 145 } }, - parent: { - textSpan: { // IntersectionType: { [K in keyof P]: ... } & Pick> - start: { line: 2, offset: 52 }, - end: { line: 2, offset: 182 } }, - parent: { - textSpan: { // same as above + parens - start: { line: 2, offset: 51 }, - end: { line: 2, offset: 183 } }, - parent: { - textSpan: { // Whole TypeNode of TypeAliasDeclaration - start: { line: 2, offset: 16 }, - end: { line: 2, offset: 183 } }, - parent: { - textSpan: { // Whole TypeAliasDeclaration - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 183 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 184 } } } } } } } } } } } } }, - ]); - }); - - it("works for object types", () => { - const getSmartSelectionRange = setup("/file.js", ` -type X = { - foo?: string; - readonly bar: { x: number }; - meh -}`); - const locations = getSmartSelectionRange([ - { line: 3, offset: 5 }, - { line: 4, offset: 5 }, - { line: 4, offset: 14 }, - { line: 4, offset: 27 }, - { line: 5, offset: 5 }, - ]); - - const allMembersUp: protocol.SelectionRange = { - textSpan: { // all members + whitespace (just inside braces) - start: { line: 2, offset: 11 }, - end: { line: 6, offset: 1 } }, - parent: { - textSpan: { // add braces - start: { line: 2, offset: 10 }, - end: { line: 6, offset: 2 } }, - parent: { - textSpan: { // whole TypeAliasDeclaration - start: { line: 2, offset: 1 }, - end: { line: 6, offset: 2 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 6, offset: 2 } } } } } }; - - const readonlyBarUp: protocol.SelectionRange = { - textSpan: { // readonly bar - start: { line: 4, offset: 5 }, - end: { line: 4, offset: 17 } }, - parent: { - textSpan: { // readonly bar: { x: number }; - start: { line: 4, offset: 5 }, - end: { line: 4, offset: 33 } }, - parent: allMembersUp } }; - - assert.deepEqual(locations![0], { - 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 } } }); - - assert.deepEqual(locations![1], { - textSpan: { // readonly - start: { line: 4, offset: 5 }, - end: { line: 4, offset: 13 } }, - parent: readonlyBarUp }); - - assert.deepEqual(locations![2], { - textSpan: { // bar - start: { line: 4, offset: 14 }, - end: { line: 4, offset: 17 } }, - parent: readonlyBarUp }); - - assert.deepEqual(locations![3], { - textSpan: { // number - start: { line: 4, offset: 24 }, - end: { line: 4, offset: 30 } }, - parent: { - textSpan: { // x: number - start: { line: 4, offset: 21 }, - end: { line: 4, offset: 30 } }, - parent: { - textSpan: { // { x: number } - start: { line: 4, offset: 19 }, - end: { line: 4, offset: 32 } }, - parent: readonlyBarUp.parent } } }); - - assert.deepEqual(locations![4], { - textSpan: { // meh - start: { line: 5, offset: 5 }, - end: { line: 5, offset: 8 } }, - parent: allMembersUp }); - }); - - it("works for string literals and template strings", () => { - // tslint:disable-next-line:no-invalid-template-strings - const getSmartSelectionRange = setup("/file.ts", "`a b ${\n 'c'\n} d`"); - const locations = getSmartSelectionRange([ - { line: 2, offset: 4 }, - { line: 1, 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 } } } } } } }, - { - 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 } } } }, - ]); - }); - - it("works for ES2015 import lists", () => { - const getSmartSelectionRange = setup("/file.ts", ` -import { x as y, z } from './z'; -import { b } from './'; - -console.log(1);`); - - const locations = getSmartSelectionRange([{ line: 2, offset: 10 }]); - assert.deepEqual(locations, [ - { - textSpan: { // x - start: { line: 2, offset: 10 }, - end: { line: 2, offset: 11 } }, - parent: { - textSpan: { // x as y - start: { line: 2, offset: 10 }, - end: { line: 2, offset: 16 } }, - parent: { - textSpan: { // x as y, z - start: { line: 2, offset: 10 }, - end: { line: 2, offset: 19 } }, - parent: { - textSpan: { // { x as y, z } - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 21 } }, - parent: { - textSpan: { // import { x as y, z } from './z'; - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 33 } }, - parent: { - textSpan: { // all imports + textSpan: { // ClassDeclaration start: { line: 2, offset: 1 }, - end: { line: 3, offset: 24 } }, + end: { line: 9, offset: 2 } }, parent: { - textSpan: { // SourceFile + textSpan: { // SourceFile (all text) start: { line: 1, offset: 1 }, - end: { line: 5, offset: 16 } } } } } } } } } - ]); - }); - - it("works for complex mapped types", () => { - const getSmartSelectionRange = setup("/file.ts", ` -type M = { -readonly [K in keyof any]-?: any };`); - - const locations = getSmartSelectionRange([ - { line: 2, offset: 12 }, // -readonly - { line: 2, offset: 14 }, // eadonly - { line: 2, offset: 22 }, // [ - { line: 2, offset: 30 }, // yof any - { line: 2, offset: 38 }, // -? - { line: 2, offset: 39 }, // ? - ]); - - const leftOfColonUp: protocol.SelectionRange = { - textSpan: { // -readonly [K in keyof any]-? - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 40 } }, - parent: { - textSpan: { // -readonly [K in keyof any]-?: any - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 45 } }, - parent: { - textSpan: { // { -readonly [K in keyof any]-?: any } - start: { line: 2, offset: 10 }, - end: { line: 2, offset: 47 } }, - parent: { - textSpan: { // whole line - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 48 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 48 } } } } } } }; - - assert.deepEqual(locations![0], { - textSpan: { // - (in -readonly) - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 13 } }, - parent: { - textSpan: { // -readonly - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 21 } }, - parent: leftOfColonUp }, - }); - - assert.deepEqual(locations![1], { - textSpan: { // readonly - start: { line: 2, offset: 13 }, - end: { line: 2, offset: 21 } }, - parent: { - textSpan: { // -readonly - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 21 } }, - parent: leftOfColonUp }, - }); - - assert.deepEqual(locations![2], { - textSpan: { // [ - start: { line: 2, offset: 22 }, - end: { line: 2, offset: 23 } }, - parent: { - textSpan: { // [K in keyof any] - start: { line: 2, offset: 22 }, - end: { line: 2, offset: 38 } }, - parent: leftOfColonUp } - }); - - assert.deepEqual(locations![3], { - textSpan: { // keyof - start: { line: 2, offset: 28 }, - end: { line: 2, offset: 33 } }, - parent: { - textSpan: { // keyof any - start: { line: 2, offset: 28 }, - end: { line: 2, offset: 37 } }, - parent: { - textSpan: { // K in keyof any - start: { line: 2, offset: 23 }, - end: { line: 2, offset: 37 } }, - parent: { - textSpan: { // [K in keyof any] - start: { line: 2, offset: 22 }, - end: { line: 2, offset: 38 } }, - parent: leftOfColonUp } } }, - }); - - assert.deepEqual(locations![4], { - textSpan: { // - (in -?) - start: { line: 2, offset: 38 }, - end: { line: 2, offset: 39 } }, - parent: { - textSpan: { // -? - start: { line: 2, offset: 38 }, - end: { line: 2, offset: 40 } }, - parent: leftOfColonUp }, - }); - - assert.deepEqual(locations![5], { - textSpan: { // ? - start: { line: 2, offset: 39 }, - end: { line: 2, offset: 40 } }, - parent: { - textSpan: { // -? - start: { line: 2, offset: 38 }, - end: { line: 2, offset: 40 } }, - parent: leftOfColonUp }, - }); - }); - - it("works for parameters", () => { - const getSmartSelectionRange = setup("/file.ts", ` -function f(p, q?, ...r: any[] = []) {}`); - - const locations = getSmartSelectionRange([ - { line: 2, offset: 12 }, // p - { line: 2, offset: 15 }, // q - { line: 2, offset: 19 }, // ... - ]); - - const allParamsUp: protocol.SelectionRange = { - textSpan: { // just inside parens - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 35 } }, - parent: { - textSpan: { - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 39 } }, - parent: { - textSpan: { - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 39 } } } } }; - - assert.deepEqual(locations![0], { - textSpan: { // p - start: { line: 2, offset: 12 }, - end: { line: 2, offset: 13 } }, - parent: allParamsUp, - }); - - assert.deepEqual(locations![1], { - textSpan: { // q - start: { line: 2, offset: 15 }, - end: { line: 2, offset: 16 } }, - parent: { - textSpan: { // q? - start: { line: 2, offset: 15 }, - end: { line: 2, offset: 17 } }, - parent: allParamsUp }, - }); - - assert.deepEqual(locations![2], { - textSpan: { // ... - start: { line: 2, offset: 19 }, - end: { line: 2, offset: 22 } }, - parent: { - textSpan: { // ...r - start: { line: 2, offset: 19 }, - end: { line: 2, offset: 23 } }, - parent: { - textSpan: { // ...r: any[] - start: { line: 2, offset: 19 }, - end: { line: 2, offset: 30 } }, - parent: { - textSpan: { // ...r: any[] = [] - start: { line: 2, offset: 19 }, - end: { line: 2, offset: 35 } }, - parent: allParamsUp } } }, - }); - }); - - it("works for binding elements", () => { - const getSmartSelectionRange = setup("/file.ts", ` -const { x, y: a, ...zs = {} } = {};`); - const locations = getSmartSelectionRange([ - { line: 2, offset: 9 }, // x - { line: 2, offset: 15 }, // a - { line: 2, offset: 21 }, // zs - ]); - - // Don’t care about checking first two locations, because - // they’re pretty boring, just want to make sure they don’t cause a crash - assert.deepEqual(locations![2], { - textSpan: { // zs - start: { line: 2, offset: 21 }, - end: { line: 2, offset: 23 } }, - parent: { - textSpan: { // ...zs - start: { line: 2, offset: 18 }, - end: { line: 2, offset: 23 } }, - parent: { - textSpan: { // ...zs = {} - start: { line: 2, offset: 18 }, - end: { line: 2, offset: 28 } }, - parent: { - textSpan: { // x, y: a, ...zs = {} - start: { line: 2, offset: 9 }, - end: { line: 2, offset: 28 } }, - parent: { - textSpan: { // { x, y: a, ...zs = {} } - start: { line: 2, offset: 7 }, - end: { line: 2, offset: 30 } }, - parent: { - textSpan: { // whole line - start: { line: 2, offset: 1 }, - end: { line: 2, offset: 36 } }, - parent: { - textSpan: { - start: { line: 1, offset: 1 }, - end: { line: 2, offset: 36 } } } } } } } } }); - }); - - it("consumes all whitespace in a multi-line function parameter list", () => { - const getSmartSelectionRange = setup("/file.ts", ` -function f( - a, - b -) {}`); - const locations = getSmartSelectionRange([{ line: 4, offset: 5 }]); // b - assert.deepEqual(locations, [{ - textSpan: { // b - start: { line: 4, offset: 5 }, - end: { line: 4, offset: 6 } }, - parent: { // all params and whitespace inside parens - textSpan: { - start: { line: 2, offset: 12 }, - end: { line: 5, offset: 1 } }, - parent: { - textSpan: { // whole function declaration - start: { line: 2, offset: 1 }, - end: { line: 5, offset: 5 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 5, offset: 5 } } } } } - }]); - }); - - it("snaps to nodes directly behind the cursor instead of trivia ahead of the cursor", () => { - const getSmartSelectionRange = setup("/file.ts", `let x: string`); - const locations = getSmartSelectionRange([{ line: 1, offset: 4 }]); - assert.deepEqual(locations![0].textSpan, { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 4 }, - }); - }); - - it("creates a stop for JSDoc ranges", () => { - const getSmartSelectionRange = setup("/file.js", "" + -`// Not a JSDoc comment -/** - * @param {number} x The number to square - */ -function square(x) { - return x * x; -}`); - const locations = getSmartSelectionRange([{ line: 5, offset: 10 }]); // square(x) - assert.deepEqual(locations, [{ - textSpan: { // square - start: { line: 5 , offset: 10 }, - end: { line: 5, offset: 16 } }, - parent: { // whole function declaration - textSpan: { - start: { line: 5, offset: 1 }, - end: { line: 7, offset: 2 } }, - parent: { - textSpan: { // add JSDoc - start: { line: 2, offset: 1 }, - end: { line: 7, offset: 2 } }, - parent: { - textSpan: { // SourceFile - start: { line: 1, offset: 1 }, - end: { line: 7, offset: 2 } } } } } }]); - }); - - it("skips lone VariableDeclarations in a declaration list", () => { - const getSmartSelectionRange = setup("/file.ts", `const x = 3;`); - const locations = getSmartSelectionRange([{ line: 1, offset: 7 }]); // x - assert.deepEqual(locations, [{ - textSpan: { - start: { line: 1, offset: 7 }, - end: { line: 1, offset: 8 } }, - parent: { - textSpan: { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 13 } } } }]); - }); - - it("never returns empty ranges", () => { - const getSmartSelectionRange = setup("/file.ts", ` -class HomePage { - componentDidMount() { - if (this.props.username) { - return ''; - } - } -}`); - const locations = getSmartSelectionRange([ - { line: 3, offset: 23 }, // componentDidMount(/**/) - { line: 4, offset: 32 }, // username/**/) - { line: 5, offset: 21 }, // return '/**/' - ]); - - assert.deepEqual(locations![0].textSpan, { // this.props.username - start: { line: 3, offset: 23 }, - end: { line: 3, offset: 24 }, - }); - - assert.deepEqual(locations![1].textSpan, { // this.props.username - start: { line: 4, offset: 32 }, - end: { line: 4, offset: 33 }, - }); - - assert.deepEqual(locations![2].textSpan, { // '' - start: { line: 5, offset: 20 }, - end: { line: 5, offset: 22 }, - }); + end: { line: 9, offset: 2 }, } } } } } } } } }]); }); }); } From eff39600207df4817b3dceb7848bf858c86f4ca3 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 24 Apr 2019 08:52:07 -0700 Subject: [PATCH 26/26] Fix baseline file name changes --- src/harness/fourslash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 66ea1ba38c429..0dea2b1e7b076 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1417,7 +1417,7 @@ Actual: ${stringify(fullActual)}`); } public baselineCurrentFileBreakpointLocations() { - const baselineFile = this.getBaselineFileName().replace(this.basePath + "/breakpointValidation", "bpSpan"); + const baselineFile = this.getBaselineFileName().replace("breakpointValidation", "bpSpan"); Harness.Baseline.runBaseline(baselineFile, this.baselineCurrentFileLocations(pos => this.getBreakpointStatementLocation(pos)!)); }