Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
35b16ab
temp
aozgaa Jun 7, 2017
6b4cd0b
Merge branch 'isInMultiLineComment' of https://github.com/aozgaa/Type…
aozgaa Jun 7, 2017
a819e4e
isInMultiLineComment
aozgaa Jun 7, 2017
8f28a02
indent block comments according to first line
aozgaa Jun 9, 2017
b02963b
make indent work with trailing comments
aozgaa Jun 9, 2017
8fc3fd9
request returns span
aozgaa Jun 10, 2017
cc88091
fix offsetting and tests
aozgaa Jun 10, 2017
efdbeba
Merge branch 'master' into isInMultiLineComment
Aug 4, 2017
777bc57
implementation comment
Aug 4, 2017
091376f
supressFormatOnKeyInComments
aozgaa Aug 7, 2017
2f5b1d3
Merge branch 'master' into isInMultiLineComment
aozgaa Aug 7, 2017
7c402d5
Merge branch 'master' into isInMultiLineComment
aozgaa Aug 9, 2017
de92e98
fix end-of-file assert failure
aozgaa Aug 10, 2017
b2188ad
cleanup
aozgaa Aug 14, 2017
472ad9d
findPrevious changes
aozgaa Aug 14, 2017
f3e0cbb
`findPrecedingToken` handles EOF child more gracefully
aozgaa Aug 15, 2017
a209db7
dont compute preceding token twice
aozgaa Aug 15, 2017
a08d18a
consolidate isInComment and getRangeOfEnclosingComment
aozgaa Aug 15, 2017
ad9c29b
add test
aozgaa Aug 15, 2017
153b94a
`JsxText` has no leading comments
aozgaa Aug 16, 2017
70e4f34
update test
aozgaa Aug 17, 2017
4b9f5a0
rename tests
aozgaa Aug 17, 2017
62f16be
add tests
aozgaa Aug 17, 2017
23ca368
Use simpler indentation for comments
aozgaa Aug 17, 2017
b7bc7d8
clarify `JsxText` handling
aozgaa Aug 17, 2017
19e2fa6
Merge branch 'master' into isInMultiLineComment
aozgaa Aug 17, 2017
6029b5c
cleanup
aozgaa Aug 17, 2017
760ef44
test if `onlyMultiLine` flag changes answer
aozgaa Aug 17, 2017
e7d2af0
remove duplicate verify
aozgaa Aug 18, 2017
e4e969a
respond to comments
aozgaa Aug 18, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2508,6 +2508,20 @@ namespace FourSlash {
}
}

public verifySpanOfEnclosingComment(negative: boolean, onlyMultiLine: boolean) {
Copy link
Member

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?

Copy link
Contributor Author

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.

const expected = !negative;
const position = this.currentCaretPosition;
const fileName = this.activeFile.fileName;
const actual = !!this.languageService.getSpanOfEnclosingComment(fileName, position, /*onlyMultiLine*/ onlyMultiLine);
if (expected !== actual) {
this.raiseError(`verifySpanOfEnclosingComment failed:
position: '${position}'
fileName: '${fileName}'
onlyMultiLine: '${onlyMultiLine}'
expected: '${expected}'.`);
}
}

/*
Check number of navigationItems which match both searchValue and matchKind,
if a filename is passed in, limit the results to that file.
Expand Down Expand Up @@ -3606,6 +3620,10 @@ namespace FourSlashInterface {
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
}

public isInCommentAtPosition(onlyMultiLine: boolean) {
this.state.verifySpanOfEnclosingComment(this.negative, onlyMultiLine);
}

public codeFixAvailable() {
this.state.verifyCodeFixAvailable(this.negative);
}
Expand Down
3 changes: 3 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,9 @@ namespace Harness.LanguageService {
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace));
}
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
}
getCodeFixesAtPosition(): ts.CodeAction[] {
throw new Error("Not supported on the shim.");
}
Expand Down
4 changes: 4 additions & 0 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ namespace ts.server {
return notImplemented();
}

getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
return notImplemented();
}

getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: number[]): CodeAction[] {
const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes };

Expand Down
16 changes: 16 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace ts.server.protocol {
/* @internal */
BraceFull = "brace-full",
BraceCompletion = "braceCompletion",
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
Change = "change",
Close = "close",
Completions = "completions",
Expand Down Expand Up @@ -241,6 +242,21 @@ namespace ts.server.protocol {
body?: TodoComment[];
}

/**
* A request to determine if the caret is inside a comment.
*/
export interface SpanOfEnclosingCommentRequest extends FileLocationRequest {
command: CommandTypes.GetSpanOfEnclosingComment;
arguments: SpanOfEnclosingCommentRequestArgs;
}

