Skip to content

Commit 96deb55

Browse files
committed
Script side implementation for Brace Completion. (#7587)
* Script side implementation for Brace Completion. This needs updated Visual Studio components to work. * Changed CharacterCodes to number, to keep the API simple * CR feedback * CR feedback and more JSX tests * Swapped 2 comments * typo
1 parent 19a9f7f commit 96deb55

12 files changed

+267
-11
lines changed

src/harness/fourslash.ts

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ namespace FourSlash {
655655
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind);
656656
}
657657
else {
658-
this.raiseError(`No completions at position '${ this.currentCaretPosition }' when looking for '${ symbol }'.`);
658+
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
659659
}
660660
}
661661

@@ -1758,13 +1758,13 @@ namespace FourSlash {
17581758
const actual = (<ts.server.SessionClient>this.languageService).getProjectInfo(
17591759
this.activeFile.fileName,
17601760
/* needFileNameList */ true
1761-
);
1761+
);
17621762
assert.equal(
17631763
expected.join(","),
1764-
actual.fileNames.map( file => {
1764+
actual.fileNames.map(file => {
17651765
return file.replace(this.basePath + "/", "");
1766-
}).join(",")
1767-
);
1766+
}).join(",")
1767+
);
17681768
}
17691769
}
17701770

@@ -1850,6 +1850,37 @@ namespace FourSlash {
18501850
});
18511851
}
18521852

1853+
public verifyBraceCompletionAtPostion(negative: boolean, openingBrace: string) {
1854+
1855+
const openBraceMap: ts.Map<ts.CharacterCodes> = {
1856+
"(": ts.CharacterCodes.openParen,
1857+
"{": ts.CharacterCodes.openBrace,
1858+
"[": ts.CharacterCodes.openBracket,
1859+
"'": ts.CharacterCodes.singleQuote,
1860+
'"': ts.CharacterCodes.doubleQuote,
1861+
"`": ts.CharacterCodes.backtick,
1862+
"<": ts.CharacterCodes.lessThan
1863+
};
1864+
1865+
const charCode = openBraceMap[openingBrace];
1866+
1867+
if (!charCode) {
1868+
this.raiseError(`Invalid openingBrace '${openingBrace}' specified.`);
1869+
}
1870+
1871+
const position = this.currentCaretPosition;
1872+
1873+
const validBraceCompletion = this.languageService.isValidBraceCompletionAtPostion(this.activeFile.fileName, position, charCode);
1874+
1875+
if (!negative && !validBraceCompletion) {
1876+
this.raiseError(`${position} is not a valid brace completion position for ${openingBrace}`);
1877+
}
1878+
1879+
if (negative && validBraceCompletion) {
1880+
this.raiseError(`${position} is a valid brace completion position for ${openingBrace}`);
1881+
}
1882+
}
1883+
18531884
public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) {
18541885
const actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition);
18551886

@@ -2239,7 +2270,7 @@ namespace FourSlash {
22392270
};
22402271

