Skip to content

Commit a3d113b

Browse files
authored
Merge pull request #16385 from aozgaa/isInMultiLineComment
multi-line comment formatting fix and handler
2 parents ecd2ae8 + e4e969a commit a3d113b

21 files changed

+397
-75
lines changed

src/compiler/utilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ namespace ts {
647647
}
648648

649649
export function getLeadingCommentRangesOfNode(node: Node, sourceFileOfNode: SourceFile) {
650-
return getLeadingCommentRanges(sourceFileOfNode.text, node.pos);
650+
return node.kind !== SyntaxKind.JsxText ? getLeadingCommentRanges(sourceFileOfNode.text, node.pos) : undefined;
651651
}
652652

653653
export function getJSDocCommentRanges(node: Node, text: string) {

src/harness/fourslash.ts

+21
Original file line numberDiff line numberDiff line change
@@ -2508,6 +2508,23 @@ namespace FourSlash {
25082508
}
25092509
}
25102510

2511+
public verifySpanOfEnclosingComment(negative: boolean, onlyMultiLineDiverges?: boolean) {
2512+
const expected = !negative;
2513+
const position = this.currentCaretPosition;
2514+
const fileName = this.activeFile.fileName;
2515+
const actual = !!this.languageService.getSpanOfEnclosingComment(fileName, position, /*onlyMultiLine*/ false);
2516+
const actualOnlyMultiLine = !!this.languageService.getSpanOfEnclosingComment(fileName, position, /*onlyMultiLine*/ true);
2517+
if (expected !== actual || onlyMultiLineDiverges === (actual === actualOnlyMultiLine)) {
2518+
this.raiseError(`verifySpanOfEnclosingComment failed:
2519+
position: '${position}'
2520+
fileName: '${fileName}'
2521+
onlyMultiLineDiverges: '${onlyMultiLineDiverges}'
2522+
actual: '${actual}'
2523+
actualOnlyMultiLine: '${actualOnlyMultiLine}'
2524+
expected: '${expected}'.`);
2525+
}
2526+
}
2527+
25112528
/*
25122529
Check number of navigationItems which match both searchValue and matchKind,
25132530
if a filename is passed in, limit the results to that file.
@@ -3648,6 +3665,10 @@ namespace FourSlashInterface {
36483665
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
36493666
}
36503667

3668+
public isInCommentAtPosition(onlyMultiLineDiverges?: boolean) {
3669+
this.state.verifySpanOfEnclosingComment(this.negative, onlyMultiLineDiverges);
3670+
}
3671+
36513672
public codeFixAvailable() {
36523673
this.state.verifyCodeFixAvailable(this.negative);
36533674
}

src/harness/harnessLanguageService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,9 @@ namespace Harness.LanguageService {
487487
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
488488
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace));
489489
}
490+
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
491+
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
492+
}
490493
getCodeFixesAtPosition(): ts.CodeAction[] {
491494
throw new Error("Not supported on the shim.");
492495
}

src/server/client.ts

+4
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ namespace ts.server {
527527
return notImplemented();
528528
}
529529

530+
getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
531+
return notImplemented();
532+
}
533+
530534
getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: number[]): CodeAction[] {
531535
const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes };
532536

src/server/protocol.ts

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace ts.server.protocol {
88
/* @internal */
99
BraceFull = "brace-full",
1010
BraceCompletion = "braceCompletion",
11+
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
1112
Change = "change",
1213
Close = "close",
1314
Completions = "completions",
@@ -241,6 +242,21 @@ namespace ts.server.protocol {
241242
body?: TodoComment[];
242243
}
243244

245+
/**
246+
* A request to determine if the caret is inside a comment.
247+
*/
248+
export interface SpanOfEnclosingCommentRequest extends FileLocationRequest {
249+
command: CommandTypes.GetSpanOfEnclosingComment;
250+
arguments: SpanOfEnclosingCommentRequestArgs;
251+
}
252+
253+
export interface SpanOfEnclosingCommentRequestArgs extends FileLocationRequestArgs {
254+
/**
255+
* Requires that the enclosing span be a multi-line comment, or else the request returns undefined.
256+
*/
257+
onlyMultiLine: boolean;
258+
}
259+
244260
/**
245261
* Request to obtain outlining spans in file.
246262
*/

src/server/session.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,14 @@ namespace ts.server {
10251025
return project.getLanguageService(/*ensureSynchronized*/ false).getDocCommentTemplateAtPosition(file, position);
10261026
}
10271027

