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
This commit is contained in:
Paul van Brenk
2016-04-15 11:38:42 -07:00
parent 19a9f7f82d
commit 96deb553d5
12 changed files with 267 additions and 11 deletions

View File

@@ -655,7 +655,7 @@ namespace FourSlash {
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind);
}
else {
this.raiseError(`No completions at position '${ this.currentCaretPosition }' when looking for '${ symbol }'.`);
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
}
}
@@ -1758,13 +1758,13 @@ namespace FourSlash {
const actual = (<ts.server.SessionClient>this.languageService).getProjectInfo(
this.activeFile.fileName,
/* needFileNameList */ true
);
);
assert.equal(
expected.join(","),
actual.fileNames.map( file => {
actual.fileNames.map(file => {
return file.replace(this.basePath + "/", "");
}).join(",")
);
}).join(",")
);
}
}
@@ -1850,6 +1850,37 @@ namespace FourSlash {
});
}
public verifyBraceCompletionAtPostion(negative: boolean, openingBrace: string) {
const openBraceMap: ts.Map<ts.CharacterCodes> = {
"(": ts.CharacterCodes.openParen,
"{": ts.CharacterCodes.openBrace,
"[": ts.CharacterCodes.openBracket,
"'": ts.CharacterCodes.singleQuote,
'"': ts.CharacterCodes.doubleQuote,
"`": ts.CharacterCodes.backtick,
"<": ts.CharacterCodes.lessThan
};
const charCode = openBraceMap[openingBrace];
if (!charCode) {
this.raiseError(`Invalid openingBrace '${openingBrace}' specified.`);
}
const position = this.currentCaretPosition;
const validBraceCompletion = this.languageService.isValidBraceCompletionAtPostion(this.activeFile.fileName, position, charCode);
if (!negative && !validBraceCompletion) {
this.raiseError(`${position} is not a valid brace completion position for ${openingBrace}`);
}
if (negative && validBraceCompletion) {
this.raiseError(`${position} is a valid brace completion position for ${openingBrace}`);
}
}
public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) {
const actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition);
@@ -2239,7 +2270,7 @@ namespace FourSlash {
};
const host = Harness.Compiler.createCompilerHost(
[ fourslashFile, testFile ],
[fourslashFile, testFile],
(fn, contents) => result = contents,
ts.ScriptTarget.Latest,
Harness.IO.useCaseSensitiveFileNames(),
@@ -2264,7 +2295,7 @@ namespace FourSlash {
function runCode(code: string, state: TestState): void {
// Compile and execute the test
const wrappedCode =
`(function(test, goTo, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) {
`(function(test, goTo, verify, edit, debug, format, cancellation, classification, verifyOperationIsCancelled) {
${code}
})`;
try {
@@ -2378,7 +2409,7 @@ ${code}
}
}
}
// TODO: should be '==='?
// TODO: should be '==='?
}
else if (line == "" || lineLength === 0) {
// Previously blank lines between fourslash content caused it to be considered as 2 files,
@@ -2870,6 +2901,10 @@ namespace FourSlashInterface {
public verifyDefinitionsName(name: string, containerName: string) {
this.state.verifyDefinitionsName(this.negative, name, containerName);
}
public isValidBraceCompletionAtPostion(openingBrace: string) {
this.state.verifyBraceCompletionAtPostion(this.negative, openingBrace);
}
}
export class Verify extends VerifyNegatable {
@@ -3088,7 +3123,7 @@ namespace FourSlashInterface {
this.state.getSemanticDiagnostics(expected);
}
public ProjectInfo(expected: string []) {
public ProjectInfo(expected: string[]) {
this.state.verifyProjectInfo(expected);
}
}

View File

@@ -437,6 +437,9 @@ namespace Harness.LanguageService {
getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion {
return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position));
}
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean {
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPostion(fileName, position, openingBrace));
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}

View File

@@ -568,6 +568,10 @@ namespace ts.server {
throw new Error("Not Implemented Yet.");
}
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean {
throw new Error("Not Implemented Yet.");
}
getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
var lineOffset = this.positionToOneBasedLineOffset(fileName, position);
var args: protocol.FileLocationRequestArgs = {

View File

@@ -1113,6 +1113,8 @@ namespace ts {
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion;
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean;
getEmitOutput(fileName: string): EmitOutput;
getProgram(): Program;
@@ -7446,6 +7448,36 @@ namespace ts {
return { newText: result, caretOffset: preamble.length };
}
function isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): boolean {
// '<' is currently not supported, figuring out if we're in a Generic Type vs. a comparison is too
// expensive to do during typing scenarios
// i.e. whether we're dealing with:
// var x = new foo<| ( with class foo<T>{} )
// or
// var y = 3 <|
if (openingBrace === CharacterCodes.lessThan) {
return false;
}
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
// Check if in a context where we don't want to perform any insertion
if (isInString(sourceFile, position) || isInComment(sourceFile, position)) {
return false;
}
if (isInsideJsxElementOrAttribute(sourceFile, position)) {
return openingBrace === CharacterCodes.openBrace;
}
if (isInTemplateString(sourceFile, position)) {
return false;
}
return true;
}
function getParametersForJsDocOwningNode(commentOwner: Node): ParameterDeclaration[] {
if (isFunctionLike(commentOwner)) {
return commentOwner.parameters;
@@ -7740,6 +7772,7 @@ namespace ts {
getFormattingEditsForDocument,
getFormattingEditsAfterKeystroke,
getDocCommentTemplateAtPosition,
isValidBraceCompletionAtPostion,
getEmitOutput,
getNonBoundSourceFile,
getProgram

View File

@@ -221,6 +221,13 @@ namespace ts {
*/
getDocCommentTemplateAtPosition(fileName: string, position: number): string;
/**
* Returns JSON-encoded boolean to indicate whether we should support brace location
* at the current position.
* E.g. we don't want brace completion inside string-literals, comments, etc.
*/
isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): string;
getEmitOutput(fileName: string): string;
}
@@ -733,6 +740,13 @@ namespace ts {
);
}
public isValidBraceCompletionAtPostion(fileName: string, position: number, openingBrace: number): string {
return this.forwardJSONCall(
`isValidBraceCompletionAtPostion('${fileName}', ${position}, ${openingBrace})`,
() => this.languageService.isValidBraceCompletionAtPostion(fileName, position, openingBrace)
);
}
/// GET SMART INDENT
public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string {
return this.forwardJSONCall(

View File

@@ -403,13 +403,53 @@ namespace ts {
export function isInString(sourceFile: SourceFile, position: number) {
let token = getTokenAtPosition(sourceFile, position);
return token && (token.kind === SyntaxKind.StringLiteral || token.kind === SyntaxKind.StringLiteralType) && position > token.getStart();
return token && (token.kind === SyntaxKind.StringLiteral || token.kind === SyntaxKind.StringLiteralType) && position > token.getStart(sourceFile);
}
export function isInComment(sourceFile: SourceFile, position: number) {
return isInCommentHelper(sourceFile, position, /*predicate*/ undefined);
}
/**
* returns true if the position is in between the open and close elements of an JSX expression.
*/
export function isInsideJsxElementOrAttribute(sourceFile: SourceFile, position: number) {
let token = getTokenAtPosition(sourceFile, position);
if (!token) {
return false;
}
// <div>Hello |</div>
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxText) {
return true;
}
// <div> { | </div> or <div a={| </div>
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxExpression) {
return true;
}
// <div> {
// |
// } < /div>
if (token && token.kind === SyntaxKind.CloseBraceToken && token.parent.kind === SyntaxKind.JsxExpression) {
return true;
}
// <div>|</div>
if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxClosingElement) {
return true;
}
return false;
}
export function isInTemplateString(sourceFile: SourceFile, position: number) {
let token = getTokenAtPosition(sourceFile, position);
return isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile);
}
/**
* Returns true if the cursor at position in sourceFile is within a comment that additionally
* satisfies predicate, and false otherwise.
@@ -417,7 +457,7 @@ namespace ts {
export function isInCommentHelper(sourceFile: SourceFile, position: number, predicate?: (c: CommentRange) => boolean): boolean {
let token = getTokenAtPosition(sourceFile, position);
if (token && position <= token.getStart()) {
if (token && position <= token.getStart(sourceFile)) {
let commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos);
// The end marker of a single-line comment does not include the newline character.