From 33333e86ebe6a37d87673bc59ab9311f0a21cd7c Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 20 Feb 2020 17:00:23 -0800 Subject: [PATCH 01/20] Added toggleLineComment, toggleMultilineComment with jsx and tests --- src/harness/client.ts | 8 + src/harness/fourslashImpl.ts | 20 ++ src/harness/fourslashInterfaceImpl.ts | 8 + src/harness/harnessLanguageService.ts | 6 + src/server/protocol.ts | 34 ++++ src/server/session.ts | 28 +++ src/services/services.ts | 189 +++++++++++++++++- src/services/shims.ts | 17 ++ src/services/types.ts | 3 + src/services/utilities.ts | 29 ++- src/testRunner/unittests/tsserver/session.ts | 2 + tests/cases/fourslash/fourslash.ts | 3 + tests/cases/fourslash/toggleLineComment1.ts | 18 ++ tests/cases/fourslash/toggleLineComment2.ts | 20 ++ tests/cases/fourslash/toggleLineComment3.ts | 26 +++ tests/cases/fourslash/toggleLineComment4.ts | 18 ++ tests/cases/fourslash/toggleLineComment5.ts | 22 ++ tests/cases/fourslash/toggleLineComment6.ts | 20 ++ .../fourslash/toggleMultilineComment1.ts | 26 +++ .../fourslash/toggleMultilineComment2.ts | 35 ++++ .../fourslash/toggleMultilineComment3.ts | 28 +++ .../fourslash/toggleMultilineComment4.ts | 7 + .../fourslash/toggleMultilineComment5.ts | 30 +++ .../fourslash/toggleMultilineComment6.ts | 43 ++++ 24 files changed, 631 insertions(+), 9 deletions(-) create mode 100644 tests/cases/fourslash/toggleLineComment1.ts create mode 100644 tests/cases/fourslash/toggleLineComment2.ts create mode 100644 tests/cases/fourslash/toggleLineComment3.ts create mode 100644 tests/cases/fourslash/toggleLineComment4.ts create mode 100644 tests/cases/fourslash/toggleLineComment5.ts create mode 100644 tests/cases/fourslash/toggleLineComment6.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment1.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment2.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment3.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment4.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment5.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment6.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 83e85cbc9a328..2609ecc84857d 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -812,6 +812,14 @@ namespace ts.server { return notImplemented(); } + toggleLineComment(): ts.TextChange[] { + throw new Error("Method not implemented."); + } + + toggleMultilineComment(): ts.TextChange[] { + throw new Error("Method not implemented."); + } + dispose(): void { throw new Error("dispose is not available through the server layer."); } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index dd4135ca40c15..694208856e2c3 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3657,6 +3657,26 @@ namespace FourSlash { public configurePlugin(pluginName: string, configuration: any): void { (this.languageService).configurePlugin(pluginName, configuration); } + + public toggleLineComment(newFileContent: string): void { + const ranges = this.getRanges(); + assert(ranges.length); + const changes = this.languageService.toggleLineComment(this.activeFile.fileName, ranges); + + this.applyEdits(this.activeFile.fileName, changes); + + this.verifyCurrentFileContent(newFileContent); + } + + public toggleMultilineComment(newFileContent: string): void { + const ranges = this.getRanges(); + assert(ranges.length); + const changes = this.languageService.toggleMultilineComment(this.activeFile.fileName, ranges); + + this.applyEdits(this.activeFile.fileName, changes); + + this.verifyCurrentFileContent(newFileContent); + } } function prefixMessage(message: string | undefined) { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index f4905c00b84b5..76548debdbf10 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -210,6 +210,14 @@ namespace FourSlashInterface { public refactorAvailable(name: string, actionName?: string) { this.state.verifyRefactorAvailable(this.negative, name, actionName); } + + public toggleLineComment(newFileContent: string) { + this.state.toggleLineComment(newFileContent); + } + + public toggleMultilineComment(newFileContent: string) { + this.state.toggleMultilineComment(newFileContent); + } } export class Verify extends VerifyNegatable { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index fbaf9ba545d0a..43d53d71b267d 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -600,6 +600,12 @@ namespace Harness.LanguageService { clearSourceMapperCache(): never { return ts.notImplemented(); } + toggleLineComment(fileName: string, textRanges: ts.TextRange[]): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRanges)); + } + toggleMultilineComment(fileName: string, textRanges: ts.TextRange[]): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRanges)); + } dispose(): void { this.shim.dispose({}); } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index b6cc2e4d8fdab..bf8cff942c981 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -136,6 +136,10 @@ namespace ts.server.protocol { SelectionRange = "selectionRange", /* @internal */ SelectionRangeFull = "selectionRange-full", + ToggleLineComment = "toggleLineComment", + ToggleLineCommentFull = "toggleLineComment-full", + ToggleMultilineComment = "toggleMultilineComment", + ToggleMultilineCommentFull = "toggleMultilineComment-full", PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", @@ -919,6 +923,18 @@ namespace ts.server.protocol { end: Location; } + export interface TextRange { + /** + * Position of the first character. + */ + pos: number; + + /** + * Position of the last character. + */ + end: number; + } + /** * Object found in response messages defining a span of text in a specific source file. */ @@ -1533,6 +1549,24 @@ namespace ts.server.protocol { parent?: SelectionRange; } + export interface ToggleLineCommentRequest extends FileRequest { + command: CommandTypes.ToggleLineComment; + arguments: ToggleLineCommentRequestArgs; + } + + export interface ToggleLineCommentRequestArgs extends FileRequestArgs { + textRanges: TextRange[]; + } + + export interface ToggleMultilineCommentRequest extends FileRequest { + command: CommandTypes.ToggleMultilineComment; + arguments: ToggleMultilineCommentRequestArgs; + } + + export interface ToggleMultilineCommentRequestArgs extends FileRequestArgs { + textRanges: TextRange[]; + } + /** * Information found in an "open" request. */ diff --git a/src/server/session.ts b/src/server/session.ts index 3f0321074a994..386c737e49a88 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2196,6 +2196,22 @@ namespace ts.server { }); } + private toggleLineComment(args: protocol.ToggleLineCommentRequestArgs, simplifiedResult: boolean) { + const { file, project } = this.getFileAndProject(args); + + const result = project.getLanguageService().toggleLineComment(file, args.textRanges); + + return simplifiedResult ? [] : result; + } + + private toggleMultilineComment(args: protocol.ToggleMultilineCommentRequestArgs, simplifiedResult: boolean) { + const { file, project } = this.getFileAndProject(args); + + const result = project.getLanguageService().toggleMultilineComment(file, args.textRanges); + + return simplifiedResult ? [] : result; + } + private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { const result: protocol.SelectionRange = { textSpan: toProtocolTextSpan(selectionRange.textSpan, scriptInfo), @@ -2641,6 +2657,18 @@ namespace ts.server { [CommandNames.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => { return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments)); }, + [CommandNames.ToggleLineComment]: (request: protocol.ToggleLineCommentRequest) => { + return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/true)); + }, + [CommandNames.ToggleLineCommentFull]: (request: protocol.ToggleLineCommentRequest) => { + return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/false)); + }, + [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { + return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/true)); + }, + [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { + return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/false)); + }, }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { diff --git a/src/services/services.ts b/src/services/services.ts index 28cf22ec473d1..d87d638bf10d9 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -773,7 +773,7 @@ namespace ts { if (!hasSyntacticModifier(node, ModifierFlags.ParameterPropertyModifier)) { break; } - // falls through + // falls through case SyntaxKind.VariableDeclaration: case SyntaxKind.BindingElement: { @@ -834,7 +834,7 @@ namespace ts { if (getAssignmentDeclarationKind(node as BinaryExpression) !== AssignmentDeclarationKind.None) { addDeclaration(node as BinaryExpression); } - // falls through + // falls through default: forEachChild(node, visit); @@ -1977,6 +1977,185 @@ namespace ts { } } + function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) { + return { + lineStarts: sourceFile.getLineStarts(), + firstLine: sourceFile.getLineAndCharacterOfPosition(textRange.pos).line, + lastLine: sourceFile.getLineAndCharacterOfPosition(textRange.end).line + } + } + + function toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + + const textChanges: TextChange[] = []; + + for (const textRange of textRanges) { + const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); + + let isCommenting = false; + let leftMostPosition = Number.MAX_VALUE; + let lineTextStarts = new Map(); + const whiteSpaceRegex = new RegExp(/\S/); + + // First check the lines before any text changes. + for (let i = firstLine; i <= lastLine; i++) { + const lineText = sourceFile.text.substring(lineStarts[i], lineStarts[i + 1]); // TODO: Validate the end of line it might go outside of range. + + // Find the start of text and the left-most character. No-op on empty lines. + const regExec = whiteSpaceRegex.exec(lineText); + if (regExec) { + leftMostPosition = Math.min(leftMostPosition, regExec.index); + lineTextStarts.set(i.toString(), regExec.index); + // let sourceFilePosition = lineStarts[i] + leftMostPosition; + if (lineText.substr(regExec.index, 3) !== "// ") { // TODO: Validate when it is inside a comment. It can only uncomment if it's inside a comment. // TODO: Check when not finishing on empty space. + isCommenting = true; + } + } + } + + for (let i = firstLine; i <= lastLine; i++) { + const lineTextStart = lineTextStarts.get(i.toString()); + // If the line is not an empty line; otherwise no-op; + if (lineTextStart !== undefined) { + if (isCommenting) { + textChanges.push({ + newText: "// ", + span: { + length: 0, + start: lineStarts[i] + leftMostPosition + } + }); + } else { + textChanges.push({ + newText: "", + span: { + length: 3, + start: lineStarts[i] + lineTextStart + } + }); + } + } + } + } + + return textChanges; + } + + function toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: TextChange[] = []; + const { text } = sourceFile; + + for (const textRange of textRanges) { + let isCommenting = false; + const positions = [] as number[] as SortedArray; + + let pos = textRange.pos; + const isJsx = isInsideJsxTags(sourceFile, pos); + + const openMultiline = isJsx ? "{/*" : "/*"; + const closeMultiline = isJsx ? "*/}" : "*/"; + const openMultilineRegex = isJsx ? "\\{\\/\\*" : "\\/\\*"; + const closeMultilineRegex = isJsx ? "\\*\\/\\}" : "\\*\\/"; + + // Get all comment positions + while (pos <= textRange.end) { + // Start of comment is considered inside comment. + const offset = text.substr(pos, openMultiline.length) === openMultiline ? openMultiline.length : 0; + const commentRange = isInComment(sourceFile, pos + offset); + + // If position is in a comment add it to the positions array. + if (commentRange) { + // Include brace positions. + if (isJsx) { + commentRange.pos--; + commentRange.end++; + } + + positions.push(commentRange.pos); + if (commentRange.kind === SyntaxKind.MultiLineCommentTrivia) { + positions.push(commentRange.end); + } + + pos = commentRange.end + 1; + } else { + isCommenting = true; + + const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); + pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; + } + } + + if (isCommenting) { + if (isInComment(sourceFile, textRange.pos)?.kind !== SyntaxKind.SingleLineCommentTrivia) { + insertSorted(positions, textRange.pos, compareValues); + } + insertSorted(positions, textRange.end, compareValues); + + // Insert open comment if the first position is not a comment already. + const firstPos = positions[0]; + if (text.substr(firstPos, openMultiline.length) !== openMultiline) { + textChanges.push({ + newText: openMultiline, + span: { + length: 0, + start: firstPos + } + }); + } + + // Insert open and close comment to all positions between first and last. Exclusive. + for (let i = 1; i < positions.length - 1; i++) { + if (text.substr(positions[i] - closeMultiline.length, closeMultiline.length) !== closeMultiline) { + textChanges.push({ + newText: closeMultiline, + span: { + length: 0, + start: positions[i] + } + }); + } + + if (text.substr(positions[i], openMultiline.length) !== openMultiline) { + textChanges.push({ + newText: openMultiline, + span: { + length: 0, + start: positions[i] + } + }); + } + } + + // Insert open comment if the last position is not a comment already. + const lastPos = positions[positions.length - 1]; + if (text.substr(lastPos - closeMultiline.length, closeMultiline.length) !== closeMultiline) { + textChanges.push({ + newText: closeMultiline, + span: { + length: 0, + start: lastPos + } + }); + } + } else { + for (let i = 0; i < positions.length; i++) { + const offset = text.substr(positions[i] - closeMultiline.length, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; + textChanges.push({ + newText: "", + span: { + length: 2, + start: positions[i] - offset + } + }); + } + } + } + + return textChanges; + } + function isUnclosedTag({ openingElement, closingElement, parent }: JsxElement): boolean { return !tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) || isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); @@ -2255,7 +2434,9 @@ namespace ts { clearSourceMapperCache: () => sourceMapper.clearCache(), prepareCallHierarchy, provideCallHierarchyIncomingCalls, - provideCallHierarchyOutgoingCalls + provideCallHierarchyOutgoingCalls, + toggleLineComment, + toggleMultilineComment }; } @@ -2319,7 +2500,7 @@ namespace ts { if (node.parent.kind === SyntaxKind.ComputedPropertyName) { return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; } - // falls through + // falls through case SyntaxKind.Identifier: return isObjectLiteralElement(node.parent) && diff --git a/src/services/shims.ts b/src/services/shims.ts index 6cecfeaa674fc..1e1669be6052b 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -277,6 +277,9 @@ namespace ts { getEmitOutput(fileName: string): string; getEmitOutputObject(fileName: string): EmitOutput; + + toggleLineComment(fileName: string, textChanges: ts.TextRange[]): string; + toggleMultilineComment(fileName: string, textChanges: ts.TextRange[]): string; } export interface ClassifierShim extends Shim { @@ -1066,6 +1069,20 @@ namespace ts { () => this.languageService.getEmitOutput(fileName), this.logPerformance) as EmitOutput; } + + public toggleLineComment(fileName: string, textRanges: ts.TextRange[]): string { + return this.forwardJSONCall( + `toggleLineComment('${fileName}', '${JSON.stringify(textRanges)}')`, + () => this.languageService.toggleLineComment(fileName, textRanges) + ); + } + + public toggleMultilineComment(fileName: string, textRanges: ts.TextRange[]): string { + return this.forwardJSONCall( + `toggleMultilineComment('${fileName}', '${JSON.stringify(textRanges)}')`, + () => this.languageService.toggleMultilineComment(fileName, textRanges) + ); + } } function convertClassifications(classifications: Classifications): { spans: string, endOfLineState: EndOfLineState } { diff --git a/src/services/types.ts b/src/services/types.ts index fd8ade8f8e6e1..39644aa9f0aec 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -486,6 +486,9 @@ namespace ts { /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; + toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + dispose(): void; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index b1799167c437b..ed20ed7365b3c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -394,7 +394,7 @@ namespace ts { case SyntaxKind.MethodSignature: return ScriptElementKind.memberFunctionElement; case SyntaxKind.PropertyAssignment: - const {initializer} = node as PropertyAssignment; + const { initializer } = node as PropertyAssignment; return isFunctionLike(initializer) ? ScriptElementKind.memberFunctionElement : ScriptElementKind.memberVariableElement; case SyntaxKind.PropertyDeclaration: case SyntaxKind.PropertySignature: @@ -557,7 +557,7 @@ namespace ts { if (!(n).arguments) { return true; } - // falls through + // falls through case SyntaxKind.CallExpression: case SyntaxKind.ParenthesizedExpression: @@ -1320,6 +1320,25 @@ namespace ts { return false; } + export function isInsideJsxTags(sourceFile: SourceFile, position: number) { + const token = getTokenAtPosition(sourceFile, position); + + switch (token.kind) { + case SyntaxKind.JsxText: + return true; + case SyntaxKind.LessThanToken: + case SyntaxKind.Identifier: + return token.parent.kind === SyntaxKind.JsxText //
Hello |
+ || token.parent.kind === SyntaxKind.JsxClosingElement //
|
+ || isJsxOpeningLikeElement(token.parent) && isJsxElement(token.parent.parent) //
|
or
+ case SyntaxKind.CloseBraceToken: + case SyntaxKind.OpenBraceToken: + return isJsxExpression(token.parent) && isJsxElement(token.parent.parent); //
{|}
or
|{}
+ } + + return false; + } + export function findPrecedingMatchingToken(token: Node, matchingTokenKind: SyntaxKind, sourceFile: SourceFile) { const tokenKind = token.kind; let remainingMatchingTokens = 0; @@ -1346,7 +1365,7 @@ namespace ts { export function removeOptionality(type: Type, isOptionalExpression: boolean, isOptionalChain: boolean) { return isOptionalExpression ? type.getNonNullableType() : isOptionalChain ? type.getNonOptionalType() : - type; + type; } export function isPossiblyTypeArgumentPosition(token: Node, sourceFile: SourceFile, checker: TypeChecker): boolean { @@ -1439,7 +1458,7 @@ namespace ts { break; case SyntaxKind.EqualsGreaterThanToken: - // falls through + // falls through case SyntaxKind.Identifier: case SyntaxKind.StringLiteral: @@ -1447,7 +1466,7 @@ namespace ts { case SyntaxKind.BigIntLiteral: case SyntaxKind.TrueKeyword: case SyntaxKind.FalseKeyword: - // falls through + // falls through case SyntaxKind.TypeOfKeyword: case SyntaxKind.ExtendsKeyword: diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index b41df99f4a2c6..b0f55affa0ea5 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -272,6 +272,8 @@ namespace ts.server { CommandNames.PrepareCallHierarchy, CommandNames.ProvideCallHierarchyIncomingCalls, CommandNames.ProvideCallHierarchyOutgoingCalls, + CommandNames.ToggleLineComment, + CommandNames.ToggleMultilineComment ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index d7d4935118d0f..e61154f2fd365 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -396,6 +396,9 @@ declare namespace FourSlashInterface { generateTypes(...options: GenerateTypesOptions[]): void; organizeImports(newContent: string): void; + + toggleLineComment(newFileContent: string): void; + toggleBlockComment(newFileContent: string): void; } class edit { backspace(count?: number): void; diff --git a/tests/cases/fourslash/toggleLineComment1.ts b/tests/cases/fourslash/toggleLineComment1.ts new file mode 100644 index 0000000000000..8e634c84d015e --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment1.ts @@ -0,0 +1,18 @@ +// Simple comment and uncomment. + +//// let var1[| = 1; +//// let var2 = 2; +//// let var3 |]= 3; +//// +//// // let var4[| = 1; +//// // let var5 = 2; +//// // let var6 |]= 3; + +verify.toggleLineComment( + `// let var1 = 1; +// let var2 = 2; +// let var3 = 3; + +let var4 = 1; +let var5 = 2; +let var6 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment2.ts b/tests/cases/fourslash/toggleLineComment2.ts new file mode 100644 index 0000000000000..ead331a289340 --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment2.ts @@ -0,0 +1,20 @@ +// When indentation is different between lines it should get the left most indentation +// and use that for all lines. +// When uncommeting, doesn't matter what indentation the line has. + +//// let var1[| = 1; +//// let var2 = 2; +//// let var3 |]= 3; +//// +//// // let var4[| = 1; +//// // let var5 = 2; +//// // let var6 |]= 3; + +verify.toggleLineComment( + `// let var1 = 1; +// let var2 = 2; +// let var3 = 3; + + let var4 = 1; + let var5 = 2; + let var6 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment3.ts b/tests/cases/fourslash/toggleLineComment3.ts new file mode 100644 index 0000000000000..e498bb4ea0067 --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment3.ts @@ -0,0 +1,26 @@ +// Comment and uncomment ignores empty lines. + +//// let var1[| = 1; +//// +//// let var2 = 2; +//// +//// let var3 |]= 3; +//// +//// // let var4[| = 1; +//// +//// // let var5 = 2; +//// +//// // let var6 |]= 3; + +verify.toggleLineComment( + `// let var1 = 1; + +// let var2 = 2; + +// let var3 = 3; + +let var4 = 1; + +let var5 = 2; + +let var6 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment4.ts b/tests/cases/fourslash/toggleLineComment4.ts new file mode 100644 index 0000000000000..72ebd7b5e073e --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment4.ts @@ -0,0 +1,18 @@ +// If at least one line is uncomment then comment all lines again. + +//// // let var1[| = 1; +//// let var2 = 2; +//// // let var3 |]= 3; +//// +//// // // let var4[| = 1; +//// // let var5 = 2; +//// // // let var6 |]= 3; + +verify.toggleLineComment( + `// // let var1 = 1; +// let var2 = 2; +// // let var3 = 3; + +// let var4 = 1; +let var5 = 2; +// let var6 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment5.ts b/tests/cases/fourslash/toggleLineComment5.ts new file mode 100644 index 0000000000000..c5e20dd27b524 --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment5.ts @@ -0,0 +1,22 @@ +// Comments inside strings are still considered comments. + +//// let var1 = ` +//// // some stri[|ng +//// // some other|] string +//// `; +//// +//// let var2 = ` +//// some stri[|ng +//// some other|] string +//// `; + +verify.toggleLineComment( + `let var1 = \` +some string +some other string +\`; + +let var2 = \` +// some string +// some other string +\`;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment6.ts b/tests/cases/fourslash/toggleLineComment6.ts new file mode 100644 index 0000000000000..a3b3d9e4a648a --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment6.ts @@ -0,0 +1,20 @@ +// Selection is at the start of jsx it's still considered js. + +//// function a() { +//// let foo = "bar"; +//// return ( +//// [|
+//// {foo}|] +////
+//// ); +//// } + +verify.toggleLineComment( + `function a() { + let foo = "bar"; + return ( + //
+ // {foo} +
+ ); +}`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment1.ts b/tests/cases/fourslash/toggleMultilineComment1.ts new file mode 100644 index 0000000000000..cfc0fe87affb0 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment1.ts @@ -0,0 +1,26 @@ +// Simple block comment and uncomment. + +//// let var1[| = 1; +//// let var2 = 2; +//// let var3 |]= 3; +//// +//// let var4/* = 1; +//// let var5 [||]= 2; +//// let var6 */= 3; +//// +//// [|/*let var7 = 1; +//// let var8 = 2; +//// let var9 = 3;*/|] + +verify.toggleBlockComment( + `let var1/* = 1; +let var2 = 2; +let var3 */= 3; + +let var4 = 1; +let var5 = 2; +let var6 = 3; + +let var7 = 1; +let var8 = 2; +let var9 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment2.ts b/tests/cases/fourslash/toggleMultilineComment2.ts new file mode 100644 index 0000000000000..62dc1f45e6246 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment2.ts @@ -0,0 +1,35 @@ +// If selection is outside of a block comment then insert comment +// instead of removing. + +//// let var1/* = 1; +//// let var2 [|= 2; +//// let var3 */= 3;|] +//// +//// [|let var4/* = 1; +//// let var5 |]= 2; +//// let var6 */= 3; +//// +//// [|let var7/* = 1; +//// let var8 = 2; +//// let var9 */= 3;|] +//// +//// /*let va[|r10 = 1;*/ +//// let var11 = 2; +//// /*let var12|] = 3;*/ + +verify.toggleBlockComment( + `let var1/* = 1; +let var2 *//*= 2; +let var3 *//*= 3;*/ + +/*let var4*//* = 1; +let var5 *//*= 2; +let var6 */= 3; + +/*let var7*//* = 1; +let var8 = 2; +let var9 *//*= 3;*/ + +/*let va*//*r10 = 1;*//* +let var11 = 2; +*//*let var12*//* = 3;*/`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment3.ts b/tests/cases/fourslash/toggleMultilineComment3.ts new file mode 100644 index 0000000000000..fab4263b3c304 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment3.ts @@ -0,0 +1,28 @@ +/// + +// If range is inside a single line comment, just add the block comment. + +//// // let va[|r1 = 1; +//// let var2 = 2; +//// // let var3|] = 3; +//// +//// // let va[|r4 = 1; +//// let var5 = 2; +//// /* let var6|] = 3;*/ +//// +//// /* let va[|r7 = 1;*/ +//// let var8 = 2; +//// // let var9|] = 3; + +verify.toggleBlockComment( + `/*// let var1 = 1; +let var2 = 2; +// let var3*/ = 3; + +/*// let var4 = 1; +let var5 = 2; +*//* let var6*//* = 3;*/ + +/* let va*//*r7 = 1;*//* +let var8 = 2; +// let var9*/ = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment4.ts b/tests/cases/fourslash/toggleMultilineComment4.ts new file mode 100644 index 0000000000000..1764c5a08e02a --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment4.ts @@ -0,0 +1,7 @@ +// This is an edgecase. The string contains a multiline comment syntax and because it is a string, +// is not actually a comment. When toggling it doesn't get escaped or appended comments. +// The result would be a portion of the selection to be "not commented". + +//// /*let s[|omeLongVa*/riable = "Some other /*long th*/in|]g"; + +verify.toggleMultilineComment(`/*let s*//*omeLongVa*//*riable = "Some other /*long th*/in*/g";`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment5.ts b/tests/cases/fourslash/toggleMultilineComment5.ts new file mode 100644 index 0000000000000..e806ef446b019 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment5.ts @@ -0,0 +1,30 @@ +// Jsx uses block comments for each line commented. + +// Common JSX comment scenarios + +//@Filename: file.tsx +//// const a =
[|
;|] +//// const b =
This is [|valid HTML &|] JSX at the same time.
; +//// const c = +//// [| +//// |] +//// ; +//// const d = +//// +//// |] +//// ; +//// const e = [|{'foo'}|]; + +verify.toggleBlockComment( + `const a =
{/*
;*/} +const b =
This is {/*valid HTML &*/} JSX at the same time.
; +const c = + {/* + */} +; +const d = + + */} +; +const e = {/*{'foo'}*/};` +); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment6.ts b/tests/cases/fourslash/toggleMultilineComment6.ts new file mode 100644 index 0000000000000..882fbbffb03fe --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment6.ts @@ -0,0 +1,43 @@ +// Jsx uses multiline comments for each line commented. + +// Selection is outside of a block comments inserts block comments instead of removing. +// There's some variations between jsx and js comments depending on the position. + +//@Filename: file.tsx +//// const var1 =
Tex{/*t1
; +//// const var2 =
Text2[|
; +//// const var3 =
Tex*/}t3
;|] +//// +//// [|const var4 =
Tex{/*t4
; +//// const var5 = Text5; +//// const var6 =
Tex*/}t6
; +//// +//// [|const var7 =
Tex{/*t7
; +//// const var8 =
Text8
; +//// const var9 =
Tex*/}t9
;|] +//// +//// const var10 =
+//// {/*
T[|ext
*/} +////
Text
+//// {/*
Text|]
*/} +////
; + +verify.toggleMultilineComment( + `const var1 =
Tex{/*t1
; +const var2 =
Text2*/}{/*
; +const var3 =
Tex*/}{/*t3
;*/} + +/*const var4 =
Tex{*//*t4
; +const var5 = Text5; +const var6 =
Tex*/}t6
; + +/*const var7 =
Tex{*//*t7
; +const var8 =
Text8
; +const var9 =
Tex*//*}t9
;*/ + +const var10 =
+ {/*
T*/}{/*ext
*/}{/* +
Text
+ */}{/*
Text*/}{/*
*/} +
;` +); \ No newline at end of file From 97de811d4809dc1cb2fd5ccc5ec37b42fd73efe9 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 20 Feb 2020 17:02:17 -0800 Subject: [PATCH 02/20] Fix multiline name --- tests/cases/fourslash/fourslash.ts | 2 +- tests/cases/fourslash/toggleMultilineComment1.ts | 4 ++-- tests/cases/fourslash/toggleMultilineComment2.ts | 2 +- tests/cases/fourslash/toggleMultilineComment3.ts | 4 ++-- tests/cases/fourslash/toggleMultilineComment5.ts | 2 +- tests/cases/fourslash/toggleMultilineComment6.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index e61154f2fd365..83db54c50d0bd 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -398,7 +398,7 @@ declare namespace FourSlashInterface { organizeImports(newContent: string): void; toggleLineComment(newFileContent: string): void; - toggleBlockComment(newFileContent: string): void; + toggleMultilineComment(newFileContent: string): void; } class edit { backspace(count?: number): void; diff --git a/tests/cases/fourslash/toggleMultilineComment1.ts b/tests/cases/fourslash/toggleMultilineComment1.ts index cfc0fe87affb0..d32e7b28ecf40 100644 --- a/tests/cases/fourslash/toggleMultilineComment1.ts +++ b/tests/cases/fourslash/toggleMultilineComment1.ts @@ -1,4 +1,4 @@ -// Simple block comment and uncomment. +// Simple multiline comment and uncomment. //// let var1[| = 1; //// let var2 = 2; @@ -12,7 +12,7 @@ //// let var8 = 2; //// let var9 = 3;*/|] -verify.toggleBlockComment( +verify.toggleMultilineComment( `let var1/* = 1; let var2 = 2; let var3 */= 3; diff --git a/tests/cases/fourslash/toggleMultilineComment2.ts b/tests/cases/fourslash/toggleMultilineComment2.ts index 62dc1f45e6246..926d29e2d2e95 100644 --- a/tests/cases/fourslash/toggleMultilineComment2.ts +++ b/tests/cases/fourslash/toggleMultilineComment2.ts @@ -17,7 +17,7 @@ //// let var11 = 2; //// /*let var12|] = 3;*/ -verify.toggleBlockComment( +verify.toggleMultilineComment( `let var1/* = 1; let var2 *//*= 2; let var3 *//*= 3;*/ diff --git a/tests/cases/fourslash/toggleMultilineComment3.ts b/tests/cases/fourslash/toggleMultilineComment3.ts index fab4263b3c304..bcfa3418d971a 100644 --- a/tests/cases/fourslash/toggleMultilineComment3.ts +++ b/tests/cases/fourslash/toggleMultilineComment3.ts @@ -1,6 +1,6 @@ /// -// If range is inside a single line comment, just add the block comment. +// If range is inside a single line comment, just add the multiline comment. //// // let va[|r1 = 1; //// let var2 = 2; @@ -14,7 +14,7 @@ //// let var8 = 2; //// // let var9|] = 3; -verify.toggleBlockComment( +verify.toggleMultilineComment( `/*// let var1 = 1; let var2 = 2; // let var3*/ = 3; diff --git a/tests/cases/fourslash/toggleMultilineComment5.ts b/tests/cases/fourslash/toggleMultilineComment5.ts index e806ef446b019..e516b0b04fb68 100644 --- a/tests/cases/fourslash/toggleMultilineComment5.ts +++ b/tests/cases/fourslash/toggleMultilineComment5.ts @@ -15,7 +15,7 @@ //// ; //// const e = [|{'foo'}|]; -verify.toggleBlockComment( +verify.toggleMultilineComment( `const a =
{/*
;*/} const b =
This is {/*valid HTML &*/} JSX at the same time.
; const c = diff --git a/tests/cases/fourslash/toggleMultilineComment6.ts b/tests/cases/fourslash/toggleMultilineComment6.ts index 882fbbffb03fe..caa60504fd17c 100644 --- a/tests/cases/fourslash/toggleMultilineComment6.ts +++ b/tests/cases/fourslash/toggleMultilineComment6.ts @@ -1,6 +1,6 @@ // Jsx uses multiline comments for each line commented. -// Selection is outside of a block comments inserts block comments instead of removing. +// Selection is outside of a multiline comments inserts multiline comments instead of removing. // There's some variations between jsx and js comments depending on the position. //@Filename: file.tsx From fc9753473e2f61ebd1cc7616d5343d7811dde4c5 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Mon, 24 Feb 2020 17:24:08 -0800 Subject: [PATCH 03/20] Added jsx to singleLineComment --- src/services/services.ts | 36 +++++++++++++++---- src/services/utilities.ts | 34 ++++++++++-------- tests/cases/fourslash/toggleLineComment1.ts | 12 +++---- tests/cases/fourslash/toggleLineComment2.ts | 18 +++++----- tests/cases/fourslash/toggleLineComment3.ts | 12 +++---- tests/cases/fourslash/toggleLineComment4.ts | 24 ++++++------- tests/cases/fourslash/toggleLineComment5.ts | 8 ++--- tests/cases/fourslash/toggleLineComment6.ts | 29 +++++++-------- tests/cases/fourslash/toggleLineComment7.ts | 29 +++++++++++++++ tests/cases/fourslash/toggleLineComment8.ts | 30 ++++++++++++++++ .../fourslash/toggleMultilineComment4.ts | 4 +-- .../fourslash/toggleMultilineComment5.ts | 6 +++- .../fourslash/toggleMultilineComment7.ts | 33 +++++++++++++++++ 13 files changed, 198 insertions(+), 77 deletions(-) create mode 100644 tests/cases/fourslash/toggleLineComment7.ts create mode 100644 tests/cases/fourslash/toggleLineComment8.ts create mode 100644 tests/cases/fourslash/toggleMultilineComment7.ts diff --git a/src/services/services.ts b/src/services/services.ts index d87d638bf10d9..03bf020acd9af 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1997,43 +1997,67 @@ namespace ts { let leftMostPosition = Number.MAX_VALUE; let lineTextStarts = new Map(); const whiteSpaceRegex = new RegExp(/\S/); + const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]) + const openComment = isJsx ? "{/*" : "//"; + const closeComment = "*/}"; // First check the lines before any text changes. for (let i = firstLine; i <= lastLine; i++) { - const lineText = sourceFile.text.substring(lineStarts[i], lineStarts[i + 1]); // TODO: Validate the end of line it might go outside of range. + const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); // Find the start of text and the left-most character. No-op on empty lines. const regExec = whiteSpaceRegex.exec(lineText); if (regExec) { leftMostPosition = Math.min(leftMostPosition, regExec.index); lineTextStarts.set(i.toString(), regExec.index); - // let sourceFilePosition = lineStarts[i] + leftMostPosition; - if (lineText.substr(regExec.index, 3) !== "// ") { // TODO: Validate when it is inside a comment. It can only uncomment if it's inside a comment. // TODO: Check when not finishing on empty space. + + if (lineText.substr(regExec.index, openComment.length) !== openComment) { // TODO: Validate when it is inside a comment. It can only uncomment if it's inside a comment. // TODO: Check when not finishing on empty space. isCommenting = true; } } } + // Push all text changes. for (let i = firstLine; i <= lastLine; i++) { const lineTextStart = lineTextStarts.get(i.toString()); // If the line is not an empty line; otherwise no-op; if (lineTextStart !== undefined) { if (isCommenting) { textChanges.push({ - newText: "// ", + newText: openComment, span: { length: 0, start: lineStarts[i] + leftMostPosition } }); + + if (isJsx) { + textChanges.push({ + newText: closeComment, + span: { + length: 0, + start: sourceFile.getLineEndOfPosition(lineStarts[i]) + } + }); + } } else { textChanges.push({ newText: "", span: { - length: 3, + length: openComment.length, start: lineStarts[i] + lineTextStart } }); + + if (isJsx) { + textChanges.push({ + newText: "", + span: { + length: closeComment.length, + start: sourceFile.getLineEndOfPosition(lineStarts[i]) - closeComment.length + } + }); + } } } } @@ -2052,7 +2076,7 @@ namespace ts { const positions = [] as number[] as SortedArray; let pos = textRange.pos; - const isJsx = isInsideJsxTags(sourceFile, pos); + const isJsx = isInsideJsxElement(sourceFile, pos); const openMultiline = isJsx ? "{/*" : "/*"; const closeMultiline = isJsx ? "*/}" : "*/"; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ed20ed7365b3c..d692099186d0d 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1320,23 +1320,29 @@ namespace ts { return false; } - export function isInsideJsxTags(sourceFile: SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); + export function isInsideJsxElement(sourceFile: SourceFile, position: number): boolean { + function isInsideJsxElementRecursion(node: Node): boolean { + while (node) { + if (node.kind >= SyntaxKind.JsxSelfClosingElement && node.kind <= SyntaxKind.JsxExpression + || node.kind === SyntaxKind.JsxText + || node.kind === SyntaxKind.LessThanToken + || node.kind === SyntaxKind.GreaterThanToken + || node.kind === SyntaxKind.Identifier + || node.kind === SyntaxKind.CloseBraceToken + || node.kind === SyntaxKind.OpenBraceToken + || node.kind === SyntaxKind.SlashToken) { + node = node.parent; + } else if (node.kind === SyntaxKind.JsxElement) { + return position > node.getStart(sourceFile) || isInsideJsxElementRecursion(node.parent); + } else { + return false; + } + } - switch (token.kind) { - case SyntaxKind.JsxText: - return true; - case SyntaxKind.LessThanToken: - case SyntaxKind.Identifier: - return token.parent.kind === SyntaxKind.JsxText //
Hello |
- || token.parent.kind === SyntaxKind.JsxClosingElement //
|
- || isJsxOpeningLikeElement(token.parent) && isJsxElement(token.parent.parent) //
|
or
- case SyntaxKind.CloseBraceToken: - case SyntaxKind.OpenBraceToken: - return isJsxExpression(token.parent) && isJsxElement(token.parent.parent); //
{|}
or
|{}
+ return false; } - return false; + return isInsideJsxElementRecursion(getTokenAtPosition(sourceFile, position)); } export function findPrecedingMatchingToken(token: Node, matchingTokenKind: SyntaxKind, sourceFile: SourceFile) { diff --git a/tests/cases/fourslash/toggleLineComment1.ts b/tests/cases/fourslash/toggleLineComment1.ts index 8e634c84d015e..1f72de7b40276 100644 --- a/tests/cases/fourslash/toggleLineComment1.ts +++ b/tests/cases/fourslash/toggleLineComment1.ts @@ -4,14 +4,14 @@ //// let var2 = 2; //// let var3 |]= 3; //// -//// // let var4[| = 1; -//// // let var5 = 2; -//// // let var6 |]= 3; +//// //let var4[| = 1; +//// //let var5 = 2; +//// //let var6 |]= 3; verify.toggleLineComment( - `// let var1 = 1; -// let var2 = 2; -// let var3 = 3; + `//let var1 = 1; +//let var2 = 2; +//let var3 = 3; let var4 = 1; let var5 = 2; diff --git a/tests/cases/fourslash/toggleLineComment2.ts b/tests/cases/fourslash/toggleLineComment2.ts index ead331a289340..710bd5ef3f7db 100644 --- a/tests/cases/fourslash/toggleLineComment2.ts +++ b/tests/cases/fourslash/toggleLineComment2.ts @@ -6,15 +6,15 @@ //// let var2 = 2; //// let var3 |]= 3; //// -//// // let var4[| = 1; -//// // let var5 = 2; -//// // let var6 |]= 3; +//// // let var4[| = 1; +//// //let var5 = 2; +//// // let var6 |]= 3; verify.toggleLineComment( - `// let var1 = 1; -// let var2 = 2; -// let var3 = 3; + ` // let var1 = 1; + //let var2 = 2; + // let var3 = 3; - let var4 = 1; - let var5 = 2; - let var6 = 3;`); \ No newline at end of file + let var4 = 1; +let var5 = 2; + let var6 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment3.ts b/tests/cases/fourslash/toggleLineComment3.ts index e498bb4ea0067..d8f9aeabb99db 100644 --- a/tests/cases/fourslash/toggleLineComment3.ts +++ b/tests/cases/fourslash/toggleLineComment3.ts @@ -6,18 +6,18 @@ //// //// let var3 |]= 3; //// -//// // let var4[| = 1; +//// //let var4[| = 1; //// -//// // let var5 = 2; +//// //let var5 = 2; //// -//// // let var6 |]= 3; +//// //let var6 |]= 3; verify.toggleLineComment( - `// let var1 = 1; + `//let var1 = 1; -// let var2 = 2; +//let var2 = 2; -// let var3 = 3; +//let var3 = 3; let var4 = 1; diff --git a/tests/cases/fourslash/toggleLineComment4.ts b/tests/cases/fourslash/toggleLineComment4.ts index 72ebd7b5e073e..1d162ca7bedae 100644 --- a/tests/cases/fourslash/toggleLineComment4.ts +++ b/tests/cases/fourslash/toggleLineComment4.ts @@ -1,18 +1,18 @@ // If at least one line is uncomment then comment all lines again. -//// // let var1[| = 1; -//// let var2 = 2; -//// // let var3 |]= 3; +//// //const a[| = 1; +//// const b = 2 +//// //const c =|] 3; //// -//// // // let var4[| = 1; -//// // let var5 = 2; -//// // // let var6 |]= 3; +//// ////const d[| = 4; +//// //const e = 5; +//// ////const e =|] 6; verify.toggleLineComment( - `// // let var1 = 1; -// let var2 = 2; -// // let var3 = 3; + `// //const a = 1; +//const b = 2 +// //const c = 3; -// let var4 = 1; -let var5 = 2; -// let var6 = 3;`); \ No newline at end of file +//const d = 4; +const e = 5; +//const e = 6;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment5.ts b/tests/cases/fourslash/toggleLineComment5.ts index c5e20dd27b524..bec00a9233323 100644 --- a/tests/cases/fourslash/toggleLineComment5.ts +++ b/tests/cases/fourslash/toggleLineComment5.ts @@ -1,8 +1,8 @@ // Comments inside strings are still considered comments. //// let var1 = ` -//// // some stri[|ng -//// // some other|] string +//// //some stri[|ng +//// //some other|] string //// `; //// //// let var2 = ` @@ -17,6 +17,6 @@ some other string \`; let var2 = \` -// some string -// some other string +//some string +//some other string \`;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment6.ts b/tests/cases/fourslash/toggleLineComment6.ts index a3b3d9e4a648a..4274cc18308ce 100644 --- a/tests/cases/fourslash/toggleLineComment6.ts +++ b/tests/cases/fourslash/toggleLineComment6.ts @@ -1,20 +1,15 @@ -// Selection is at the start of jsx it's still considered js. +// Selection is at the start of jsx its still js. -//// function a() { -//// let foo = "bar"; -//// return ( -//// [|
-//// {foo}|] -////
-//// ); -//// } +//@Filename: file.tsx +//// let a = ( +//// [|
+//// some text|] +////
+//// ); verify.toggleLineComment( - `function a() { - let foo = "bar"; - return ( - //
- // {foo} -
- ); -}`); \ No newline at end of file + `let a = ( + //
+ // some text +
+);`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment7.ts b/tests/cases/fourslash/toggleLineComment7.ts new file mode 100644 index 0000000000000..5a6cbb002fbf5 --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment7.ts @@ -0,0 +1,29 @@ +// Common comment line cases. + +//@Filename: file.tsx +//// const a = +//// [| +//// |] +//// ; +//// const b = +//// {/**/} +//// {/**/} +//// ; +//// const c = [| +//// +//// +//// ; + +verify.toggleLineComment( + `const a = + {/**/} + {/**/} +; +const b = + + +; +//const c = +// +// +;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment8.ts b/tests/cases/fourslash/toggleLineComment8.ts new file mode 100644 index 0000000000000..1c3bed3fd8eb1 --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment8.ts @@ -0,0 +1,30 @@ +// When indentation is different between lines it should get the left most indentation +// and use that for all lines. +// When uncommeting, doesn't matter what indentation the line has. + +//@Filename: file.tsx +//// const a =
+//// [|
+//// SomeText +////
|] +////
; +//// +//// const b =
+//// {/*[|
*/} +//// {/* SomeText*/} +//// {/*
|]*/} +////
; + + +verify.toggleLineComment( + `const a =
+ {/*
*/} + {/* SomeText*/} + {/*
*/} +
; + +const b =
+
+ SomeText +
+
;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment4.ts b/tests/cases/fourslash/toggleMultilineComment4.ts index 1764c5a08e02a..216a308ae44d5 100644 --- a/tests/cases/fourslash/toggleMultilineComment4.ts +++ b/tests/cases/fourslash/toggleMultilineComment4.ts @@ -1,5 +1,5 @@ -// This is an edgecase. The string contains a multiline comment syntax and because it is a string, -// is not actually a comment. When toggling it doesn't get escaped or appended comments. +// This is an edgecase. The string contains a multiline comment syntax but it is a string +// and not actually a comment. When toggling it doesn't get escaped or appended comments. // The result would be a portion of the selection to be "not commented". //// /*let s[|omeLongVa*/riable = "Some other /*long th*/in|]g"; diff --git a/tests/cases/fourslash/toggleMultilineComment5.ts b/tests/cases/fourslash/toggleMultilineComment5.ts index e516b0b04fb68..0dea4ebbff44e 100644 --- a/tests/cases/fourslash/toggleMultilineComment5.ts +++ b/tests/cases/fourslash/toggleMultilineComment5.ts @@ -14,6 +14,8 @@ //// |] ////
; //// const e = [|{'foo'}|]; +//// const f =
Some text;|] +//// const g =
Some text<[|/div>;|] verify.toggleMultilineComment( `const a =
{/*
;*/} @@ -26,5 +28,7 @@ const d = */} ; -const e = {/*{'foo'}*/};` +const e = {/*{'foo'}*/}; +const f =
Some text;*/} +const g =
Some text<{/*/div>;*/}` ); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment7.ts b/tests/cases/fourslash/toggleMultilineComment7.ts new file mode 100644 index 0000000000000..d4c711aee68bb --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment7.ts @@ -0,0 +1,33 @@ +// Cases where the cursor is inside JSX like sintax but it's actually js. + +//@Filename: file.tsx +//// const a = ( +//// [|
+//// some text|] +////
+//// ); +//// const b = ; +//// const c = ; +//// const d = ; +//// const e = ;|] +//// const f = [ +//// [|
  • First item
  • , +////
  • Second item
  • ,|] +////
  • Third item
  • , +//// ]; + +verify.toggleMultilineComment( + `const a = ( + /*
    + some text*/ +
    +); +const b = ; +const c = ; +const d = ; +const e = ;*/ +const f = [ + /*
  • First item
  • , +
  • Second item
  • ,*/ +
  • Third item
  • , +];`); \ No newline at end of file From e5829881b52936ee8832d10d8deb3ef3cd5b95d8 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Tue, 25 Feb 2020 13:58:14 -0800 Subject: [PATCH 04/20] Fixed toggleLineComment jsx cases --- src/services/services.ts | 45 ++++++-------------- tests/cases/fourslash/toggleLineComment10.ts | 11 +++++ tests/cases/fourslash/toggleLineComment9.ts | 18 ++++++++ 3 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 tests/cases/fourslash/toggleLineComment10.ts create mode 100644 tests/cases/fourslash/toggleLineComment9.ts diff --git a/src/services/services.ts b/src/services/services.ts index 03bf020acd9af..9bc392c58d81c 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1999,9 +1999,8 @@ namespace ts { const whiteSpaceRegex = new RegExp(/\S/); const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]) const openComment = isJsx ? "{/*" : "//"; - const closeComment = "*/}"; - // First check the lines before any text changes. + // Check each line before any text changes. for (let i = firstLine; i <= lastLine; i++) { const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); @@ -2011,7 +2010,7 @@ namespace ts { leftMostPosition = Math.min(leftMostPosition, regExec.index); lineTextStarts.set(i.toString(), regExec.index); - if (lineText.substr(regExec.index, openComment.length) !== openComment) { // TODO: Validate when it is inside a comment. It can only uncomment if it's inside a comment. // TODO: Check when not finishing on empty space. + if (lineText.substr(regExec.index, openComment.length) !== openComment) { isCommenting = true; } } @@ -2020,9 +2019,12 @@ namespace ts { // Push all text changes. for (let i = firstLine; i <= lastLine; i++) { const lineTextStart = lineTextStarts.get(i.toString()); - // If the line is not an empty line; otherwise no-op; + + // If the line is not an empty line; otherwise no-op. if (lineTextStart !== undefined) { - if (isCommenting) { + if (isJsx) { + textChanges.push(...toggleMultilineComment(fileName, [{ pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }], isCommenting, isJsx)); + } else if (isCommenting) { textChanges.push({ newText: openComment, span: { @@ -2030,16 +2032,6 @@ namespace ts { start: lineStarts[i] + leftMostPosition } }); - - if (isJsx) { - textChanges.push({ - newText: closeComment, - span: { - length: 0, - start: sourceFile.getLineEndOfPosition(lineStarts[i]) - } - }); - } } else { textChanges.push({ newText: "", @@ -2048,16 +2040,6 @@ namespace ts { start: lineStarts[i] + lineTextStart } }); - - if (isJsx) { - textChanges.push({ - newText: "", - span: { - length: closeComment.length, - start: sourceFile.getLineEndOfPosition(lineStarts[i]) - closeComment.length - } - }); - } } } } @@ -2066,17 +2048,17 @@ namespace ts { return textChanges; } - function toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[] { + function toggleMultilineComment(fileName: string, textRanges: TextRange[], insertComment?: boolean, isInsideJsx?: boolean): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const textChanges: TextChange[] = []; const { text } = sourceFile; for (const textRange of textRanges) { - let isCommenting = false; + let isCommenting = insertComment !== undefined ? insertComment : false; const positions = [] as number[] as SortedArray; let pos = textRange.pos; - const isJsx = isInsideJsxElement(sourceFile, pos); + const isJsx = isInsideJsx !== undefined ? isInsideJsx : isInsideJsxElement(sourceFile, pos); const openMultiline = isJsx ? "{/*" : "/*"; const closeMultiline = isJsx ? "*/}" : "*/"; @@ -2091,7 +2073,7 @@ namespace ts { // If position is in a comment add it to the positions array. if (commentRange) { - // Include brace positions. + // Comment range doesn't include the brace character. Increase it to include them. if (isJsx) { commentRange.pos--; commentRange.end++; @@ -2103,7 +2085,7 @@ namespace ts { } pos = commentRange.end + 1; - } else { + } else { // If it's not in a comment range, then we need to comment the uncommented portions. isCommenting = true; const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); @@ -2164,12 +2146,13 @@ namespace ts { }); } } else { + // If is not commenting then remove all comments found. for (let i = 0; i < positions.length; i++) { const offset = text.substr(positions[i] - closeMultiline.length, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; textChanges.push({ newText: "", span: { - length: 2, + length: openMultiline.length, start: positions[i] - offset } }); diff --git a/tests/cases/fourslash/toggleLineComment10.ts b/tests/cases/fourslash/toggleLineComment10.ts new file mode 100644 index 0000000000000..5acf9c4a27e9a --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment10.ts @@ -0,0 +1,11 @@ +// Close and open multiline comments if the line already contains more. + +//@Filename: file.tsx +//// const a =
    +//// Som[||]e{/* T */}ext +////
    ; + +verify.toggleLineComment( + `const a =
    + {/*Some*/}{/* T */}{/*ext*/} +
    ;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleLineComment9.ts b/tests/cases/fourslash/toggleLineComment9.ts new file mode 100644 index 0000000000000..562e77bf4db7a --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment9.ts @@ -0,0 +1,18 @@ +// If at least one line is uncomment then comment all lines again. +// TODO: Not sure about this one. The default behavior for line comment is to add en extra +// layer of comments (see toggleLineComment4 test). For jsx this doesn't work right as it's actually +// multiline comment. Figure out what to do. + +//@Filename: file.tsx +//// const a =
    +//// {/*[|
    */} +//// SomeText +//// {/*
    |]*/} +////
    ; + +verify.toggleLineComment( + `const a =
    + {/*
    */} + {/* SomeText*/} + {/*
    */} +
    ;`); \ No newline at end of file From 937e3e88e122e3ffb62d686a2c0f34d42cbfb769 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Tue, 25 Feb 2020 14:34:31 -0800 Subject: [PATCH 05/20] Added simplified result to ToggleComment --- src/server/session.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 386c737e49a88..d94f0df7f5adc 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2196,20 +2196,32 @@ namespace ts.server { }); } - private toggleLineComment(args: protocol.ToggleLineCommentRequestArgs, simplifiedResult: boolean) { + private toggleLineComment(args: protocol.ToggleLineCommentRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, project } = this.getFileAndProject(args); - const result = project.getLanguageService().toggleLineComment(file, args.textRanges); + const textChanges = project.getLanguageService().toggleLineComment(file, args.textRanges); - return simplifiedResult ? [] : result; + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; } - private toggleMultilineComment(args: protocol.ToggleMultilineCommentRequestArgs, simplifiedResult: boolean) { + private toggleMultilineComment(args: protocol.ToggleMultilineCommentRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, project } = this.getFileAndProject(args); - const result = project.getLanguageService().toggleMultilineComment(file, args.textRanges); + const textChanges = project.getLanguageService().toggleMultilineComment(file, args.textRanges); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } - return simplifiedResult ? [] : result; + return textChanges; } private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { From 090b38daa1a3ed1c2e86e9433bc62b3b10c71704 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Tue, 25 Feb 2020 16:08:45 -0800 Subject: [PATCH 06/20] Updated d.ts baselines --- .../reference/api/tsserverlibrary.d.ts | 32 +++++++++++++++++++ tests/baselines/reference/api/typescript.d.ts | 2 ++ 2 files changed, 34 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 70e563b7e1cf7..94a2fa743acd9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5315,6 +5315,8 @@ declare namespace ts { getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; getProgram(): Program | undefined; + toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[]; dispose(): void; } interface JsxClosingTagInfo { @@ -6300,6 +6302,10 @@ declare namespace ts.server.protocol { GetEditsForFileRename = "getEditsForFileRename", ConfigurePlugin = "configurePlugin", SelectionRange = "selectionRange", + ToggleLineComment = "toggleLineComment", + ToggleLineCommentFull = "toggleLineComment-full", + ToggleMultilineComment = "toggleMultilineComment", + ToggleMultilineCommentFull = "toggleMultilineComment-full", PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls" @@ -6875,6 +6881,16 @@ declare namespace ts.server.protocol { */ end: Location; } + interface TextRange { + /** + * Position of the first character. + */ + pos: number; + /** + * Position of the last character. + */ + end: number; + } /** * Object found in response messages defining a span of text in a specific source file. */ @@ -7324,6 +7340,20 @@ declare namespace ts.server.protocol { textSpan: TextSpan; parent?: SelectionRange; } + interface ToggleLineCommentRequest extends FileRequest { + command: CommandTypes.ToggleLineComment; + arguments: ToggleLineCommentRequestArgs; + } + interface ToggleLineCommentRequestArgs extends FileRequestArgs { + textRanges: TextRange[]; + } + interface ToggleMultilineCommentRequest extends FileRequest { + command: CommandTypes.ToggleMultilineComment; + arguments: ToggleMultilineCommentRequestArgs; + } + interface ToggleMultilineCommentRequestArgs extends FileRequestArgs { + textRanges: TextRange[]; + } /** * Information found in an "open" request. */ @@ -9677,6 +9707,8 @@ declare namespace ts.server { private getDiagnosticsForProject; private configurePlugin; private getSmartSelectionRange; + private toggleLineComment; + private toggleMultilineComment; private mapSelectionRange; private getScriptInfoFromProjectService; private toProtocolCallHierarchyItem; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index b6397f88523d9..9ef800f41e4d2 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5315,6 +5315,8 @@ declare namespace ts { getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; getProgram(): Program | undefined; + toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[]; dispose(): void; } interface JsxClosingTagInfo { From 381dd8427a5356d8b394042e3683a9e843ce906c Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 27 Feb 2020 12:57:51 -0800 Subject: [PATCH 07/20] Removed TextRange and added FileRangeRequestArgs --- src/harness/fourslashImpl.ts | 36 ++-- src/harness/harnessLanguageService.ts | 8 +- src/server/protocol.ts | 24 +-- src/server/session.ts | 23 ++- src/services/services.ts | 270 +++++++++++++------------- src/services/shims.ts | 16 +- src/services/types.ts | 4 +- 7 files changed, 183 insertions(+), 198 deletions(-) diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 694208856e2c3..d18f6b84b94af 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3190,7 +3190,7 @@ namespace FourSlash { this.raiseError( `Expected to find a fix with the name '${fixName}', but none exists.` + - availableFixes.length + availableFixes.length ? ` Available fixes: ${availableFixes.map(fix => `${fix.fixName} (${fix.fixId ? "with" : "without"} fix-all)`).join(", ")}` : "" ); @@ -3429,13 +3429,13 @@ namespace FourSlash { const incomingCalls = direction === CallHierarchyItemDirection.Outgoing ? { result: "skip" } as const : - alreadySeen ? { result: "seen" } as const : - { result: "show", values: this.languageService.provideCallHierarchyIncomingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const; + alreadySeen ? { result: "seen" } as const : + { result: "show", values: this.languageService.provideCallHierarchyIncomingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const; const outgoingCalls = direction === CallHierarchyItemDirection.Incoming ? { result: "skip" } as const : - alreadySeen ? { result: "seen" } as const : - { result: "show", values: this.languageService.provideCallHierarchyOutgoingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const; + alreadySeen ? { result: "seen" } as const : + { result: "show", values: this.languageService.provideCallHierarchyOutgoingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const; let text = ""; text += `${prefix}╭ name: ${callHierarchyItem.name}\n`; @@ -3446,7 +3446,7 @@ namespace FourSlash { text += `${prefix}├ selectionSpan:\n`; text += this.formatCallHierarchyItemSpan(file, callHierarchyItem.selectionSpan, `${prefix}│ `, incomingCalls.result !== "skip" || outgoingCalls.result !== "skip" ? `${prefix}│ ` : - `${trailingPrefix}╰ `); + `${trailingPrefix}╰ `); if (incomingCalls.result === "seen") { if (outgoingCalls.result === "skip") { @@ -3475,8 +3475,8 @@ namespace FourSlash { text += `${prefix}│ ├ fromSpans:\n`; text += this.formatCallHierarchyItemSpans(file, incomingCall.fromSpans, `${prefix}│ │ `, i < incomingCalls.values.length - 1 ? `${prefix}│ ╰ ` : - outgoingCalls.result !== "skip" ? `${prefix}│ ╰ ` : - `${trailingPrefix}╰ ╰ `); + outgoingCalls.result !== "skip" ? `${prefix}│ ╰ ` : + `${trailingPrefix}╰ ╰ `); } } } @@ -3497,7 +3497,7 @@ namespace FourSlash { text += `${prefix}│ ├ fromSpans:\n`; text += this.formatCallHierarchyItemSpans(file, outgoingCall.fromSpans, `${prefix}│ │ `, i < outgoingCalls.values.length - 1 ? `${prefix}│ ╰ ` : - `${trailingPrefix}╰ ╰ `); + `${trailingPrefix}╰ ╰ `); } } } @@ -3659,20 +3659,22 @@ namespace FourSlash { } public toggleLineComment(newFileContent: string): void { - const ranges = this.getRanges(); - assert(ranges.length); - const changes = this.languageService.toggleLineComment(this.activeFile.fileName, ranges); - + let changes: ts.TextChange[] = []; + for (let range of this.getRanges()) { + changes.push.apply(changes, this.languageService.toggleLineComment(this.activeFile.fileName, range)); + } + this.applyEdits(this.activeFile.fileName, changes); this.verifyCurrentFileContent(newFileContent); } public toggleMultilineComment(newFileContent: string): void { - const ranges = this.getRanges(); - assert(ranges.length); - const changes = this.languageService.toggleMultilineComment(this.activeFile.fileName, ranges); - + let changes: ts.TextChange[] = []; + for (let range of this.getRanges()) { + changes.push.apply(changes, this.languageService.toggleMultilineComment(this.activeFile.fileName, range)); + } + this.applyEdits(this.activeFile.fileName, changes); this.verifyCurrentFileContent(newFileContent); diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 43d53d71b267d..06c85842e2176 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -600,11 +600,11 @@ namespace Harness.LanguageService { clearSourceMapperCache(): never { return ts.notImplemented(); } - toggleLineComment(fileName: string, textRanges: ts.TextRange[]): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRanges)); + toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); } - toggleMultilineComment(fileName: string, textRanges: ts.TextRange[]): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRanges)); + toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); } dispose(): void { this.shim.dispose({}); } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index bf8cff942c981..c459222fff95e 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -923,18 +923,6 @@ namespace ts.server.protocol { end: Location; } - export interface TextRange { - /** - * Position of the first character. - */ - pos: number; - - /** - * Position of the last character. - */ - end: number; - } - /** * Object found in response messages defining a span of text in a specific source file. */ @@ -1551,20 +1539,12 @@ namespace ts.server.protocol { export interface ToggleLineCommentRequest extends FileRequest { command: CommandTypes.ToggleLineComment; - arguments: ToggleLineCommentRequestArgs; - } - - export interface ToggleLineCommentRequestArgs extends FileRequestArgs { - textRanges: TextRange[]; + arguments: FileRangeRequestArgs; } export interface ToggleMultilineCommentRequest extends FileRequest { command: CommandTypes.ToggleMultilineComment; - arguments: ToggleMultilineCommentRequestArgs; - } - - export interface ToggleMultilineCommentRequestArgs extends FileRequestArgs { - textRanges: TextRange[]; + arguments: FileRangeRequestArgs; } /** diff --git a/src/server/session.ts b/src/server/session.ts index d94f0df7f5adc..7553997b7605a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1957,8 +1957,7 @@ namespace ts.server { position = getPosition(args); } else { - const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); - textRange = { pos: startPosition, end: endPosition }; + textRange = this.getRange(args, scriptInfo); } return Debug.checkDefined(position === undefined ? textRange : position); @@ -1967,6 +1966,12 @@ namespace ts.server { } } + private getRange(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo): TextRange { + const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); + + return { pos: startPosition, end: endPosition }; + } + private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; @@ -2196,10 +2201,12 @@ namespace ts.server { }); } - private toggleLineComment(args: protocol.ToggleLineCommentRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + private toggleLineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); - const textChanges = project.getLanguageService().toggleLineComment(file, args.textRanges); + const textChanges = project.getLanguageService().toggleLineComment(file, textRange); if (simplifiedResult) { const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; @@ -2210,10 +2217,12 @@ namespace ts.server { return textChanges; } - private toggleMultilineComment(args: protocol.ToggleMultilineCommentRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + private toggleMultilineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); - const textChanges = project.getLanguageService().toggleMultilineComment(file, args.textRanges); + const textChanges = project.getLanguageService().toggleMultilineComment(file, textRange); if (simplifiedResult) { const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; @@ -2678,7 +2687,7 @@ namespace ts.server { [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/true)); }, - [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { + [CommandNames.ToggleMultilineCommentFull]: (request: protocol.ToggleMultilineCommentRequest) => { return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/false)); }, }); diff --git a/src/services/services.ts b/src/services/services.ts index 9bc392c58d81c..1e5f8f5043f92 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -5,8 +5,8 @@ namespace ts { function createNode(kind: TKind, pos: number, end: number, parent: Node): NodeObject | TokenObject | IdentifierObject | PrivateIdentifierObject { const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) : kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) : - kind === SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(SyntaxKind.PrivateIdentifier, pos, end) : - new TokenObject(kind, pos, end); + kind === SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(SyntaxKind.PrivateIdentifier, pos, end) : + new TokenObject(kind, pos, end); node.parent = parent; node.flags = parent.flags & NodeFlags.ContextFlags; return node; @@ -1985,62 +1985,58 @@ namespace ts { } } - function toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[] { + function toggleLineComment(fileName: string, textRange: TextRange): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const textChanges: TextChange[] = []; - - for (const textRange of textRanges) { - const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); - - let isCommenting = false; - let leftMostPosition = Number.MAX_VALUE; - let lineTextStarts = new Map(); - const whiteSpaceRegex = new RegExp(/\S/); - const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]) - const openComment = isJsx ? "{/*" : "//"; - - // Check each line before any text changes. - for (let i = firstLine; i <= lastLine; i++) { - const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); - - // Find the start of text and the left-most character. No-op on empty lines. - const regExec = whiteSpaceRegex.exec(lineText); - if (regExec) { - leftMostPosition = Math.min(leftMostPosition, regExec.index); - lineTextStarts.set(i.toString(), regExec.index); - - if (lineText.substr(regExec.index, openComment.length) !== openComment) { - isCommenting = true; - } + const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); + + let isCommenting = false; + let leftMostPosition = Number.MAX_VALUE; + let lineTextStarts = new Map(); + const whiteSpaceRegex = new RegExp(/\S/); + const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]) + const openComment = isJsx ? "{/*" : "//"; + + // Check each line before any text changes. + for (let i = firstLine; i <= lastLine; i++) { + const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); + + // Find the start of text and the left-most character. No-op on empty lines. + const regExec = whiteSpaceRegex.exec(lineText); + if (regExec) { + leftMostPosition = Math.min(leftMostPosition, regExec.index); + lineTextStarts.set(i.toString(), regExec.index); + + if (lineText.substr(regExec.index, openComment.length) !== openComment) { + isCommenting = true; } } + } - // Push all text changes. - for (let i = firstLine; i <= lastLine; i++) { - const lineTextStart = lineTextStarts.get(i.toString()); - - // If the line is not an empty line; otherwise no-op. - if (lineTextStart !== undefined) { - if (isJsx) { - textChanges.push(...toggleMultilineComment(fileName, [{ pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }], isCommenting, isJsx)); - } else if (isCommenting) { - textChanges.push({ - newText: openComment, - span: { - length: 0, - start: lineStarts[i] + leftMostPosition - } - }); - } else { - textChanges.push({ - newText: "", - span: { - length: openComment.length, - start: lineStarts[i] + lineTextStart - } - }); - } + // Push all text changes. + for (let i = firstLine; i <= lastLine; i++) { + const lineTextStart = lineTextStarts.get(i.toString()); + + // If the line is not an empty line; otherwise no-op. + if (lineTextStart !== undefined) { + if (isJsx) { + textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }, isCommenting, isJsx)); + } else if (isCommenting) { + textChanges.push({ + newText: openComment, + span: { + length: 0, + start: lineStarts[i] + leftMostPosition + } + }); + } else { + textChanges.push({ + newText: "", + span: { + length: openComment.length, + start: lineStarts[i] + lineTextStart + } + }); } } } @@ -2048,116 +2044,114 @@ namespace ts { return textChanges; } - function toggleMultilineComment(fileName: string, textRanges: TextRange[], insertComment?: boolean, isInsideJsx?: boolean): TextChange[] { + function toggleMultilineComment(fileName: string, textRange: TextRange, insertComment?: boolean, isInsideJsx?: boolean): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const textChanges: TextChange[] = []; const { text } = sourceFile; - for (const textRange of textRanges) { - let isCommenting = insertComment !== undefined ? insertComment : false; - const positions = [] as number[] as SortedArray; - - let pos = textRange.pos; - const isJsx = isInsideJsx !== undefined ? isInsideJsx : isInsideJsxElement(sourceFile, pos); - - const openMultiline = isJsx ? "{/*" : "/*"; - const closeMultiline = isJsx ? "*/}" : "*/"; - const openMultilineRegex = isJsx ? "\\{\\/\\*" : "\\/\\*"; - const closeMultilineRegex = isJsx ? "\\*\\/\\}" : "\\*\\/"; - - // Get all comment positions - while (pos <= textRange.end) { - // Start of comment is considered inside comment. - const offset = text.substr(pos, openMultiline.length) === openMultiline ? openMultiline.length : 0; - const commentRange = isInComment(sourceFile, pos + offset); - - // If position is in a comment add it to the positions array. - if (commentRange) { - // Comment range doesn't include the brace character. Increase it to include them. - if (isJsx) { - commentRange.pos--; - commentRange.end++; - } - - positions.push(commentRange.pos); - if (commentRange.kind === SyntaxKind.MultiLineCommentTrivia) { - positions.push(commentRange.end); - } - - pos = commentRange.end + 1; - } else { // If it's not in a comment range, then we need to comment the uncommented portions. - isCommenting = true; - - const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); - pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; + let isCommenting = insertComment !== undefined ? insertComment : false; + const positions = [] as number[] as SortedArray; + + let pos = textRange.pos; + const isJsx = isInsideJsx !== undefined ? isInsideJsx : isInsideJsxElement(sourceFile, pos); + + const openMultiline = isJsx ? "{/*" : "/*"; + const closeMultiline = isJsx ? "*/}" : "*/"; + const openMultilineRegex = isJsx ? "\\{\\/\\*" : "\\/\\*"; + const closeMultilineRegex = isJsx ? "\\*\\/\\}" : "\\*\\/"; + + // Get all comment positions + while (pos <= textRange.end) { + // Start of comment is considered inside comment. + const offset = text.substr(pos, openMultiline.length) === openMultiline ? openMultiline.length : 0; + const commentRange = isInComment(sourceFile, pos + offset); + + // If position is in a comment add it to the positions array. + if (commentRange) { + // Comment range doesn't include the brace character. Increase it to include them. + if (isJsx) { + commentRange.pos--; + commentRange.end++; } - } - if (isCommenting) { - if (isInComment(sourceFile, textRange.pos)?.kind !== SyntaxKind.SingleLineCommentTrivia) { - insertSorted(positions, textRange.pos, compareValues); + positions.push(commentRange.pos); + if (commentRange.kind === SyntaxKind.MultiLineCommentTrivia) { + positions.push(commentRange.end); } - insertSorted(positions, textRange.end, compareValues); - // Insert open comment if the first position is not a comment already. - const firstPos = positions[0]; - if (text.substr(firstPos, openMultiline.length) !== openMultiline) { - textChanges.push({ - newText: openMultiline, - span: { - length: 0, - start: firstPos - } - }); - } + pos = commentRange.end + 1; + } else { // If it's not in a comment range, then we need to comment the uncommented portions. + isCommenting = true; - // Insert open and close comment to all positions between first and last. Exclusive. - for (let i = 1; i < positions.length - 1; i++) { - if (text.substr(positions[i] - closeMultiline.length, closeMultiline.length) !== closeMultiline) { - textChanges.push({ - newText: closeMultiline, - span: { - length: 0, - start: positions[i] - } - }); - } + const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); + pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; + } + } - if (text.substr(positions[i], openMultiline.length) !== openMultiline) { - textChanges.push({ - newText: openMultiline, - span: { - length: 0, - start: positions[i] - } - }); + if (isCommenting) { + if (isInComment(sourceFile, textRange.pos)?.kind !== SyntaxKind.SingleLineCommentTrivia) { + insertSorted(positions, textRange.pos, compareValues); + } + insertSorted(positions, textRange.end, compareValues); + + // Insert open comment if the first position is not a comment already. + const firstPos = positions[0]; + if (text.substr(firstPos, openMultiline.length) !== openMultiline) { + textChanges.push({ + newText: openMultiline, + span: { + length: 0, + start: firstPos } - } + }); + } - // Insert open comment if the last position is not a comment already. - const lastPos = positions[positions.length - 1]; - if (text.substr(lastPos - closeMultiline.length, closeMultiline.length) !== closeMultiline) { + // Insert open and close comment to all positions between first and last. Exclusive. + for (let i = 1; i < positions.length - 1; i++) { + if (text.substr(positions[i] - closeMultiline.length, closeMultiline.length) !== closeMultiline) { textChanges.push({ newText: closeMultiline, span: { length: 0, - start: lastPos + start: positions[i] } }); } - } else { - // If is not commenting then remove all comments found. - for (let i = 0; i < positions.length; i++) { - const offset = text.substr(positions[i] - closeMultiline.length, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; + + if (text.substr(positions[i], openMultiline.length) !== openMultiline) { textChanges.push({ - newText: "", + newText: openMultiline, span: { - length: openMultiline.length, - start: positions[i] - offset + length: 0, + start: positions[i] } }); } } + + // Insert open comment if the last position is not a comment already. + const lastPos = positions[positions.length - 1]; + if (text.substr(lastPos - closeMultiline.length, closeMultiline.length) !== closeMultiline) { + textChanges.push({ + newText: closeMultiline, + span: { + length: 0, + start: lastPos + } + }); + } + } else { + // If is not commenting then remove all comments found. + for (let i = 0; i < positions.length; i++) { + const offset = text.substr(positions[i] - closeMultiline.length, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; + textChanges.push({ + newText: "", + span: { + length: openMultiline.length, + start: positions[i] - offset + } + }); + } } return textChanges; diff --git a/src/services/shims.ts b/src/services/shims.ts index 1e1669be6052b..5bf8e4235d7a1 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -278,8 +278,8 @@ namespace ts { getEmitOutput(fileName: string): string; getEmitOutputObject(fileName: string): EmitOutput; - toggleLineComment(fileName: string, textChanges: ts.TextRange[]): string; - toggleMultilineComment(fileName: string, textChanges: ts.TextRange[]): string; + toggleLineComment(fileName: string, textChange: ts.TextRange): string; + toggleMultilineComment(fileName: string, textChange: ts.TextRange): string; } export interface ClassifierShim extends Shim { @@ -1070,17 +1070,17 @@ namespace ts { this.logPerformance) as EmitOutput; } - public toggleLineComment(fileName: string, textRanges: ts.TextRange[]): string { + public toggleLineComment(fileName: string, textRange: ts.TextRange): string { return this.forwardJSONCall( - `toggleLineComment('${fileName}', '${JSON.stringify(textRanges)}')`, - () => this.languageService.toggleLineComment(fileName, textRanges) + `toggleLineComment('${fileName}', '${JSON.stringify(textRange)}')`, + () => this.languageService.toggleLineComment(fileName, textRange) ); } - public toggleMultilineComment(fileName: string, textRanges: ts.TextRange[]): string { + public toggleMultilineComment(fileName: string, textRange: ts.TextRange): string { return this.forwardJSONCall( - `toggleMultilineComment('${fileName}', '${JSON.stringify(textRanges)}')`, - () => this.languageService.toggleMultilineComment(fileName, textRanges) + `toggleMultilineComment('${fileName}', '${JSON.stringify(textRange)}')`, + () => this.languageService.toggleMultilineComment(fileName, textRange) ); } } diff --git a/src/services/types.ts b/src/services/types.ts index 39644aa9f0aec..e508c3200da44 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -486,8 +486,8 @@ namespace ts { /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; - toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[]; - toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; dispose(): void; } From fe91f317de7adf6dd0af648505c5e06116e58214 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 27 Feb 2020 17:53:31 -0800 Subject: [PATCH 08/20] Fixed uncomment bug --- src/services/services.ts | 12 ++++----- src/services/utilities.ts | 26 +++++++++++++------ .../fourslash/toggleMultilineComment8.ts | 12 +++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 tests/cases/fourslash/toggleMultilineComment8.ts diff --git a/src/services/services.ts b/src/services/services.ts index 1e5f8f5043f92..4e655b8775315 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2081,9 +2081,9 @@ namespace ts { pos = commentRange.end + 1; } else { // If it's not in a comment range, then we need to comment the uncommented portions. - isCommenting = true; + let newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); - const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); + isCommenting = isCommenting || !isTextWhiteSpaceLike(text, pos, newPos === -1 ? textRange.end : pos + newPos); pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; } } @@ -2130,20 +2130,20 @@ namespace ts { } // Insert open comment if the last position is not a comment already. - const lastPos = positions[positions.length - 1]; - if (text.substr(lastPos - closeMultiline.length, closeMultiline.length) !== closeMultiline) { + if (textChanges.length % 2 !== 0) { textChanges.push({ newText: closeMultiline, span: { length: 0, - start: lastPos + start: positions[positions.length - 1] } }); } } else { // If is not commenting then remove all comments found. for (let i = 0; i < positions.length; i++) { - const offset = text.substr(positions[i] - closeMultiline.length, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; + const from = positions[i] - closeMultiline.length > 0 ? positions[i] - closeMultiline.length : 0; + const offset = text.substr(from, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; textChanges.push({ newText: "", span: { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index d692099186d0d..e7642cb80f2d9 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -877,14 +877,14 @@ namespace ts { // specially by `getSymbolAtLocation`. if (isModifier(node) && (forRename || node.kind !== SyntaxKind.DefaultKeyword) ? contains(parent.modifiers, node) : node.kind === SyntaxKind.ClassKeyword ? isClassDeclaration(parent) || isClassExpression(node) : - node.kind === SyntaxKind.FunctionKeyword ? isFunctionDeclaration(parent) || isFunctionExpression(node) : - node.kind === SyntaxKind.InterfaceKeyword ? isInterfaceDeclaration(parent) : - node.kind === SyntaxKind.EnumKeyword ? isEnumDeclaration(parent) : - node.kind === SyntaxKind.TypeKeyword ? isTypeAliasDeclaration(parent) : - node.kind === SyntaxKind.NamespaceKeyword || node.kind === SyntaxKind.ModuleKeyword ? isModuleDeclaration(parent) : - node.kind === SyntaxKind.ImportKeyword ? isImportEqualsDeclaration(parent) : - node.kind === SyntaxKind.GetKeyword ? isGetAccessorDeclaration(parent) : - node.kind === SyntaxKind.SetKeyword && isSetAccessorDeclaration(parent)) { + node.kind === SyntaxKind.FunctionKeyword ? isFunctionDeclaration(parent) || isFunctionExpression(node) : + node.kind === SyntaxKind.InterfaceKeyword ? isInterfaceDeclaration(parent) : + node.kind === SyntaxKind.EnumKeyword ? isEnumDeclaration(parent) : + node.kind === SyntaxKind.TypeKeyword ? isTypeAliasDeclaration(parent) : + node.kind === SyntaxKind.NamespaceKeyword || node.kind === SyntaxKind.ModuleKeyword ? isModuleDeclaration(parent) : + node.kind === SyntaxKind.ImportKeyword ? isImportEqualsDeclaration(parent) : + node.kind === SyntaxKind.GetKeyword ? isGetAccessorDeclaration(parent) : + node.kind === SyntaxKind.SetKeyword && isSetAccessorDeclaration(parent)) { const location = getAdjustedLocationForDeclaration(parent, forRename); if (location) { return location; @@ -1947,6 +1947,16 @@ namespace ts { return undefined; } + export function isTextWhiteSpaceLike(text: string, startPos: number, endPos: number): boolean { + for (let i = startPos; i < endPos; i++) { + if (!isWhiteSpaceLike(text.charCodeAt(i))) { + return false; + } + } + + return true; + } + // #endregion // Display-part writer helpers diff --git a/tests/cases/fourslash/toggleMultilineComment8.ts b/tests/cases/fourslash/toggleMultilineComment8.ts new file mode 100644 index 0000000000000..724c597dd0639 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment8.ts @@ -0,0 +1,12 @@ +// If the range only contains comments, uncomment all. + +//// /*let var[|1 = 1;*/ +//// /*let var2 = 2;*/ +//// +//// /*let var3 |]= 3;*/ + +verify.toggleMultilineComment( + `let var1 = 1; +let var2 = 2; + +let var3 = 3;`); \ No newline at end of file From ee37d8e8d34e2c88fc32657c3a2515748bdc476d Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Fri, 28 Feb 2020 18:45:56 -0800 Subject: [PATCH 09/20] Added comment and uncomment selection --- src/harness/client.ts | 1662 +++++++------- src/harness/fourslashImpl.ts | 22 + src/harness/fourslashInterfaceImpl.ts | 8 + src/harness/harnessLanguageService.ts | 1976 +++++++++-------- src/server/protocol.ts | 19 +- src/server/session.ts | 44 + src/services/services.ts | 43 +- src/services/shims.ts | 16 + src/services/types.ts | 2 + src/testRunner/unittests/tsserver/session.ts | 4 +- .../reference/api/tsserverlibrary.d.ts | 39 +- tests/baselines/reference/api/typescript.d.ts | 6 +- tests/cases/fourslash/commentSelection1.ts | 18 + tests/cases/fourslash/commentSelection2.ts | 29 + tests/cases/fourslash/fourslash.ts | 2 + .../fourslash/toggleMultilineComment2.ts | 2 +- tests/cases/fourslash/uncommentSelection1.ts | 30 + tests/cases/fourslash/uncommentSelection2.ts | 26 + tests/cases/fourslash/uncommentSelection3.ts | 34 + tests/cases/fourslash/uncommentSelection4.ts | 40 + 20 files changed, 2177 insertions(+), 1845 deletions(-) create mode 100644 tests/cases/fourslash/commentSelection1.ts create mode 100644 tests/cases/fourslash/commentSelection2.ts create mode 100644 tests/cases/fourslash/uncommentSelection1.ts create mode 100644 tests/cases/fourslash/uncommentSelection2.ts create mode 100644 tests/cases/fourslash/uncommentSelection3.ts create mode 100644 tests/cases/fourslash/uncommentSelection4.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 2609ecc84857d..5440b85ceae8d 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -1,827 +1,835 @@ -namespace ts.server { - export interface SessionClientHost extends LanguageServiceHost { - writeMessage(message: string): void; - } - - interface RenameEntry { - readonly renameInfo: RenameInfo; - readonly inputs: { - readonly fileName: string; - readonly position: number; - readonly findInStrings: boolean; - readonly findInComments: boolean; - }; - readonly locations: RenameLocation[]; - } - - /* @internal */ - export function extractMessage(message: string): string { - // Read the content length - const contentLengthPrefix = "Content-Length: "; - const lines = message.split(/\r?\n/); - Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); - - const contentLengthText = lines[0]; - Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); - const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); - - // Read the body - const responseBody = lines[2]; - - // Verify content length - Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); - return responseBody; - } - - export class SessionClient implements LanguageService { - private sequence = 0; - private lineMaps: Map = createMap(); - private messages: string[] = []; - private lastRenameEntry: RenameEntry | undefined; - - constructor(private host: SessionClientHost) { - } - - public onMessage(message: string): void { - this.messages.push(message); - } - - private writeMessage(message: string): void { - this.host.writeMessage(message); - } - - private getLineMap(fileName: string): number[] { - let lineMap = this.lineMaps.get(fileName); - if (!lineMap) { - lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); - this.lineMaps.set(fileName, lineMap); - } - return lineMap; - } - - private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { - lineMap = lineMap || this.getLineMap(fileName); - return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); - } - - private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { - const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); - return { - line: lineOffset.line + 1, - offset: lineOffset.character + 1 - }; - } - - private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { - return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; - } - - private processRequest(command: string, args: T["arguments"]): T { - const request: protocol.Request = { - seq: this.sequence, - type: "request", - arguments: args, - command - }; - this.sequence++; - - this.writeMessage(JSON.stringify(request)); - - return request; - } - - private processResponse(request: protocol.Request, expectEmptyBody = false): T { - let foundResponseMessage = false; - let response!: T; - while (!foundResponseMessage) { - const lastMessage = this.messages.shift()!; - Debug.assert(!!lastMessage, "Did not receive any responses."); - const responseBody = extractMessage(lastMessage); - try { - response = JSON.parse(responseBody); - // the server may emit events before emitting the response. We - // want to ignore these events for testing purpose. - if (response.type === "response") { - foundResponseMessage = true; - } - } - catch (e) { - throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); - } - } - - // verify the sequence numbers - Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); - - // unmarshal errors - if (!response.success) { - throw new Error("Error " + response.message); - } - - Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); - Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); - - return response; - } - - /*@internal*/ - configure(preferences: UserPreferences) { - const args: protocol.ConfigureRequestArguments = { preferences }; - const request = this.processRequest(CommandNames.Configure, args); - this.processResponse(request, /*expectEmptyBody*/ true); - } - - openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { - const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; - this.processRequest(CommandNames.Open, args); - } - - closeFile(file: string): void { - const args: protocol.FileRequestArgs = { file }; - this.processRequest(CommandNames.Close, args); - } - - createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { - return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; - } - - changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { - // clear the line map after an edit - this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 - this.processRequest(CommandNames.Change, args); - } - - toLineColumnOffset(fileName: string, position: number) { - const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); - return { line, character: offset }; - } - - getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Quickinfo, args); - const response = this.processResponse(request); - const body = response.body!; // TODO: GH#18217 - - return { - kind: body.kind, - kindModifiers: body.kindModifiers, - textSpan: this.decodeSpan(body, fileName), - displayParts: [{ kind: "text", text: body.displayString }], - documentation: [{ kind: "text", text: body.documentation }], - tags: body.tags - }; - } - - getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { - const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; - - const request = this.processRequest(CommandNames.ProjectInfo, args); - const response = this.processResponse(request); - - return { - configFileName: response.body!.configFileName, // TODO: GH#18217 - fileNames: response.body!.fileNames - }; - } - - getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { - // Not passing along 'preferences' because server should already have those from the 'configure' command - const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Completions, args); - const response = this.processResponse(request); - - return { - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation: false, - entries: response.body!.map(entry => { // TODO: GH#18217 - if (entry.replacementSpan !== undefined) { - const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; - // TODO: GH#241 - const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; - return res; - } - - return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 - }) - }; - } - - getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { - const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; - - const request = this.processRequest(CommandNames.CompletionDetails, args); - const response = this.processResponse(request); - Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); - const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); - return { ...response.body![0], codeActions: convertedCodeActions }; - } - - getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { - return notImplemented(); - } - - getNavigateToItems(searchValue: string): NavigateToItem[] { - const args: protocol.NavtoRequestArgs = { - searchValue, - file: this.host.getScriptFileNames()[0] - }; - - const request = this.processRequest(CommandNames.Navto, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - name: entry.name, - containerName: entry.containerName || "", - containerKind: entry.containerKind || ScriptElementKind.unknown, - kind: entry.kind, - kindModifiers: entry.kindModifiers || "", - matchKind: entry.matchKind as keyof typeof PatternMatchKind, - isCaseSensitive: entry.isCaseSensitive, - fileName: entry.file, - textSpan: this.decodeSpan(entry), - })); - } - - getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { - const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); - - - // TODO: handle FormatCodeOptions - const request = this.processRequest(CommandNames.Format, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 - } - - getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { - return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); - } - - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { - const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; - - // TODO: handle FormatCodeOptions - const request = this.processRequest(CommandNames.Formatonkey, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 - } - - getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Definition, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })); - } - - getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); - const response = this.processResponse(request); - const body = Debug.checkDefined(response.body); // TODO: GH#18217 - - return { - definitions: body.definitions.map(entry => ({ - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })), - textSpan: this.decodeSpan(body.textSpan, request.arguments.file) - }; - } - - getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.TypeDefinition, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })); - } - - getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Implementation, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - displayParts: [] - })); - } - - findReferences(_fileName: string, _position: number): ReferencedSymbol[] { - // Not yet implemented. - return []; - } - - getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.References, args); - const response = this.processResponse(request); - - return response.body!.refs.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - isWriteAccess: entry.isWriteAccess, - isDefinition: entry.isDefinition, - })); - } - - getEmitOutput(file: string): EmitOutput { - const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); - const response = this.processResponse(request); - return response.body as EmitOutput; - } - - getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { - return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); - } - getSemanticDiagnostics(file: string): Diagnostic[] { - return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); - } - getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { - return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); - } - - private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { - const request = this.processRequest(command, { file, includeLinePosition: true }); - const response = this.processResponse(request); - const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); - const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! - - return (response.body).map((entry): DiagnosticWithLocation => { - const category = firstDefined(Object.keys(DiagnosticCategory), id => - isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory)[id] : undefined); - return { - file: fakeSourceFile, - start: entry.start, - length: entry.length, - messageText: entry.message, - category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), - code: entry.code, - reportsUnnecessary: entry.reportsUnnecessary, - }; - }); - } - - getCompilerOptionsDiagnostics(): Diagnostic[] { - return notImplemented(); - } - - getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { - // Not passing along 'options' because server should already have those from the 'configure' command - const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; - - const request = this.processRequest(CommandNames.Rename, args); - const response = this.processResponse(request); - const body = response.body!; // TODO: GH#18217 - const locations: RenameLocation[] = []; - for (const entry of body.locs) { - const fileName = entry.file; - for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { - locations.push({ - textSpan: this.decodeSpan({ start, end }, fileName), - fileName, - ...(contextStart !== undefined ? - { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : - undefined), - ...prefixSuffixText - }); - } - } - - const renameInfo = body.info.canRename - ? identity({ - canRename: body.info.canRename, - fileToRename: body.info.fileToRename, - displayName: body.info.displayName, - fullDisplayName: body.info.fullDisplayName, - kind: body.info.kind, - kindModifiers: body.info.kindModifiers, - triggerSpan: createTextSpanFromBounds(position, position), - }) - : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); - this.lastRenameEntry = { - renameInfo, - inputs: { - fileName, - position, - findInStrings: !!findInStrings, - findInComments: !!findInComments, - }, - locations, - }; - return renameInfo; - } - - getSmartSelectionRange() { - return notImplemented(); - } - - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { - if (!this.lastRenameEntry || - this.lastRenameEntry.inputs.fileName !== fileName || - this.lastRenameEntry.inputs.position !== position || - this.lastRenameEntry.inputs.findInStrings !== findInStrings || - this.lastRenameEntry.inputs.findInComments !== findInComments) { - this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); - } - - return this.lastRenameEntry!.locations; - } - - private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { - if (!items) { - return []; - } - - return items.map(item => ({ - text: item.text, - kind: item.kind, - kindModifiers: item.kindModifiers || "", - spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), - childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), - indent: item.indent, - bolded: false, - grayed: false - })); - } - - getNavigationBarItems(file: string): NavigationBarItem[] { - const request = this.processRequest(CommandNames.NavBar, { file }); - const response = this.processResponse(request); - - const lineMap = this.getLineMap(file); - return this.decodeNavigationBarItems(response.body, file, lineMap); - } - - private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { - return { - text: tree.text, - kind: tree.kind, - kindModifiers: tree.kindModifiers, - spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), - nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), - childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) - }; - } - - getNavigationTree(file: string): NavigationTree { - const request = this.processRequest(CommandNames.NavTree, { file }); - const response = this.processResponse(request); - - const lineMap = this.getLineMap(file); - return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 - } - - private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; - private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; - private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { - fileName = fileName || span.file; - lineMap = lineMap || this.getLineMap(fileName); - return createTextSpanFromBounds( - this.lineOffsetToPosition(fileName, span.start, lineMap), - this.lineOffsetToPosition(fileName, span.end, lineMap)); - } - - getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { - return notImplemented(); - } - - getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { - return notImplemented(); - } - - getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { - const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.SignatureHelp, args); - const response = this.processResponse(request); - - if (!response.body) { - return undefined; - } - - const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; - - const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); - - return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; - } - - getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Occurrences, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - isWriteAccess: entry.isWriteAccess, - isDefinition: false - })); - } - - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { - const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; - - const request = this.processRequest(CommandNames.DocumentHighlights, args); - const response = this.processResponse(request); - - return response.body!.map(item => ({ // TODO: GH#18217 - fileName: item.file, - highlightSpans: item.highlightSpans.map(span => ({ - textSpan: this.decodeSpan(span, item.file), - kind: span.kind - })), - })); - } - - getOutliningSpans(file: string): OutliningSpan[] { - const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); - const response = this.processResponse(request); - - return response.body!.map(item => ({ - textSpan: this.decodeSpan(item.textSpan, file), - hintSpan: this.decodeSpan(item.hintSpan, file), - bannerText: item.bannerText, - autoCollapse: item.autoCollapse, - kind: item.kind - })); - } - - getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { - return notImplemented(); - } - - getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion { - return notImplemented(); - } - - isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { - return notImplemented(); - } - - getJsxClosingTagAtPosition(_fileName: string, _position: number): never { - return notImplemented(); - } - - getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { - return notImplemented(); - } - - getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { - const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; - - const request = this.processRequest(CommandNames.GetCodeFixes, args); - const response = this.processResponse(request); - - return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 - ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); - } - - getCombinedCodeFix = notImplemented; - - applyCodeActionCommand = notImplemented; - - private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { - return typeof positionOrRange === "number" - ? this.createFileLocationRequestArgs(fileName, positionOrRange) - : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); - } - - private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { - const { line, offset } = this.positionToOneBasedLineOffset(file, position); - return { file, line, offset }; - } - - private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { - const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); - const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); - return { file, startLine, startOffset, endLine, endOffset }; - } - - private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { - const { line, offset } = this.positionToOneBasedLineOffset(file, start); - const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); - return { file, line, offset, endLine, endOffset }; - } - - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); - - const request = this.processRequest(CommandNames.GetApplicableRefactors, args); - const response = this.processResponse(request); - return response.body!; // TODO: GH#18217 - } - - getEditsForRefactor( - fileName: string, - _formatOptions: FormatCodeSettings, - positionOrRange: number | TextRange, - refactorName: string, - actionName: string): RefactorEditInfo { - - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; - args.refactor = refactorName; - args.action = actionName; - - const request = this.processRequest(CommandNames.GetEditsForRefactor, args); - const response = this.processResponse(request); - - if (!response.body) { - return { edits: [], renameFilename: undefined, renameLocation: undefined }; - } - - const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); - - const renameFilename: string | undefined = response.body.renameFilename; - let renameLocation: number | undefined; - if (renameFilename !== undefined) { - renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 - } - - return { - edits, - renameFilename, - renameLocation - }; - } - - organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { - return notImplemented(); - } - - getEditsForFileRename() { - return notImplemented(); - } - - private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { - return edits.map(edit => { - const fileName = edit.fileName; - return { - fileName, - textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) - }; - }); - } - - private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { - return changes.map(change => ({ - fileName: change.fileName, - textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) - })); - } - - convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { - return { - span: this.decodeSpan(change, fileName), - newText: change.newText ? change.newText : "" - }; - } - - getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Brace, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 - } - - configurePlugin(pluginName: string, configuration: any): void { - const request = this.processRequest("configurePlugin", { pluginName, configuration }); - this.processResponse(request, /*expectEmptyBody*/ true); - } - - getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { - return notImplemented(); - } - - getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); - } - - getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); - } - - getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); - } - - getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); - } - - private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { - return { - file: item.file, - name: item.name, - kind: item.kind, - span: this.decodeSpan(item.span, item.file), - selectionSpan: this.decodeSpan(item.selectionSpan, item.file) - }; - } - - prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); - } - - private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { - return { - from: this.convertCallHierarchyItem(item.from), - fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) - }; - } - - provideCallHierarchyIncomingCalls(fileName: string, position: number) { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); - } - - private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { - return { - to: this.convertCallHierarchyItem(item.to), - fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) - }; - } - - provideCallHierarchyOutgoingCalls(fileName: string, position: number) { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); - } - - getProgram(): Program { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - getNonBoundSourceFile(_fileName: string): SourceFile { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - getSourceFile(_fileName: string): SourceFile { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - cleanupSemanticCache(): void { - throw new Error("cleanupSemanticCache is not available through the server layer."); - } - - getSourceMapper(): never { - return notImplemented(); - } - - clearSourceMapperCache(): never { - return notImplemented(); - } - - toggleLineComment(): ts.TextChange[] { - throw new Error("Method not implemented."); - } - - toggleMultilineComment(): ts.TextChange[] { - throw new Error("Method not implemented."); - } - - dispose(): void { - throw new Error("dispose is not available through the server layer."); - } - } -} +namespace ts.server { + export interface SessionClientHost extends LanguageServiceHost { + writeMessage(message: string): void; + } + + interface RenameEntry { + readonly renameInfo: RenameInfo; + readonly inputs: { + readonly fileName: string; + readonly position: number; + readonly findInStrings: boolean; + readonly findInComments: boolean; + }; + readonly locations: RenameLocation[]; + } + + /* @internal */ + export function extractMessage(message: string): string { + // Read the content length + const contentLengthPrefix = "Content-Length: "; + const lines = message.split(/\r?\n/); + Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); + + const contentLengthText = lines[0]; + Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); + const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); + + // Read the body + const responseBody = lines[2]; + + // Verify content length + Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); + return responseBody; + } + + export class SessionClient implements LanguageService { + private sequence = 0; + private lineMaps: Map = createMap(); + private messages: string[] = []; + private lastRenameEntry: RenameEntry | undefined; + + constructor(private host: SessionClientHost) { + } + + public onMessage(message: string): void { + this.messages.push(message); + } + + private writeMessage(message: string): void { + this.host.writeMessage(message); + } + + private getLineMap(fileName: string): number[] { + let lineMap = this.lineMaps.get(fileName); + if (!lineMap) { + lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); + this.lineMaps.set(fileName, lineMap); + } + return lineMap; + } + + private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { + lineMap = lineMap || this.getLineMap(fileName); + return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); + } + + private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { + const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); + return { + line: lineOffset.line + 1, + offset: lineOffset.character + 1 + }; + } + + private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { + return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; + } + + private processRequest(command: string, args: T["arguments"]): T { + const request: protocol.Request = { + seq: this.sequence, + type: "request", + arguments: args, + command + }; + this.sequence++; + + this.writeMessage(JSON.stringify(request)); + + return request; + } + + private processResponse(request: protocol.Request, expectEmptyBody = false): T { + let foundResponseMessage = false; + let response!: T; + while (!foundResponseMessage) { + const lastMessage = this.messages.shift()!; + Debug.assert(!!lastMessage, "Did not receive any responses."); + const responseBody = extractMessage(lastMessage); + try { + response = JSON.parse(responseBody); + // the server may emit events before emitting the response. We + // want to ignore these events for testing purpose. + if (response.type === "response") { + foundResponseMessage = true; + } + } + catch (e) { + throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); + } + } + + // verify the sequence numbers + Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); + + // unmarshal errors + if (!response.success) { + throw new Error("Error " + response.message); + } + + Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); + Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); + + return response; + } + + /*@internal*/ + configure(preferences: UserPreferences) { + const args: protocol.ConfigureRequestArguments = { preferences }; + const request = this.processRequest(CommandNames.Configure, args); + this.processResponse(request, /*expectEmptyBody*/ true); + } + + openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { + const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; + this.processRequest(CommandNames.Open, args); + } + + closeFile(file: string): void { + const args: protocol.FileRequestArgs = { file }; + this.processRequest(CommandNames.Close, args); + } + + createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { + return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; + } + + changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { + // clear the line map after an edit + this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 + this.processRequest(CommandNames.Change, args); + } + + toLineColumnOffset(fileName: string, position: number) { + const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); + return { line, character: offset }; + } + + getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Quickinfo, args); + const response = this.processResponse(request); + const body = response.body!; // TODO: GH#18217 + + return { + kind: body.kind, + kindModifiers: body.kindModifiers, + textSpan: this.decodeSpan(body, fileName), + displayParts: [{ kind: "text", text: body.displayString }], + documentation: [{ kind: "text", text: body.documentation }], + tags: body.tags + }; + } + + getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { + const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; + + const request = this.processRequest(CommandNames.ProjectInfo, args); + const response = this.processResponse(request); + + return { + configFileName: response.body!.configFileName, // TODO: GH#18217 + fileNames: response.body!.fileNames + }; + } + + getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { + // Not passing along 'preferences' because server should already have those from the 'configure' command + const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Completions, args); + const response = this.processResponse(request); + + return { + isGlobalCompletion: false, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: response.body!.map(entry => { // TODO: GH#18217 + if (entry.replacementSpan !== undefined) { + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; + // TODO: GH#241 + const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; + return res; + } + + return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 + }) + }; + } + + getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { + const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; + + const request = this.processRequest(CommandNames.CompletionDetails, args); + const response = this.processResponse(request); + Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); + const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); + return { ...response.body![0], codeActions: convertedCodeActions }; + } + + getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { + return notImplemented(); + } + + getNavigateToItems(searchValue: string): NavigateToItem[] { + const args: protocol.NavtoRequestArgs = { + searchValue, + file: this.host.getScriptFileNames()[0] + }; + + const request = this.processRequest(CommandNames.Navto, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + name: entry.name, + containerName: entry.containerName || "", + containerKind: entry.containerKind || ScriptElementKind.unknown, + kind: entry.kind, + kindModifiers: entry.kindModifiers || "", + matchKind: entry.matchKind as keyof typeof PatternMatchKind, + isCaseSensitive: entry.isCaseSensitive, + fileName: entry.file, + textSpan: this.decodeSpan(entry), + })); + } + + getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { + const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); + + + // TODO: handle FormatCodeOptions + const request = this.processRequest(CommandNames.Format, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 + } + + getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { + return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); + } + + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { + const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; + + // TODO: handle FormatCodeOptions + const request = this.processRequest(CommandNames.Formatonkey, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 + } + + getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Definition, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })); + } + + getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); + const response = this.processResponse(request); + const body = Debug.checkDefined(response.body); // TODO: GH#18217 + + return { + definitions: body.definitions.map(entry => ({ + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })), + textSpan: this.decodeSpan(body.textSpan, request.arguments.file) + }; + } + + getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.TypeDefinition, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })); + } + + getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Implementation, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + displayParts: [] + })); + } + + findReferences(_fileName: string, _position: number): ReferencedSymbol[] { + // Not yet implemented. + return []; + } + + getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.References, args); + const response = this.processResponse(request); + + return response.body!.refs.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + isDefinition: entry.isDefinition, + })); + } + + getEmitOutput(file: string): EmitOutput { + const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); + const response = this.processResponse(request); + return response.body as EmitOutput; + } + + getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { + return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); + } + getSemanticDiagnostics(file: string): Diagnostic[] { + return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); + } + getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { + return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); + } + + private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { + const request = this.processRequest(command, { file, includeLinePosition: true }); + const response = this.processResponse(request); + const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); + const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! + + return (response.body).map((entry): DiagnosticWithLocation => { + const category = firstDefined(Object.keys(DiagnosticCategory), id => + isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory)[id] : undefined); + return { + file: fakeSourceFile, + start: entry.start, + length: entry.length, + messageText: entry.message, + category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), + code: entry.code, + reportsUnnecessary: entry.reportsUnnecessary, + }; + }); + } + + getCompilerOptionsDiagnostics(): Diagnostic[] { + return notImplemented(); + } + + getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { + // Not passing along 'options' because server should already have those from the 'configure' command + const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; + + const request = this.processRequest(CommandNames.Rename, args); + const response = this.processResponse(request); + const body = response.body!; // TODO: GH#18217 + const locations: RenameLocation[] = []; + for (const entry of body.locs) { + const fileName = entry.file; + for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { + locations.push({ + textSpan: this.decodeSpan({ start, end }, fileName), + fileName, + ...(contextStart !== undefined ? + { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : + undefined), + ...prefixSuffixText + }); + } + } + + const renameInfo = body.info.canRename + ? identity({ + canRename: body.info.canRename, + fileToRename: body.info.fileToRename, + displayName: body.info.displayName, + fullDisplayName: body.info.fullDisplayName, + kind: body.info.kind, + kindModifiers: body.info.kindModifiers, + triggerSpan: createTextSpanFromBounds(position, position), + }) + : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); + this.lastRenameEntry = { + renameInfo, + inputs: { + fileName, + position, + findInStrings: !!findInStrings, + findInComments: !!findInComments, + }, + locations, + }; + return renameInfo; + } + + getSmartSelectionRange() { + return notImplemented(); + } + + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { + if (!this.lastRenameEntry || + this.lastRenameEntry.inputs.fileName !== fileName || + this.lastRenameEntry.inputs.position !== position || + this.lastRenameEntry.inputs.findInStrings !== findInStrings || + this.lastRenameEntry.inputs.findInComments !== findInComments) { + this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); + } + + return this.lastRenameEntry!.locations; + } + + private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { + if (!items) { + return []; + } + + return items.map(item => ({ + text: item.text, + kind: item.kind, + kindModifiers: item.kindModifiers || "", + spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), + indent: item.indent, + bolded: false, + grayed: false + })); + } + + getNavigationBarItems(file: string): NavigationBarItem[] { + const request = this.processRequest(CommandNames.NavBar, { file }); + const response = this.processResponse(request); + + const lineMap = this.getLineMap(file); + return this.decodeNavigationBarItems(response.body, file, lineMap); + } + + private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { + return { + text: tree.text, + kind: tree.kind, + kindModifiers: tree.kindModifiers, + spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), + childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) + }; + } + + getNavigationTree(file: string): NavigationTree { + const request = this.processRequest(CommandNames.NavTree, { file }); + const response = this.processResponse(request); + + const lineMap = this.getLineMap(file); + return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 + } + + private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; + private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; + private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { + fileName = fileName || span.file; + lineMap = lineMap || this.getLineMap(fileName); + return createTextSpanFromBounds( + this.lineOffsetToPosition(fileName, span.start, lineMap), + this.lineOffsetToPosition(fileName, span.end, lineMap)); + } + + getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { + return notImplemented(); + } + + getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { + return notImplemented(); + } + + getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { + const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.SignatureHelp, args); + const response = this.processResponse(request); + + if (!response.body) { + return undefined; + } + + const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; + + const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); + + return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; + } + + getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Occurrences, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + isDefinition: false + })); + } + + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { + const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; + + const request = this.processRequest(CommandNames.DocumentHighlights, args); + const response = this.processResponse(request); + + return response.body!.map(item => ({ // TODO: GH#18217 + fileName: item.file, + highlightSpans: item.highlightSpans.map(span => ({ + textSpan: this.decodeSpan(span, item.file), + kind: span.kind + })), + })); + } + + getOutliningSpans(file: string): OutliningSpan[] { + const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); + const response = this.processResponse(request); + + return response.body!.map(item => ({ + textSpan: this.decodeSpan(item.textSpan, file), + hintSpan: this.decodeSpan(item.hintSpan, file), + bannerText: item.bannerText, + autoCollapse: item.autoCollapse, + kind: item.kind + })); + } + + getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { + return notImplemented(); + } + + getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion { + return notImplemented(); + } + + isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { + return notImplemented(); + } + + getJsxClosingTagAtPosition(_fileName: string, _position: number): never { + return notImplemented(); + } + + getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { + return notImplemented(); + } + + getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { + const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; + + const request = this.processRequest(CommandNames.GetCodeFixes, args); + const response = this.processResponse(request); + + return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 + ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); + } + + getCombinedCodeFix = notImplemented; + + applyCodeActionCommand = notImplemented; + + private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { + return typeof positionOrRange === "number" + ? this.createFileLocationRequestArgs(fileName, positionOrRange) + : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); + } + + private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { + const { line, offset } = this.positionToOneBasedLineOffset(file, position); + return { file, line, offset }; + } + + private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { + const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); + return { file, startLine, startOffset, endLine, endOffset }; + } + + private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { + const { line, offset } = this.positionToOneBasedLineOffset(file, start); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); + return { file, line, offset, endLine, endOffset }; + } + + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(CommandNames.GetApplicableRefactors, args); + const response = this.processResponse(request); + return response.body!; // TODO: GH#18217 + } + + getEditsForRefactor( + fileName: string, + _formatOptions: FormatCodeSettings, + positionOrRange: number | TextRange, + refactorName: string, + actionName: string): RefactorEditInfo { + + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; + args.refactor = refactorName; + args.action = actionName; + + const request = this.processRequest(CommandNames.GetEditsForRefactor, args); + const response = this.processResponse(request); + + if (!response.body) { + return { edits: [], renameFilename: undefined, renameLocation: undefined }; + } + + const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); + + const renameFilename: string | undefined = response.body.renameFilename; + let renameLocation: number | undefined; + if (renameFilename !== undefined) { + renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 + } + + return { + edits, + renameFilename, + renameLocation + }; + } + + organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { + return notImplemented(); + } + + getEditsForFileRename() { + return notImplemented(); + } + + private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { + return edits.map(edit => { + const fileName = edit.fileName; + return { + fileName, + textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) + }; + }); + } + + private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { + return changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) + })); + } + + convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { + return { + span: this.decodeSpan(change, fileName), + newText: change.newText ? change.newText : "" + }; + } + + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Brace, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 + } + + configurePlugin(pluginName: string, configuration: any): void { + const request = this.processRequest("configurePlugin", { pluginName, configuration }); + this.processResponse(request, /*expectEmptyBody*/ true); + } + + getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { + return notImplemented(); + } + + getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { + return notImplemented(); + } + + getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { + return notImplemented(); + } + + getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { + return notImplemented(); + } + + getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { + return notImplemented(); + } + + private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { + return { + file: item.file, + name: item.name, + kind: item.kind, + span: this.decodeSpan(item.span, item.file), + selectionSpan: this.decodeSpan(item.selectionSpan, item.file) + }; + } + + prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); + } + + private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { + return { + from: this.convertCallHierarchyItem(item.from), + fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) + }; + } + + provideCallHierarchyIncomingCalls(fileName: string, position: number) { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); + } + + private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { + return { + to: this.convertCallHierarchyItem(item.to), + fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) + }; + } + + provideCallHierarchyOutgoingCalls(fileName: string, position: number) { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); + } + + getProgram(): Program { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getNonBoundSourceFile(_fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getSourceFile(_fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + cleanupSemanticCache(): void { + throw new Error("cleanupSemanticCache is not available through the server layer."); + } + + getSourceMapper(): never { + return notImplemented(); + } + + clearSourceMapperCache(): never { + return notImplemented(); + } + + toggleLineComment(): ts.TextChange[] { + throw new Error("Method not implemented."); + } + + toggleMultilineComment(): ts.TextChange[] { + throw new Error("Method not implemented."); + } + + commentSelection(): ts.TextChange[] { + throw new Error("Method not implemented."); + } + + uncommentSelection(): ts.TextChange[] { + throw new Error("Method not implemented."); + } + + dispose(): void { + throw new Error("dispose is not available through the server layer."); + } + } +} \ No newline at end of file diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index d18f6b84b94af..62a488821352c 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3679,6 +3679,28 @@ namespace FourSlash { this.verifyCurrentFileContent(newFileContent); } + + public commentSelection(newFileContent: string): void { + let changes: ts.TextChange[] = []; + for (let range of this.getRanges()) { + changes.push.apply(changes, this.languageService.commentSelection(this.activeFile.fileName, range)); + } + + this.applyEdits(this.activeFile.fileName, changes); + + this.verifyCurrentFileContent(newFileContent); + } + + public uncommentSelection(newFileContent: string): void { + let changes: ts.TextChange[] = []; + for (let range of this.getRanges()) { + changes.push.apply(changes, this.languageService.uncommentSelection(this.activeFile.fileName, range)); + } + + this.applyEdits(this.activeFile.fileName, changes); + + this.verifyCurrentFileContent(newFileContent); + } } function prefixMessage(message: string | undefined) { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 76548debdbf10..dc958dcc8ff0d 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -218,6 +218,14 @@ namespace FourSlashInterface { public toggleMultilineComment(newFileContent: string) { this.state.toggleMultilineComment(newFileContent); } + + public commentSelection(newFileContent: string) { + this.state.commentSelection(newFileContent); + } + + public uncommentSelection(newFileContent: string) { + this.state.uncommentSelection(newFileContent); + } } export class Verify extends VerifyNegatable { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 06c85842e2176..7bc137cb4e802 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -1,985 +1,991 @@ -namespace Harness.LanguageService { - - export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { - const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null - const langSvc: any = info.languageService; - for (const k of Object.keys(langSvc)) { - // eslint-disable-next-line only-arrow-functions - proxy[k] = function () { - return langSvc[k].apply(langSvc, arguments); - }; - } - return proxy; - } - - export class ScriptInfo { - public version = 1; - public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; - private lineMap: number[] | undefined; - - constructor(public fileName: string, public content: string, public isRootFile: boolean) { - this.setContent(content); - } - - private setContent(content: string): void { - this.content = content; - this.lineMap = undefined; - } - - public getLineMap(): number[] { - return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); - } - - public updateContent(content: string): void { - this.editRanges = []; - this.setContent(content); - this.version++; - } - - public editContent(start: number, end: number, newText: string): void { - // Apply edits - const prefix = this.content.substring(0, start); - const middle = newText; - const suffix = this.content.substring(end); - this.setContent(prefix + middle + suffix); - - // Store edit range + new length of script - this.editRanges.push({ - length: this.content.length, - textChangeRange: ts.createTextChangeRange( - ts.createTextSpanFromBounds(start, end), newText.length) - }); - - // Update version # - this.version++; - } - - public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { - if (startVersion === endVersion) { - // No edits! - return ts.unchangedTextChangeRange; - } - - const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); - const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); - - const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); - return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); - } - } - - class ScriptSnapshot implements ts.IScriptSnapshot { - public textSnapshot: string; - public version: number; - - constructor(public scriptInfo: ScriptInfo) { - this.textSnapshot = scriptInfo.content; - this.version = scriptInfo.version; - } - - public getText(start: number, end: number): string { - return this.textSnapshot.substring(start, end); - } - - public getLength(): number { - return this.textSnapshot.length; - } - - public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { - const oldShim = oldScript; - return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); - } - } - - class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { - constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { - } - - public getText(start: number, end: number): string { - return this.scriptSnapshot.getText(start, end); - } - - public getLength(): number { - return this.scriptSnapshot.getLength(); - } - - public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { - const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); - return range && JSON.stringify(range); - } - } - - class DefaultHostCancellationToken implements ts.HostCancellationToken { - public static readonly instance = new DefaultHostCancellationToken(); - - public isCancellationRequested() { - return false; - } - } - - export interface LanguageServiceAdapter { - getHost(): LanguageServiceAdapterHost; - getLanguageService(): ts.LanguageService; - getClassifier(): ts.Classifier; - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; - } - - export abstract class LanguageServiceAdapterHost { - public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); - public typesRegistry: ts.Map | undefined; - private scriptInfos: collections.SortedMap; - - constructor(protected cancellationToken = DefaultHostCancellationToken.instance, - protected settings = ts.getDefaultCompilerOptions()) { - this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); - } - - public get vfs() { - return this.sys.vfs; - } - - public getNewLine(): string { - return harnessNewLine; - } - - public getFilenames(): string[] { - const fileNames: string[] = []; - this.scriptInfos.forEach(scriptInfo => { - if (scriptInfo.isRootFile) { - // only include root files here - // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. - fileNames.push(scriptInfo.fileName); - } - }); - return fileNames; - } - - public getScriptInfo(fileName: string): ScriptInfo | undefined { - return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); - } - - public addScript(fileName: string, content: string, isRootFile: boolean): void { - this.vfs.mkdirpSync(vpath.dirname(fileName)); - this.vfs.writeFileSync(fileName, content); - this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); - } - - public renameFileOrDirectory(oldPath: string, newPath: string): void { - this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); - this.vfs.renameSync(oldPath, newPath); - - const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); - this.scriptInfos.forEach((scriptInfo, key) => { - const newFileName = updater(key); - if (newFileName !== undefined) { - this.scriptInfos.delete(key); - this.scriptInfos.set(newFileName, scriptInfo); - scriptInfo.fileName = newFileName; - } - }); - } - - public editScript(fileName: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(fileName); - if (script) { - script.editContent(start, end, newText); - this.vfs.mkdirpSync(vpath.dirname(fileName)); - this.vfs.writeFileSync(fileName, script.content); - return; - } - - throw new Error("No script with name '" + fileName + "'"); - } - - public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } - - /** - * @param line 0 based index - * @param col 0 based index - */ - public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { - const script: ScriptInfo = this.getScriptInfo(fileName)!; - assert.isOk(script); - return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); - } - - public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { - const script: ScriptInfo = this.getScriptInfo(fileName)!; - assert.isOk(script); - return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); - } - - useCaseSensitiveFileNames() { - return !this.vfs.ignoreCase; - } - } - - /// Native adapter - class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { - isKnownTypesPackageName(name: string): boolean { - return !!this.typesRegistry && this.typesRegistry.has(name); - } - - getGlobalTypingsCacheLocation() { - return "/Library/Caches/typescript"; - } - - installPackage = ts.notImplemented; - - getCompilationSettings() { return this.settings; } - - getCancellationToken() { return this.cancellationToken; } - - getDirectories(path: string): string[] { - return this.sys.getDirectories(path); - } - - getCurrentDirectory(): string { return virtualFileSystemRoot; } - - getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } - - getScriptFileNames(): string[] { - return this.getFilenames().filter(ts.isAnySupportedFileExtension); - } - - getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { - const script = this.getScriptInfo(fileName); - return script ? new ScriptSnapshot(script) : undefined; - } - - getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } - - getScriptVersion(fileName: string): string { - const script = this.getScriptInfo(fileName); - return script ? script.version.toString() : undefined!; // TODO: GH#18217 - } - - directoryExists(dirName: string): boolean { - return this.sys.directoryExists(dirName); - } - - fileExists(fileName: string): boolean { - return this.sys.fileExists(fileName); - } - - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.sys.readDirectory(path, extensions, exclude, include, depth); - } - - readFile(path: string): string | undefined { - return this.sys.readFile(path); - } - - realpath(path: string): string { - return this.sys.realpath(path); - } - - getTypeRootsVersion() { - return 0; - } - - log = ts.noop; - trace = ts.noop; - error = ts.noop; - } - - export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { - private host: NativeLanguageServiceHost; - constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - this.host = new NativeLanguageServiceHost(cancellationToken, options); - } - getHost(): LanguageServiceAdapterHost { return this.host; } - getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } - getClassifier(): ts.Classifier { return ts.createClassifier(); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } - } - - /// Shim adapter - class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { - private nativeHost: NativeLanguageServiceHost; - - public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; - public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; - - constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - super(cancellationToken, options); - this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); - - if (preprocessToResolve) { - const compilerOptions = this.nativeHost.getCompilationSettings(); - const moduleResolutionHost: ts.ModuleResolutionHost = { - fileExists: fileName => this.getScriptInfo(fileName) !== undefined, - readFile: fileName => { - const scriptInfo = this.getScriptInfo(fileName); - return scriptInfo && scriptInfo.content; - } - }; - this.getModuleResolutionsForFile = (fileName) => { - const scriptInfo = this.getScriptInfo(fileName)!; - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); - const imports: ts.MapLike = {}; - for (const module of preprocessInfo.importedFiles) { - const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); - if (resolutionInfo.resolvedModule) { - imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; - } - } - return JSON.stringify(imports); - }; - this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { - const scriptInfo = this.getScriptInfo(fileName); - if (scriptInfo) { - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); - const resolutions: ts.MapLike = {}; - const settings = this.nativeHost.getCompilationSettings(); - for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { - const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); - if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { - resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; - } - } - return JSON.stringify(resolutions); - } - else { - return "[]"; - } - }; - } - } - - getFilenames(): string[] { return this.nativeHost.getFilenames(); } - getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } - addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } - editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } - positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } - - getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } - getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } - getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } - getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } - getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } - getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } - getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { - const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 - return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); - } - getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } - getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } - getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } - - readDirectory = ts.notImplemented; - readDirectoryNames = ts.notImplemented; - readFileNames = ts.notImplemented; - fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } - readFile(fileName: string) { - const snapshot = this.nativeHost.getScriptSnapshot(fileName); - return snapshot && ts.getSnapshotText(snapshot); - } - log(s: string): void { this.nativeHost.log(s); } - trace(s: string): void { this.nativeHost.trace(s); } - error(s: string): void { this.nativeHost.error(s); } - directoryExists(): boolean { - // for tests pessimistically assume that directory always exists - return true; - } - } - - class ClassifierShimProxy implements ts.Classifier { - constructor(private shim: ts.ClassifierShim) { - } - getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { - return ts.notImplemented(); - } - getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { - const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); - const entries: ts.ClassificationInfo[] = []; - let i = 0; - let position = 0; - - for (; i < result.length - 1; i += 2) { - const t = entries[i / 2] = { - length: parseInt(result[i]), - classification: parseInt(result[i + 1]) - }; - - assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); - position += t.length; - } - const finalLexState = parseInt(result[result.length - 1]); - - assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); - - return { - finalLexState, - entries - }; - } - } - - function unwrapJSONCallResult(result: string): any { - const parsedResult = JSON.parse(result); - if (parsedResult.error) { - throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); - } - else if (parsedResult.canceled) { - throw new ts.OperationCanceledException(); - } - return parsedResult.result; - } - - class LanguageServiceShimProxy implements ts.LanguageService { - constructor(private shim: ts.LanguageServiceShim) { - } - cleanupSemanticCache(): void { - this.shim.cleanupSemanticCache(); - } - getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); - } - getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); - } - getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); - } - getCompilerOptionsDiagnostics(): ts.Diagnostic[] { - return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); - } - getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); - } - getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length)); - } - getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); - } - getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); - } - getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { - return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); - } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { - return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); - } - getCompletionEntrySymbol(): ts.Symbol { - throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); - } - getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { - return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); - } - getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); - } - getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); - } - getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { - return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); - } - getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { - return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); - } - 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)); - } - getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { - return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); - } - getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { - return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); - } - getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { - return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); - } - getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { - return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); - } - getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { - return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); - } - findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { - return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); - } - getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { - return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); - } - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { - return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); - } - getNavigateToItems(searchValue: string): ts.NavigateToItem[] { - return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); - } - getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { - return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); - } - getNavigationTree(fileName: string): ts.NavigationTree { - return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); - } - getOutliningSpans(fileName: string): ts.OutliningSpan[] { - return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); - } - getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { - return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); - } - getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { - return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); - } - getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { - return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); - } - getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); - } - getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); - } - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); - } - getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion { - return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position)); - } - isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { - return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); - } - getJsxClosingTagAtPosition(): never { - throw new Error("Not supported on the shim."); - } - getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); - } - getCodeFixesAtPosition(): never { - throw new Error("Not supported on the shim."); - } - getCombinedCodeFix = ts.notImplemented; - applyCodeActionCommand = ts.notImplemented; - getCodeFixDiagnostics(): ts.Diagnostic[] { - throw new Error("Not supported on the shim."); - } - getEditsForRefactor(): ts.RefactorEditInfo { - throw new Error("Not supported on the shim."); - } - getApplicableRefactors(): ts.ApplicableRefactorInfo[] { - throw new Error("Not supported on the shim."); - } - organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { - throw new Error("Not supported on the shim."); - } - getEditsForFileRename(): readonly ts.FileTextChanges[] { - throw new Error("Not supported on the shim."); - } - prepareCallHierarchy(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); - } - provideCallHierarchyIncomingCalls(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); - } - provideCallHierarchyOutgoingCalls(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); - } - getEmitOutput(fileName: string): ts.EmitOutput { - return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); - } - getProgram(): ts.Program { - throw new Error("Program can not be marshaled across the shim layer."); - } - getNonBoundSourceFile(): ts.SourceFile { - throw new Error("SourceFile can not be marshaled across the shim layer."); - } - getSourceFile(): ts.SourceFile { - throw new Error("SourceFile can not be marshaled across the shim layer."); - } - getSourceMapper(): never { - return ts.notImplemented(); - } - clearSourceMapperCache(): never { - return ts.notImplemented(); - } - toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); - } - toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); - } - dispose(): void { this.shim.dispose({}); } - } - - export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { - private host: ShimLanguageServiceHost; - private factory: ts.TypeScriptServicesFactory; - constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); - this.factory = new ts.TypeScriptServicesFactory(); - } - getHost() { return this.host; } - getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } - getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { - const coreServicesShim = this.factory.createCoreServicesShim(this.host); - const shimResult: { - referencedFiles: ts.ShimsFileReference[]; - typeReferenceDirectives: ts.ShimsFileReference[]; - importedFiles: ts.ShimsFileReference[]; - isLibFile: boolean; - } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); - - const convertResult: ts.PreProcessedFileInfo = { - referencedFiles: [], - importedFiles: [], - ambientExternalModules: [], - isLibFile: shimResult.isLibFile, - typeReferenceDirectives: [], - libReferenceDirectives: [] - }; - - ts.forEach(shimResult.referencedFiles, refFile => { - convertResult.referencedFiles.push({ - fileName: refFile.path, - pos: refFile.position, - end: refFile.position + refFile.length - }); - }); - - ts.forEach(shimResult.importedFiles, importedFile => { - convertResult.importedFiles.push({ - fileName: importedFile.path, - pos: importedFile.position, - end: importedFile.position + importedFile.length - }); - }); - - ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { - convertResult.importedFiles.push({ - fileName: typeRefDirective.path, - pos: typeRefDirective.position, - end: typeRefDirective.position + typeRefDirective.length - }); - }); - return convertResult; - } - } - - // Server adapter - class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { - private client!: ts.server.SessionClient; - - constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { - super(cancellationToken, settings); - } - - onMessage = ts.noop; - writeMessage = ts.noop; - - setClient(client: ts.server.SessionClient) { - this.client = client; - } - - openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { - super.openFile(fileName, content, scriptKindName); - this.client.openFile(fileName, content, scriptKindName); - } - - editScript(fileName: string, start: number, end: number, newText: string) { - const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); - super.editScript(fileName, start, end, newText); - this.client.changeFile(fileName, changeArgs); - } - } - - class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { - args: string[] = []; - newLine: string; - useCaseSensitiveFileNames = false; - - constructor(private host: NativeLanguageServiceHost) { - this.newLine = this.host.getNewLine(); - } - - onMessage = ts.noop; - writeMessage = ts.noop; // overridden - write(message: string): void { - this.writeMessage(message); - } - - readFile(fileName: string): string | undefined { - if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { - fileName = Compiler.defaultLibFileName; - } - - const snapshot = this.host.getScriptSnapshot(fileName); - return snapshot && ts.getSnapshotText(snapshot); - } - - writeFile = ts.noop; - - resolvePath(path: string): string { - return path; - } - - fileExists(path: string): boolean { - return !!this.host.getScriptSnapshot(path); - } - - directoryExists(): boolean { - // for tests assume that directory exists - return true; - } - - getExecutingFilePath(): string { - return ""; - } - - exit = ts.noop; - - createDirectory(_directoryName: string): void { - return ts.notImplemented(); - } - - getCurrentDirectory(): string { - return this.host.getCurrentDirectory(); - } - - getDirectories(path: string): string[] { - return this.host.getDirectories(path); - } - - getEnvironmentVariable(name: string): string { - return ts.sys.getEnvironmentVariable(name); - } - - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.host.readDirectory(path, extensions, exclude, include, depth); - } - - watchFile(): ts.FileWatcher { - return { close: ts.noop }; - } - - watchDirectory(): ts.FileWatcher { - return { close: ts.noop }; - } - - close = ts.noop; - - info(message: string): void { - this.host.log(message); - } - - msg(message: string): void { - this.host.log(message); - } - - loggingEnabled() { - return true; - } - - getLogFileName(): string | undefined { - return undefined; - } - - hasLevel() { - return false; - } - - startGroup() { throw ts.notImplemented(); } - endGroup() { throw ts.notImplemented(); } - - perftrc(message: string): void { - return this.host.log(message); - } - - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { - // eslint-disable-next-line no-restricted-globals - return setTimeout(callback, ms, args); - } - - clearTimeout(timeoutId: any): void { - // eslint-disable-next-line no-restricted-globals - clearTimeout(timeoutId); - } - - setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { - // eslint-disable-next-line no-restricted-globals - return setImmediate(callback, args); - } - - clearImmediate(timeoutId: any): void { - // eslint-disable-next-line no-restricted-globals - clearImmediate(timeoutId); - } - - createHash(s: string) { - return mockHash(s); - } - - require(_initialDir: string, _moduleName: string): ts.RequireResult { - switch (_moduleName) { - // Adds to the Quick Info a fixed string and a string from the config file - // and replaces the first display part - case "quickinfo-augmeneter": - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = makeDefaultProxy(info); - const langSvc: any = info.languageService; - // eslint-disable-next-line only-arrow-functions - proxy.getQuickInfoAtPosition = function () { - const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); - if (parts.displayParts.length > 0) { - parts.displayParts[0].text = "Proxied"; - } - parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); - return parts; - }; - - return proxy; - } - }), - error: undefined - }; - - // Throws during initialization - case "create-thrower": - return { - module: () => ({ - create() { - throw new Error("I am not a well-behaved plugin"); - } - }), - error: undefined - }; - - // Adds another diagnostic - case "diagnostic-adder": - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Warning, - file: sourceFile, - code: 9999, - length: 3, - messageText: `Plugin diagnostic`, - start: 0 - }); - return prev; - }; - return proxy; - } - }), - error: undefined - }; - - // Accepts configurations - case "configurable-diagnostic-adder": - let customMessage = "default message"; - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - customMessage = info.config.message; - const proxy = makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Error, - file: sourceFile, - code: 9999, - length: 3, - messageText: customMessage, - start: 0 - }); - return prev; - }; - return proxy; - }, - onConfigurationChanged(config: any) { - customMessage = config.message; - } - }), - error: undefined - }; - - default: - return { - module: undefined, - error: new Error("Could not resolve module") - }; - } - } - } - - class FourslashSession extends ts.server.Session { - getText(fileName: string) { - return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); - } - } - - export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { - private host: SessionClientHost; - private client: ts.server.SessionClient; - private server: FourslashSession; - constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - // This is the main host that tests use to direct tests - const clientHost = new SessionClientHost(cancellationToken, options); - const client = new ts.server.SessionClient(clientHost); - - // This host is just a proxy for the clientHost, it uses the client - // host to answer server queries about files on disk - const serverHost = new SessionServerHost(clientHost); - const opts: ts.server.SessionOptions = { - host: serverHost, - cancellationToken: ts.server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: serverHost, - canUseEvents: true - }; - this.server = new FourslashSession(opts); - - - // Fake the connection between the client and the server - serverHost.writeMessage = client.onMessage.bind(client); - clientHost.writeMessage = this.server.onMessage.bind(this.server); - - // Wire the client to the host to get notifications when a file is open - // or edited. - clientHost.setClient(client); - - // Set the properties - this.client = client; - this.host = clientHost; - } - getHost() { return this.host; } - getLanguageService(): ts.LanguageService { return this.client; } - getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } - getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } - assertTextConsistent(fileName: string) { - const serverText = this.server.getText(fileName); - const clientText = this.host.readFile(fileName); - ts.Debug.assert(serverText === clientText, [ - "Server and client text are inconsistent.", - "", - "\x1b[1mServer\x1b[0m\x1b[31m:", - serverText, - "", - "\x1b[1mClient\x1b[0m\x1b[31m:", - clientText, - "", - "This probably means something is wrong with the fourslash infrastructure, not with the test." - ].join(ts.sys.newLine)); - } - } -} +namespace Harness.LanguageService { + + export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { + const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null + const langSvc: any = info.languageService; + for (const k of Object.keys(langSvc)) { + // eslint-disable-next-line only-arrow-functions + proxy[k] = function () { + return langSvc[k].apply(langSvc, arguments); + }; + } + return proxy; + } + + export class ScriptInfo { + public version = 1; + public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; + private lineMap: number[] | undefined; + + constructor(public fileName: string, public content: string, public isRootFile: boolean) { + this.setContent(content); + } + + private setContent(content: string): void { + this.content = content; + this.lineMap = undefined; + } + + public getLineMap(): number[] { + return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); + } + + public updateContent(content: string): void { + this.editRanges = []; + this.setContent(content); + this.version++; + } + + public editContent(start: number, end: number, newText: string): void { + // Apply edits + const prefix = this.content.substring(0, start); + const middle = newText; + const suffix = this.content.substring(end); + this.setContent(prefix + middle + suffix); + + // Store edit range + new length of script + this.editRanges.push({ + length: this.content.length, + textChangeRange: ts.createTextChangeRange( + ts.createTextSpanFromBounds(start, end), newText.length) + }); + + // Update version # + this.version++; + } + + public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { + if (startVersion === endVersion) { + // No edits! + return ts.unchangedTextChangeRange; + } + + const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); + const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); + + const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); + return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); + } + } + + class ScriptSnapshot implements ts.IScriptSnapshot { + public textSnapshot: string; + public version: number; + + constructor(public scriptInfo: ScriptInfo) { + this.textSnapshot = scriptInfo.content; + this.version = scriptInfo.version; + } + + public getText(start: number, end: number): string { + return this.textSnapshot.substring(start, end); + } + + public getLength(): number { + return this.textSnapshot.length; + } + + public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { + const oldShim = oldScript; + return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); + } + } + + class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { + constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { + } + + public getText(start: number, end: number): string { + return this.scriptSnapshot.getText(start, end); + } + + public getLength(): number { + return this.scriptSnapshot.getLength(); + } + + public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { + const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); + return range && JSON.stringify(range); + } + } + + class DefaultHostCancellationToken implements ts.HostCancellationToken { + public static readonly instance = new DefaultHostCancellationToken(); + + public isCancellationRequested() { + return false; + } + } + + export interface LanguageServiceAdapter { + getHost(): LanguageServiceAdapterHost; + getLanguageService(): ts.LanguageService; + getClassifier(): ts.Classifier; + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; + } + + export abstract class LanguageServiceAdapterHost { + public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); + public typesRegistry: ts.Map | undefined; + private scriptInfos: collections.SortedMap; + + constructor(protected cancellationToken = DefaultHostCancellationToken.instance, + protected settings = ts.getDefaultCompilerOptions()) { + this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + } + + public get vfs() { + return this.sys.vfs; + } + + public getNewLine(): string { + return harnessNewLine; + } + + public getFilenames(): string[] { + const fileNames: string[] = []; + this.scriptInfos.forEach(scriptInfo => { + if (scriptInfo.isRootFile) { + // only include root files here + // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. + fileNames.push(scriptInfo.fileName); + } + }); + return fileNames; + } + + public getScriptInfo(fileName: string): ScriptInfo | undefined { + return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); + } + + public addScript(fileName: string, content: string, isRootFile: boolean): void { + this.vfs.mkdirpSync(vpath.dirname(fileName)); + this.vfs.writeFileSync(fileName, content); + this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); + } + + public renameFileOrDirectory(oldPath: string, newPath: string): void { + this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); + this.vfs.renameSync(oldPath, newPath); + + const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); + this.scriptInfos.forEach((scriptInfo, key) => { + const newFileName = updater(key); + if (newFileName !== undefined) { + this.scriptInfos.delete(key); + this.scriptInfos.set(newFileName, scriptInfo); + scriptInfo.fileName = newFileName; + } + }); + } + + public editScript(fileName: string, start: number, end: number, newText: string) { + const script = this.getScriptInfo(fileName); + if (script) { + script.editContent(start, end, newText); + this.vfs.mkdirpSync(vpath.dirname(fileName)); + this.vfs.writeFileSync(fileName, script.content); + return; + } + + throw new Error("No script with name '" + fileName + "'"); + } + + public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } + + /** + * @param line 0 based index + * @param col 0 based index + */ + public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { + const script: ScriptInfo = this.getScriptInfo(fileName)!; + assert.isOk(script); + return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); + } + + public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { + const script: ScriptInfo = this.getScriptInfo(fileName)!; + assert.isOk(script); + return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); + } + + useCaseSensitiveFileNames() { + return !this.vfs.ignoreCase; + } + } + + /// Native adapter + class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { + isKnownTypesPackageName(name: string): boolean { + return !!this.typesRegistry && this.typesRegistry.has(name); + } + + getGlobalTypingsCacheLocation() { + return "/Library/Caches/typescript"; + } + + installPackage = ts.notImplemented; + + getCompilationSettings() { return this.settings; } + + getCancellationToken() { return this.cancellationToken; } + + getDirectories(path: string): string[] { + return this.sys.getDirectories(path); + } + + getCurrentDirectory(): string { return virtualFileSystemRoot; } + + getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } + + getScriptFileNames(): string[] { + return this.getFilenames().filter(ts.isAnySupportedFileExtension); + } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { + const script = this.getScriptInfo(fileName); + return script ? new ScriptSnapshot(script) : undefined; + } + + getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } + + getScriptVersion(fileName: string): string { + const script = this.getScriptInfo(fileName); + return script ? script.version.toString() : undefined!; // TODO: GH#18217 + } + + directoryExists(dirName: string): boolean { + return this.sys.directoryExists(dirName); + } + + fileExists(fileName: string): boolean { + return this.sys.fileExists(fileName); + } + + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { + return this.sys.readDirectory(path, extensions, exclude, include, depth); + } + + readFile(path: string): string | undefined { + return this.sys.readFile(path); + } + + realpath(path: string): string { + return this.sys.realpath(path); + } + + getTypeRootsVersion() { + return 0; + } + + log = ts.noop; + trace = ts.noop; + error = ts.noop; + } + + export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { + private host: NativeLanguageServiceHost; + constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + this.host = new NativeLanguageServiceHost(cancellationToken, options); + } + getHost(): LanguageServiceAdapterHost { return this.host; } + getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } + getClassifier(): ts.Classifier { return ts.createClassifier(); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } + } + + /// Shim adapter + class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { + private nativeHost: NativeLanguageServiceHost; + + public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; + public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; + + constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + super(cancellationToken, options); + this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); + + if (preprocessToResolve) { + const compilerOptions = this.nativeHost.getCompilationSettings(); + const moduleResolutionHost: ts.ModuleResolutionHost = { + fileExists: fileName => this.getScriptInfo(fileName) !== undefined, + readFile: fileName => { + const scriptInfo = this.getScriptInfo(fileName); + return scriptInfo && scriptInfo.content; + } + }; + this.getModuleResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName)!; + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); + const imports: ts.MapLike = {}; + for (const module of preprocessInfo.importedFiles) { + const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); + if (resolutionInfo.resolvedModule) { + imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; + } + } + return JSON.stringify(imports); + }; + this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName); + if (scriptInfo) { + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); + const resolutions: ts.MapLike = {}; + const settings = this.nativeHost.getCompilationSettings(); + for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { + const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); + if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { + resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; + } + } + return JSON.stringify(resolutions); + } + else { + return "[]"; + } + }; + } + } + + getFilenames(): string[] { return this.nativeHost.getFilenames(); } + getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } + addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } + editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } + positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } + + getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } + getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } + getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } + getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } + getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } + getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } + getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { + const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 + return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); + } + getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } + getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } + getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } + + readDirectory = ts.notImplemented; + readDirectoryNames = ts.notImplemented; + readFileNames = ts.notImplemented; + fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } + readFile(fileName: string) { + const snapshot = this.nativeHost.getScriptSnapshot(fileName); + return snapshot && ts.getSnapshotText(snapshot); + } + log(s: string): void { this.nativeHost.log(s); } + trace(s: string): void { this.nativeHost.trace(s); } + error(s: string): void { this.nativeHost.error(s); } + directoryExists(): boolean { + // for tests pessimistically assume that directory always exists + return true; + } + } + + class ClassifierShimProxy implements ts.Classifier { + constructor(private shim: ts.ClassifierShim) { + } + getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { + return ts.notImplemented(); + } + getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { + const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); + const entries: ts.ClassificationInfo[] = []; + let i = 0; + let position = 0; + + for (; i < result.length - 1; i += 2) { + const t = entries[i / 2] = { + length: parseInt(result[i]), + classification: parseInt(result[i + 1]) + }; + + assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); + position += t.length; + } + const finalLexState = parseInt(result[result.length - 1]); + + assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); + + return { + finalLexState, + entries + }; + } + } + + function unwrapJSONCallResult(result: string): any { + const parsedResult = JSON.parse(result); + if (parsedResult.error) { + throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); + } + else if (parsedResult.canceled) { + throw new ts.OperationCanceledException(); + } + return parsedResult.result; + } + + class LanguageServiceShimProxy implements ts.LanguageService { + constructor(private shim: ts.LanguageServiceShim) { + } + cleanupSemanticCache(): void { + this.shim.cleanupSemanticCache(); + } + getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); + } + getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); + } + getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); + } + getCompilerOptionsDiagnostics(): ts.Diagnostic[] { + return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); + } + getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); + } + getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length)); + } + getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); + } + getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); + } + getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { + return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); + } + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { + return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); + } + getCompletionEntrySymbol(): ts.Symbol { + throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); + } + getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { + return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); + } + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); + } + getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); + } + getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { + return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); + } + getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { + return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); + } + 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)); + } + getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { + return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); + } + getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { + return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); + } + getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { + return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); + } + getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { + return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); + } + getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); + } + findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { + return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); + } + getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); + } + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { + return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); + } + getNavigateToItems(searchValue: string): ts.NavigateToItem[] { + return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); + } + getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { + return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); + } + getNavigationTree(fileName: string): ts.NavigationTree { + return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); + } + getOutliningSpans(fileName: string): ts.OutliningSpan[] { + return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); + } + getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { + return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); + } + getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { + return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); + } + getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { + return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); + } + getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); + } + getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); + } + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); + } + getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion { + return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position)); + } + isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { + return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); + } + getJsxClosingTagAtPosition(): never { + throw new Error("Not supported on the shim."); + } + getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); + } + getCodeFixesAtPosition(): never { + throw new Error("Not supported on the shim."); + } + getCombinedCodeFix = ts.notImplemented; + applyCodeActionCommand = ts.notImplemented; + getCodeFixDiagnostics(): ts.Diagnostic[] { + throw new Error("Not supported on the shim."); + } + getEditsForRefactor(): ts.RefactorEditInfo { + throw new Error("Not supported on the shim."); + } + getApplicableRefactors(): ts.ApplicableRefactorInfo[] { + throw new Error("Not supported on the shim."); + } + organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { + throw new Error("Not supported on the shim."); + } + getEditsForFileRename(): readonly ts.FileTextChanges[] { + throw new Error("Not supported on the shim."); + } + prepareCallHierarchy(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); + } + provideCallHierarchyIncomingCalls(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); + } + provideCallHierarchyOutgoingCalls(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); + } + getEmitOutput(fileName: string): ts.EmitOutput { + return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); + } + getProgram(): ts.Program { + throw new Error("Program can not be marshaled across the shim layer."); + } + getNonBoundSourceFile(): ts.SourceFile { + throw new Error("SourceFile can not be marshaled across the shim layer."); + } + getSourceFile(): ts.SourceFile { + throw new Error("SourceFile can not be marshaled across the shim layer."); + } + getSourceMapper(): never { + return ts.notImplemented(); + } + clearSourceMapperCache(): never { + return ts.notImplemented(); + } + toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); + } + toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); + } + commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange)); + } + uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange)); + } + dispose(): void { this.shim.dispose({}); } + } + + export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { + private host: ShimLanguageServiceHost; + private factory: ts.TypeScriptServicesFactory; + constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); + this.factory = new ts.TypeScriptServicesFactory(); + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } + getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { + const coreServicesShim = this.factory.createCoreServicesShim(this.host); + const shimResult: { + referencedFiles: ts.ShimsFileReference[]; + typeReferenceDirectives: ts.ShimsFileReference[]; + importedFiles: ts.ShimsFileReference[]; + isLibFile: boolean; + } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); + + const convertResult: ts.PreProcessedFileInfo = { + referencedFiles: [], + importedFiles: [], + ambientExternalModules: [], + isLibFile: shimResult.isLibFile, + typeReferenceDirectives: [], + libReferenceDirectives: [] + }; + + ts.forEach(shimResult.referencedFiles, refFile => { + convertResult.referencedFiles.push({ + fileName: refFile.path, + pos: refFile.position, + end: refFile.position + refFile.length + }); + }); + + ts.forEach(shimResult.importedFiles, importedFile => { + convertResult.importedFiles.push({ + fileName: importedFile.path, + pos: importedFile.position, + end: importedFile.position + importedFile.length + }); + }); + + ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { + convertResult.importedFiles.push({ + fileName: typeRefDirective.path, + pos: typeRefDirective.position, + end: typeRefDirective.position + typeRefDirective.length + }); + }); + return convertResult; + } + } + + // Server adapter + class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { + private client!: ts.server.SessionClient; + + constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { + super(cancellationToken, settings); + } + + onMessage = ts.noop; + writeMessage = ts.noop; + + setClient(client: ts.server.SessionClient) { + this.client = client; + } + + openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { + super.openFile(fileName, content, scriptKindName); + this.client.openFile(fileName, content, scriptKindName); + } + + editScript(fileName: string, start: number, end: number, newText: string) { + const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); + super.editScript(fileName, start, end, newText); + this.client.changeFile(fileName, changeArgs); + } + } + + class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { + args: string[] = []; + newLine: string; + useCaseSensitiveFileNames = false; + + constructor(private host: NativeLanguageServiceHost) { + this.newLine = this.host.getNewLine(); + } + + onMessage = ts.noop; + writeMessage = ts.noop; // overridden + write(message: string): void { + this.writeMessage(message); + } + + readFile(fileName: string): string | undefined { + if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { + fileName = Compiler.defaultLibFileName; + } + + const snapshot = this.host.getScriptSnapshot(fileName); + return snapshot && ts.getSnapshotText(snapshot); + } + + writeFile = ts.noop; + + resolvePath(path: string): string { + return path; + } + + fileExists(path: string): boolean { + return !!this.host.getScriptSnapshot(path); + } + + directoryExists(): boolean { + // for tests assume that directory exists + return true; + } + + getExecutingFilePath(): string { + return ""; + } + + exit = ts.noop; + + createDirectory(_directoryName: string): void { + return ts.notImplemented(); + } + + getCurrentDirectory(): string { + return this.host.getCurrentDirectory(); + } + + getDirectories(path: string): string[] { + return this.host.getDirectories(path); + } + + getEnvironmentVariable(name: string): string { + return ts.sys.getEnvironmentVariable(name); + } + + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { + return this.host.readDirectory(path, extensions, exclude, include, depth); + } + + watchFile(): ts.FileWatcher { + return { close: ts.noop }; + } + + watchDirectory(): ts.FileWatcher { + return { close: ts.noop }; + } + + close = ts.noop; + + info(message: string): void { + this.host.log(message); + } + + msg(message: string): void { + this.host.log(message); + } + + loggingEnabled() { + return true; + } + + getLogFileName(): string | undefined { + return undefined; + } + + hasLevel() { + return false; + } + + startGroup() { throw ts.notImplemented(); } + endGroup() { throw ts.notImplemented(); } + + perftrc(message: string): void { + return this.host.log(message); + } + + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { + // eslint-disable-next-line no-restricted-globals + return setTimeout(callback, ms, args); + } + + clearTimeout(timeoutId: any): void { + // eslint-disable-next-line no-restricted-globals + clearTimeout(timeoutId); + } + + setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { + // eslint-disable-next-line no-restricted-globals + return setImmediate(callback, args); + } + + clearImmediate(timeoutId: any): void { + // eslint-disable-next-line no-restricted-globals + clearImmediate(timeoutId); + } + + createHash(s: string) { + return mockHash(s); + } + + require(_initialDir: string, _moduleName: string): ts.RequireResult { + switch (_moduleName) { + // Adds to the Quick Info a fixed string and a string from the config file + // and replaces the first display part + case "quickinfo-augmeneter": + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = makeDefaultProxy(info); + const langSvc: any = info.languageService; + // eslint-disable-next-line only-arrow-functions + proxy.getQuickInfoAtPosition = function () { + const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); + if (parts.displayParts.length > 0) { + parts.displayParts[0].text = "Proxied"; + } + parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); + return parts; + }; + + return proxy; + } + }), + error: undefined + }; + + // Throws during initialization + case "create-thrower": + return { + module: () => ({ + create() { + throw new Error("I am not a well-behaved plugin"); + } + }), + error: undefined + }; + + // Adds another diagnostic + case "diagnostic-adder": + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Warning, + file: sourceFile, + code: 9999, + length: 3, + messageText: `Plugin diagnostic`, + start: 0 + }); + return prev; + }; + return proxy; + } + }), + error: undefined + }; + + // Accepts configurations + case "configurable-diagnostic-adder": + let customMessage = "default message"; + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + customMessage = info.config.message; + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Error, + file: sourceFile, + code: 9999, + length: 3, + messageText: customMessage, + start: 0 + }); + return prev; + }; + return proxy; + }, + onConfigurationChanged(config: any) { + customMessage = config.message; + } + }), + error: undefined + }; + + default: + return { + module: undefined, + error: new Error("Could not resolve module") + }; + } + } + } + + class FourslashSession extends ts.server.Session { + getText(fileName: string) { + return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); + } + } + + export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { + private host: SessionClientHost; + private client: ts.server.SessionClient; + private server: FourslashSession; + constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + // This is the main host that tests use to direct tests + const clientHost = new SessionClientHost(cancellationToken, options); + const client = new ts.server.SessionClient(clientHost); + + // This host is just a proxy for the clientHost, it uses the client + // host to answer server queries about files on disk + const serverHost = new SessionServerHost(clientHost); + const opts: ts.server.SessionOptions = { + host: serverHost, + cancellationToken: ts.server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: serverHost, + canUseEvents: true + }; + this.server = new FourslashSession(opts); + + + // Fake the connection between the client and the server + serverHost.writeMessage = client.onMessage.bind(client); + clientHost.writeMessage = this.server.onMessage.bind(this.server); + + // Wire the client to the host to get notifications when a file is open + // or edited. + clientHost.setClient(client); + + // Set the properties + this.client = client; + this.host = clientHost; + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return this.client; } + getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } + getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } + assertTextConsistent(fileName: string) { + const serverText = this.server.getText(fileName); + const clientText = this.host.readFile(fileName); + ts.Debug.assert(serverText === clientText, [ + "Server and client text are inconsistent.", + "", + "\x1b[1mServer\x1b[0m\x1b[31m:", + serverText, + "", + "\x1b[1mClient\x1b[0m\x1b[31m:", + clientText, + "", + "This probably means something is wrong with the fourslash infrastructure, not with the test." + ].join(ts.sys.newLine)); + } + } +} diff --git a/src/server/protocol.ts b/src/server/protocol.ts index c459222fff95e..597c69d474f6b 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -137,10 +137,17 @@ namespace ts.server.protocol { /* @internal */ SelectionRangeFull = "selectionRange-full", ToggleLineComment = "toggleLineComment", + /* @internal */ ToggleLineCommentFull = "toggleLineComment-full", ToggleMultilineComment = "toggleMultilineComment", + /* @internal */ ToggleMultilineCommentFull = "toggleMultilineComment-full", - + CommentSelection = "commentSelection", + /* @internal */ + CommentSelectionFull = "commentSelection-full", + UncommentSelection = "uncommentSelection", + /* @internal */ + UncommentSelectionFull = "uncommentSelection-full", PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls", @@ -1547,6 +1554,16 @@ namespace ts.server.protocol { arguments: FileRangeRequestArgs; } + export interface CommentSelectionRequest extends FileRequest { + command: CommandTypes.CommentSelection; + arguments: FileRangeRequestArgs; + } + + export interface UncommentSelectionRequest extends FileRequest { + command: CommandTypes.UncommentSelection; + arguments: FileRangeRequestArgs; + } + /** * Information found in an "open" request. */ diff --git a/src/server/session.ts b/src/server/session.ts index 7553997b7605a..5ede17856d0cf 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2233,6 +2233,38 @@ namespace ts.server { return textChanges; } + private commentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); + + const textChanges = project.getLanguageService().commentSelection(file, textRange); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; + } + + private uncommentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); + + const textChanges = project.getLanguageService().uncommentSelection(file, textRange); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; + } + private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { const result: protocol.SelectionRange = { textSpan: toProtocolTextSpan(selectionRange.textSpan, scriptInfo), @@ -2690,6 +2722,18 @@ namespace ts.server { [CommandNames.ToggleMultilineCommentFull]: (request: protocol.ToggleMultilineCommentRequest) => { return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/false)); }, + [CommandNames.CommentSelection]: (request: protocol.CommentSelectionRequest) => { + return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/true)); + }, + [CommandNames.CommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { + return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/false)); + }, + [CommandNames.UncommentSelection]: (request: protocol.UncommentSelectionRequest) => { + return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/true)); + }, + [CommandNames.UncommentSelectionFull]: (request: protocol.UncommentSelectionRequest) => { + return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/false)); + }, }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { diff --git a/src/services/services.ts b/src/services/services.ts index 4e655b8775315..fe63930a155ff 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1985,12 +1985,12 @@ namespace ts { } } - function toggleLineComment(fileName: string, textRange: TextRange): TextChange[] { + function toggleLineComment(fileName: string, textRange: TextRange, insertComment?: boolean): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const textChanges: TextChange[] = []; const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); - let isCommenting = false; + let isCommenting = insertComment || false; let leftMostPosition = Number.MAX_VALUE; let lineTextStarts = new Map(); const whiteSpaceRegex = new RegExp(/\S/); @@ -2008,7 +2008,7 @@ namespace ts { lineTextStarts.set(i.toString(), regExec.index); if (lineText.substr(regExec.index, openComment.length) !== openComment) { - isCommenting = true; + isCommenting = insertComment !== undefined ? insertComment : true; } } } @@ -2029,7 +2029,7 @@ namespace ts { start: lineStarts[i] + leftMostPosition } }); - } else { + } else if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) { textChanges.push({ newText: "", span: { @@ -2049,7 +2049,7 @@ namespace ts { const textChanges: TextChange[] = []; const { text } = sourceFile; - let isCommenting = insertComment !== undefined ? insertComment : false; + let isCommenting = insertComment || false; const positions = [] as number[] as SortedArray; let pos = textRange.pos; @@ -2083,7 +2083,9 @@ namespace ts { } else { // If it's not in a comment range, then we need to comment the uncommented portions. let newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); - isCommenting = isCommenting || !isTextWhiteSpaceLike(text, pos, newPos === -1 ? textRange.end : pos + newPos); + isCommenting = insertComment !== undefined + ? insertComment + : isCommenting || !isTextWhiteSpaceLike(text, pos, newPos === -1 ? textRange.end : pos + newPos); // If isCommenting is already true we don't need to check whitespace again. pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length; } } @@ -2157,6 +2159,31 @@ namespace ts { return textChanges; } + function commentSelection(fileName: string, textRange: TextRange): TextChange[] { + return toggleLineComment(fileName, textRange, true); + } + function uncommentSelection(fileName: string, textRange: TextRange): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: TextChange[] = []; + + for (let i = textRange.pos; i <= textRange.end; i++) { + let commentRange = isInComment(sourceFile, i); + if (commentRange) { + switch (commentRange.kind) { + case SyntaxKind.SingleLineCommentTrivia: + textChanges.push.apply(textChanges, toggleLineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, false)); + break; + case SyntaxKind.MultiLineCommentTrivia: + textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, false)); + } + + i = commentRange.end + 1; + } + } + + return textChanges; + } + function isUnclosedTag({ openingElement, closingElement, parent }: JsxElement): boolean { return !tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) || isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); @@ -2437,7 +2464,9 @@ namespace ts { provideCallHierarchyIncomingCalls, provideCallHierarchyOutgoingCalls, toggleLineComment, - toggleMultilineComment + toggleMultilineComment, + commentSelection, + uncommentSelection, }; } diff --git a/src/services/shims.ts b/src/services/shims.ts index 5bf8e4235d7a1..00ace356119aa 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -280,6 +280,8 @@ namespace ts { toggleLineComment(fileName: string, textChange: ts.TextRange): string; toggleMultilineComment(fileName: string, textChange: ts.TextRange): string; + commentSelection(fileName: string, textChange: ts.TextRange): string; + uncommentSelection(fileName: string, textChange: ts.TextRange): string; } export interface ClassifierShim extends Shim { @@ -1083,6 +1085,20 @@ namespace ts { () => this.languageService.toggleMultilineComment(fileName, textRange) ); } + + public commentSelection(fileName: string, textRange: ts.TextRange): string { + return this.forwardJSONCall( + `commentSelection('${fileName}', '${JSON.stringify(textRange)}')`, + () => this.languageService.commentSelection(fileName, textRange) + ); + } + + public uncommentSelection(fileName: string, textRange: ts.TextRange): string { + return this.forwardJSONCall( + `uncommentSelection('${fileName}', '${JSON.stringify(textRange)}')`, + () => this.languageService.uncommentSelection(fileName, textRange) + ); + } } function convertClassifications(classifications: Classifications): { spans: string, endOfLineState: EndOfLineState } { diff --git a/src/services/types.ts b/src/services/types.ts index e508c3200da44..0684ffaf88f74 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -488,6 +488,8 @@ namespace ts { toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; + commentSelection(fileName: string, textRanges: TextRange): TextChange[]; + uncommentSelection(fileName: string, textRanges: TextRange): TextChange[]; dispose(): void; } diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index b0f55affa0ea5..5ca88f4adb961 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -273,7 +273,9 @@ namespace ts.server { CommandNames.ProvideCallHierarchyIncomingCalls, CommandNames.ProvideCallHierarchyOutgoingCalls, CommandNames.ToggleLineComment, - CommandNames.ToggleMultilineComment + CommandNames.ToggleMultilineComment, + CommandNames.CommentSelection, + CommandNames.UncommentSelection, ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 94a2fa743acd9..8971b73149e1f 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5315,8 +5315,10 @@ declare namespace ts { getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; getProgram(): Program | undefined; - toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[]; - toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; + commentSelection(fileName: string, textRanges: TextRange): TextChange[]; + uncommentSelection(fileName: string, textRanges: TextRange): TextChange[]; dispose(): void; } interface JsxClosingTagInfo { @@ -6303,9 +6305,9 @@ declare namespace ts.server.protocol { ConfigurePlugin = "configurePlugin", SelectionRange = "selectionRange", ToggleLineComment = "toggleLineComment", - ToggleLineCommentFull = "toggleLineComment-full", ToggleMultilineComment = "toggleMultilineComment", - ToggleMultilineCommentFull = "toggleMultilineComment-full", + CommentSelection = "commentSelection", + UncommentSelection = "uncommentSelection", PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls" @@ -6881,16 +6883,6 @@ declare namespace ts.server.protocol { */ end: Location; } - interface TextRange { - /** - * Position of the first character. - */ - pos: number; - /** - * Position of the last character. - */ - end: number; - } /** * Object found in response messages defining a span of text in a specific source file. */ @@ -7342,17 +7334,19 @@ declare namespace ts.server.protocol { } interface ToggleLineCommentRequest extends FileRequest { command: CommandTypes.ToggleLineComment; - arguments: ToggleLineCommentRequestArgs; - } - interface ToggleLineCommentRequestArgs extends FileRequestArgs { - textRanges: TextRange[]; + arguments: FileRangeRequestArgs; } interface ToggleMultilineCommentRequest extends FileRequest { command: CommandTypes.ToggleMultilineComment; - arguments: ToggleMultilineCommentRequestArgs; + arguments: FileRangeRequestArgs; + } + interface CommentSelectionRequest extends FileRequest { + command: CommandTypes.CommentSelection; + arguments: FileRangeRequestArgs; } - interface ToggleMultilineCommentRequestArgs extends FileRequestArgs { - textRanges: TextRange[]; + interface UncommentSelectionRequest extends FileRequest { + command: CommandTypes.UncommentSelection; + arguments: FileRangeRequestArgs; } /** * Information found in an "open" request. @@ -9690,6 +9684,7 @@ declare namespace ts.server { private getSupportedCodeFixes; private isLocation; private extractPositionOrRange; + private getRange; private getApplicableRefactors; private getEditsForRefactor; private organizeImports; @@ -9709,6 +9704,8 @@ declare namespace ts.server { private getSmartSelectionRange; private toggleLineComment; private toggleMultilineComment; + private commentSelection; + private uncommentSelection; private mapSelectionRange; private getScriptInfoFromProjectService; private toProtocolCallHierarchyItem; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 9ef800f41e4d2..2a2402984380d 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5315,8 +5315,10 @@ declare namespace ts { getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; getProgram(): Program | undefined; - toggleLineComment(fileName: string, textRanges: TextRange[]): TextChange[]; - toggleMultilineComment(fileName: string, textRanges: TextRange[]): TextChange[]; + toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; + commentSelection(fileName: string, textRanges: TextRange): TextChange[]; + uncommentSelection(fileName: string, textRanges: TextRange): TextChange[]; dispose(): void; } interface JsxClosingTagInfo { diff --git a/tests/cases/fourslash/commentSelection1.ts b/tests/cases/fourslash/commentSelection1.ts new file mode 100644 index 0000000000000..523f1f3a4f2d4 --- /dev/null +++ b/tests/cases/fourslash/commentSelection1.ts @@ -0,0 +1,18 @@ +// Simple comment selection cases. + +//// let var1[| = 1; +//// let var2 = 2; +//// let var3 |]= 3; +//// +//// //let var4[| = 4; +//// //let var5 = 5; +//// //let var6 |]= 6; + +verify.commentSelection( + `//let var1 = 1; +//let var2 = 2; +//let var3 = 3; + +////let var4 = 4; +////let var5 = 5; +////let var6 = 6;`); \ No newline at end of file diff --git a/tests/cases/fourslash/commentSelection2.ts b/tests/cases/fourslash/commentSelection2.ts new file mode 100644 index 0000000000000..31c56a60a2bb4 --- /dev/null +++ b/tests/cases/fourslash/commentSelection2.ts @@ -0,0 +1,29 @@ +// Common jsx insert comment. + +//@Filename: file.tsx +//// const a = +//// [| +//// |] +//// ; +//// const b = +//// {/**/} +//// {/**/} +//// ; +//// const c = [| +//// +//// +//// ; + +verify.commentSelection( + `const a = + {/**/} + {/**/} +; +const b = + {/**/} + {/**/} +; +//const c = +// +// +;`); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 83db54c50d0bd..a807a2fe97540 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -399,6 +399,8 @@ declare namespace FourSlashInterface { toggleLineComment(newFileContent: string): void; toggleMultilineComment(newFileContent: string): void; + commentSelection(newFileContent: string): void; + uncommentSelection(newFileContent: string): void; } class edit { backspace(count?: number): void; diff --git a/tests/cases/fourslash/toggleMultilineComment2.ts b/tests/cases/fourslash/toggleMultilineComment2.ts index 926d29e2d2e95..a3a8cf70f786d 100644 --- a/tests/cases/fourslash/toggleMultilineComment2.ts +++ b/tests/cases/fourslash/toggleMultilineComment2.ts @@ -1,4 +1,4 @@ -// If selection is outside of a block comment then insert comment +// If selection is outside of a multiline comment then insert comment // instead of removing. //// let var1/* = 1; diff --git a/tests/cases/fourslash/uncommentSelection1.ts b/tests/cases/fourslash/uncommentSelection1.ts new file mode 100644 index 0000000000000..42c567d3ca7e9 --- /dev/null +++ b/tests/cases/fourslash/uncommentSelection1.ts @@ -0,0 +1,30 @@ +// Simple comment selection cases. + +//// //let var1[| = 1; +//// //let var2 = 2; +//// //let var3 |]= 3; +//// +//// //let var4[| = 4; +//// /*let var5 = 5;*/ +//// //let var6 = 6; +//// +//// let var7 |]= 7; +//// +//// let var8/* = 1; +//// let var9 [||]= 2; +//// let var10 */= 3; + +verify.uncommentSelection( + `let var1 = 1; +let var2 = 2; +let var3 = 3; + +let var4 = 4; +let var5 = 5; +let var6 = 6; + +let var7 = 7; + +let var8 = 1; +let var9 = 2; +let var10 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/uncommentSelection2.ts b/tests/cases/fourslash/uncommentSelection2.ts new file mode 100644 index 0000000000000..55a84555cae3e --- /dev/null +++ b/tests/cases/fourslash/uncommentSelection2.ts @@ -0,0 +1,26 @@ +// Common uncomment jsx cases + +//@Filename: file.tsx +//// const a = +//// {/**/} +//// {/**/} +//// ; +//// +//// const b =
    +//// {/*[|
    */} +//// SomeText +//// {/*
    |]*/} +////
    ; + + +verify.uncommentSelection( + `const a = + + +; + +const b =
    +
    + SomeText +
    +
    ;`); \ No newline at end of file diff --git a/tests/cases/fourslash/uncommentSelection3.ts b/tests/cases/fourslash/uncommentSelection3.ts new file mode 100644 index 0000000000000..9ad68476ceb64 --- /dev/null +++ b/tests/cases/fourslash/uncommentSelection3.ts @@ -0,0 +1,34 @@ +// Remove all comments within the selection + +//// let var1/* = 1; +//// let var2 [|= 2; +//// let var3 */= 3;|] +//// +//// [|let var4/* = 1; +//// let var5 |]= 2; +//// let var6 */= 3; +//// +//// [|let var7/* = 1; +//// let var8 = 2; +//// let var9 */= 3;|] +//// +//// /*let va[|r10 = 1;*/ +//// let var11 = 2; +//// /*let var12|] = 3;*/ + +verify.uncommentSelection( + `let var1 = 1; +let var2 = 2; +let var3 = 3; + +let var4 = 1; +let var5 = 2; +let var6 = 3; + +let var7 = 1; +let var8 = 2; +let var9 = 3; + +let var10 = 1; +let var11 = 2; +let var12 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/uncommentSelection4.ts b/tests/cases/fourslash/uncommentSelection4.ts new file mode 100644 index 0000000000000..63faedaeddb20 --- /dev/null +++ b/tests/cases/fourslash/uncommentSelection4.ts @@ -0,0 +1,40 @@ +// Remove all comments in jsx. + +//@Filename: file.tsx +//// const var1 =
    Tex{/*t1
    ; +//// const var2 =
    Text2[|
    ; +//// const var3 =
    Tex*/}t3
    ;|] +//// +//// [|const var4 =
    Tex{/*t4
    ; +//// const var5 = Text5
    ; +//// const var6 =
    Tex*/}t6
    ; +//// +//// [|const var7 =
    Tex{/*t7
    ; +//// const var8 =
    Text8
    ; +//// const var9 =
    Tex*/}t9
    ;|] +//// +//// const var10 =
    +//// {/*
    T[|ext
    */} +////
    Text
    +//// {/*
    Text|]
    */} +////
    ; + +verify.uncommentSelection( + `const var1 =
    Text1
    ; +const var2 =
    Text2
    ; +const var3 =
    Text3
    ; + +const var4 =
    Text4
    ; +const var5 =
    Text5
    ; +const var6 =
    Text6
    ; + +const var7 =
    Text7
    ; +const var8 =
    Text8
    ; +const var9 =
    Text9
    ; + +const var10 =
    +
    Text
    +
    Text
    +
    Text
    +
    ;` +); \ No newline at end of file From 35a3d8547b82626de5f14525ae5fe8f846849f72 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Mon, 2 Mar 2020 16:30:42 -0800 Subject: [PATCH 10/20] Fixed lint issues --- src/harness/client.ts | 2 +- src/harness/fourslashImpl.ts | 16 ++++++++-------- src/server/session.ts | 16 ++++++++-------- src/services/services.ts | 35 ++++++++++++++++++++--------------- src/services/shims.ts | 16 ++++++++-------- src/services/utilities.ts | 6 ++++-- 6 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index 5440b85ceae8d..ba2e920ac590d 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -832,4 +832,4 @@ namespace ts.server { throw new Error("dispose is not available through the server layer."); } } -} \ No newline at end of file +} diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 62a488821352c..b2007786720df 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3659,8 +3659,8 @@ namespace FourSlash { } public toggleLineComment(newFileContent: string): void { - let changes: ts.TextChange[] = []; - for (let range of this.getRanges()) { + const changes: ts.TextChange[] = []; + for (const range of this.getRanges()) { changes.push.apply(changes, this.languageService.toggleLineComment(this.activeFile.fileName, range)); } @@ -3670,8 +3670,8 @@ namespace FourSlash { } public toggleMultilineComment(newFileContent: string): void { - let changes: ts.TextChange[] = []; - for (let range of this.getRanges()) { + const changes: ts.TextChange[] = []; + for (const range of this.getRanges()) { changes.push.apply(changes, this.languageService.toggleMultilineComment(this.activeFile.fileName, range)); } @@ -3681,8 +3681,8 @@ namespace FourSlash { } public commentSelection(newFileContent: string): void { - let changes: ts.TextChange[] = []; - for (let range of this.getRanges()) { + const changes: ts.TextChange[] = []; + for (const range of this.getRanges()) { changes.push.apply(changes, this.languageService.commentSelection(this.activeFile.fileName, range)); } @@ -3692,8 +3692,8 @@ namespace FourSlash { } public uncommentSelection(newFileContent: string): void { - let changes: ts.TextChange[] = []; - for (let range of this.getRanges()) { + const changes: ts.TextChange[] = []; + for (const range of this.getRanges()) { changes.push.apply(changes, this.languageService.uncommentSelection(this.activeFile.fileName, range)); } diff --git a/src/server/session.ts b/src/server/session.ts index 5ede17856d0cf..96a4b67690747 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2711,28 +2711,28 @@ namespace ts.server { return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments)); }, [CommandNames.ToggleLineComment]: (request: protocol.ToggleLineCommentRequest) => { - return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/true)); + return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.ToggleLineCommentFull]: (request: protocol.ToggleLineCommentRequest) => { - return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/false)); + return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { - return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/true)); + return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.ToggleMultilineCommentFull]: (request: protocol.ToggleMultilineCommentRequest) => { - return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/false)); + return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.CommentSelection]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/true)); + return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.CommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/false)); + return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ false)); }, [CommandNames.UncommentSelection]: (request: protocol.UncommentSelectionRequest) => { - return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/true)); + return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.UncommentSelectionFull]: (request: protocol.UncommentSelectionRequest) => { - return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/false)); + return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ false)); }, }); diff --git a/src/services/services.ts b/src/services/services.ts index fe63930a155ff..b6003b4826e0a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1982,7 +1982,7 @@ namespace ts { lineStarts: sourceFile.getLineStarts(), firstLine: sourceFile.getLineAndCharacterOfPosition(textRange.pos).line, lastLine: sourceFile.getLineAndCharacterOfPosition(textRange.end).line - } + }; } function toggleLineComment(fileName: string, textRange: TextRange, insertComment?: boolean): TextChange[] { @@ -1992,9 +1992,9 @@ namespace ts { let isCommenting = insertComment || false; let leftMostPosition = Number.MAX_VALUE; - let lineTextStarts = new Map(); + const lineTextStarts = new Map(); const whiteSpaceRegex = new RegExp(/\S/); - const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]) + const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]); const openComment = isJsx ? "{/*" : "//"; // Check each line before any text changes. @@ -2021,7 +2021,8 @@ namespace ts { if (lineTextStart !== undefined) { if (isJsx) { textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }, isCommenting, isJsx)); - } else if (isCommenting) { + } + else if (isCommenting) { textChanges.push({ newText: openComment, span: { @@ -2029,7 +2030,8 @@ namespace ts { start: lineStarts[i] + leftMostPosition } }); - } else if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) { + } + else if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) { textChanges.push({ newText: "", span: { @@ -2080,8 +2082,9 @@ namespace ts { } pos = commentRange.end + 1; - } else { // If it's not in a comment range, then we need to comment the uncommented portions. - let newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); + } + else { // If it's not in a comment range, then we need to comment the uncommented portions. + const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`); isCommenting = insertComment !== undefined ? insertComment @@ -2141,16 +2144,17 @@ namespace ts { } }); } - } else { + } + else { // If is not commenting then remove all comments found. - for (let i = 0; i < positions.length; i++) { - const from = positions[i] - closeMultiline.length > 0 ? positions[i] - closeMultiline.length : 0; + for (const pos of positions) { + const from = pos - closeMultiline.length > 0 ? pos - closeMultiline.length : 0; const offset = text.substr(from, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0; textChanges.push({ newText: "", span: { length: openMultiline.length, - start: positions[i] - offset + start: pos - offset } }); } @@ -2160,21 +2164,22 @@ namespace ts { } function commentSelection(fileName: string, textRange: TextRange): TextChange[] { - return toggleLineComment(fileName, textRange, true); + return toggleLineComment(fileName, textRange, /*insertComment*/ true); } + function uncommentSelection(fileName: string, textRange: TextRange): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const textChanges: TextChange[] = []; for (let i = textRange.pos; i <= textRange.end; i++) { - let commentRange = isInComment(sourceFile, i); + const commentRange = isInComment(sourceFile, i); if (commentRange) { switch (commentRange.kind) { case SyntaxKind.SingleLineCommentTrivia: - textChanges.push.apply(textChanges, toggleLineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, false)); + textChanges.push.apply(textChanges, toggleLineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false)); break; case SyntaxKind.MultiLineCommentTrivia: - textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, false)); + textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false)); } i = commentRange.end + 1; diff --git a/src/services/shims.ts b/src/services/shims.ts index 00ace356119aa..15b0c1cb488ab 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -278,10 +278,10 @@ namespace ts { getEmitOutput(fileName: string): string; getEmitOutputObject(fileName: string): EmitOutput; - toggleLineComment(fileName: string, textChange: ts.TextRange): string; - toggleMultilineComment(fileName: string, textChange: ts.TextRange): string; - commentSelection(fileName: string, textChange: ts.TextRange): string; - uncommentSelection(fileName: string, textChange: ts.TextRange): string; + toggleLineComment(fileName: string, textChange: TextRange): string; + toggleMultilineComment(fileName: string, textChange:TextRange): string; + commentSelection(fileName: string, textChange: TextRange): string; + uncommentSelection(fileName: string, textChange: TextRange): string; } export interface ClassifierShim extends Shim { @@ -1072,28 +1072,28 @@ namespace ts { this.logPerformance) as EmitOutput; } - public toggleLineComment(fileName: string, textRange: ts.TextRange): string { + public toggleLineComment(fileName: string, textRange: TextRange): string { return this.forwardJSONCall( `toggleLineComment('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.toggleLineComment(fileName, textRange) ); } - public toggleMultilineComment(fileName: string, textRange: ts.TextRange): string { + public toggleMultilineComment(fileName: string, textRange: TextRange): string { return this.forwardJSONCall( `toggleMultilineComment('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.toggleMultilineComment(fileName, textRange) ); } - public commentSelection(fileName: string, textRange: ts.TextRange): string { + public commentSelection(fileName: string, textRange: TextRange): string { return this.forwardJSONCall( `commentSelection('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.commentSelection(fileName, textRange) ); } - public uncommentSelection(fileName: string, textRange: ts.TextRange): string { + public uncommentSelection(fileName: string, textRange: TextRange): string { return this.forwardJSONCall( `uncommentSelection('${fileName}', '${JSON.stringify(textRange)}')`, () => this.languageService.uncommentSelection(fileName, textRange) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index e7642cb80f2d9..5f936a68e0948 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1332,9 +1332,11 @@ namespace ts { || node.kind === SyntaxKind.OpenBraceToken || node.kind === SyntaxKind.SlashToken) { node = node.parent; - } else if (node.kind === SyntaxKind.JsxElement) { + } + else if (node.kind === SyntaxKind.JsxElement) { return position > node.getStart(sourceFile) || isInsideJsxElementRecursion(node.parent); - } else { + } + else { return false; } } From 413a3d3eb4a1a3b21d8fb4b553b36ff27a078f96 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Mon, 2 Mar 2020 17:13:15 -0800 Subject: [PATCH 11/20] Fixed more lint issues. --- src/harness/client.ts | 16 ++++++++-------- src/services/shims.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index ba2e920ac590d..4f11c9d1a92ba 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -812,20 +812,20 @@ namespace ts.server { return notImplemented(); } - toggleLineComment(): ts.TextChange[] { - throw new Error("Method not implemented."); + toggleLineComment(): TextChange[] { + return notImplemented(); } - toggleMultilineComment(): ts.TextChange[] { - throw new Error("Method not implemented."); + toggleMultilineComment(): TextChange[] { + return notImplemented(); } - commentSelection(): ts.TextChange[] { - throw new Error("Method not implemented."); + commentSelection(): TextChange[] { + return notImplemented(); } - uncommentSelection(): ts.TextChange[] { - throw new Error("Method not implemented."); + uncommentSelection(): TextChange[] { + return notImplemented(); } dispose(): void { diff --git a/src/services/shims.ts b/src/services/shims.ts index 15b0c1cb488ab..e3082b114e6bc 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -279,7 +279,7 @@ namespace ts { getEmitOutputObject(fileName: string): EmitOutput; toggleLineComment(fileName: string, textChange: TextRange): string; - toggleMultilineComment(fileName: string, textChange:TextRange): string; + toggleMultilineComment(fileName: string, textChange: TextRange): string; commentSelection(fileName: string, textChange: TextRange): string; uncommentSelection(fileName: string, textChange: TextRange): string; } From 89429ac6aaef2392265c5c30815a8df9fc2df2d7 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Fri, 22 May 2020 18:22:28 -0700 Subject: [PATCH 12/20] Fixed lint errors --- src/harness/client.ts | 1670 ++++++++++----------- src/harness/harnessLanguageService.ts | 1982 ++++++++++++------------- 2 files changed, 1826 insertions(+), 1826 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index 4f11c9d1a92ba..e97a87be8b299 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -1,835 +1,835 @@ -namespace ts.server { - export interface SessionClientHost extends LanguageServiceHost { - writeMessage(message: string): void; - } - - interface RenameEntry { - readonly renameInfo: RenameInfo; - readonly inputs: { - readonly fileName: string; - readonly position: number; - readonly findInStrings: boolean; - readonly findInComments: boolean; - }; - readonly locations: RenameLocation[]; - } - - /* @internal */ - export function extractMessage(message: string): string { - // Read the content length - const contentLengthPrefix = "Content-Length: "; - const lines = message.split(/\r?\n/); - Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); - - const contentLengthText = lines[0]; - Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); - const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); - - // Read the body - const responseBody = lines[2]; - - // Verify content length - Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); - return responseBody; - } - - export class SessionClient implements LanguageService { - private sequence = 0; - private lineMaps: Map = createMap(); - private messages: string[] = []; - private lastRenameEntry: RenameEntry | undefined; - - constructor(private host: SessionClientHost) { - } - - public onMessage(message: string): void { - this.messages.push(message); - } - - private writeMessage(message: string): void { - this.host.writeMessage(message); - } - - private getLineMap(fileName: string): number[] { - let lineMap = this.lineMaps.get(fileName); - if (!lineMap) { - lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); - this.lineMaps.set(fileName, lineMap); - } - return lineMap; - } - - private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { - lineMap = lineMap || this.getLineMap(fileName); - return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); - } - - private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { - const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); - return { - line: lineOffset.line + 1, - offset: lineOffset.character + 1 - }; - } - - private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { - return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; - } - - private processRequest(command: string, args: T["arguments"]): T { - const request: protocol.Request = { - seq: this.sequence, - type: "request", - arguments: args, - command - }; - this.sequence++; - - this.writeMessage(JSON.stringify(request)); - - return request; - } - - private processResponse(request: protocol.Request, expectEmptyBody = false): T { - let foundResponseMessage = false; - let response!: T; - while (!foundResponseMessage) { - const lastMessage = this.messages.shift()!; - Debug.assert(!!lastMessage, "Did not receive any responses."); - const responseBody = extractMessage(lastMessage); - try { - response = JSON.parse(responseBody); - // the server may emit events before emitting the response. We - // want to ignore these events for testing purpose. - if (response.type === "response") { - foundResponseMessage = true; - } - } - catch (e) { - throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); - } - } - - // verify the sequence numbers - Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); - - // unmarshal errors - if (!response.success) { - throw new Error("Error " + response.message); - } - - Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); - Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); - - return response; - } - - /*@internal*/ - configure(preferences: UserPreferences) { - const args: protocol.ConfigureRequestArguments = { preferences }; - const request = this.processRequest(CommandNames.Configure, args); - this.processResponse(request, /*expectEmptyBody*/ true); - } - - openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { - const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; - this.processRequest(CommandNames.Open, args); - } - - closeFile(file: string): void { - const args: protocol.FileRequestArgs = { file }; - this.processRequest(CommandNames.Close, args); - } - - createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { - return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; - } - - changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { - // clear the line map after an edit - this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 - this.processRequest(CommandNames.Change, args); - } - - toLineColumnOffset(fileName: string, position: number) { - const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); - return { line, character: offset }; - } - - getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Quickinfo, args); - const response = this.processResponse(request); - const body = response.body!; // TODO: GH#18217 - - return { - kind: body.kind, - kindModifiers: body.kindModifiers, - textSpan: this.decodeSpan(body, fileName), - displayParts: [{ kind: "text", text: body.displayString }], - documentation: [{ kind: "text", text: body.documentation }], - tags: body.tags - }; - } - - getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { - const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; - - const request = this.processRequest(CommandNames.ProjectInfo, args); - const response = this.processResponse(request); - - return { - configFileName: response.body!.configFileName, // TODO: GH#18217 - fileNames: response.body!.fileNames - }; - } - - getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { - // Not passing along 'preferences' because server should already have those from the 'configure' command - const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Completions, args); - const response = this.processResponse(request); - - return { - isGlobalCompletion: false, - isMemberCompletion: false, - isNewIdentifierLocation: false, - entries: response.body!.map(entry => { // TODO: GH#18217 - if (entry.replacementSpan !== undefined) { - const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; - // TODO: GH#241 - const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; - return res; - } - - return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 - }) - }; - } - - getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { - const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; - - const request = this.processRequest(CommandNames.CompletionDetails, args); - const response = this.processResponse(request); - Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); - const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); - return { ...response.body![0], codeActions: convertedCodeActions }; - } - - getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { - return notImplemented(); - } - - getNavigateToItems(searchValue: string): NavigateToItem[] { - const args: protocol.NavtoRequestArgs = { - searchValue, - file: this.host.getScriptFileNames()[0] - }; - - const request = this.processRequest(CommandNames.Navto, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - name: entry.name, - containerName: entry.containerName || "", - containerKind: entry.containerKind || ScriptElementKind.unknown, - kind: entry.kind, - kindModifiers: entry.kindModifiers || "", - matchKind: entry.matchKind as keyof typeof PatternMatchKind, - isCaseSensitive: entry.isCaseSensitive, - fileName: entry.file, - textSpan: this.decodeSpan(entry), - })); - } - - getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { - const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); - - - // TODO: handle FormatCodeOptions - const request = this.processRequest(CommandNames.Format, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 - } - - getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { - return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); - } - - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { - const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; - - // TODO: handle FormatCodeOptions - const request = this.processRequest(CommandNames.Formatonkey, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 - } - - getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Definition, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })); - } - - getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); - const response = this.processResponse(request); - const body = Debug.checkDefined(response.body); // TODO: GH#18217 - - return { - definitions: body.definitions.map(entry => ({ - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })), - textSpan: this.decodeSpan(body.textSpan, request.arguments.file) - }; - } - - getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { - const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.TypeDefinition, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - name: "" - })); - } - - getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Implementation, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - kind: ScriptElementKind.unknown, - displayParts: [] - })); - } - - findReferences(_fileName: string, _position: number): ReferencedSymbol[] { - // Not yet implemented. - return []; - } - - getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.References, args); - const response = this.processResponse(request); - - return response.body!.refs.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - isWriteAccess: entry.isWriteAccess, - isDefinition: entry.isDefinition, - })); - } - - getEmitOutput(file: string): EmitOutput { - const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); - const response = this.processResponse(request); - return response.body as EmitOutput; - } - - getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { - return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); - } - getSemanticDiagnostics(file: string): Diagnostic[] { - return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); - } - getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { - return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); - } - - private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { - const request = this.processRequest(command, { file, includeLinePosition: true }); - const response = this.processResponse(request); - const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); - const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! - - return (response.body).map((entry): DiagnosticWithLocation => { - const category = firstDefined(Object.keys(DiagnosticCategory), id => - isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory)[id] : undefined); - return { - file: fakeSourceFile, - start: entry.start, - length: entry.length, - messageText: entry.message, - category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), - code: entry.code, - reportsUnnecessary: entry.reportsUnnecessary, - }; - }); - } - - getCompilerOptionsDiagnostics(): Diagnostic[] { - return notImplemented(); - } - - getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { - // Not passing along 'options' because server should already have those from the 'configure' command - const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; - - const request = this.processRequest(CommandNames.Rename, args); - const response = this.processResponse(request); - const body = response.body!; // TODO: GH#18217 - const locations: RenameLocation[] = []; - for (const entry of body.locs) { - const fileName = entry.file; - for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { - locations.push({ - textSpan: this.decodeSpan({ start, end }, fileName), - fileName, - ...(contextStart !== undefined ? - { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : - undefined), - ...prefixSuffixText - }); - } - } - - const renameInfo = body.info.canRename - ? identity({ - canRename: body.info.canRename, - fileToRename: body.info.fileToRename, - displayName: body.info.displayName, - fullDisplayName: body.info.fullDisplayName, - kind: body.info.kind, - kindModifiers: body.info.kindModifiers, - triggerSpan: createTextSpanFromBounds(position, position), - }) - : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); - this.lastRenameEntry = { - renameInfo, - inputs: { - fileName, - position, - findInStrings: !!findInStrings, - findInComments: !!findInComments, - }, - locations, - }; - return renameInfo; - } - - getSmartSelectionRange() { - return notImplemented(); - } - - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { - if (!this.lastRenameEntry || - this.lastRenameEntry.inputs.fileName !== fileName || - this.lastRenameEntry.inputs.position !== position || - this.lastRenameEntry.inputs.findInStrings !== findInStrings || - this.lastRenameEntry.inputs.findInComments !== findInComments) { - this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); - } - - return this.lastRenameEntry!.locations; - } - - private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { - if (!items) { - return []; - } - - return items.map(item => ({ - text: item.text, - kind: item.kind, - kindModifiers: item.kindModifiers || "", - spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), - childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), - indent: item.indent, - bolded: false, - grayed: false - })); - } - - getNavigationBarItems(file: string): NavigationBarItem[] { - const request = this.processRequest(CommandNames.NavBar, { file }); - const response = this.processResponse(request); - - const lineMap = this.getLineMap(file); - return this.decodeNavigationBarItems(response.body, file, lineMap); - } - - private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { - return { - text: tree.text, - kind: tree.kind, - kindModifiers: tree.kindModifiers, - spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), - nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), - childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) - }; - } - - getNavigationTree(file: string): NavigationTree { - const request = this.processRequest(CommandNames.NavTree, { file }); - const response = this.processResponse(request); - - const lineMap = this.getLineMap(file); - return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 - } - - private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; - private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; - private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { - fileName = fileName || span.file; - lineMap = lineMap || this.getLineMap(fileName); - return createTextSpanFromBounds( - this.lineOffsetToPosition(fileName, span.start, lineMap), - this.lineOffsetToPosition(fileName, span.end, lineMap)); - } - - getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { - return notImplemented(); - } - - getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { - return notImplemented(); - } - - getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { - const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.SignatureHelp, args); - const response = this.processResponse(request); - - if (!response.body) { - return undefined; - } - - const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; - - const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); - - return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; - } - - getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Occurrences, args); - const response = this.processResponse(request); - - return response.body!.map(entry => ({ // TODO: GH#18217 - fileName: entry.file, - textSpan: this.decodeSpan(entry), - isWriteAccess: entry.isWriteAccess, - isDefinition: false - })); - } - - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { - const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; - - const request = this.processRequest(CommandNames.DocumentHighlights, args); - const response = this.processResponse(request); - - return response.body!.map(item => ({ // TODO: GH#18217 - fileName: item.file, - highlightSpans: item.highlightSpans.map(span => ({ - textSpan: this.decodeSpan(span, item.file), - kind: span.kind - })), - })); - } - - getOutliningSpans(file: string): OutliningSpan[] { - const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); - const response = this.processResponse(request); - - return response.body!.map(item => ({ - textSpan: this.decodeSpan(item.textSpan, file), - hintSpan: this.decodeSpan(item.hintSpan, file), - bannerText: item.bannerText, - autoCollapse: item.autoCollapse, - kind: item.kind - })); - } - - getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { - return notImplemented(); - } - - getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion { - return notImplemented(); - } - - isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { - return notImplemented(); - } - - getJsxClosingTagAtPosition(_fileName: string, _position: number): never { - return notImplemented(); - } - - getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { - return notImplemented(); - } - - getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { - const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; - - const request = this.processRequest(CommandNames.GetCodeFixes, args); - const response = this.processResponse(request); - - return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 - ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); - } - - getCombinedCodeFix = notImplemented; - - applyCodeActionCommand = notImplemented; - - private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { - return typeof positionOrRange === "number" - ? this.createFileLocationRequestArgs(fileName, positionOrRange) - : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); - } - - private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { - const { line, offset } = this.positionToOneBasedLineOffset(file, position); - return { file, line, offset }; - } - - private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { - const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); - const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); - return { file, startLine, startOffset, endLine, endOffset }; - } - - private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { - const { line, offset } = this.positionToOneBasedLineOffset(file, start); - const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); - return { file, line, offset, endLine, endOffset }; - } - - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); - - const request = this.processRequest(CommandNames.GetApplicableRefactors, args); - const response = this.processResponse(request); - return response.body!; // TODO: GH#18217 - } - - getEditsForRefactor( - fileName: string, - _formatOptions: FormatCodeSettings, - positionOrRange: number | TextRange, - refactorName: string, - actionName: string): RefactorEditInfo { - - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; - args.refactor = refactorName; - args.action = actionName; - - const request = this.processRequest(CommandNames.GetEditsForRefactor, args); - const response = this.processResponse(request); - - if (!response.body) { - return { edits: [], renameFilename: undefined, renameLocation: undefined }; - } - - const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); - - const renameFilename: string | undefined = response.body.renameFilename; - let renameLocation: number | undefined; - if (renameFilename !== undefined) { - renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 - } - - return { - edits, - renameFilename, - renameLocation - }; - } - - organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { - return notImplemented(); - } - - getEditsForFileRename() { - return notImplemented(); - } - - private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { - return edits.map(edit => { - const fileName = edit.fileName; - return { - fileName, - textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) - }; - }); - } - - private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { - return changes.map(change => ({ - fileName: change.fileName, - textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) - })); - } - - convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { - return { - span: this.decodeSpan(change, fileName), - newText: change.newText ? change.newText : "" - }; - } - - getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { - const args = this.createFileLocationRequestArgs(fileName, position); - - const request = this.processRequest(CommandNames.Brace, args); - const response = this.processResponse(request); - - return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 - } - - configurePlugin(pluginName: string, configuration: any): void { - const request = this.processRequest("configurePlugin", { pluginName, configuration }); - this.processResponse(request, /*expectEmptyBody*/ true); - } - - getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { - return notImplemented(); - } - - getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); - } - - getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { - return notImplemented(); - } - - getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); - } - - getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { - return notImplemented(); - } - - private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { - return { - file: item.file, - name: item.name, - kind: item.kind, - span: this.decodeSpan(item.span, item.file), - selectionSpan: this.decodeSpan(item.selectionSpan, item.file) - }; - } - - prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); - } - - private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { - return { - from: this.convertCallHierarchyItem(item.from), - fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) - }; - } - - provideCallHierarchyIncomingCalls(fileName: string, position: number) { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); - } - - private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { - return { - to: this.convertCallHierarchyItem(item.to), - fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) - }; - } - - provideCallHierarchyOutgoingCalls(fileName: string, position: number) { - const args = this.createFileLocationRequestArgs(fileName, position); - const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); - const response = this.processResponse(request); - return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); - } - - getProgram(): Program { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - getNonBoundSourceFile(_fileName: string): SourceFile { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - getSourceFile(_fileName: string): SourceFile { - throw new Error("SourceFile objects are not serializable through the server protocol."); - } - - cleanupSemanticCache(): void { - throw new Error("cleanupSemanticCache is not available through the server layer."); - } - - getSourceMapper(): never { - return notImplemented(); - } - - clearSourceMapperCache(): never { - return notImplemented(); - } - - toggleLineComment(): TextChange[] { - return notImplemented(); - } - - toggleMultilineComment(): TextChange[] { - return notImplemented(); - } - - commentSelection(): TextChange[] { - return notImplemented(); - } - - uncommentSelection(): TextChange[] { - return notImplemented(); - } - - dispose(): void { - throw new Error("dispose is not available through the server layer."); - } - } -} +namespace ts.server { + export interface SessionClientHost extends LanguageServiceHost { + writeMessage(message: string): void; + } + + interface RenameEntry { + readonly renameInfo: RenameInfo; + readonly inputs: { + readonly fileName: string; + readonly position: number; + readonly findInStrings: boolean; + readonly findInComments: boolean; + }; + readonly locations: RenameLocation[]; + } + + /* @internal */ + export function extractMessage(message: string): string { + // Read the content length + const contentLengthPrefix = "Content-Length: "; + const lines = message.split(/\r?\n/); + Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); + + const contentLengthText = lines[0]; + Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); + const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); + + // Read the body + const responseBody = lines[2]; + + // Verify content length + Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); + return responseBody; + } + + export class SessionClient implements LanguageService { + private sequence = 0; + private lineMaps: Map = createMap(); + private messages: string[] = []; + private lastRenameEntry: RenameEntry | undefined; + + constructor(private host: SessionClientHost) { + } + + public onMessage(message: string): void { + this.messages.push(message); + } + + private writeMessage(message: string): void { + this.host.writeMessage(message); + } + + private getLineMap(fileName: string): number[] { + let lineMap = this.lineMaps.get(fileName); + if (!lineMap) { + lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); + this.lineMaps.set(fileName, lineMap); + } + return lineMap; + } + + private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { + lineMap = lineMap || this.getLineMap(fileName); + return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); + } + + private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { + const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); + return { + line: lineOffset.line + 1, + offset: lineOffset.character + 1 + }; + } + + private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { + return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; + } + + private processRequest(command: string, args: T["arguments"]): T { + const request: protocol.Request = { + seq: this.sequence, + type: "request", + arguments: args, + command + }; + this.sequence++; + + this.writeMessage(JSON.stringify(request)); + + return request; + } + + private processResponse(request: protocol.Request, expectEmptyBody = false): T { + let foundResponseMessage = false; + let response!: T; + while (!foundResponseMessage) { + const lastMessage = this.messages.shift()!; + Debug.assert(!!lastMessage, "Did not receive any responses."); + const responseBody = extractMessage(lastMessage); + try { + response = JSON.parse(responseBody); + // the server may emit events before emitting the response. We + // want to ignore these events for testing purpose. + if (response.type === "response") { + foundResponseMessage = true; + } + } + catch (e) { + throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); + } + } + + // verify the sequence numbers + Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); + + // unmarshal errors + if (!response.success) { + throw new Error("Error " + response.message); + } + + Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); + Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); + + return response; + } + + /*@internal*/ + configure(preferences: UserPreferences) { + const args: protocol.ConfigureRequestArguments = { preferences }; + const request = this.processRequest(CommandNames.Configure, args); + this.processResponse(request, /*expectEmptyBody*/ true); + } + + openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { + const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; + this.processRequest(CommandNames.Open, args); + } + + closeFile(file: string): void { + const args: protocol.FileRequestArgs = { file }; + this.processRequest(CommandNames.Close, args); + } + + createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { + return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; + } + + changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { + // clear the line map after an edit + this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 + this.processRequest(CommandNames.Change, args); + } + + toLineColumnOffset(fileName: string, position: number) { + const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); + return { line, character: offset }; + } + + getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Quickinfo, args); + const response = this.processResponse(request); + const body = response.body!; // TODO: GH#18217 + + return { + kind: body.kind, + kindModifiers: body.kindModifiers, + textSpan: this.decodeSpan(body, fileName), + displayParts: [{ kind: "text", text: body.displayString }], + documentation: [{ kind: "text", text: body.documentation }], + tags: body.tags + }; + } + + getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { + const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; + + const request = this.processRequest(CommandNames.ProjectInfo, args); + const response = this.processResponse(request); + + return { + configFileName: response.body!.configFileName, // TODO: GH#18217 + fileNames: response.body!.fileNames + }; + } + + getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { + // Not passing along 'preferences' because server should already have those from the 'configure' command + const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Completions, args); + const response = this.processResponse(request); + + return { + isGlobalCompletion: false, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: response.body!.map(entry => { // TODO: GH#18217 + if (entry.replacementSpan !== undefined) { + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; + // TODO: GH#241 + const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; + return res; + } + + return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 + }) + }; + } + + getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { + const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; + + const request = this.processRequest(CommandNames.CompletionDetails, args); + const response = this.processResponse(request); + Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); + const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); + return { ...response.body![0], codeActions: convertedCodeActions }; + } + + getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { + return notImplemented(); + } + + getNavigateToItems(searchValue: string): NavigateToItem[] { + const args: protocol.NavtoRequestArgs = { + searchValue, + file: this.host.getScriptFileNames()[0] + }; + + const request = this.processRequest(CommandNames.Navto, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + name: entry.name, + containerName: entry.containerName || "", + containerKind: entry.containerKind || ScriptElementKind.unknown, + kind: entry.kind, + kindModifiers: entry.kindModifiers || "", + matchKind: entry.matchKind as keyof typeof PatternMatchKind, + isCaseSensitive: entry.isCaseSensitive, + fileName: entry.file, + textSpan: this.decodeSpan(entry), + })); + } + + getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { + const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); + + + // TODO: handle FormatCodeOptions + const request = this.processRequest(CommandNames.Format, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 + } + + getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { + return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); + } + + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { + const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; + + // TODO: handle FormatCodeOptions + const request = this.processRequest(CommandNames.Formatonkey, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 + } + + getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Definition, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })); + } + + getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.DefinitionAndBoundSpan, args); + const response = this.processResponse(request); + const body = Debug.checkDefined(response.body); // TODO: GH#18217 + + return { + definitions: body.definitions.map(entry => ({ + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })), + textSpan: this.decodeSpan(body.textSpan, request.arguments.file) + }; + } + + getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.TypeDefinition, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + name: "" + })); + } + + getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Implementation, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + kind: ScriptElementKind.unknown, + displayParts: [] + })); + } + + findReferences(_fileName: string, _position: number): ReferencedSymbol[] { + // Not yet implemented. + return []; + } + + getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.References, args); + const response = this.processResponse(request); + + return response.body!.refs.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + isDefinition: entry.isDefinition, + })); + } + + getEmitOutput(file: string): EmitOutput { + const request = this.processRequest(protocol.CommandTypes.EmitOutput, { file }); + const response = this.processResponse(request); + return response.body as EmitOutput; + } + + getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { + return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); + } + getSemanticDiagnostics(file: string): Diagnostic[] { + return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); + } + getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { + return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); + } + + private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { + const request = this.processRequest(command, { file, includeLinePosition: true }); + const response = this.processResponse(request); + const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); + const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! + + return (response.body).map((entry): DiagnosticWithLocation => { + const category = firstDefined(Object.keys(DiagnosticCategory), id => + isString(id) && entry.category === id.toLowerCase() ? (DiagnosticCategory)[id] : undefined); + return { + file: fakeSourceFile, + start: entry.start, + length: entry.length, + messageText: entry.message, + category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), + code: entry.code, + reportsUnnecessary: entry.reportsUnnecessary, + }; + }); + } + + getCompilerOptionsDiagnostics(): Diagnostic[] { + return notImplemented(); + } + + getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { + // Not passing along 'options' because server should already have those from the 'configure' command + const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; + + const request = this.processRequest(CommandNames.Rename, args); + const response = this.processResponse(request); + const body = response.body!; // TODO: GH#18217 + const locations: RenameLocation[] = []; + for (const entry of body.locs) { + const fileName = entry.file; + for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { + locations.push({ + textSpan: this.decodeSpan({ start, end }, fileName), + fileName, + ...(contextStart !== undefined ? + { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : + undefined), + ...prefixSuffixText + }); + } + } + + const renameInfo = body.info.canRename + ? identity({ + canRename: body.info.canRename, + fileToRename: body.info.fileToRename, + displayName: body.info.displayName, + fullDisplayName: body.info.fullDisplayName, + kind: body.info.kind, + kindModifiers: body.info.kindModifiers, + triggerSpan: createTextSpanFromBounds(position, position), + }) + : identity({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); + this.lastRenameEntry = { + renameInfo, + inputs: { + fileName, + position, + findInStrings: !!findInStrings, + findInComments: !!findInComments, + }, + locations, + }; + return renameInfo; + } + + getSmartSelectionRange() { + return notImplemented(); + } + + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { + if (!this.lastRenameEntry || + this.lastRenameEntry.inputs.fileName !== fileName || + this.lastRenameEntry.inputs.position !== position || + this.lastRenameEntry.inputs.findInStrings !== findInStrings || + this.lastRenameEntry.inputs.findInComments !== findInComments) { + this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); + } + + return this.lastRenameEntry!.locations; + } + + private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { + if (!items) { + return []; + } + + return items.map(item => ({ + text: item.text, + kind: item.kind, + kindModifiers: item.kindModifiers || "", + spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), + indent: item.indent, + bolded: false, + grayed: false + })); + } + + getNavigationBarItems(file: string): NavigationBarItem[] { + const request = this.processRequest(CommandNames.NavBar, { file }); + const response = this.processResponse(request); + + const lineMap = this.getLineMap(file); + return this.decodeNavigationBarItems(response.body, file, lineMap); + } + + private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { + return { + text: tree.text, + kind: tree.kind, + kindModifiers: tree.kindModifiers, + spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), + nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), + childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) + }; + } + + getNavigationTree(file: string): NavigationTree { + const request = this.processRequest(CommandNames.NavTree, { file }); + const response = this.processResponse(request); + + const lineMap = this.getLineMap(file); + return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 + } + + private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; + private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; + private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { + fileName = fileName || span.file; + lineMap = lineMap || this.getLineMap(fileName); + return createTextSpanFromBounds( + this.lineOffsetToPosition(fileName, span.start, lineMap), + this.lineOffsetToPosition(fileName, span.end, lineMap)); + } + + getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { + return notImplemented(); + } + + getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { + return notImplemented(); + } + + getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { + const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.SignatureHelp, args); + const response = this.processResponse(request); + + if (!response.body) { + return undefined; + } + + const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; + + const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); + + return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; + } + + getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Occurrences, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + isDefinition: false + })); + } + + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { + const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; + + const request = this.processRequest(CommandNames.DocumentHighlights, args); + const response = this.processResponse(request); + + return response.body!.map(item => ({ // TODO: GH#18217 + fileName: item.file, + highlightSpans: item.highlightSpans.map(span => ({ + textSpan: this.decodeSpan(span, item.file), + kind: span.kind + })), + })); + } + + getOutliningSpans(file: string): OutliningSpan[] { + const request = this.processRequest(CommandNames.GetOutliningSpans, { file }); + const response = this.processResponse(request); + + return response.body!.map(item => ({ + textSpan: this.decodeSpan(item.textSpan, file), + hintSpan: this.decodeSpan(item.hintSpan, file), + bannerText: item.bannerText, + autoCollapse: item.autoCollapse, + kind: item.kind + })); + } + + getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { + return notImplemented(); + } + + getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion { + return notImplemented(); + } + + isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { + return notImplemented(); + } + + getJsxClosingTagAtPosition(_fileName: string, _position: number): never { + return notImplemented(); + } + + getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { + return notImplemented(); + } + + getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { + const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; + + const request = this.processRequest(CommandNames.GetCodeFixes, args); + const response = this.processResponse(request); + + return response.body!.map(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 + ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); + } + + getCombinedCodeFix = notImplemented; + + applyCodeActionCommand = notImplemented; + + private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { + return typeof positionOrRange === "number" + ? this.createFileLocationRequestArgs(fileName, positionOrRange) + : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); + } + + private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { + const { line, offset } = this.positionToOneBasedLineOffset(file, position); + return { file, line, offset }; + } + + private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { + const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); + return { file, startLine, startOffset, endLine, endOffset }; + } + + private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { + const { line, offset } = this.positionToOneBasedLineOffset(file, start); + const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); + return { file, line, offset, endLine, endOffset }; + } + + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(CommandNames.GetApplicableRefactors, args); + const response = this.processResponse(request); + return response.body!; // TODO: GH#18217 + } + + getEditsForRefactor( + fileName: string, + _formatOptions: FormatCodeSettings, + positionOrRange: number | TextRange, + refactorName: string, + actionName: string): RefactorEditInfo { + + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; + args.refactor = refactorName; + args.action = actionName; + + const request = this.processRequest(CommandNames.GetEditsForRefactor, args); + const response = this.processResponse(request); + + if (!response.body) { + return { edits: [], renameFilename: undefined, renameLocation: undefined }; + } + + const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); + + const renameFilename: string | undefined = response.body.renameFilename; + let renameLocation: number | undefined; + if (renameFilename !== undefined) { + renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 + } + + return { + edits, + renameFilename, + renameLocation + }; + } + + organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { + return notImplemented(); + } + + getEditsForFileRename() { + return notImplemented(); + } + + private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { + return edits.map(edit => { + const fileName = edit.fileName; + return { + fileName, + textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) + }; + }); + } + + private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { + return changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) + })); + } + + convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { + return { + span: this.decodeSpan(change, fileName), + newText: change.newText ? change.newText : "" + }; + } + + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(CommandNames.Brace, args); + const response = this.processResponse(request); + + return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 + } + + configurePlugin(pluginName: string, configuration: any): void { + const request = this.processRequest("configurePlugin", { pluginName, configuration }); + this.processResponse(request, /*expectEmptyBody*/ true); + } + + getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { + return notImplemented(); + } + + getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { + return notImplemented(); + } + + getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { + return notImplemented(); + } + + getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { + return notImplemented(); + } + + getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications { + return notImplemented(); + } + + private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { + return { + file: item.file, + name: item.name, + kind: item.kind, + span: this.decodeSpan(item.span, item.file), + selectionSpan: this.decodeSpan(item.selectionSpan, item.file) + }; + } + + prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); + } + + private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { + return { + from: this.convertCallHierarchyItem(item.from), + fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) + }; + } + + provideCallHierarchyIncomingCalls(fileName: string, position: number) { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); + } + + private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { + return { + to: this.convertCallHierarchyItem(item.to), + fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) + }; + } + + provideCallHierarchyOutgoingCalls(fileName: string, position: number) { + const args = this.createFileLocationRequestArgs(fileName, position); + const request = this.processRequest(CommandNames.PrepareCallHierarchy, args); + const response = this.processResponse(request); + return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); + } + + getProgram(): Program { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getNonBoundSourceFile(_fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getSourceFile(_fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + cleanupSemanticCache(): void { + throw new Error("cleanupSemanticCache is not available through the server layer."); + } + + getSourceMapper(): never { + return notImplemented(); + } + + clearSourceMapperCache(): never { + return notImplemented(); + } + + toggleLineComment(): TextChange[] { + return notImplemented(); + } + + toggleMultilineComment(): TextChange[] { + return notImplemented(); + } + + commentSelection(): TextChange[] { + return notImplemented(); + } + + uncommentSelection(): TextChange[] { + return notImplemented(); + } + + dispose(): void { + throw new Error("dispose is not available through the server layer."); + } + } +} diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 7bc137cb4e802..6c6b1e9ea599f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -1,991 +1,991 @@ -namespace Harness.LanguageService { - - export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { - const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null - const langSvc: any = info.languageService; - for (const k of Object.keys(langSvc)) { - // eslint-disable-next-line only-arrow-functions - proxy[k] = function () { - return langSvc[k].apply(langSvc, arguments); - }; - } - return proxy; - } - - export class ScriptInfo { - public version = 1; - public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; - private lineMap: number[] | undefined; - - constructor(public fileName: string, public content: string, public isRootFile: boolean) { - this.setContent(content); - } - - private setContent(content: string): void { - this.content = content; - this.lineMap = undefined; - } - - public getLineMap(): number[] { - return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); - } - - public updateContent(content: string): void { - this.editRanges = []; - this.setContent(content); - this.version++; - } - - public editContent(start: number, end: number, newText: string): void { - // Apply edits - const prefix = this.content.substring(0, start); - const middle = newText; - const suffix = this.content.substring(end); - this.setContent(prefix + middle + suffix); - - // Store edit range + new length of script - this.editRanges.push({ - length: this.content.length, - textChangeRange: ts.createTextChangeRange( - ts.createTextSpanFromBounds(start, end), newText.length) - }); - - // Update version # - this.version++; - } - - public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { - if (startVersion === endVersion) { - // No edits! - return ts.unchangedTextChangeRange; - } - - const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); - const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); - - const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); - return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); - } - } - - class ScriptSnapshot implements ts.IScriptSnapshot { - public textSnapshot: string; - public version: number; - - constructor(public scriptInfo: ScriptInfo) { - this.textSnapshot = scriptInfo.content; - this.version = scriptInfo.version; - } - - public getText(start: number, end: number): string { - return this.textSnapshot.substring(start, end); - } - - public getLength(): number { - return this.textSnapshot.length; - } - - public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { - const oldShim = oldScript; - return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); - } - } - - class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { - constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { - } - - public getText(start: number, end: number): string { - return this.scriptSnapshot.getText(start, end); - } - - public getLength(): number { - return this.scriptSnapshot.getLength(); - } - - public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { - const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); - return range && JSON.stringify(range); - } - } - - class DefaultHostCancellationToken implements ts.HostCancellationToken { - public static readonly instance = new DefaultHostCancellationToken(); - - public isCancellationRequested() { - return false; - } - } - - export interface LanguageServiceAdapter { - getHost(): LanguageServiceAdapterHost; - getLanguageService(): ts.LanguageService; - getClassifier(): ts.Classifier; - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; - } - - export abstract class LanguageServiceAdapterHost { - public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); - public typesRegistry: ts.Map | undefined; - private scriptInfos: collections.SortedMap; - - constructor(protected cancellationToken = DefaultHostCancellationToken.instance, - protected settings = ts.getDefaultCompilerOptions()) { - this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); - } - - public get vfs() { - return this.sys.vfs; - } - - public getNewLine(): string { - return harnessNewLine; - } - - public getFilenames(): string[] { - const fileNames: string[] = []; - this.scriptInfos.forEach(scriptInfo => { - if (scriptInfo.isRootFile) { - // only include root files here - // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. - fileNames.push(scriptInfo.fileName); - } - }); - return fileNames; - } - - public getScriptInfo(fileName: string): ScriptInfo | undefined { - return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); - } - - public addScript(fileName: string, content: string, isRootFile: boolean): void { - this.vfs.mkdirpSync(vpath.dirname(fileName)); - this.vfs.writeFileSync(fileName, content); - this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); - } - - public renameFileOrDirectory(oldPath: string, newPath: string): void { - this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); - this.vfs.renameSync(oldPath, newPath); - - const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); - this.scriptInfos.forEach((scriptInfo, key) => { - const newFileName = updater(key); - if (newFileName !== undefined) { - this.scriptInfos.delete(key); - this.scriptInfos.set(newFileName, scriptInfo); - scriptInfo.fileName = newFileName; - } - }); - } - - public editScript(fileName: string, start: number, end: number, newText: string) { - const script = this.getScriptInfo(fileName); - if (script) { - script.editContent(start, end, newText); - this.vfs.mkdirpSync(vpath.dirname(fileName)); - this.vfs.writeFileSync(fileName, script.content); - return; - } - - throw new Error("No script with name '" + fileName + "'"); - } - - public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } - - /** - * @param line 0 based index - * @param col 0 based index - */ - public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { - const script: ScriptInfo = this.getScriptInfo(fileName)!; - assert.isOk(script); - return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); - } - - public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { - const script: ScriptInfo = this.getScriptInfo(fileName)!; - assert.isOk(script); - return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); - } - - useCaseSensitiveFileNames() { - return !this.vfs.ignoreCase; - } - } - - /// Native adapter - class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { - isKnownTypesPackageName(name: string): boolean { - return !!this.typesRegistry && this.typesRegistry.has(name); - } - - getGlobalTypingsCacheLocation() { - return "/Library/Caches/typescript"; - } - - installPackage = ts.notImplemented; - - getCompilationSettings() { return this.settings; } - - getCancellationToken() { return this.cancellationToken; } - - getDirectories(path: string): string[] { - return this.sys.getDirectories(path); - } - - getCurrentDirectory(): string { return virtualFileSystemRoot; } - - getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } - - getScriptFileNames(): string[] { - return this.getFilenames().filter(ts.isAnySupportedFileExtension); - } - - getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { - const script = this.getScriptInfo(fileName); - return script ? new ScriptSnapshot(script) : undefined; - } - - getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } - - getScriptVersion(fileName: string): string { - const script = this.getScriptInfo(fileName); - return script ? script.version.toString() : undefined!; // TODO: GH#18217 - } - - directoryExists(dirName: string): boolean { - return this.sys.directoryExists(dirName); - } - - fileExists(fileName: string): boolean { - return this.sys.fileExists(fileName); - } - - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.sys.readDirectory(path, extensions, exclude, include, depth); - } - - readFile(path: string): string | undefined { - return this.sys.readFile(path); - } - - realpath(path: string): string { - return this.sys.realpath(path); - } - - getTypeRootsVersion() { - return 0; - } - - log = ts.noop; - trace = ts.noop; - error = ts.noop; - } - - export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { - private host: NativeLanguageServiceHost; - constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - this.host = new NativeLanguageServiceHost(cancellationToken, options); - } - getHost(): LanguageServiceAdapterHost { return this.host; } - getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } - getClassifier(): ts.Classifier { return ts.createClassifier(); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } - } - - /// Shim adapter - class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { - private nativeHost: NativeLanguageServiceHost; - - public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; - public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; - - constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - super(cancellationToken, options); - this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); - - if (preprocessToResolve) { - const compilerOptions = this.nativeHost.getCompilationSettings(); - const moduleResolutionHost: ts.ModuleResolutionHost = { - fileExists: fileName => this.getScriptInfo(fileName) !== undefined, - readFile: fileName => { - const scriptInfo = this.getScriptInfo(fileName); - return scriptInfo && scriptInfo.content; - } - }; - this.getModuleResolutionsForFile = (fileName) => { - const scriptInfo = this.getScriptInfo(fileName)!; - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); - const imports: ts.MapLike = {}; - for (const module of preprocessInfo.importedFiles) { - const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); - if (resolutionInfo.resolvedModule) { - imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; - } - } - return JSON.stringify(imports); - }; - this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { - const scriptInfo = this.getScriptInfo(fileName); - if (scriptInfo) { - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); - const resolutions: ts.MapLike = {}; - const settings = this.nativeHost.getCompilationSettings(); - for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { - const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); - if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { - resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; - } - } - return JSON.stringify(resolutions); - } - else { - return "[]"; - } - }; - } - } - - getFilenames(): string[] { return this.nativeHost.getFilenames(); } - getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } - addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } - editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } - positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } - - getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } - getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } - getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } - getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } - getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } - getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } - getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { - const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 - return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); - } - getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } - getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } - getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } - - readDirectory = ts.notImplemented; - readDirectoryNames = ts.notImplemented; - readFileNames = ts.notImplemented; - fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } - readFile(fileName: string) { - const snapshot = this.nativeHost.getScriptSnapshot(fileName); - return snapshot && ts.getSnapshotText(snapshot); - } - log(s: string): void { this.nativeHost.log(s); } - trace(s: string): void { this.nativeHost.trace(s); } - error(s: string): void { this.nativeHost.error(s); } - directoryExists(): boolean { - // for tests pessimistically assume that directory always exists - return true; - } - } - - class ClassifierShimProxy implements ts.Classifier { - constructor(private shim: ts.ClassifierShim) { - } - getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { - return ts.notImplemented(); - } - getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { - const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); - const entries: ts.ClassificationInfo[] = []; - let i = 0; - let position = 0; - - for (; i < result.length - 1; i += 2) { - const t = entries[i / 2] = { - length: parseInt(result[i]), - classification: parseInt(result[i + 1]) - }; - - assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); - position += t.length; - } - const finalLexState = parseInt(result[result.length - 1]); - - assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); - - return { - finalLexState, - entries - }; - } - } - - function unwrapJSONCallResult(result: string): any { - const parsedResult = JSON.parse(result); - if (parsedResult.error) { - throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); - } - else if (parsedResult.canceled) { - throw new ts.OperationCanceledException(); - } - return parsedResult.result; - } - - class LanguageServiceShimProxy implements ts.LanguageService { - constructor(private shim: ts.LanguageServiceShim) { - } - cleanupSemanticCache(): void { - this.shim.cleanupSemanticCache(); - } - getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); - } - getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); - } - getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { - return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); - } - getCompilerOptionsDiagnostics(): ts.Diagnostic[] { - return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); - } - getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); - } - getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { - return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length)); - } - getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); - } - getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { - return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); - } - getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { - return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); - } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { - return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); - } - getCompletionEntrySymbol(): ts.Symbol { - throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); - } - getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { - return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); - } - getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); - } - getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); - } - getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { - return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); - } - getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { - return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); - } - 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)); - } - getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { - return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); - } - getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { - return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); - } - getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { - return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); - } - getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { - return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); - } - getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { - return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); - } - findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { - return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); - } - getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { - return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); - } - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { - return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); - } - getNavigateToItems(searchValue: string): ts.NavigateToItem[] { - return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); - } - getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { - return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); - } - getNavigationTree(fileName: string): ts.NavigationTree { - return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); - } - getOutliningSpans(fileName: string): ts.OutliningSpan[] { - return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); - } - getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { - return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); - } - getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { - return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); - } - getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { - return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); - } - getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); - } - getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); - } - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); - } - getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion { - return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position)); - } - isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { - return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); - } - getJsxClosingTagAtPosition(): never { - throw new Error("Not supported on the shim."); - } - getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { - return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); - } - getCodeFixesAtPosition(): never { - throw new Error("Not supported on the shim."); - } - getCombinedCodeFix = ts.notImplemented; - applyCodeActionCommand = ts.notImplemented; - getCodeFixDiagnostics(): ts.Diagnostic[] { - throw new Error("Not supported on the shim."); - } - getEditsForRefactor(): ts.RefactorEditInfo { - throw new Error("Not supported on the shim."); - } - getApplicableRefactors(): ts.ApplicableRefactorInfo[] { - throw new Error("Not supported on the shim."); - } - organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { - throw new Error("Not supported on the shim."); - } - getEditsForFileRename(): readonly ts.FileTextChanges[] { - throw new Error("Not supported on the shim."); - } - prepareCallHierarchy(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); - } - provideCallHierarchyIncomingCalls(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); - } - provideCallHierarchyOutgoingCalls(fileName: string, position: number) { - return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); - } - getEmitOutput(fileName: string): ts.EmitOutput { - return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); - } - getProgram(): ts.Program { - throw new Error("Program can not be marshaled across the shim layer."); - } - getNonBoundSourceFile(): ts.SourceFile { - throw new Error("SourceFile can not be marshaled across the shim layer."); - } - getSourceFile(): ts.SourceFile { - throw new Error("SourceFile can not be marshaled across the shim layer."); - } - getSourceMapper(): never { - return ts.notImplemented(); - } - clearSourceMapperCache(): never { - return ts.notImplemented(); - } - toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); - } - toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); - } - commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange)); - } - uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { - return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange)); - } - dispose(): void { this.shim.dispose({}); } - } - - export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { - private host: ShimLanguageServiceHost; - private factory: ts.TypeScriptServicesFactory; - constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); - this.factory = new ts.TypeScriptServicesFactory(); - } - getHost() { return this.host; } - getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } - getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { - const coreServicesShim = this.factory.createCoreServicesShim(this.host); - const shimResult: { - referencedFiles: ts.ShimsFileReference[]; - typeReferenceDirectives: ts.ShimsFileReference[]; - importedFiles: ts.ShimsFileReference[]; - isLibFile: boolean; - } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); - - const convertResult: ts.PreProcessedFileInfo = { - referencedFiles: [], - importedFiles: [], - ambientExternalModules: [], - isLibFile: shimResult.isLibFile, - typeReferenceDirectives: [], - libReferenceDirectives: [] - }; - - ts.forEach(shimResult.referencedFiles, refFile => { - convertResult.referencedFiles.push({ - fileName: refFile.path, - pos: refFile.position, - end: refFile.position + refFile.length - }); - }); - - ts.forEach(shimResult.importedFiles, importedFile => { - convertResult.importedFiles.push({ - fileName: importedFile.path, - pos: importedFile.position, - end: importedFile.position + importedFile.length - }); - }); - - ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { - convertResult.importedFiles.push({ - fileName: typeRefDirective.path, - pos: typeRefDirective.position, - end: typeRefDirective.position + typeRefDirective.length - }); - }); - return convertResult; - } - } - - // Server adapter - class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { - private client!: ts.server.SessionClient; - - constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { - super(cancellationToken, settings); - } - - onMessage = ts.noop; - writeMessage = ts.noop; - - setClient(client: ts.server.SessionClient) { - this.client = client; - } - - openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { - super.openFile(fileName, content, scriptKindName); - this.client.openFile(fileName, content, scriptKindName); - } - - editScript(fileName: string, start: number, end: number, newText: string) { - const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); - super.editScript(fileName, start, end, newText); - this.client.changeFile(fileName, changeArgs); - } - } - - class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { - args: string[] = []; - newLine: string; - useCaseSensitiveFileNames = false; - - constructor(private host: NativeLanguageServiceHost) { - this.newLine = this.host.getNewLine(); - } - - onMessage = ts.noop; - writeMessage = ts.noop; // overridden - write(message: string): void { - this.writeMessage(message); - } - - readFile(fileName: string): string | undefined { - if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { - fileName = Compiler.defaultLibFileName; - } - - const snapshot = this.host.getScriptSnapshot(fileName); - return snapshot && ts.getSnapshotText(snapshot); - } - - writeFile = ts.noop; - - resolvePath(path: string): string { - return path; - } - - fileExists(path: string): boolean { - return !!this.host.getScriptSnapshot(path); - } - - directoryExists(): boolean { - // for tests assume that directory exists - return true; - } - - getExecutingFilePath(): string { - return ""; - } - - exit = ts.noop; - - createDirectory(_directoryName: string): void { - return ts.notImplemented(); - } - - getCurrentDirectory(): string { - return this.host.getCurrentDirectory(); - } - - getDirectories(path: string): string[] { - return this.host.getDirectories(path); - } - - getEnvironmentVariable(name: string): string { - return ts.sys.getEnvironmentVariable(name); - } - - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { - return this.host.readDirectory(path, extensions, exclude, include, depth); - } - - watchFile(): ts.FileWatcher { - return { close: ts.noop }; - } - - watchDirectory(): ts.FileWatcher { - return { close: ts.noop }; - } - - close = ts.noop; - - info(message: string): void { - this.host.log(message); - } - - msg(message: string): void { - this.host.log(message); - } - - loggingEnabled() { - return true; - } - - getLogFileName(): string | undefined { - return undefined; - } - - hasLevel() { - return false; - } - - startGroup() { throw ts.notImplemented(); } - endGroup() { throw ts.notImplemented(); } - - perftrc(message: string): void { - return this.host.log(message); - } - - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { - // eslint-disable-next-line no-restricted-globals - return setTimeout(callback, ms, args); - } - - clearTimeout(timeoutId: any): void { - // eslint-disable-next-line no-restricted-globals - clearTimeout(timeoutId); - } - - setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { - // eslint-disable-next-line no-restricted-globals - return setImmediate(callback, args); - } - - clearImmediate(timeoutId: any): void { - // eslint-disable-next-line no-restricted-globals - clearImmediate(timeoutId); - } - - createHash(s: string) { - return mockHash(s); - } - - require(_initialDir: string, _moduleName: string): ts.RequireResult { - switch (_moduleName) { - // Adds to the Quick Info a fixed string and a string from the config file - // and replaces the first display part - case "quickinfo-augmeneter": - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = makeDefaultProxy(info); - const langSvc: any = info.languageService; - // eslint-disable-next-line only-arrow-functions - proxy.getQuickInfoAtPosition = function () { - const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); - if (parts.displayParts.length > 0) { - parts.displayParts[0].text = "Proxied"; - } - parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); - return parts; - }; - - return proxy; - } - }), - error: undefined - }; - - // Throws during initialization - case "create-thrower": - return { - module: () => ({ - create() { - throw new Error("I am not a well-behaved plugin"); - } - }), - error: undefined - }; - - // Adds another diagnostic - case "diagnostic-adder": - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - const proxy = makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Warning, - file: sourceFile, - code: 9999, - length: 3, - messageText: `Plugin diagnostic`, - start: 0 - }); - return prev; - }; - return proxy; - } - }), - error: undefined - }; - - // Accepts configurations - case "configurable-diagnostic-adder": - let customMessage = "default message"; - return { - module: () => ({ - create(info: ts.server.PluginCreateInfo) { - customMessage = info.config.message; - const proxy = makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: ts.DiagnosticCategory.Error, - file: sourceFile, - code: 9999, - length: 3, - messageText: customMessage, - start: 0 - }); - return prev; - }; - return proxy; - }, - onConfigurationChanged(config: any) { - customMessage = config.message; - } - }), - error: undefined - }; - - default: - return { - module: undefined, - error: new Error("Could not resolve module") - }; - } - } - } - - class FourslashSession extends ts.server.Session { - getText(fileName: string) { - return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); - } - } - - export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { - private host: SessionClientHost; - private client: ts.server.SessionClient; - private server: FourslashSession; - constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { - // This is the main host that tests use to direct tests - const clientHost = new SessionClientHost(cancellationToken, options); - const client = new ts.server.SessionClient(clientHost); - - // This host is just a proxy for the clientHost, it uses the client - // host to answer server queries about files on disk - const serverHost = new SessionServerHost(clientHost); - const opts: ts.server.SessionOptions = { - host: serverHost, - cancellationToken: ts.server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: serverHost, - canUseEvents: true - }; - this.server = new FourslashSession(opts); - - - // Fake the connection between the client and the server - serverHost.writeMessage = client.onMessage.bind(client); - clientHost.writeMessage = this.server.onMessage.bind(this.server); - - // Wire the client to the host to get notifications when a file is open - // or edited. - clientHost.setClient(client); - - // Set the properties - this.client = client; - this.host = clientHost; - } - getHost() { return this.host; } - getLanguageService(): ts.LanguageService { return this.client; } - getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } - getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } - assertTextConsistent(fileName: string) { - const serverText = this.server.getText(fileName); - const clientText = this.host.readFile(fileName); - ts.Debug.assert(serverText === clientText, [ - "Server and client text are inconsistent.", - "", - "\x1b[1mServer\x1b[0m\x1b[31m:", - serverText, - "", - "\x1b[1mClient\x1b[0m\x1b[31m:", - clientText, - "", - "This probably means something is wrong with the fourslash infrastructure, not with the test." - ].join(ts.sys.newLine)); - } - } -} +namespace Harness.LanguageService { + + export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService { + const proxy = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null + const langSvc: any = info.languageService; + for (const k of Object.keys(langSvc)) { + // eslint-disable-next-line only-arrow-functions + proxy[k] = function () { + return langSvc[k].apply(langSvc, arguments); + }; + } + return proxy; + } + + export class ScriptInfo { + public version = 1; + public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; + private lineMap: number[] | undefined; + + constructor(public fileName: string, public content: string, public isRootFile: boolean) { + this.setContent(content); + } + + private setContent(content: string): void { + this.content = content; + this.lineMap = undefined; + } + + public getLineMap(): number[] { + return this.lineMap || (this.lineMap = ts.computeLineStarts(this.content)); + } + + public updateContent(content: string): void { + this.editRanges = []; + this.setContent(content); + this.version++; + } + + public editContent(start: number, end: number, newText: string): void { + // Apply edits + const prefix = this.content.substring(0, start); + const middle = newText; + const suffix = this.content.substring(end); + this.setContent(prefix + middle + suffix); + + // Store edit range + new length of script + this.editRanges.push({ + length: this.content.length, + textChangeRange: ts.createTextChangeRange( + ts.createTextSpanFromBounds(start, end), newText.length) + }); + + // Update version # + this.version++; + } + + public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { + if (startVersion === endVersion) { + // No edits! + return ts.unchangedTextChangeRange; + } + + const initialEditRangeIndex = this.editRanges.length - (this.version - startVersion); + const lastEditRangeIndex = this.editRanges.length - (this.version - endVersion); + + const entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex); + return ts.collapseTextChangeRangesAcrossMultipleVersions(entries.map(e => e.textChangeRange)); + } + } + + class ScriptSnapshot implements ts.IScriptSnapshot { + public textSnapshot: string; + public version: number; + + constructor(public scriptInfo: ScriptInfo) { + this.textSnapshot = scriptInfo.content; + this.version = scriptInfo.version; + } + + public getText(start: number, end: number): string { + return this.textSnapshot.substring(start, end); + } + + public getLength(): number { + return this.textSnapshot.length; + } + + public getChangeRange(oldScript: ts.IScriptSnapshot): ts.TextChangeRange { + const oldShim = oldScript; + return this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version); + } + } + + class ScriptSnapshotProxy implements ts.ScriptSnapshotShim { + constructor(private readonly scriptSnapshot: ts.IScriptSnapshot) { + } + + public getText(start: number, end: number): string { + return this.scriptSnapshot.getText(start, end); + } + + public getLength(): number { + return this.scriptSnapshot.getLength(); + } + + public getChangeRange(oldScript: ts.ScriptSnapshotShim): string | undefined { + const range = this.scriptSnapshot.getChangeRange((oldScript as ScriptSnapshotProxy).scriptSnapshot); + return range && JSON.stringify(range); + } + } + + class DefaultHostCancellationToken implements ts.HostCancellationToken { + public static readonly instance = new DefaultHostCancellationToken(); + + public isCancellationRequested() { + return false; + } + } + + export interface LanguageServiceAdapter { + getHost(): LanguageServiceAdapterHost; + getLanguageService(): ts.LanguageService; + getClassifier(): ts.Classifier; + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo; + } + + export abstract class LanguageServiceAdapterHost { + public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot })); + public typesRegistry: ts.Map | undefined; + private scriptInfos: collections.SortedMap; + + constructor(protected cancellationToken = DefaultHostCancellationToken.instance, + protected settings = ts.getDefaultCompilerOptions()) { + this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" }); + } + + public get vfs() { + return this.sys.vfs; + } + + public getNewLine(): string { + return harnessNewLine; + } + + public getFilenames(): string[] { + const fileNames: string[] = []; + this.scriptInfos.forEach(scriptInfo => { + if (scriptInfo.isRootFile) { + // only include root files here + // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. + fileNames.push(scriptInfo.fileName); + } + }); + return fileNames; + } + + public getScriptInfo(fileName: string): ScriptInfo | undefined { + return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName)); + } + + public addScript(fileName: string, content: string, isRootFile: boolean): void { + this.vfs.mkdirpSync(vpath.dirname(fileName)); + this.vfs.writeFileSync(fileName, content); + this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile)); + } + + public renameFileOrDirectory(oldPath: string, newPath: string): void { + this.vfs.mkdirpSync(ts.getDirectoryPath(newPath)); + this.vfs.renameSync(oldPath, newPath); + + const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined); + this.scriptInfos.forEach((scriptInfo, key) => { + const newFileName = updater(key); + if (newFileName !== undefined) { + this.scriptInfos.delete(key); + this.scriptInfos.set(newFileName, scriptInfo); + scriptInfo.fileName = newFileName; + } + }); + } + + public editScript(fileName: string, start: number, end: number, newText: string) { + const script = this.getScriptInfo(fileName); + if (script) { + script.editContent(start, end, newText); + this.vfs.mkdirpSync(vpath.dirname(fileName)); + this.vfs.writeFileSync(fileName, script.content); + return; + } + + throw new Error("No script with name '" + fileName + "'"); + } + + public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ } + + /** + * @param line 0 based index + * @param col 0 based index + */ + public positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { + const script: ScriptInfo = this.getScriptInfo(fileName)!; + assert.isOk(script); + return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position); + } + + public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number { + const script: ScriptInfo = this.getScriptInfo(fileName)!; + assert.isOk(script); + return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character); + } + + useCaseSensitiveFileNames() { + return !this.vfs.ignoreCase; + } + } + + /// Native adapter + class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { + isKnownTypesPackageName(name: string): boolean { + return !!this.typesRegistry && this.typesRegistry.has(name); + } + + getGlobalTypingsCacheLocation() { + return "/Library/Caches/typescript"; + } + + installPackage = ts.notImplemented; + + getCompilationSettings() { return this.settings; } + + getCancellationToken() { return this.cancellationToken; } + + getDirectories(path: string): string[] { + return this.sys.getDirectories(path); + } + + getCurrentDirectory(): string { return virtualFileSystemRoot; } + + getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } + + getScriptFileNames(): string[] { + return this.getFilenames().filter(ts.isAnySupportedFileExtension); + } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { + const script = this.getScriptInfo(fileName); + return script ? new ScriptSnapshot(script) : undefined; + } + + getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } + + getScriptVersion(fileName: string): string { + const script = this.getScriptInfo(fileName); + return script ? script.version.toString() : undefined!; // TODO: GH#18217 + } + + directoryExists(dirName: string): boolean { + return this.sys.directoryExists(dirName); + } + + fileExists(fileName: string): boolean { + return this.sys.fileExists(fileName); + } + + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { + return this.sys.readDirectory(path, extensions, exclude, include, depth); + } + + readFile(path: string): string | undefined { + return this.sys.readFile(path); + } + + realpath(path: string): string { + return this.sys.realpath(path); + } + + getTypeRootsVersion() { + return 0; + } + + log = ts.noop; + trace = ts.noop; + error = ts.noop; + } + + export class NativeLanguageServiceAdapter implements LanguageServiceAdapter { + private host: NativeLanguageServiceHost; + constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + this.host = new NativeLanguageServiceHost(cancellationToken, options); + } + getHost(): LanguageServiceAdapterHost { return this.host; } + getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } + getClassifier(): ts.Classifier { return ts.createClassifier(); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJSFileExtension(fileName)); } + } + + /// Shim adapter + class ShimLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceShimHost, ts.CoreServicesShimHost { + private nativeHost: NativeLanguageServiceHost; + + public getModuleResolutionsForFile: ((fileName: string) => string) | undefined; + public getTypeReferenceDirectiveResolutionsForFile: ((fileName: string) => string) | undefined; + + constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + super(cancellationToken, options); + this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); + + if (preprocessToResolve) { + const compilerOptions = this.nativeHost.getCompilationSettings(); + const moduleResolutionHost: ts.ModuleResolutionHost = { + fileExists: fileName => this.getScriptInfo(fileName) !== undefined, + readFile: fileName => { + const scriptInfo = this.getScriptInfo(fileName); + return scriptInfo && scriptInfo.content; + } + }; + this.getModuleResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName)!; + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); + const imports: ts.MapLike = {}; + for (const module of preprocessInfo.importedFiles) { + const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); + if (resolutionInfo.resolvedModule) { + imports[module.fileName] = resolutionInfo.resolvedModule.resolvedFileName; + } + } + return JSON.stringify(imports); + }; + this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName); + if (scriptInfo) { + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); + const resolutions: ts.MapLike = {}; + const settings = this.nativeHost.getCompilationSettings(); + for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { + const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, settings, moduleResolutionHost); + if (resolutionInfo.resolvedTypeReferenceDirective!.resolvedFileName) { + resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective!; + } + } + return JSON.stringify(resolutions); + } + else { + return "[]"; + } + }; + } + } + + getFilenames(): string[] { return this.nativeHost.getFilenames(); } + getScriptInfo(fileName: string): ScriptInfo | undefined { return this.nativeHost.getScriptInfo(fileName); } + addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } + editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } + positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } + + getCompilationSettings(): string { return JSON.stringify(this.nativeHost.getCompilationSettings()); } + getCancellationToken(): ts.HostCancellationToken { return this.nativeHost.getCancellationToken(); } + getCurrentDirectory(): string { return this.nativeHost.getCurrentDirectory(); } + getDirectories(path: string): string { return JSON.stringify(this.nativeHost.getDirectories(path)); } + getDefaultLibFileName(): string { return this.nativeHost.getDefaultLibFileName(); } + getScriptFileNames(): string { return JSON.stringify(this.nativeHost.getScriptFileNames()); } + getScriptSnapshot(fileName: string): ts.ScriptSnapshotShim { + const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName)!; // TODO: GH#18217 + return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); + } + getScriptKind(): ts.ScriptKind { return this.nativeHost.getScriptKind(); } + getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } + getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } + + readDirectory = ts.notImplemented; + readDirectoryNames = ts.notImplemented; + readFileNames = ts.notImplemented; + fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } + readFile(fileName: string) { + const snapshot = this.nativeHost.getScriptSnapshot(fileName); + return snapshot && ts.getSnapshotText(snapshot); + } + log(s: string): void { this.nativeHost.log(s); } + trace(s: string): void { this.nativeHost.trace(s); } + error(s: string): void { this.nativeHost.error(s); } + directoryExists(): boolean { + // for tests pessimistically assume that directory always exists + return true; + } + } + + class ClassifierShimProxy implements ts.Classifier { + constructor(private shim: ts.ClassifierShim) { + } + getEncodedLexicalClassifications(_text: string, _lexState: ts.EndOfLineState, _classifyKeywordsInGenerics?: boolean): ts.Classifications { + return ts.notImplemented(); + } + getClassificationsForLine(text: string, lexState: ts.EndOfLineState, classifyKeywordsInGenerics?: boolean): ts.ClassificationResult { + const result = this.shim.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics).split("\n"); + const entries: ts.ClassificationInfo[] = []; + let i = 0; + let position = 0; + + for (; i < result.length - 1; i += 2) { + const t = entries[i / 2] = { + length: parseInt(result[i]), + classification: parseInt(result[i + 1]) + }; + + assert.isTrue(t.length > 0, "Result length should be greater than 0, got :" + t.length); + position += t.length; + } + const finalLexState = parseInt(result[result.length - 1]); + + assert.equal(position, text.length, "Expected cumulative length of all entries to match the length of the source. expected: " + text.length + ", but got: " + position); + + return { + finalLexState, + entries + }; + } + } + + function unwrapJSONCallResult(result: string): any { + const parsedResult = JSON.parse(result); + if (parsedResult.error) { + throw new Error("Language Service Shim Error: " + JSON.stringify(parsedResult.error)); + } + else if (parsedResult.canceled) { + throw new ts.OperationCanceledException(); + } + return parsedResult.result; + } + + class LanguageServiceShimProxy implements ts.LanguageService { + constructor(private shim: ts.LanguageServiceShim) { + } + cleanupSemanticCache(): void { + this.shim.cleanupSemanticCache(); + } + getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSyntacticDiagnostics(fileName)); + } + getSemanticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSemanticDiagnostics(fileName)); + } + getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] { + return unwrapJSONCallResult(this.shim.getSuggestionDiagnostics(fileName)); + } + getCompilerOptionsDiagnostics(): ts.Diagnostic[] { + return unwrapJSONCallResult(this.shim.getCompilerOptionsDiagnostics()); + } + getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + return unwrapJSONCallResult(this.shim.getSyntacticClassifications(fileName, span.start, span.length)); + } + getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[] { + return unwrapJSONCallResult(this.shim.getSemanticClassifications(fileName, span.start, span.length)); + } + getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + return unwrapJSONCallResult(this.shim.getEncodedSyntacticClassifications(fileName, span.start, span.length)); + } + getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications { + return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length)); + } + getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { + return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); + } + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { + return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); + } + getCompletionEntrySymbol(): ts.Symbol { + throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); + } + getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo { + return unwrapJSONCallResult(this.shim.getQuickInfoAtPosition(fileName, position)); + } + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getNameOrDottedNameSpan(fileName, startPos, endPos)); + } + getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); + } + getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { + return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); + } + getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo { + return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position, options)); + } + 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)); + } + getDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { + return unwrapJSONCallResult(this.shim.getDefinitionAtPosition(fileName, position)); + } + getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan { + return unwrapJSONCallResult(this.shim.getDefinitionAndBoundSpan(fileName, position)); + } + getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] { + return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position)); + } + getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] { + return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position)); + } + getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position)); + } + findReferences(fileName: string, position: number): ts.ReferencedSymbol[] { + return unwrapJSONCallResult(this.shim.findReferences(fileName, position)); + } + getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); + } + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { + return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); + } + getNavigateToItems(searchValue: string): ts.NavigateToItem[] { + return unwrapJSONCallResult(this.shim.getNavigateToItems(searchValue)); + } + getNavigationBarItems(fileName: string): ts.NavigationBarItem[] { + return unwrapJSONCallResult(this.shim.getNavigationBarItems(fileName)); + } + getNavigationTree(fileName: string): ts.NavigationTree { + return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); + } + getOutliningSpans(fileName: string): ts.OutliningSpan[] { + return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); + } + getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] { + return unwrapJSONCallResult(this.shim.getTodoComments(fileName, JSON.stringify(descriptors))); + } + getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] { + return unwrapJSONCallResult(this.shim.getBraceMatchingAtPosition(fileName, position)); + } + getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions): number { + return unwrapJSONCallResult(this.shim.getIndentationAtPosition(fileName, position, JSON.stringify(options))); + } + getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsForRange(fileName, start, end, JSON.stringify(options))); + } + getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsForDocument(fileName, JSON.stringify(options))); + } + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options))); + } + getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion { + return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position)); + } + isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { + return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); + } + getJsxClosingTagAtPosition(): never { + throw new Error("Not supported on the shim."); + } + getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan { + return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); + } + getCodeFixesAtPosition(): never { + throw new Error("Not supported on the shim."); + } + getCombinedCodeFix = ts.notImplemented; + applyCodeActionCommand = ts.notImplemented; + getCodeFixDiagnostics(): ts.Diagnostic[] { + throw new Error("Not supported on the shim."); + } + getEditsForRefactor(): ts.RefactorEditInfo { + throw new Error("Not supported on the shim."); + } + getApplicableRefactors(): ts.ApplicableRefactorInfo[] { + throw new Error("Not supported on the shim."); + } + organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { + throw new Error("Not supported on the shim."); + } + getEditsForFileRename(): readonly ts.FileTextChanges[] { + throw new Error("Not supported on the shim."); + } + prepareCallHierarchy(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.prepareCallHierarchy(fileName, position)); + } + provideCallHierarchyIncomingCalls(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.provideCallHierarchyIncomingCalls(fileName, position)); + } + provideCallHierarchyOutgoingCalls(fileName: string, position: number) { + return unwrapJSONCallResult(this.shim.provideCallHierarchyOutgoingCalls(fileName, position)); + } + getEmitOutput(fileName: string): ts.EmitOutput { + return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); + } + getProgram(): ts.Program { + throw new Error("Program can not be marshaled across the shim layer."); + } + getNonBoundSourceFile(): ts.SourceFile { + throw new Error("SourceFile can not be marshaled across the shim layer."); + } + getSourceFile(): ts.SourceFile { + throw new Error("SourceFile can not be marshaled across the shim layer."); + } + getSourceMapper(): never { + return ts.notImplemented(); + } + clearSourceMapperCache(): never { + return ts.notImplemented(); + } + toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleLineComment(fileName, textRange)); + } + toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.toggleMultilineComment(fileName, textRange)); + } + commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.commentSelection(fileName, textRange)); + } + uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] { + return unwrapJSONCallResult(this.shim.uncommentSelection(fileName, textRange)); + } + dispose(): void { this.shim.dispose({}); } + } + + export class ShimLanguageServiceAdapter implements LanguageServiceAdapter { + private host: ShimLanguageServiceHost; + private factory: ts.TypeScriptServicesFactory; + constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + this.host = new ShimLanguageServiceHost(preprocessToResolve, cancellationToken, options); + this.factory = new ts.TypeScriptServicesFactory(); + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return new LanguageServiceShimProxy(this.factory.createLanguageServiceShim(this.host)); } + getClassifier(): ts.Classifier { return new ClassifierShimProxy(this.factory.createClassifierShim(this.host)); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { + const coreServicesShim = this.factory.createCoreServicesShim(this.host); + const shimResult: { + referencedFiles: ts.ShimsFileReference[]; + typeReferenceDirectives: ts.ShimsFileReference[]; + importedFiles: ts.ShimsFileReference[]; + isLibFile: boolean; + } = unwrapJSONCallResult(coreServicesShim.getPreProcessedFileInfo(fileName, ts.ScriptSnapshot.fromString(fileContents))); + + const convertResult: ts.PreProcessedFileInfo = { + referencedFiles: [], + importedFiles: [], + ambientExternalModules: [], + isLibFile: shimResult.isLibFile, + typeReferenceDirectives: [], + libReferenceDirectives: [] + }; + + ts.forEach(shimResult.referencedFiles, refFile => { + convertResult.referencedFiles.push({ + fileName: refFile.path, + pos: refFile.position, + end: refFile.position + refFile.length + }); + }); + + ts.forEach(shimResult.importedFiles, importedFile => { + convertResult.importedFiles.push({ + fileName: importedFile.path, + pos: importedFile.position, + end: importedFile.position + importedFile.length + }); + }); + + ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { + convertResult.importedFiles.push({ + fileName: typeRefDirective.path, + pos: typeRefDirective.position, + end: typeRefDirective.position + typeRefDirective.length + }); + }); + return convertResult; + } + } + + // Server adapter + class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { + private client!: ts.server.SessionClient; + + constructor(cancellationToken: ts.HostCancellationToken | undefined, settings: ts.CompilerOptions | undefined) { + super(cancellationToken, settings); + } + + onMessage = ts.noop; + writeMessage = ts.noop; + + setClient(client: ts.server.SessionClient) { + this.client = client; + } + + openFile(fileName: string, content?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { + super.openFile(fileName, content, scriptKindName); + this.client.openFile(fileName, content, scriptKindName); + } + + editScript(fileName: string, start: number, end: number, newText: string) { + const changeArgs = this.client.createChangeFileRequestArgs(fileName, start, end, newText); + super.editScript(fileName, start, end, newText); + this.client.changeFile(fileName, changeArgs); + } + } + + class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { + args: string[] = []; + newLine: string; + useCaseSensitiveFileNames = false; + + constructor(private host: NativeLanguageServiceHost) { + this.newLine = this.host.getNewLine(); + } + + onMessage = ts.noop; + writeMessage = ts.noop; // overridden + write(message: string): void { + this.writeMessage(message); + } + + readFile(fileName: string): string | undefined { + if (ts.stringContains(fileName, Compiler.defaultLibFileName)) { + fileName = Compiler.defaultLibFileName; + } + + const snapshot = this.host.getScriptSnapshot(fileName); + return snapshot && ts.getSnapshotText(snapshot); + } + + writeFile = ts.noop; + + resolvePath(path: string): string { + return path; + } + + fileExists(path: string): boolean { + return !!this.host.getScriptSnapshot(path); + } + + directoryExists(): boolean { + // for tests assume that directory exists + return true; + } + + getExecutingFilePath(): string { + return ""; + } + + exit = ts.noop; + + createDirectory(_directoryName: string): void { + return ts.notImplemented(); + } + + getCurrentDirectory(): string { + return this.host.getCurrentDirectory(); + } + + getDirectories(path: string): string[] { + return this.host.getDirectories(path); + } + + getEnvironmentVariable(name: string): string { + return ts.sys.getEnvironmentVariable(name); + } + + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] { + return this.host.readDirectory(path, extensions, exclude, include, depth); + } + + watchFile(): ts.FileWatcher { + return { close: ts.noop }; + } + + watchDirectory(): ts.FileWatcher { + return { close: ts.noop }; + } + + close = ts.noop; + + info(message: string): void { + this.host.log(message); + } + + msg(message: string): void { + this.host.log(message); + } + + loggingEnabled() { + return true; + } + + getLogFileName(): string | undefined { + return undefined; + } + + hasLevel() { + return false; + } + + startGroup() { throw ts.notImplemented(); } + endGroup() { throw ts.notImplemented(); } + + perftrc(message: string): void { + return this.host.log(message); + } + + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { + // eslint-disable-next-line no-restricted-globals + return setTimeout(callback, ms, args); + } + + clearTimeout(timeoutId: any): void { + // eslint-disable-next-line no-restricted-globals + clearTimeout(timeoutId); + } + + setImmediate(callback: (...args: any[]) => void, _ms: number, ...args: any[]): any { + // eslint-disable-next-line no-restricted-globals + return setImmediate(callback, args); + } + + clearImmediate(timeoutId: any): void { + // eslint-disable-next-line no-restricted-globals + clearImmediate(timeoutId); + } + + createHash(s: string) { + return mockHash(s); + } + + require(_initialDir: string, _moduleName: string): ts.RequireResult { + switch (_moduleName) { + // Adds to the Quick Info a fixed string and a string from the config file + // and replaces the first display part + case "quickinfo-augmeneter": + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = makeDefaultProxy(info); + const langSvc: any = info.languageService; + // eslint-disable-next-line only-arrow-functions + proxy.getQuickInfoAtPosition = function () { + const parts = langSvc.getQuickInfoAtPosition.apply(langSvc, arguments); + if (parts.displayParts.length > 0) { + parts.displayParts[0].text = "Proxied"; + } + parts.displayParts.push({ text: info.config.message, kind: "punctuation" }); + return parts; + }; + + return proxy; + } + }), + error: undefined + }; + + // Throws during initialization + case "create-thrower": + return { + module: () => ({ + create() { + throw new Error("I am not a well-behaved plugin"); + } + }), + error: undefined + }; + + // Adds another diagnostic + case "diagnostic-adder": + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Warning, + file: sourceFile, + code: 9999, + length: 3, + messageText: `Plugin diagnostic`, + start: 0 + }); + return prev; + }; + return proxy; + } + }), + error: undefined + }; + + // Accepts configurations + case "configurable-diagnostic-adder": + let customMessage = "default message"; + return { + module: () => ({ + create(info: ts.server.PluginCreateInfo) { + customMessage = info.config.message; + const proxy = makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: ts.SourceFile = info.project.getSourceFile(ts.toPath(filename, /*basePath*/ undefined, ts.createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; + prev.push({ + category: ts.DiagnosticCategory.Error, + file: sourceFile, + code: 9999, + length: 3, + messageText: customMessage, + start: 0 + }); + return prev; + }; + return proxy; + }, + onConfigurationChanged(config: any) { + customMessage = config.message; + } + }), + error: undefined + }; + + default: + return { + module: undefined, + error: new Error("Could not resolve module") + }; + } + } + } + + class FourslashSession extends ts.server.Session { + getText(fileName: string) { + return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!); + } + } + + export class ServerLanguageServiceAdapter implements LanguageServiceAdapter { + private host: SessionClientHost; + private client: ts.server.SessionClient; + private server: FourslashSession; + constructor(cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { + // This is the main host that tests use to direct tests + const clientHost = new SessionClientHost(cancellationToken, options); + const client = new ts.server.SessionClient(clientHost); + + // This host is just a proxy for the clientHost, it uses the client + // host to answer server queries about files on disk + const serverHost = new SessionServerHost(clientHost); + const opts: ts.server.SessionOptions = { + host: serverHost, + cancellationToken: ts.server.nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, // TODO: GH#18217 + byteLength: Utils.byteLength, + hrtime: process.hrtime, + logger: serverHost, + canUseEvents: true + }; + this.server = new FourslashSession(opts); + + + // Fake the connection between the client and the server + serverHost.writeMessage = client.onMessage.bind(client); + clientHost.writeMessage = this.server.onMessage.bind(this.server); + + // Wire the client to the host to get notifications when a file is open + // or edited. + clientHost.setClient(client); + + // Set the properties + this.client = client; + this.host = clientHost; + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return this.client; } + getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } + getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } + assertTextConsistent(fileName: string) { + const serverText = this.server.getText(fileName); + const clientText = this.host.readFile(fileName); + ts.Debug.assert(serverText === clientText, [ + "Server and client text are inconsistent.", + "", + "\x1b[1mServer\x1b[0m\x1b[31m:", + serverText, + "", + "\x1b[1mClient\x1b[0m\x1b[31m:", + clientText, + "", + "This probably means something is wrong with the fourslash infrastructure, not with the test." + ].join(ts.sys.newLine)); + } + } +} From 40751ba89b343acab7731689d44b9f67b43bb7bf Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Fri, 22 May 2020 21:50:34 -0700 Subject: [PATCH 13/20] Removed public commands --- src/server/protocol.ts | 19 ----- src/server/session.ts | 74 ++++--------------- src/testRunner/unittests/tsserver/session.ts | 8 +- .../reference/api/tsserverlibrary.d.ts | 17 ----- 4 files changed, 19 insertions(+), 99 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 597c69d474f6b..cc027f8e9500f 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -136,16 +136,12 @@ namespace ts.server.protocol { SelectionRange = "selectionRange", /* @internal */ SelectionRangeFull = "selectionRange-full", - ToggleLineComment = "toggleLineComment", /* @internal */ ToggleLineCommentFull = "toggleLineComment-full", - ToggleMultilineComment = "toggleMultilineComment", /* @internal */ ToggleMultilineCommentFull = "toggleMultilineComment-full", - CommentSelection = "commentSelection", /* @internal */ CommentSelectionFull = "commentSelection-full", - UncommentSelection = "uncommentSelection", /* @internal */ UncommentSelectionFull = "uncommentSelection-full", PrepareCallHierarchy = "prepareCallHierarchy", @@ -1544,23 +1540,8 @@ namespace ts.server.protocol { parent?: SelectionRange; } - export interface ToggleLineCommentRequest extends FileRequest { - command: CommandTypes.ToggleLineComment; - arguments: FileRangeRequestArgs; - } - - export interface ToggleMultilineCommentRequest extends FileRequest { - command: CommandTypes.ToggleMultilineComment; - arguments: FileRangeRequestArgs; - } - export interface CommentSelectionRequest extends FileRequest { - command: CommandTypes.CommentSelection; - arguments: FileRangeRequestArgs; - } - export interface UncommentSelectionRequest extends FileRequest { - command: CommandTypes.UncommentSelection; arguments: FileRangeRequestArgs; } diff --git a/src/server/session.ts b/src/server/session.ts index 96a4b67690747..e631578fb51fc 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2201,68 +2201,36 @@ namespace ts.server { }); } - private toggleLineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + private toggleLineComment(args: protocol.FileRangeRequestArgs): TextChange[] { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - const textChanges = project.getLanguageService().toggleLineComment(file, textRange); - - if (simplifiedResult) { - const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; - - return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); - } - - return textChanges; + return project.getLanguageService().toggleLineComment(file, textRange); } - private toggleMultilineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + private toggleMultilineComment(args: protocol.FileRangeRequestArgs): TextChange[] { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - const textChanges = project.getLanguageService().toggleMultilineComment(file, textRange); - - if (simplifiedResult) { - const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; - - return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); - } - - return textChanges; + return project.getLanguageService().toggleMultilineComment(file, textRange); } - private commentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + private commentSelection(args: protocol.FileRangeRequestArgs): TextChange[] { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - const textChanges = project.getLanguageService().commentSelection(file, textRange); - - if (simplifiedResult) { - const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; - - return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); - } - - return textChanges; + return project.getLanguageService().commentSelection(file, textRange); } - private uncommentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + private uncommentSelection(args: protocol.FileRangeRequestArgs): TextChange[] { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - const textChanges = project.getLanguageService().uncommentSelection(file, textRange); - - if (simplifiedResult) { - const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; - - return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); - } - - return textChanges; + return project.getLanguageService().uncommentSelection(file, textRange); } private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { @@ -2710,29 +2678,17 @@ namespace ts.server { [CommandNames.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => { return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments)); }, - [CommandNames.ToggleLineComment]: (request: protocol.ToggleLineCommentRequest) => { - return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ true)); - }, - [CommandNames.ToggleLineCommentFull]: (request: protocol.ToggleLineCommentRequest) => { - return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ false)); + [CommandNames.ToggleLineCommentFull]: (request: protocol.CommentSelectionRequest) => { + return this.requiredResponse(this.toggleLineComment(request.arguments)); }, - [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { - return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ true)); - }, - [CommandNames.ToggleMultilineCommentFull]: (request: protocol.ToggleMultilineCommentRequest) => { - return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ false)); - }, - [CommandNames.CommentSelection]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ true)); + [CommandNames.ToggleMultilineCommentFull]: (request: protocol.CommentSelectionRequest) => { + return this.requiredResponse(this.toggleMultilineComment(request.arguments)); }, [CommandNames.CommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ false)); - }, - [CommandNames.UncommentSelection]: (request: protocol.UncommentSelectionRequest) => { - return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ true)); + return this.requiredResponse(this.commentSelection(request.arguments)); }, - [CommandNames.UncommentSelectionFull]: (request: protocol.UncommentSelectionRequest) => { - return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ false)); + [CommandNames.UncommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { + return this.requiredResponse(this.uncommentSelection(request.arguments)); }, }); diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 5ca88f4adb961..8203f5187aebb 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -272,10 +272,10 @@ namespace ts.server { CommandNames.PrepareCallHierarchy, CommandNames.ProvideCallHierarchyIncomingCalls, CommandNames.ProvideCallHierarchyOutgoingCalls, - CommandNames.ToggleLineComment, - CommandNames.ToggleMultilineComment, - CommandNames.CommentSelection, - CommandNames.UncommentSelection, + CommandNames.ToggleLineCommentFull, + CommandNames.ToggleMultilineCommentFull, + CommandNames.CommentSelectionFull, + CommandNames.UncommentSelectionFull, ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8971b73149e1f..d8ee834cfa7f0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6304,10 +6304,6 @@ declare namespace ts.server.protocol { GetEditsForFileRename = "getEditsForFileRename", ConfigurePlugin = "configurePlugin", SelectionRange = "selectionRange", - ToggleLineComment = "toggleLineComment", - ToggleMultilineComment = "toggleMultilineComment", - CommentSelection = "commentSelection", - UncommentSelection = "uncommentSelection", PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls" @@ -7332,20 +7328,7 @@ declare namespace ts.server.protocol { textSpan: TextSpan; parent?: SelectionRange; } - interface ToggleLineCommentRequest extends FileRequest { - command: CommandTypes.ToggleLineComment; - arguments: FileRangeRequestArgs; - } - interface ToggleMultilineCommentRequest extends FileRequest { - command: CommandTypes.ToggleMultilineComment; - arguments: FileRangeRequestArgs; - } interface CommentSelectionRequest extends FileRequest { - command: CommandTypes.CommentSelection; - arguments: FileRangeRequestArgs; - } - interface UncommentSelectionRequest extends FileRequest { - command: CommandTypes.UncommentSelection; arguments: FileRangeRequestArgs; } /** From 9f03d7bad7e10a425edcc232a1e9be562f23916a Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Tue, 26 May 2020 17:34:38 -0700 Subject: [PATCH 14/20] Use getFileAndLanguageServiceForSyntacticOperation --- src/server/session.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index e631578fb51fc..900564f934db3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2202,35 +2202,35 @@ namespace ts.server { } private toggleLineComment(args: protocol.FileRangeRequestArgs): TextChange[] { - const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfo(file)!; const textRange = this.getRange(args, scriptInfo); - return project.getLanguageService().toggleLineComment(file, textRange); + return languageService.toggleLineComment(file, textRange); } private toggleMultilineComment(args: protocol.FileRangeRequestArgs): TextChange[] { - const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - return project.getLanguageService().toggleMultilineComment(file, textRange); + return languageService.toggleMultilineComment(file, textRange); } private commentSelection(args: protocol.FileRangeRequestArgs): TextChange[] { - const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - return project.getLanguageService().commentSelection(file, textRange); + return languageService.commentSelection(file, textRange); } private uncommentSelection(args: protocol.FileRangeRequestArgs): TextChange[] { - const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - return project.getLanguageService().uncommentSelection(file, textRange); + return languageService.uncommentSelection(file, textRange); } private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { From 0985afd51a90b29b5e929833f91d2544c10054a7 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Tue, 26 May 2020 19:26:28 -0700 Subject: [PATCH 15/20] Fixed uncomment bug --- src/services/services.ts | 12 ++++++++++-- tests/cases/fourslash/toggleLineComment10.ts | 2 +- tests/cases/fourslash/toggleLineComment4.ts | 2 +- tests/cases/fourslash/toggleLineComment9.ts | 2 +- tests/cases/fourslash/uncommentSelection1.ts | 10 +++++++++- tests/cases/fourslash/uncommentSelection2.ts | 10 +++++++++- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/services/services.ts b/src/services/services.ts index b6003b4826e0a..cf764be8fb25e 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2054,7 +2054,7 @@ namespace ts { let isCommenting = insertComment || false; const positions = [] as number[] as SortedArray; - let pos = textRange.pos; + let { pos } = textRange; const isJsx = isInsideJsx !== undefined ? isInsideJsx : isInsideJsxElement(sourceFile, pos); const openMultiline = isJsx ? "{/*" : "/*"; @@ -2170,8 +2170,16 @@ namespace ts { function uncommentSelection(fileName: string, textRange: TextRange): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const textChanges: TextChange[] = []; + const { pos } = textRange; + let { end } = textRange; - for (let i = textRange.pos; i <= textRange.end; i++) { + // If cursor is not a selection we need to increase the end position + // to include the start of the comment. + if (pos === end) { + end += isInsideJsxElement(sourceFile, pos) ? 2 : 1; + } + + for (let i = pos; i <= end; i++) { const commentRange = isInComment(sourceFile, i); if (commentRange) { switch (commentRange.kind) { diff --git a/tests/cases/fourslash/toggleLineComment10.ts b/tests/cases/fourslash/toggleLineComment10.ts index 5acf9c4a27e9a..0f6c3d271988b 100644 --- a/tests/cases/fourslash/toggleLineComment10.ts +++ b/tests/cases/fourslash/toggleLineComment10.ts @@ -1,4 +1,4 @@ -// Close and open multiline comments if the line already contains more. +// Close and open multiline comments if the line already contains more comments. //@Filename: file.tsx //// const a =
    diff --git a/tests/cases/fourslash/toggleLineComment4.ts b/tests/cases/fourslash/toggleLineComment4.ts index 1d162ca7bedae..5e4beb6070a7e 100644 --- a/tests/cases/fourslash/toggleLineComment4.ts +++ b/tests/cases/fourslash/toggleLineComment4.ts @@ -1,4 +1,4 @@ -// If at least one line is uncomment then comment all lines again. +// If at least one line is not commented then comment all lines again. //// //const a[| = 1; //// const b = 2 diff --git a/tests/cases/fourslash/toggleLineComment9.ts b/tests/cases/fourslash/toggleLineComment9.ts index 562e77bf4db7a..1fbaea7ea383a 100644 --- a/tests/cases/fourslash/toggleLineComment9.ts +++ b/tests/cases/fourslash/toggleLineComment9.ts @@ -1,4 +1,4 @@ -// If at least one line is uncomment then comment all lines again. +// If at least one line is not commented then comment all lines again. // TODO: Not sure about this one. The default behavior for line comment is to add en extra // layer of comments (see toggleLineComment4 test). For jsx this doesn't work right as it's actually // multiline comment. Figure out what to do. diff --git a/tests/cases/fourslash/uncommentSelection1.ts b/tests/cases/fourslash/uncommentSelection1.ts index 42c567d3ca7e9..77066e49152a0 100644 --- a/tests/cases/fourslash/uncommentSelection1.ts +++ b/tests/cases/fourslash/uncommentSelection1.ts @@ -13,6 +13,10 @@ //// let var8/* = 1; //// let var9 [||]= 2; //// let var10 */= 3; +//// +//// let var11[||]/* = 1; +//// let var12 = 2; +//// let var13 */= 3; verify.uncommentSelection( `let var1 = 1; @@ -27,4 +31,8 @@ let var7 = 7; let var8 = 1; let var9 = 2; -let var10 = 3;`); \ No newline at end of file +let var10 = 3; + +let var11 = 1; +let var12 = 2; +let var13 = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/uncommentSelection2.ts b/tests/cases/fourslash/uncommentSelection2.ts index 55a84555cae3e..745000d9c4d28 100644 --- a/tests/cases/fourslash/uncommentSelection2.ts +++ b/tests/cases/fourslash/uncommentSelection2.ts @@ -11,6 +11,10 @@ //// SomeText //// {/*
    |]*/} ////
    ; +//// +//// const c = +//// [||]{/**/} +//// ; verify.uncommentSelection( @@ -23,4 +27,8 @@ const b =
    SomeText
    -
    ;`); \ No newline at end of file +
    ; + +const c = + +;`); \ No newline at end of file From 55a1b50e7a515c248e67bf08ad473ba31ac5c581 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 9 Jul 2020 15:55:51 -0700 Subject: [PATCH 16/20] Revert "Removed public commands" This reverts commit 40751ba89b343acab7731689d44b9f67b43bb7bf. --- src/server/protocol.ts | 19 +++++ src/server/session.ts | 74 +++++++++++++++---- src/testRunner/unittests/tsserver/session.ts | 8 +- .../reference/api/tsserverlibrary.d.ts | 17 +++++ 4 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index cc027f8e9500f..597c69d474f6b 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -136,12 +136,16 @@ namespace ts.server.protocol { SelectionRange = "selectionRange", /* @internal */ SelectionRangeFull = "selectionRange-full", + ToggleLineComment = "toggleLineComment", /* @internal */ ToggleLineCommentFull = "toggleLineComment-full", + ToggleMultilineComment = "toggleMultilineComment", /* @internal */ ToggleMultilineCommentFull = "toggleMultilineComment-full", + CommentSelection = "commentSelection", /* @internal */ CommentSelectionFull = "commentSelection-full", + UncommentSelection = "uncommentSelection", /* @internal */ UncommentSelectionFull = "uncommentSelection-full", PrepareCallHierarchy = "prepareCallHierarchy", @@ -1540,8 +1544,23 @@ namespace ts.server.protocol { parent?: SelectionRange; } + export interface ToggleLineCommentRequest extends FileRequest { + command: CommandTypes.ToggleLineComment; + arguments: FileRangeRequestArgs; + } + + export interface ToggleMultilineCommentRequest extends FileRequest { + command: CommandTypes.ToggleMultilineComment; + arguments: FileRangeRequestArgs; + } + export interface CommentSelectionRequest extends FileRequest { + command: CommandTypes.CommentSelection; + arguments: FileRangeRequestArgs; + } + export interface UncommentSelectionRequest extends FileRequest { + command: CommandTypes.UncommentSelection; arguments: FileRangeRequestArgs; } diff --git a/src/server/session.ts b/src/server/session.ts index 900564f934db3..c6cc9084e89d9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2201,36 +2201,68 @@ namespace ts.server { }); } - private toggleLineComment(args: protocol.FileRangeRequestArgs): TextChange[] { + private toggleLineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = this.projectService.getScriptInfo(file)!; const textRange = this.getRange(args, scriptInfo); - return languageService.toggleLineComment(file, textRange); + const textChanges = languageService.toggleLineComment(file, textRange); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; } - private toggleMultilineComment(args: protocol.FileRangeRequestArgs): TextChange[] { + private toggleMultilineComment(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - return languageService.toggleMultilineComment(file, textRange); + const textChanges = languageService.toggleMultilineComment(file, textRange); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; } - private commentSelection(args: protocol.FileRangeRequestArgs): TextChange[] { + private commentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - return languageService.commentSelection(file, textRange); + const textChanges = languageService.commentSelection(file, textRange); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; } - private uncommentSelection(args: protocol.FileRangeRequestArgs): TextChange[] { + private uncommentSelection(args: protocol.FileRangeRequestArgs, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const textRange = this.getRange(args, scriptInfo); - return languageService.uncommentSelection(file, textRange); + const textChanges = languageService.uncommentSelection(file, textRange); + + if (simplifiedResult) { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + + return textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)); + } + + return textChanges; } private mapSelectionRange(selectionRange: SelectionRange, scriptInfo: ScriptInfo): protocol.SelectionRange { @@ -2678,17 +2710,29 @@ namespace ts.server { [CommandNames.ProvideCallHierarchyOutgoingCalls]: (request: protocol.ProvideCallHierarchyOutgoingCallsRequest) => { return this.requiredResponse(this.provideCallHierarchyOutgoingCalls(request.arguments)); }, - [CommandNames.ToggleLineCommentFull]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.toggleLineComment(request.arguments)); + [CommandNames.ToggleLineComment]: (request: protocol.ToggleLineCommentRequest) => { + return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.ToggleLineCommentFull]: (request: protocol.ToggleLineCommentRequest) => { + return this.requiredResponse(this.toggleLineComment(request.arguments, /*simplifiedResult*/ false)); }, - [CommandNames.ToggleMultilineCommentFull]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.toggleMultilineComment(request.arguments)); + [CommandNames.ToggleMultilineComment]: (request: protocol.ToggleMultilineCommentRequest) => { + return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.ToggleMultilineCommentFull]: (request: protocol.ToggleMultilineCommentRequest) => { + return this.requiredResponse(this.toggleMultilineComment(request.arguments, /*simplifiedResult*/ false)); + }, + [CommandNames.CommentSelection]: (request: protocol.CommentSelectionRequest) => { + return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ true)); }, [CommandNames.CommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.commentSelection(request.arguments)); + return this.requiredResponse(this.commentSelection(request.arguments, /*simplifiedResult*/ false)); + }, + [CommandNames.UncommentSelection]: (request: protocol.UncommentSelectionRequest) => { + return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ true)); }, - [CommandNames.UncommentSelectionFull]: (request: protocol.CommentSelectionRequest) => { - return this.requiredResponse(this.uncommentSelection(request.arguments)); + [CommandNames.UncommentSelectionFull]: (request: protocol.UncommentSelectionRequest) => { + return this.requiredResponse(this.uncommentSelection(request.arguments, /*simplifiedResult*/ false)); }, }); diff --git a/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 8203f5187aebb..5ca88f4adb961 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -272,10 +272,10 @@ namespace ts.server { CommandNames.PrepareCallHierarchy, CommandNames.ProvideCallHierarchyIncomingCalls, CommandNames.ProvideCallHierarchyOutgoingCalls, - CommandNames.ToggleLineCommentFull, - CommandNames.ToggleMultilineCommentFull, - CommandNames.CommentSelectionFull, - CommandNames.UncommentSelectionFull, + CommandNames.ToggleLineComment, + CommandNames.ToggleMultilineComment, + CommandNames.CommentSelection, + CommandNames.UncommentSelection, ]; it("should not throw when commands are executed with invalid arguments", () => { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index d8ee834cfa7f0..8971b73149e1f 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6304,6 +6304,10 @@ declare namespace ts.server.protocol { GetEditsForFileRename = "getEditsForFileRename", ConfigurePlugin = "configurePlugin", SelectionRange = "selectionRange", + ToggleLineComment = "toggleLineComment", + ToggleMultilineComment = "toggleMultilineComment", + CommentSelection = "commentSelection", + UncommentSelection = "uncommentSelection", PrepareCallHierarchy = "prepareCallHierarchy", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls" @@ -7328,7 +7332,20 @@ declare namespace ts.server.protocol { textSpan: TextSpan; parent?: SelectionRange; } + interface ToggleLineCommentRequest extends FileRequest { + command: CommandTypes.ToggleLineComment; + arguments: FileRangeRequestArgs; + } + interface ToggleMultilineCommentRequest extends FileRequest { + command: CommandTypes.ToggleMultilineComment; + arguments: FileRangeRequestArgs; + } interface CommentSelectionRequest extends FileRequest { + command: CommandTypes.CommentSelection; + arguments: FileRangeRequestArgs; + } + interface UncommentSelectionRequest extends FileRequest { + command: CommandTypes.UncommentSelection; arguments: FileRangeRequestArgs; } /** From b81f240e96c0cdb3dca5c865e1a116c5573614f7 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 9 Jul 2020 18:35:54 -0700 Subject: [PATCH 17/20] PR comments --- src/services/services.ts | 14 ++++++++++---- src/services/types.ts | 8 ++++---- tests/cases/fourslash/commentSelection1.ts | 20 ++++++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/services/services.ts b/src/services/services.ts index cf764be8fb25e..7c3670fbae816 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1993,7 +1993,7 @@ namespace ts { let isCommenting = insertComment || false; let leftMostPosition = Number.MAX_VALUE; const lineTextStarts = new Map(); - const whiteSpaceRegex = new RegExp(/\S/); + const firstNonWhitespaceCharacterRegex = new RegExp(/\S/); const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]); const openComment = isJsx ? "{/*" : "//"; @@ -2002,13 +2002,13 @@ namespace ts { const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i])); // Find the start of text and the left-most character. No-op on empty lines. - const regExec = whiteSpaceRegex.exec(lineText); + const regExec = firstNonWhitespaceCharacterRegex.exec(lineText); if (regExec) { leftMostPosition = Math.min(leftMostPosition, regExec.index); lineTextStarts.set(i.toString(), regExec.index); if (lineText.substr(regExec.index, openComment.length) !== openComment) { - isCommenting = insertComment !== undefined ? insertComment : true; + isCommenting = insertComment === undefined || insertComment; } } } @@ -2164,7 +2164,13 @@ namespace ts { } function commentSelection(fileName: string, textRange: TextRange): TextChange[] { - return toggleLineComment(fileName, textRange, /*insertComment*/ true); + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const { firstLine, lastLine } = getLinesForRange(sourceFile, textRange); + + // If there is a selection that is on the same line, add multiline. + return firstLine === lastLine && textRange.pos !== textRange.end + ? toggleMultilineComment(fileName, textRange, /*insertComment*/ true) + : toggleLineComment(fileName, textRange, /*insertComment*/ true); } function uncommentSelection(fileName: string, textRange: TextRange): TextChange[] { diff --git a/src/services/types.ts b/src/services/types.ts index 0684ffaf88f74..786688348a0ed 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -486,10 +486,10 @@ namespace ts { /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; - toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; - toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; - commentSelection(fileName: string, textRanges: TextRange): TextChange[]; - uncommentSelection(fileName: string, textRanges: TextRange): TextChange[]; + toggleLineComment(fileName: string, textRange: TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRange: TextRange): TextChange[]; + commentSelection(fileName: string, textRange: TextRange): TextChange[]; + uncommentSelection(fileName: string, textRange: TextRange): TextChange[]; dispose(): void; } diff --git a/tests/cases/fourslash/commentSelection1.ts b/tests/cases/fourslash/commentSelection1.ts index 523f1f3a4f2d4..d5a7846395594 100644 --- a/tests/cases/fourslash/commentSelection1.ts +++ b/tests/cases/fourslash/commentSelection1.ts @@ -4,15 +4,23 @@ //// let var2 = 2; //// let var3 |]= 3; //// -//// //let var4[| = 4; -//// //let var5 = 5; -//// //let var6 |]= 6; +//// let var4[| = 4;|] +//// +//// let [||]var5 = 5; +//// +//// //let var6[| = 6; +//// //let var7 = 7; +//// //let var8 |]= 8; verify.commentSelection( `//let var1 = 1; //let var2 = 2; //let var3 = 3; -////let var4 = 4; -////let var5 = 5; -////let var6 = 6;`); \ No newline at end of file +let var4/* = 4;*/ + +//let var5 = 5; + +////let var6 = 6; +////let var7 = 7; +////let var8 = 8;`); \ No newline at end of file From 6fd91ee59ff38d397810907dbc7a4809cbe99572 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 9 Jul 2020 21:34:34 -0700 Subject: [PATCH 18/20] Fixed baseline --- tests/baselines/reference/api/tsserverlibrary.d.ts | 8 ++++---- tests/baselines/reference/api/typescript.d.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8971b73149e1f..7c8c0ee02a8e9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5315,10 +5315,10 @@ declare namespace ts { getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; getProgram(): Program | undefined; - toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; - toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; - commentSelection(fileName: string, textRanges: TextRange): TextChange[]; - uncommentSelection(fileName: string, textRanges: TextRange): TextChange[]; + toggleLineComment(fileName: string, textRange: TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRange: TextRange): TextChange[]; + commentSelection(fileName: string, textRange: TextRange): TextChange[]; + uncommentSelection(fileName: string, textRange: TextRange): TextChange[]; dispose(): void; } interface JsxClosingTagInfo { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 2a2402984380d..ef74029191ff8 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5315,10 +5315,10 @@ declare namespace ts { getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; getProgram(): Program | undefined; - toggleLineComment(fileName: string, textRanges: TextRange): TextChange[]; - toggleMultilineComment(fileName: string, textRanges: TextRange): TextChange[]; - commentSelection(fileName: string, textRanges: TextRange): TextChange[]; - uncommentSelection(fileName: string, textRanges: TextRange): TextChange[]; + toggleLineComment(fileName: string, textRange: TextRange): TextChange[]; + toggleMultilineComment(fileName: string, textRange: TextRange): TextChange[]; + commentSelection(fileName: string, textRange: TextRange): TextChange[]; + uncommentSelection(fileName: string, textRange: TextRange): TextChange[]; dispose(): void; } interface JsxClosingTagInfo { From a534f2aa977e0bd160da6898b79633f7ee83e147 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Thu, 9 Jul 2020 22:15:02 -0700 Subject: [PATCH 19/20] Fixed syntax error --- src/services/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/services.ts b/src/services/services.ts index e311f351dbc85..36638f78671ea 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2009,7 +2009,7 @@ namespace ts { let isCommenting = insertComment || false; let leftMostPosition = Number.MAX_VALUE; - const lineTextStarts = new Map(); + const lineTextStarts = new Map(); const firstNonWhitespaceCharacterRegex = new RegExp(/\S/); const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]); const openComment = isJsx ? "{/*" : "//"; From 0d38f09e3618aad701ef620c7650d5ca7329e0e8 Mon Sep 17 00:00:00 2001 From: Armando Aguirre Date: Fri, 10 Jul 2020 17:44:02 -0700 Subject: [PATCH 20/20] PR comments and minor bugs --- src/services/services.ts | 6 +++- src/services/utilities.ts | 12 ++++---- .../fourslash/toggleMultilineComment1.ts | 6 +++- .../fourslash/toggleMultilineComment9.ts | 30 +++++++++++++++++++ tests/cases/fourslash/uncommentSelection1.ts | 6 +++- 5 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/cases/fourslash/toggleMultilineComment9.ts diff --git a/src/services/services.ts b/src/services/services.ts index 36638f78671ea..983439f27c78d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2068,6 +2068,7 @@ namespace ts { const textChanges: TextChange[] = []; const { text } = sourceFile; + let hasComment = false; let isCommenting = insertComment || false; const positions = [] as number[] as SortedArray; @@ -2098,6 +2099,7 @@ namespace ts { positions.push(commentRange.end); } + hasComment = true; pos = commentRange.end + 1; } else { // If it's not in a comment range, then we need to comment the uncommented portions. @@ -2110,7 +2112,9 @@ namespace ts { } } - if (isCommenting) { + // If it didn't found a comment and isCommenting is false means is only empty space. + // We want to insert comment in this scenario. + if (isCommenting || !hasComment) { if (isInComment(sourceFile, textRange.pos)?.kind !== SyntaxKind.SingleLineCommentTrivia) { insertSorted(positions, textRange.pos, compareValues); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 18bdf9557fdc5..29c091024241e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1321,7 +1321,7 @@ namespace ts { } export function isInsideJsxElement(sourceFile: SourceFile, position: number): boolean { - function isInsideJsxElementRecursion(node: Node): boolean { + function isInsideJsxElementTraversal(node: Node): boolean { while (node) { if (node.kind >= SyntaxKind.JsxSelfClosingElement && node.kind <= SyntaxKind.JsxExpression || node.kind === SyntaxKind.JsxText @@ -1334,7 +1334,9 @@ namespace ts { node = node.parent; } else if (node.kind === SyntaxKind.JsxElement) { - return position > node.getStart(sourceFile) || isInsideJsxElementRecursion(node.parent); + if (position > node.getStart(sourceFile)) return true; + + node = node.parent; } else { return false; @@ -1344,7 +1346,7 @@ namespace ts { return false; } - return isInsideJsxElementRecursion(getTokenAtPosition(sourceFile, position)); + return isInsideJsxElementTraversal(getTokenAtPosition(sourceFile, position)); } export function findPrecedingMatchingToken(token: Node, matchingTokenKind: SyntaxKind, sourceFile: SourceFile) { @@ -2279,8 +2281,8 @@ namespace ts { // This only happens for leaf nodes - internal nodes always see their children change. const clone = isStringLiteral(node) ? setOriginalNode(factory.createStringLiteralFromNode(node), node) as Node as T : - isNumericLiteral(node) ? setOriginalNode(factory.createNumericLiteral(node.text, node.numericLiteralFlags), node) as Node as T : - factory.cloneNode(node); + isNumericLiteral(node) ? setOriginalNode(factory.createNumericLiteral(node.text, node.numericLiteralFlags), node) as Node as T : + factory.cloneNode(node); return setTextRange(clone, node); } diff --git a/tests/cases/fourslash/toggleMultilineComment1.ts b/tests/cases/fourslash/toggleMultilineComment1.ts index d32e7b28ecf40..f76e9d564734f 100644 --- a/tests/cases/fourslash/toggleMultilineComment1.ts +++ b/tests/cases/fourslash/toggleMultilineComment1.ts @@ -11,6 +11,8 @@ //// [|/*let var7 = 1; //// let var8 = 2; //// let var9 = 3;*/|] +//// +//// let var10[||] = 10; verify.toggleMultilineComment( `let var1/* = 1; @@ -23,4 +25,6 @@ let var6 = 3; let var7 = 1; let var8 = 2; -let var9 = 3;`); \ No newline at end of file +let var9 = 3; + +let var10/**/ = 10;`); \ No newline at end of file diff --git a/tests/cases/fourslash/toggleMultilineComment9.ts b/tests/cases/fourslash/toggleMultilineComment9.ts new file mode 100644 index 0000000000000..f9761a98773d5 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment9.ts @@ -0,0 +1,30 @@ +// When there's is only whitespace, insert comment. If there is whitespace but theres a comment in bewteen, then uncomment. + +//// /*let var1[| = 1;*/ +//// |] +//// +//// [| +//// /*let var2 = 2;*/|] +//// +//// [| +//// +//// |] +//// +//// [||] +//// +//// let var3[||] = 3; + +verify.toggleMultilineComment( + `let var1 = 1; + + + +let var2 = 2; + +/* + +*/ + + /**/ + +let var3/**/ = 3;`); \ No newline at end of file diff --git a/tests/cases/fourslash/uncommentSelection1.ts b/tests/cases/fourslash/uncommentSelection1.ts index 77066e49152a0..2245cbc2d007a 100644 --- a/tests/cases/fourslash/uncommentSelection1.ts +++ b/tests/cases/fourslash/uncommentSelection1.ts @@ -17,6 +17,8 @@ //// let var11[||]/* = 1; //// let var12 = 2; //// let var13 */= 3; +//// +//// ////let var14 [||]= 14; verify.uncommentSelection( `let var1 = 1; @@ -35,4 +37,6 @@ let var10 = 3; let var11 = 1; let var12 = 2; -let var13 = 3;`); \ No newline at end of file +let var13 = 3; + +//let var14 = 14;`); \ No newline at end of file