1028+
private getSpanOfEnclosingComment(args: protocol.SpanOfEnclosingCommentRequestArgs) {
1029+
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
1030+
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
1031+
const onlyMultiLine = args.onlyMultiLine;
1032+
const position = this.getPosition(args, scriptInfo);
1033+
return project.getLanguageService(/*ensureSynchronized*/ false).getSpanOfEnclosingComment(file, position, onlyMultiLine);
1034+
}
1035+
10281036
private getIndentation(args: protocol.IndentationRequestArgs) {
10291037
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
10301038
const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file));
@@ -1765,6 +1773,9 @@ namespace ts.server {
17651773
[CommandNames.DocCommentTemplate]: (request: protocol.DocCommentTemplateRequest) => {
17661774
return this.requiredResponse(this.getDocCommentTemplate(request.arguments));
17671775
},
1776+
[CommandNames.GetSpanOfEnclosingComment]: (request: protocol.SpanOfEnclosingCommentRequest) => {
1777+
return this.requiredResponse(this.getSpanOfEnclosingComment(request.arguments));
1778+
},
17681779
[CommandNames.Format]: (request: protocol.FormatRequest) => {
17691780
return this.requiredResponse(this.getFormattingEditsForRange(request.arguments));
17701781
},

src/services/formatting/formatting.ts

+50
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,56 @@ namespace ts.formatting {
11501150
}
11511151
}
11521152

1153+
/**
1154+
* @param precedingToken pass `null` if preceding token was already computed and result was `undefined`.
1155+
*/
1156+
export function getRangeOfEnclosingComment(
1157+
sourceFile: SourceFile,
1158+
position: number,
1159+
onlyMultiLine: boolean,
1160+
precedingToken?: Node | null, // tslint:disable-line:no-null-keyword
1161+
tokenAtPosition = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false),
1162+
predicate?: (c: CommentRange) => boolean): CommentRange | undefined {
1163+
const tokenStart = tokenAtPosition.getStart(sourceFile);
1164+
if (tokenStart <= position && position < tokenAtPosition.getEnd()) {
1165+
return undefined;
1166+
}
1167+
1168+
if (precedingToken === undefined) {
1169+
precedingToken = findPrecedingToken(position, sourceFile);
1170+
}
1171+
1172+
// Between two consecutive tokens, all comments are either trailing on the former
1173+
// or leading on the latter (and none are in both lists).
1174+
const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end);
1175+
const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile);
1176+
const commentRanges = trailingRangesOfPreviousToken && leadingCommentRangesOfNextToken ?
1177+
trailingRangesOfPreviousToken.concat(leadingCommentRangesOfNextToken) :
1178+
trailingRangesOfPreviousToken || leadingCommentRangesOfNextToken;
1179+
if (commentRanges) {
1180+
for (const range of commentRanges) {
1181+
// The end marker of a single-line comment does not include the newline character.
1182+
// With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position):
1183+
//
1184+
// // asdf ^\n
1185+
//
1186+
// But for closed multi-line comments, we don't want to be inside the comment in the following case:
1187+
//
1188+
// /* asdf */^
1189+
//
1190+
// However, unterminated multi-line comments *do* contain their end.
1191+
//
1192+
// Internally, we represent the end of the comment at the newline and closing '/', respectively.
1193+
//
1194+
if ((range.pos < position && position < range.end ||
1195+
position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth()))) {
1196+
return (range.kind === SyntaxKind.MultiLineCommentTrivia || !onlyMultiLine) && (!predicate || predicate(range)) ? range : undefined;
1197+
}
1198+
}
1199+
}
1200+
return undefined;
1201+
}
1202+
11531203
function getOpenTokenForList(node: Node, list: ReadonlyArray<Node>) {
11541204
switch (node.kind) {
11551205
case SyntaxKind.Constructor:

src/services/formatting/smartIndenter.ts

+31-8
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,36 @@ namespace ts.formatting {
3232
}
3333

3434
const precedingToken = findPrecedingToken(position, sourceFile);
35+
36+
const enclosingCommentRange = getRangeOfEnclosingComment(sourceFile, position, /*onlyMultiLine*/ true, precedingToken || null); // tslint:disable-line:no-null-keyword
37+
if (enclosingCommentRange) {
38+
const previousLine = getLineAndCharacterOfPosition(sourceFile, position).line - 1;
39+
const commentStartLine = getLineAndCharacterOfPosition(sourceFile, enclosingCommentRange.pos).line;
40+
41+
Debug.assert(commentStartLine >= 0);
42+
43+
if (previousLine <= commentStartLine) {
44+
return findFirstNonWhitespaceColumn(getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options);
45+
}
46+
47+
const startPostionOfLine = getStartPositionOfLine(previousLine, sourceFile);
48+
const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPostionOfLine, position, sourceFile, options);
49+
50+
if (column === 0) {
51+
return column;
52+
}
53+
54+
const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPostionOfLine + character);
55+
return firstNonWhitespaceCharacterCode === CharacterCodes.asterisk ? column - 1 : column;
56+
}
57+
3558
if (!precedingToken) {
3659
return getBaseIndentation(options);
3760
}
3861