export interface SpanOfEnclosingCommentRequestArgs extends FileLocationRequestArgs {
/**
* Requires that the enclosing span be a multi-line comment, or else the request returns undefined.
*/
onlyMultiLine: boolean;
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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).

}

/**
* Request to obtain outlining spans in file.
*/
Expand Down
11 changes: 11 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,14 @@ namespace ts.server {
return project.getLanguageService(/*ensureSynchronized*/ false).getDocCommentTemplateAtPosition(file, position);
}

private getSpanOfEnclosingComment(args: protocol.SpanOfEnclosingCommentRequestArgs) {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
const onlyMultiLine = args.onlyMultiLine;
const position = this.getPosition(args, scriptInfo);
return project.getLanguageService(/*ensureSynchronized*/ false).getSpanOfEnclosingComment(file, position, onlyMultiLine);
}

private getIndentation(args: protocol.IndentationRequestArgs) {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file));
Expand Down Expand Up @@ -1767,6 +1775,9 @@ namespace ts.server {
[CommandNames.DocCommentTemplate]: (request: protocol.DocCommentTemplateRequest) => {
return this.requiredResponse(this.getDocCommentTemplate(request.arguments));
},
[CommandNames.GetSpanOfEnclosingComment]: (request: protocol.SpanOfEnclosingCommentRequest) => {
return this.requiredResponse(this.getSpanOfEnclosingComment(request.arguments));
},
[CommandNames.Format]: (request: protocol.FormatRequest) => {
return this.requiredResponse(this.getFormattingEditsForRange(request.arguments));
},
Expand Down
42 changes: 42 additions & 0 deletions src/services/formatting/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,48 @@ namespace ts.formatting {
}
}

/**
* Gets the indentation level of the multi-line comment enclosing position,
* and a negative value if the position is not in a multi-line comment.
*/
export function getIndentationOfEnclosingMultiLineComment(sourceFile: SourceFile, position: number, options: EditorSettings): number {
const range = getRangeOfEnclosingComment(sourceFile, position, /*onlyMultiLine*/ true);
if (range) {
Copy link
Member

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.

Copy link
Contributor Author

@aozgaa aozgaa Aug 17, 2017

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).

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);
Copy link
Member

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.

}
return undefined;
}

export function getRangeOfEnclosingComment(sourceFile: SourceFile, position: number, onlyMultiLine: boolean): CommentRange | undefined {
// Considering a fixed position,
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arbitrary.

Copy link
Member

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).

// - trailing comments are those following and on the same line as the position.
// - leading comments are those in the range [position, start of next non-trivia token)
// that are not trailing comments of that position.
//
// 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 precedingToken = findPrecedingToken(position, sourceFile);
const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end);
Copy link
Member

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.

const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false), sourceFile);
Copy link
Member

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?

const commentRanges = trailingRangesOfPreviousToken && leadingCommentRangesOfNextToken ?
trailingRangesOfPreviousToken.concat(leadingCommentRangesOfNextToken) :
trailingRangesOfPreviousToken || leadingCommentRangesOfNextToken;
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added above.

if (commentRanges) {
for (const range of commentRanges) {
// We need to extend the range when in an unclosed multi-line comment.
Copy link
Member

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?

if (range.pos < position && position < range.end ||
position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is sourceFile.getFullWidth()?

return onlyMultiLine && range.kind !== SyntaxKind.MultiLineCommentTrivia ? undefined : range;
}
}
}
return undefined;
}

function getOpenTokenForList(node: Node, list: ReadonlyArray<Node>) {
switch (node.kind) {
case SyntaxKind.Constructor:
Expand Down
21 changes: 13 additions & 8 deletions src/services/formatting/smartIndenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ namespace ts.formatting {
return 0;
}

const indentationOfEnclosingMultiLineComment = getIndentationOfEnclosingMultiLineComment(sourceFile, position, options);
if (indentationOfEnclosingMultiLineComment >= 0) {
return indentationOfEnclosingMultiLineComment;
}

const precedingToken = findPrecedingToken(position, sourceFile);
if (!precedingToken) {
return getBaseIndentation(options);
}

// no indentation in string \regex\template literals
const precedingTokenIsLiteral = isStringOrRegularExpressionOrTemplateLiteral(precedingToken.kind);
if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && precedingToken.end > position) {
if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && position < precedingToken.end) {
return 0;
}

Expand Down Expand Up @@ -405,13 +410,13 @@ namespace ts.formatting {
return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options);
}

