mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-18 16:34:36 -05:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user