3962
// no indentation in string \regex\template literals
4063
const precedingTokenIsLiteral = isStringOrRegularExpressionOrTemplateLiteral(precedingToken.kind);
41-
if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && precedingToken.end > position) {
64+
if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && position < precedingToken.end) {
4265
return 0;
4366
}
4467

@@ -405,13 +428,13 @@ namespace ts.formatting {
405428
return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options);
406429
}
407430

408-
/*
409-
Character is the actual index of the character since the beginning of the line.
410-
Column - position of the character after expanding tabs to spaces
411-
"0\t2$"
412-
value of 'character' for '$' is 3
413-
value of 'column' for '$' is 6 (assuming that tab size is 4)
414-
*/
431+
/**
432+
* Character is the actual index of the character since the beginning of the line.
433+
* Column - position of the character after expanding tabs to spaces.
434+
* "0\t2$"
435+
* value of 'character' for '$' is 3
436+
* value of 'column' for '$' is 6 (assuming that tab size is 4)
437+
*/
415438
export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings) {
416439
let character = 0;
417440
let column = 0;

src/services/services.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,14 @@ namespace ts {
107107
scanner.setTextPos(pos);
108108
while (pos < end) {
109109
const token = scanner.scan();
110-
Debug.assert(token !== SyntaxKind.EndOfFileToken); // Else it would infinitely loop
111110
const textPos = scanner.getTextPos();
112111
if (textPos <= end) {
113112
nodes.push(createNode(token, pos, textPos, this));
114113
}
115114
pos = textPos;
115+
if (token === SyntaxKind.EndOfFileToken) {
116+
break;
117+
}
116118
}
117119
return pos;
118120
}
@@ -1757,17 +1759,20 @@ namespace ts {
17571759
function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
17581760
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
17591761
const settings = toEditorSettings(options);
1760-
if (key === "{") {
1761-
return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings);
1762-
}
1763-
else if (key === "}") {
1764-
return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings);
1765-
}
1766-
else if (key === ";") {
1767-
return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(settings), settings);
1768-
}
1769-
else if (key === "\n") {
1770-
return formatting.formatOnEnter(position, sourceFile, getRuleProvider(settings), settings);
1762+
1763+
if (!isInComment(sourceFile, position)) {
1764+
if (key === "{") {
1765+
return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings);
1766+
}
1767+
else if (key === "}") {
1768+
return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings);
1769+
}
1770+
else if (key === ";") {
1771+
return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(settings), settings);
1772+
}
1773+
else if (key === "\n") {
1774+
return formatting.formatOnEnter(position, sourceFile, getRuleProvider(settings), settings);
1775+
}
17711776
}
17721777

17731778
return [];
@@ -1826,6 +1831,12 @@ namespace ts {
18261831
return true;
18271832
}
18281833

1834+
function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean) {
1835+
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
1836+
const range = ts.formatting.getRangeOfEnclosingComment(sourceFile, position, onlyMultiLine);
1837+
return range && createTextSpanFromRange(range);
1838+
}
1839+
18291840
function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] {
18301841
// Note: while getting todo comments seems like a syntactic operation, we actually
18311842
// treat it as a semantic operation here. This is because we expect our host to call
@@ -2050,6 +2061,7 @@ namespace ts {
20502061
getFormattingEditsAfterKeystroke,
20512062
getDocCommentTemplateAtPosition,
20522063
isValidBraceCompletionAtPosition,
2064+
getSpanOfEnclosingComment,
20532065
getCodeFixesAtPosition,
20542066
getEmitOutput,
20552067
getNonBoundSourceFile,

src/services/shims.ts

+12
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ namespace ts {
254254
*/
255255
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string;
256256

257+
/**
258+
* Returns a JSON-encoded TextSpan | undefined indicating the range of the enclosing comment, if it exists.
259+
*/
260+
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string;
261+
257262
getEmitOutput(fileName: string): string;
258263
getEmitOutputObject(fileName: string): EmitOutput;
259264
}
@@ -815,6 +820,13 @@ namespace ts {
815820
);
816821
}
817822

823+
public getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string {
824+
return this.forwardJSONCall(
825+
`getSpanOfEnclosingComment('${fileName}', ${position})`,
826+
() => this.languageService.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)
827+
);
828+
}
829+
818830
/// GET SMART INDENT
819831
public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string {
820832
return this.forwardJSONCall(

src/services/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ namespace ts {
270270

271271
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
272272

273+
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan;
274+
273275
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[];
274276
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
275277
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined;

0 commit comments

Comments
 (0)