-
Notifications
You must be signed in to change notification settings - Fork 12.8k
multi-line comment formatting fix and handler #16385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/harness/fourslash.ts
Outdated
@@ -2500,6 +2500,16 @@ namespace FourSlash { | |||
} | |||
} | |||
|
|||
public verifyisInMultiLineCommentAtPosition(negative: boolean) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"verifyIs"?
src/harness/fourslash.ts
Outdated
const fileName = this.activeFile.fileName; | ||
const actual = this.languageService.getisInMultiLineCommentAtPosition(fileName, position); | ||
if (expected !== actual) { | ||
this.raiseError(`verifyIsInDocComment failed: at position '${position}' in '${fileName}', expected '${expected}'.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy-paste error?
src/harness/fourslash.ts
Outdated
const expected = !negative; | ||
const position = this.currentCaretPosition; | ||
const fileName = this.activeFile.fileName; | ||
const actual = this.languageService.getisInMultiLineCommentAtPosition(fileName, position); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"getis" -> "is"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e.g. isValidBraceCompletion
@@ -1117,6 +1117,27 @@ namespace ts.formatting { | |||
} | |||
} | |||
|
|||
/** | |||
* @returns -1 iff the position is not in a multi-line comment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's common to simply say "a negative value" rather than "-1" to retain flexibility for future refactoring.
Also, what does it return if it is in a multi-line comment?
if (leadingCommentRanges) { | ||
loop: for (const range of leadingCommentRanges) { | ||
// We need to extend the range when in an unclosed multi-line comment. | ||
if (range.pos < position && (position < range.end || position === range.end && position === sourceFile.getFullWidth())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does range.pos < position
mean that the opening slash doesn't count?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that important?
return 0; | ||
} | ||
|
||
const lineAtPosition = sourceFile.getLineAndCharacterOfPosition(position).line; | ||
|
||
const indentationOfEnclosingMultiLineComment = getIndentationOfEnclosingMultiLineComment(sourceFile, position); | ||
if (indentationOfEnclosingMultiLineComment !== -1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
>= 0
?
src/services/shims.ts
Outdated
@@ -835,6 +840,14 @@ namespace ts { | |||
); | |||
} | |||
|
|||
/// GET IS IN MULTI-LINE COMMENT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment isn't obviously helpful.
verify.isInMultiLineCommentAtPosition(); | ||
} | ||
|
||
for (let i = 0; i < 2; ++i) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the loop makes this harder to read.
@@ -0,0 +1,36 @@ | |||
/// <reference path="fourslash.ts" /> | |||
|
|||
//// /* x */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be interesting to test a comment that spans multiple lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
* @returns -1 iff the position is not in a multi-line comment. | ||
*/ | ||
export function getIndentationOfEnclosingMultiLineComment(sourceFile: SourceFile, position: number): number { | ||
const token = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a test that is affected by the value of includeJsDocComment
?
const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false), sourceFile); | ||
const commentRanges = trailingRangesOfPreviousToken && leadingCommentRangesOfNextToken ? | ||
trailingRangesOfPreviousToken.concat(leadingCommentRangesOfNextToken) : | ||
trailingRangesOfPreviousToken || leadingCommentRangesOfNextToken; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add some explanatory comments for the logic in here? It assumes the reader knows a lot of detail about how we keep comments in the trees attached to tokens. And I'm guessing there's some edge cases or patterns here this logic needs to handle that makes it non-trivial. That knowledge would be good for future readers/maintains not to have to figure out again for themselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added above.
return 0; | ||
} | ||
|
||
const lineAtPosition = sourceFile.getLineAndCharacterOfPosition(position).line; | ||
|
||
const indentationOfEnclosingMultiLineComment = getIndentationOfEnclosingMultiLineComment(sourceFile, position, options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utilities.ts already exports a brief isInComment
function. It it worth enclosing this block in an if
check on that to make it clearer?
const commentStart = range.pos; | ||
const commentLineStart = getLineStartPositionForPosition(commentStart, sourceFile); | ||
const { column, character } = SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(commentLineStart, commentStart, sourceFile, options); | ||
return column + /*length after whitespace ends*/ range.pos - (commentLineStart + character); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify what the above logic is doing? Its a lot more involved than I would have expected, and I can't figure out the reason for some of it.
Minor comment to clarify some logic, and make sure the builds/checks are passing before checking in. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some question inline.
loop: for (const range of leadingCommentRanges) { | ||
export function getIndentationOfEnclosingMultiLineComment(sourceFile: SourceFile, position: number, options: EditorSettings): number { | ||
const range = getRangeOfEnclosingComment(sourceFile, position, /*onlyMultiLine*/ true); | ||
if (range) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: if you return immediately after determining that range
is undefined, the rest of the method reads as straight-line code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just in-lined the helper since it's only called once. Additionally, the logic has changed because I discovered an issue with determining the columns when we have text which is non-whitespace (because determining the number of graphemes in a string is non-trivial).
src/harness/fourslash.ts
Outdated
@@ -2508,6 +2508,20 @@ namespace FourSlash { | |||
} | |||
} | |||
|
|||
public verifySpanOfEnclosingComment(negative: boolean, onlyMultiLine: boolean) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just pass expected
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Methods in VerifyNegatable
often call into the TestState
by passing in negative
. I'm merely following that idiom.
|
||
goTo.position(singleLineCommentStart + 1); | ||
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true); | ||
verify.isInCommentAtPosition(/*onlyMultiLine*/ false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably, the result for false
matches the result for true
in every case but this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I thought it felt a bit heavy-handed to query each case in the test that is checked in, but I'm open to checking more thoroughly if it seems worthwhile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe isInCommentAtPosition
could check both unless either true
or false
is specified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
/** | ||
* Requires that the enclosing span be a multi-line comment, or else the request returns undefined. | ||
*/ | ||
onlyMultiLine: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this just here for the generality of the API? We don't appear to actually use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only ever pass in onlyMultiLine: true
right now with the proposed change in the VS extension.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have mixed feeling about this but don't object as long as we have test coverage (which we do).
} | ||
|
||
export function getRangeOfEnclosingComment(sourceFile: SourceFile, position: number, onlyMultiLine: boolean): CommentRange | undefined { | ||
// Considering a fixed position, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an arbitrary position or is it assumed to be at a token boundary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arbitrary.
// tokens contain all comments in a sourcefile disjointly. | ||
const precedingToken = findPrecedingToken(position, sourceFile); | ||
const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end); | ||
const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false), sourceFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the impact of /*includeJsDocComment*/ false
? Is there a test that depends on the value?
for (const range of commentRanges) { | ||
// We need to extend the range when in an unclosed multi-line comment. | ||
if (range.pos < position && position < range.end || | ||
position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is sourceFile.getFullWidth()
?
trailingRangesOfPreviousToken || leadingCommentRangesOfNextToken; | ||
if (commentRanges) { | ||
for (const range of commentRanges) { | ||
// We need to extend the range when in an unclosed multi-line comment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does the check below detect unclosed multi-line comments?
else if (key === "\n") { | ||
return formatting.formatOnEnter(position, sourceFile, getRuleProvider(settings), settings); | ||
|
||
if (!isInComment(sourceFile, position)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be worthwhile to test this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a test to fourslash and will add an integration test on the managed side to make sure the experience works end-to-end.
src/services/utilities.ts
Outdated
// we need to find the last token in a previous child. | ||
// 2) `position` is within the same span: we recurse on `child`. | ||
// * JsxText is exceptional in that its tokens are (non-trivia) whitespace, which we do not want to return. | ||
// TODO(arozga): shouldn't `findRightmost...` need to handle JsxText? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment seems like something you should do before check-in - especially as you have zero test-cases covering JSX.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed and tests have been added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add more tests. You have one test file with static text (i.e. no editing occurring) and a well-formed comment. Please ensure behavior in unclosed comments, nested comments, etc.
sourceFile: SourceFile, | ||
position: number, | ||
onlyMultiLine: boolean, | ||
precedingToken: Node | null | undefined = findPrecedingToken(position, sourceFile), // tslint:disable-line:no-null-keyword |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
findPrecedingToken || null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It shouldn't matter in what follows, as the null
value is only used to prevent the default initializer from being evaluated.
* When in a multi-line comment, we would have liked to use the start of the comment as a reference point for the indentation inside the comment, but determining the number of columns shifted for the comment start woudl require determining the length w/r/t graphemes, which we do not currently implement. We would like to avoid taking on a runtime dependency on a grapheme-parsing library. Instead, we look at the indentation level on the previoud line or start of the comment as a reference point, and correct shift for lines starting with an asterisk.
@amcasey and @billti , changes have been addressed, can you take another look? @RyanCavanaugh , can you comment on whether the treatment of @uniqueiniquity , thoughts? |
@@ -647,7 +647,7 @@ namespace ts { | |||
} | |||
|
|||
export function getLeadingCommentRangesOfNode(node: Node, sourceFileOfNode: SourceFile) { | |||
return getLeadingCommentRanges(sourceFileOfNode.text, node.pos); | |||
return node.kind !== SyntaxKind.JsxText ? getLeadingCommentRanges(sourceFileOfNode.text, node.pos) : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's special about jsx? Does it apply to tsx as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JsxText
nodes are children of JsxElement
s that are not JsxElements
themselves or curly-brace-delimited assignment expressions. In the following example, the underlined is JsxText
:
<div>Click me!<h1/></div>
~~~~~~~~~
We essentially treat them like string literals, though differences in the way they are delimited mean that getLeadingCommentRanges
cannot handle them correctly without some information from the parser, which getLeadingCommentRanges
has been designed to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And that covers tsx as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
// Note, `node.start` is the start-position of the first comment following the previous | ||
// token that is not a trailing comment, so the leading and trailing comments of all | ||
// tokens contain all comments in a sourcefile disjointly. | ||
const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably short-circuit if position is within tokenAtPosition
.
precedingToken: Node | null | undefined = findPrecedingToken(position, sourceFile), // tslint:disable-line:no-null-keyword | ||
tokenAtPosition = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false), | ||
predicate?: (c: CommentRange) => boolean): CommentRange | undefined { | ||
// Considering a fixed position, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would simplify this comment to something like
// Between two consecutive tokens, all comments are either trailing on the former or leading on the latter (and none are in both lists).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, modulo minor comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a lot of work to put an asterisk at the right indentation :-o Thanks!
All PR's from the last day appear to be failing Travis with node 4 due to a gulp-typescript dependency. See ivogabe/gulp-typescript#536 for the fix. Going to ignore this and go ahead and merge. |
Fixes an issue where indentation behavior in VS was poor in multi-line comments. Two changes:
in order for VS to recognize that it needs to add asterisks on newlines, the client needs to know that it is in a multi-line comment when a newline is typed. The new request provides precisely this information.
The level of indentation for lines in the middle of a multi-line comment corresponds to the position where the multi-line comment starts. ie:Instead*, we determine indentation in by looking at the first non-whitespace character on the previous line or start of the comment, whichever is later, and shifting one column left if there is an asterisk character. In the case of jsdoc-style comments, this result precisely coincides with the level of indentation of the start of the comment (provided the comment started on its own line). This behavior is consistent with what Roslyn does in CSharp.
* The original strategy was problematic because finding the number of columns to displace for the opening comment marker would require us to determine how many columns non-whitespace characters take up. That is, we would need to start splitting source text into graphemes, which we haven't done up to this point. To handle this, we would either need to (a) add a runtime dependency or (b) implement it ourselves. Since we don't currently have runtime dependencies, we are biased towards (b). But the added complexity of handling graphemes correctly is not worthwhile for what appears to be a rare use-case.
EDIT (8/16/2017): re-ordered items, and changed description of indentation strategy.