22412272
const host = Harness.Compiler.createCompilerHost(
2242-
[ fourslashFile, testFile ],
2273+
[fourslashFile, testFile],
22432274
(fn, contents) => result = contents,
22442275
ts.ScriptTarget.Latest,
22452276
Harness.IO.useCaseSensitiveFileNames(),
@@ -2264,7 +2295,7 @@ namespace FourSlash {
22642295
function runCode(code: string, state: TestState): void {
22652296
// Compile and execute the test
22662297
const wrappedCode =
2267-
`(function(test, goTo, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) {
2298+
`(function(test, goTo, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) {
22682299
${code}
22692300
})`;
22702301
try {
@@ -2378,7 +2409,7 @@ ${code}
23782409
}
23792410
}
23802411
}
2381-
// TODO: should be '==='?
2412+
// TODO: should be '==='?
23822413
}
23832414
else if (line == "" || lineLength === 0) {
23842415
// Previously blank lines between fourslash content caused it to be considered as 2 files,
@@ -2870,6 +2901,10 @@ namespace FourSlashInterface {
28702901
public verifyDefinitionsName(name: string, containerName: string) {
28712902
this.state.verifyDefinitionsName(this.negative, name, containerName);
28722903
}
2904+
2905+
public isValidBraceCompletionAtPostion(openingBrace: string) {
2906+
this.state.verifyBraceCompletionAtPostion(this.negative, openingBrace);
2907+
}
28732908
}
28742909

28752910
export class Verify extends VerifyNegatable {
@@ -3088,7 +3123,7 @@ namespace FourSlashInterface {
30883123
this.state.getSemanticDiagnostics(expected);
30893124
}
30903125

3091-
public ProjectInfo(expected: string []) {
3126+
public ProjectInfo(expected: string[]) {
30923127
this.state.verifyProjectInfo(expected);
30933128
}
30943129
}

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,9 @@ namespace Harness.LanguageService {
437437
getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion {
438438
return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position));
439439
}
440+
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean {
441+
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPostion(fileName, position, openingBrace));
442+
}
440443
getEmitOutput(fileName: string): ts.EmitOutput {
441444
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
442445
}

src/server/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,10 @@ namespace ts.server {
568568
throw new Error("Not Implemented Yet.");
569569
}
570570

571+
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean {
572+
throw new Error("Not Implemented Yet.");
573+
}
574+
571575
getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
572576
var lineOffset = this.positionToOneBasedLineOffset(fileName, position);
573577
var args: protocol.FileLocationRequestArgs = {

src/services/services.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,8 @@ namespace ts {
11131113

11141114
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion;
11151115

1116+
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean;
1117+
11161118
getEmitOutput(fileName: string): EmitOutput;
11171119

11181120
getProgram(): Program;
@@ -7446,6 +7448,36 @@ namespace ts {
74467448
return { newText: result, caretOffset: preamble.length };
74477449
}
74487450

7451+
function isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean {
7452+
7453+
// '<' is currently not supported, figuring out if we're in a Generic Type vs. a comparison is too
7454+
// expensive to do during typing scenarios
7455+
// i.e. whether we're dealing with:
7456+
// var x = new foo<| ( with class foo<T>{} )
7457+
// or
7458+
// var y = 3 <|
7459+
if (openingBrace === CharacterCodes.lessThan) {
7460+
return false;
7461+
}
7462+
7463+
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
7464+
7465+
// Check if in a context where we don't want to perform any insertion
7466+
if (isInString(sourceFile, position) || isInComment(sourceFile, position)) {
7467+
return false;
7468+
}
7469+
7470+
if (isInsideJsxElementOrAttribute(sourceFile, position)) {
7471+
return openingBrace === CharacterCodes.openBrace;
7472+
}
7473+
7474+
if (isInTemplateString(sourceFile, position)) {
7475+
return false;
7476+
}
7477+
7478+
return true;
7479+
}
7480+
74497481
function getParametersForJsDocOwningNode(commentOwner: Node): ParameterDeclaration[] {
74507482
if (isFunctionLike(commentOwner)) {
74517483
return commentOwner.parameters;
@@ -7740,6 +7772,7 @@ namespace ts {
77407772
getFormattingEditsForDocument,
77417773
getFormattingEditsAfterKeystroke,
77427774
getDocCommentTemplateAtPosition,
7775+
isValidBraceCompletionAtPostion,
77437776
getEmitOutput,
77447777
getNonBoundSourceFile,
77457778
getProgram

src/services/shims.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ namespace ts {
221221
*/
222222
getDocCommentTemplateAtPosition(fileName: string, position: number): string;
223223

224+
/**
225+
* Returns JSON-encoded boolean to indicate whether we should support brace location
226+
* at the current position.
227+
* E.g. we don't want brace completion inside string-literals, comments, etc.
228+
*/
229+
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): string;
230+
224231
getEmitOutput(fileName: string): string;
225232
}
226233

@@ -733,6 +740,13 @@ namespace ts {
733740
);
734741
}
735742

743+
public isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): string {
744+
return this.forwardJSONCall(
745+
`isValidBraceCompletionAtPostion('${fileName}', ${position}, ${openingBrace})`,
746+
() => this.languageService.isValidBraceCompletionAtPostion(fileName, position, openingBrace)
747+
);
748+
}
749+
736750
/// GET SMART INDENT
737751
public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string {
738752
return this.forwardJSONCall(

src/services/utilities.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,21 +403,61 @@ namespace ts {
403403

404404
export function isInString(sourceFile: SourceFile, position: number) {
405405
let token = getTokenAtPosition(sourceFile, position);
406-
return token && (token.kind === SyntaxKind.StringLiteral || token.kind === SyntaxKind.StringLiteralType) && position > token.getStart();
406+
return token && (token.kind === SyntaxKind.StringLiteral || token.kind === SyntaxKind.StringLiteralType) && position > token.getStart(sourceFile);
407407
}
408408

409409
export function isInComment(sourceFile: SourceFile, position: number) {
410410
return isInCommentHelper(sourceFile, position, /*predicate*/ undefined);
411411
}
412412

413+
/**
414+
* returns true if the position is in between the open and close elements of an JSX expression.
415+
*/
416+
export function isInsideJsxElementOrAttribute(sourceFile: SourceFile, position: number) {
417+
let token = getTokenAtPosition(sourceFile, position);
418+
419+
if (!token) {
420+
return false;
421+
}
422+
423+
// <div>Hello |</div>
424+
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxText) {
425+
return true;
426+
}
427+
428+
// <div> { | </div> or <div a={| </div>
429+
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxExpression) {
430+
return true;
431+
}
432+
433+
// <div> {
434+
// |
435+
// } < /div>
436+
if (token && token.kind === SyntaxKind.CloseBraceToken && token.parent.kind === SyntaxKind.JsxExpression) {
437+
return true;
438+
}
439+
440+
// <div>|</div>
441+
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxClosingElement) {
442+
return true;
443+
}
444+
445+
return false;
446+
}
447+
448+
export function isInTemplateString(sourceFile: SourceFile, position: number) {
449+
let token = getTokenAtPosition(sourceFile, position);
450+
return isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile);
451+
}
452+
413453
/**
414454
* Returns true if the cursor at position in sourceFile is within a comment that additionally
415455
* satisfies predicate, and false otherwise.
416456
*/
417457
export function isInCommentHelper(sourceFile: SourceFile, position: number, predicate?: (c: CommentRange) => boolean): boolean {
418458
let token = getTokenAtPosition(sourceFile, position);
419459

420-
if (token && position <= token.getStart()) {
460+
if (token && position <= token.getStart(sourceFile)) {
421461
let commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos);
422462

423463
// The end marker of a single-line comment does not include the newline character.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// /**
4+
//// * inside jsdoc /*1*/
5+
//// */
6+
//// function f() {
7+
//// // inside regular comment /*2*/
8+
//// var c = "";
9+
////
10+
//// /* inside multi-
11+
//// line comment /*3*/
12+
//// */
13+
//// var y =12;
14+
//// }
15+
16+
goTo.marker('1');
17+
verify.not.isValidBraceCompletionAtPostion('(');
18+
19+
goTo.marker('2');
20+
verify.not.isValidBraceCompletionAtPostion('(');
21+
22+
goTo.marker('3');
23+
verify.not.isValidBraceCompletionAtPostion('(');

tests/cases/fourslash/fourslash.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ declare namespace FourSlashInterface {
136136
typeDefinitionCountIs(expectedCount: number): void;
137137
definitionLocationExists(): void;
138138
verifyDefinitionsName(name: string, containerName: string): void;
139+
isValidBraceCompletionAtPostion(openingBrace?: string): void;
139140
}
140141
class verify extends verifyNegatable {
141142
assertHasRanges(ranges: FourSlash.Range[]): void;
@@ -173,6 +174,7 @@ declare namespace FourSlashInterface {
173174
noMatchingBracePositionInCurrentFile(bracePosition: number): void;
174175
DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void;
175176
noDocCommentTemplate(): void;
177+
176178
getScriptLexicalStructureListCount(count: number): void;
177179
getScriptLexicalStructureListContains(name: string, kind: string, fileName?: string, parentName?: string, isAdditionalSpan?: boolean, markerPosition?: number): void;
178180
navigationItemsListCount(count: number, searchValue: string, matchKind?: string): void;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//@Filename: file.tsx
4+
//// declare var React: any;
5+
////
6+
//// var x = <div a="/*2*/" b={/*3*/}>
7+
//// /*1*/
8+
//// </div>;
9+
//// var y = <div>/*4*/</div>
10+
//// var z = <div>
11+
//// hello /*5*/
12+
//// </div>
13+
//// var z2 = <div> { /*6*/
14+
//// </div>
15+
//// var z3 = <div>
16+
//// {
17+
//// /*7*/
18+
//// }
19+
//// </div>
20+
21+
goTo.marker('1');
22+
verify.not.isValidBraceCompletionAtPostion('(');
23+
verify.isValidBraceCompletionAtPostion('{');
24+
25+
goTo.marker('2');
26+
verify.not.isValidBraceCompletionAtPostion('(');
27+
verify.not.isValidBraceCompletionAtPostion('{');
28+
29+
goTo.marker('3');
30+
verify.not.isValidBraceCompletionAtPostion('(');
31+
verify.isValidBraceCompletionAtPostion('{');
32+
33+
goTo.marker('4');
34+
verify.not.isValidBraceCompletionAtPostion('(');
35+
verify.isValidBraceCompletionAtPostion('{');
36+
37+
goTo.marker('5');
38+
verify.not.isValidBraceCompletionAtPostion('(');
39+
verify.isValidBraceCompletionAtPostion('{');
40+
41+
goTo.marker('6');
42+
verify.not.isValidBraceCompletionAtPostion('(');
43+
verify.isValidBraceCompletionAtPostion('{');
44+
45+
goTo.marker('7');
46+
verify.not.isValidBraceCompletionAtPostion('(');
47+
verify.isValidBraceCompletionAtPostion('{');
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// var x = "/*1*/";
4+
//// var x = '/*2*/';
5+
//// var x = "hello \
6+
//// /*3*/";
7+
8+
goTo.marker('1');
9+
verify.not.isValidBraceCompletionAtPostion('(');
10+
11+
goTo.marker('2');
12+
verify.not.isValidBraceCompletionAtPostion('(');
13+
14+
goTo.marker('3');
15+
verify.not.isValidBraceCompletionAtPostion('(');
16+

0 commit comments

Comments
 (0)