/*
Character is the actual index of the character since the beginning of the line.
Column - position of the character after expanding tabs to spaces
"0\t2$"
value of 'character' for '$' is 3
value of 'column' for '$' is 6 (assuming that tab size is 4)
*/
/**
* Character is the actual index of the character since the beginning of the line.
* Column - position of the character after expanding tabs to spaces.
* "0\t2$"
* value of 'character' for '$' is 3
* value of 'column' for '$' is 6 (assuming that tab size is 4)
*/
export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings) {
let character = 0;
let column = 0;
Expand Down
32 changes: 21 additions & 11 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1757,17 +1757,20 @@ namespace ts {
function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
const settings = toEditorSettings(options);
if (key === "{") {
return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings);
}
else if (key === "}") {
return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings);
}
else if (key === ";") {
return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(settings), settings);
}
else if (key === "\n") {
return formatting.formatOnEnter(position, sourceFile, getRuleProvider(settings), settings);

if (!isInComment(sourceFile, position)) {
Copy link
Member

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?

Copy link
Contributor Author

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.

if (key === "{") {
return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings);
}
else if (key === "}") {
return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings);
}
else if (key === ";") {
return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(settings), settings);
}
else if (key === "\n") {
return formatting.formatOnEnter(position, sourceFile, getRuleProvider(settings), settings);
}
}

return [];
Expand Down Expand Up @@ -1826,6 +1829,12 @@ namespace ts {
return true;
}

function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean) {
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
const range = ts.formatting.getRangeOfEnclosingComment(sourceFile, position, onlyMultiLine);
return range && createTextSpanFromRange(range);
}

function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] {
// Note: while getting todo comments seems like a syntactic operation, we actually
// treat it as a semantic operation here. This is because we expect our host to call
Expand Down Expand Up @@ -2050,6 +2059,7 @@ namespace ts {
getFormattingEditsAfterKeystroke,
getDocCommentTemplateAtPosition,
isValidBraceCompletionAtPosition,
getSpanOfEnclosingComment,
getCodeFixesAtPosition,
getEmitOutput,
getNonBoundSourceFile,
Expand Down
12 changes: 12 additions & 0 deletions src/services/shims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ namespace ts {
*/
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string;

/**
* Returns a JSON-encoded TextSpan | undefined indicating the range of the enclosing comment, if it exists.
*/
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string;

getEmitOutput(fileName: string): string;
getEmitOutputObject(fileName: string): EmitOutput;
}
Expand Down Expand Up @@ -815,6 +820,13 @@ namespace ts {
);
}

public getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string {
return this.forwardJSONCall(
`getSpanOfEnclosingComment('${fileName}', ${position})`,
() => this.languageService.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)
);
}

/// GET SMART INDENT
public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string {
return this.forwardJSONCall(
Expand Down
2 changes: 2 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ namespace ts {

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

getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan;

getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[];

getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
Expand Down
1 change: 1 addition & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ declare namespace FourSlashInterface {
typeDefinitionCountIs(expectedCount: number): void;
implementationListIsEmpty(): void;
isValidBraceCompletionAtPosition(openingBrace?: string): void;
isInCommentAtPosition(onlyMultiLine: boolean): void;
codeFixAvailable(): void;
applicableRefactorAvailableAtMarker(markerName: string): void;
codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], diagnosticCode?: number): void;
Expand Down
47 changes: 47 additions & 0 deletions tests/cases/fourslash/isInMultiLineComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/// <reference path="fourslash.ts" />

//// /* x */
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

//// /**
//// * @param this doesn't make sense here.
//// */
//// // x
//// let x = 1; /*
//// *

const firstCommentStart = 0;
const firstCommentEnd = 7;
goTo.position(firstCommentStart);
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true);

goTo.position(firstCommentStart + 1);
verify.isInCommentAtPosition(/*onlyMultiLine*/ true);
goTo.position(firstCommentEnd - 1);
verify.isInCommentAtPosition(/*onlyMultiLine*/ true);

goTo.position(firstCommentEnd);
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true);

const multilineJsDocStart = firstCommentEnd + 1;
const multilineJsDocEnd = multilineJsDocStart + 49;

goTo.position(multilineJsDocStart);
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true);
goTo.position(multilineJsDocStart + 1);
verify.isInCommentAtPosition(/*onlyMultiLine*/ true);
goTo.position(multilineJsDocEnd - 1);
verify.isInCommentAtPosition(/*onlyMultiLine*/ true);
goTo.position(multilineJsDocEnd);
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true);

const singleLineCommentStart = multilineJsDocEnd + 1;

goTo.position(singleLineCommentStart + 1);
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true);
verify.isInCommentAtPosition(/*onlyMultiLine*/ false);
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.


const postNodeCommentStart = singleLineCommentStart + 16;

goTo.position(postNodeCommentStart);
verify.not.isInCommentAtPosition(/*onlyMultiLine*/ true);
goTo.position(postNodeCommentStart + 1);
verify.isInCommentAtPosition(/*onlyMultiLine*/ true);