diff --git a/src/harness/client.ts b/src/harness/client.ts index c040ee60ae58c..32db9e92a7a17 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -825,6 +825,22 @@ namespace ts.server { 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/fourslashImpl.ts b/src/harness/fourslashImpl.ts index ca95ff34ebb72..283a1d3c93d93 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3189,7 +3189,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(", ")}` : "" ); @@ -3428,13 +3428,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`; @@ -3448,7 +3448,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") { @@ -3477,8 +3477,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}╰ ╰ `); } } } @@ -3499,7 +3499,7 @@ namespace FourSlash { text += `${prefix}│ ├ fromSpans:\n`; text += this.formatCallHierarchyItemSpans(file, outgoingCall.fromSpans, `${prefix}│ │ `, i < outgoingCalls.values.length - 1 ? `${prefix}│ ╰ ` : - `${trailingPrefix}╰ ╰ `); + `${trailingPrefix}╰ ╰ `); } } } @@ -3659,6 +3659,50 @@ namespace FourSlash { public configurePlugin(pluginName: string, configuration: any): void { (this.languageService).configurePlugin(pluginName, configuration); } + + public toggleLineComment(newFileContent: string): void { + const changes: ts.TextChange[] = []; + for (const 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 changes: ts.TextChange[] = []; + for (const range of this.getRanges()) { + changes.push.apply(changes, this.languageService.toggleMultilineComment(this.activeFile.fileName, range)); + } + + this.applyEdits(this.activeFile.fileName, changes); + + this.verifyCurrentFileContent(newFileContent); + } + + public commentSelection(newFileContent: string): void { + const changes: ts.TextChange[] = []; + for (const 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 { + const changes: ts.TextChange[] = []; + for (const 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 dcbd07b4dbf09..c23a4ac801f95 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -214,6 +214,22 @@ namespace FourSlashInterface { public refactorAvailableForTriggerReason(triggerReason: ts.RefactorTriggerReason, name: string, actionName?: string) { this.state.verifyRefactorAvailable(this.negative, triggerReason, name, actionName); } + + public toggleLineComment(newFileContent: string) { + this.state.toggleLineComment(newFileContent); + } + + 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 bc3c738a6b8c6..6c3ee40eee068 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -603,6 +603,18 @@ namespace Harness.LanguageService { 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({}); } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index de974058fa167..cdeaad1a4fc22 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -136,7 +136,18 @@ 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", ProvideCallHierarchyIncomingCalls = "provideCallHierarchyIncomingCalls", ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls", @@ -1542,6 +1553,26 @@ 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; + } + /** * Information found in an "open" request. */ diff --git a/src/server/session.ts b/src/server/session.ts index d8c35c7924c2a..c559fb8faa989 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2012,8 +2012,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); @@ -2022,6 +2021,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)!; @@ -2251,6 +2256,70 @@ namespace ts.server { }); } + 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); + + 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, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); + + 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, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); + + 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, simplifiedResult: boolean): TextChange[] | protocol.CodeEdit[] { + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; + const textRange = this.getRange(args, scriptInfo); + + 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 { const result: protocol.SelectionRange = { textSpan: toProtocolTextSpan(selectionRange.textSpan, scriptInfo), @@ -2697,6 +2766,30 @@ 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.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 eb1e9659648de..fa0f02a9b95df 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; @@ -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); @@ -1994,6 +1994,236 @@ 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, textRange: TextRange, insertComment?: boolean): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: TextChange[] = []; + const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange); + + let isCommenting = insertComment || false; + let leftMostPosition = Number.MAX_VALUE; + const lineTextStarts = new Map(); + const firstNonWhitespaceCharacterRegex = 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 = 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; + } + } + } + + // 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 if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) { + textChanges.push({ + newText: "", + span: { + length: openComment.length, + start: lineStarts[i] + lineTextStart + } + }); + } + } + } + + return textChanges; + } + + function toggleMultilineComment(fileName: string, textRange: TextRange, insertComment?: boolean, isInsideJsx?: boolean): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: TextChange[] = []; + const { text } = sourceFile; + + let hasComment = false; + let isCommenting = insertComment || false; + const positions = [] as number[] as SortedArray; + + let { pos } = textRange; + 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); + } + + hasComment = true; + pos = commentRange.end + 1; + } + 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 + : 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; + } + } + + // 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); + } + 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. + if (textChanges.length % 2 !== 0) { + textChanges.push({ + newText: closeMultiline, + span: { + length: 0, + start: positions[positions.length - 1] + } + }); + } + } + else { + // If is not commenting then remove all comments found. + 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: pos - offset + } + }); + } + } + + return textChanges; + } + + function commentSelection(fileName: string, textRange: TextRange): TextChange[] { + 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[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const textChanges: TextChange[] = []; + const { pos } = textRange; + let { end } = textRange; + + // 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) { + case SyntaxKind.SingleLineCommentTrivia: + 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 }, /*insertComment*/ 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); @@ -2274,7 +2504,11 @@ namespace ts { clearSourceMapperCache: () => sourceMapper.clearCache(), prepareCallHierarchy, provideCallHierarchyIncomingCalls, - provideCallHierarchyOutgoingCalls + provideCallHierarchyOutgoingCalls, + toggleLineComment, + toggleMultilineComment, + commentSelection, + uncommentSelection, }; if (syntaxOnly) { @@ -2348,7 +2582,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 c8149620b8c6e..8729ce6cc0bb6 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -277,6 +277,11 @@ namespace ts { getEmitOutput(fileName: string): string; getEmitOutputObject(fileName: string): EmitOutput; + + 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 { @@ -1068,6 +1073,34 @@ namespace ts { () => this.languageService.getEmitOutput(fileName), this.logPerformance) as EmitOutput; } + + 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: TextRange): string { + return this.forwardJSONCall( + `toggleMultilineComment('${fileName}', '${JSON.stringify(textRange)}')`, + () => this.languageService.toggleMultilineComment(fileName, textRange) + ); + } + + 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: 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 5ccf5f49d4a89..73080249abdbb 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -506,6 +506,11 @@ namespace ts { /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; /* @internal */ getAutoImportProvider(): Program | undefined; + 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/src/services/utilities.ts b/src/services/utilities.ts index d89a68c96edbd..4c841278f42d4 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: @@ -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; @@ -1320,6 +1320,35 @@ namespace ts { return false; } + export function isInsideJsxElement(sourceFile: SourceFile, position: number): boolean { + function isInsideJsxElementTraversal(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) { + if (position > node.getStart(sourceFile)) return true; + + node = node.parent; + } + else { + return false; + } + } + + return false; + } + + return isInsideJsxElementTraversal(getTokenAtPosition(sourceFile, position)); + } + export function findPrecedingMatchingToken(token: Node, matchingTokenKind: SyntaxKind, sourceFile: SourceFile) { const tokenKind = token.kind; let remainingMatchingTokens = 0; @@ -1346,7 +1375,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 +1468,7 @@ namespace ts { break; case SyntaxKind.EqualsGreaterThanToken: - // falls through + // falls through case SyntaxKind.Identifier: case SyntaxKind.StringLiteral: @@ -1447,7 +1476,7 @@ namespace ts { case SyntaxKind.BigIntLiteral: case SyntaxKind.TrueKeyword: case SyntaxKind.FalseKeyword: - // falls through + // falls through case SyntaxKind.TypeOfKeyword: case SyntaxKind.ExtendsKeyword: @@ -1934,6 +1963,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 @@ -2242,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/src/testRunner/unittests/tsserver/session.ts b/src/testRunner/unittests/tsserver/session.ts index 7a144945340fa..8084b79056b30 100644 --- a/src/testRunner/unittests/tsserver/session.ts +++ b/src/testRunner/unittests/tsserver/session.ts @@ -272,6 +272,10 @@ namespace ts.server { CommandNames.PrepareCallHierarchy, CommandNames.ProvideCallHierarchyIncomingCalls, CommandNames.ProvideCallHierarchyOutgoingCalls, + 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 c0a87752b8cb4..8442b93e4f686 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5487,6 +5487,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, 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 { @@ -6482,6 +6486,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" @@ -7514,6 +7522,22 @@ 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; + } /** * Information found in an "open" request. */ @@ -9870,6 +9894,7 @@ declare namespace ts.server { private getSupportedCodeFixes; private isLocation; private extractPositionOrRange; + private getRange; private getApplicableRefactors; private getEditsForRefactor; private organizeImports; @@ -9887,6 +9912,10 @@ declare namespace ts.server { private getDiagnosticsForProject; private configurePlugin; 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 9a549e1ffa4af..3f7f682342456 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5487,6 +5487,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, 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/cases/fourslash/commentSelection1.ts b/tests/cases/fourslash/commentSelection1.ts new file mode 100644 index 0000000000000..d5a7846395594 --- /dev/null +++ b/tests/cases/fourslash/commentSelection1.ts @@ -0,0 +1,26 @@ +// 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 |]= 8; + +verify.commentSelection( + `//let var1 = 1; +//let var2 = 2; +//let var3 = 3; + +let var4/* = 4;*/ + +//let var5 = 5; + +////let var6 = 6; +////let var7 = 7; +////let var8 = 8;`); \ 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 a2be4e57a2e0a..1b439bf64917d 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -397,6 +397,11 @@ declare namespace FourSlashInterface { generateTypes(...options: GenerateTypesOptions[]): void; organizeImports(newContent: string): void; + + 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/toggleLineComment1.ts b/tests/cases/fourslash/toggleLineComment1.ts new file mode 100644 index 0000000000000..1f72de7b40276 --- /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/toggleLineComment10.ts b/tests/cases/fourslash/toggleLineComment10.ts new file mode 100644 index 0000000000000..0f6c3d271988b --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment10.ts @@ -0,0 +1,11 @@ +// Close and open multiline comments if the line already contains more comments. + +//@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/toggleLineComment2.ts b/tests/cases/fourslash/toggleLineComment2.ts new file mode 100644 index 0000000000000..710bd5ef3f7db --- /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..d8f9aeabb99db --- /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..5e4beb6070a7e --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment4.ts @@ -0,0 +1,18 @@ +// If at least one line is not commented then comment all lines again. + +//// //const a[| = 1; +//// const b = 2 +//// //const c =|] 3; +//// +//// ////const d[| = 4; +//// //const e = 5; +//// ////const e =|] 6; + +verify.toggleLineComment( + `// //const a = 1; +//const b = 2 +// //const c = 3; + +//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 new file mode 100644 index 0000000000000..bec00a9233323 --- /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..4274cc18308ce --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment6.ts @@ -0,0 +1,15 @@ +// Selection is at the start of jsx its still js. + +//@Filename: file.tsx +//// let a = ( +//// [|
+//// some text|] +////
+//// ); + +verify.toggleLineComment( + `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/toggleLineComment9.ts b/tests/cases/fourslash/toggleLineComment9.ts new file mode 100644 index 0000000000000..1fbaea7ea383a --- /dev/null +++ b/tests/cases/fourslash/toggleLineComment9.ts @@ -0,0 +1,18 @@ +// 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. + +//@Filename: file.tsx +//// const a =
+//// {/*[|
*/} +//// SomeText +//// {/*
|]*/} +////
; + +verify.toggleLineComment( + `const a =
+ {/*
*/} + {/* SomeText*/} + {/*
*/} +
;`); \ 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..f76e9d564734f --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment1.ts @@ -0,0 +1,30 @@ +// Simple multiline 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;*/|] +//// +//// let var10[||] = 10; + +verify.toggleMultilineComment( + `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/**/ = 10;`); \ 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..a3a8cf70f786d --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment2.ts @@ -0,0 +1,35 @@ +// If selection is outside of a multiline 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.toggleMultilineComment( + `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..bcfa3418d971a --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment3.ts @@ -0,0 +1,28 @@ +/// + +// If range is inside a single line comment, just add the multiline 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.toggleMultilineComment( + `/*// 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..216a308ae44d5 --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment4.ts @@ -0,0 +1,7 @@ +// 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"; + +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..0dea4ebbff44e --- /dev/null +++ b/tests/cases/fourslash/toggleMultilineComment5.ts @@ -0,0 +1,34 @@ +// 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'}|]; +//// const f =
Some text;|] +//// const g =
Some text<[|/div>;|] + +verify.toggleMultilineComment( + `const a =
{/*
;*/} +const b =
This is {/*valid HTML &*/} JSX at the same time.
; +const c = + {/* + */} +; +const d = + + */} +; +const e = {/*{'foo'}*/}; +const f =
Some text;*/} +const g =
Some text<{/*/div>;*/}` +); \ 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..caa60504fd17c --- /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 multiline comments inserts multiline 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 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 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 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 new file mode 100644 index 0000000000000..2245cbc2d007a --- /dev/null +++ b/tests/cases/fourslash/uncommentSelection1.ts @@ -0,0 +1,42 @@ +// 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; +//// +//// let var11[||]/* = 1; +//// let var12 = 2; +//// let var13 */= 3; +//// +//// ////let var14 [||]= 14; + +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; + +let var11 = 1; +let var12 = 2; +let var13 = 3; + +//let var14 = 14;`); \ 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..745000d9c4d28 --- /dev/null +++ b/tests/cases/fourslash/uncommentSelection2.ts @@ -0,0 +1,34 @@ +// Common uncomment jsx cases + +//@Filename: file.tsx +//// const a = +//// {/**/} +//// {/**/} +//// ; +//// +//// const b =
    +//// {/*[|
    */} +//// SomeText +//// {/*
    |]*/} +////
    ; +//// +//// const c = +//// [||]{/**/} +//// ; + + +verify.uncommentSelection( + `const a = + + +; + +const b =
    +
    + SomeText +
    +
    ; + +const c = + +;`); \ 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