From 22f2b118b724013e146265a34cac71638f48ade6 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 20 Apr 2018 10:25:58 -0700 Subject: [PATCH] Add 'triggerCharacter' option for completions requests (#23491) * Add 'triggerCharacter' option for completions requests * mhegazy code review * More code review * Handle '<' in comment --- src/harness/fourslash.ts | 3 ++ src/server/protocol.ts | 1 + src/server/session.ts | 1 + src/services/completions.ts | 30 +++++++++++++- src/services/services.ts | 3 +- src/services/shims.ts | 2 +- src/services/types.ts | 3 +- .../reference/api/tsserverlibrary.d.ts | 4 +- tests/baselines/reference/api/typescript.d.ts | 3 +- .../fourslash/completionsTriggerCharacter.ts | 39 +++++++++++++++++++ tests/cases/fourslash/fourslash.ts | 2 + 11 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 tests/cases/fourslash/completionsTriggerCharacter.ts diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 75915c915ed..f65681909fa 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -842,6 +842,7 @@ namespace FourSlash { const actualCompletions = this.getCompletionListAtCaret(options); if (!actualCompletions) { + if (expected === undefined) return; this.raiseError(`No completions at position '${this.currentCaretPosition}'.`); } @@ -4653,10 +4654,12 @@ namespace FourSlashInterface { export type ExpectedCompletionEntry = string | { name: string, insertText?: string, replacementSpan?: FourSlash.Range }; export interface CompletionsAtOptions extends Partial { + triggerCharacter?: string; isNewIdentifierLocation?: boolean; } export interface VerifyCompletionListContainsOptions extends ts.UserPreferences { + triggerCharacter?: string; sourceDisplay: string; isRecommended?: true; insertText?: string; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 7a258a904c2..55ad2a6c36b 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1749,6 +1749,7 @@ namespace ts.server.protocol { * Optional prefix to apply to possible completions. */ prefix?: string; + triggerCharacter?: string; /** * @deprecated Use UserPreferences.includeCompletionsForModuleExports */ diff --git a/src/server/session.ts b/src/server/session.ts index 27044ec369d..363369c4057 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1287,6 +1287,7 @@ namespace ts.server { const completions = project.getLanguageService().getCompletionsAtPosition(file, position, { ...this.getPreferences(file), + triggerCharacter: args.triggerCharacter, includeExternalModuleExports: args.includeExternalModuleExports, includeInsertTextCompletions: args.includeInsertTextCompletions }); diff --git a/src/services/completions.ts b/src/services/completions.ts index 3100e4a13a9..f887d28cf10 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -25,7 +25,7 @@ namespace ts.Completions { const enum GlobalsSearch { Continue, Success, Fail } - export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences): CompletionInfo | undefined { + export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences, triggerCharacter: string | undefined): CompletionInfo | undefined { const typeChecker = program.getTypeChecker(); const compilerOptions = program.getCompilerOptions(); if (isInReferenceComment(sourceFile, position)) { @@ -34,6 +34,7 @@ namespace ts.Completions { } const contextToken = findPrecedingToken(position, sourceFile); + if (triggerCharacter && !isValidTrigger(sourceFile, triggerCharacter, contextToken, position)) return undefined; if (isInString(sourceFile, position, contextToken)) { return !contextToken || !isStringLiteralLike(contextToken) @@ -2197,4 +2198,31 @@ namespace ts.Completions { function hasIndexSignature(type: Type): boolean { return !!type.getStringIndexType() || !!type.getNumberIndexType(); } + + function isValidTrigger(sourceFile: SourceFile, triggerCharacter: string, contextToken: Node, position: number): boolean { + switch (triggerCharacter) { + case '"': + case "'": + case "`": + // Only automatically bring up completions if this is an opening quote. + return isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1; + case "<": + // Opening JSX tag + return contextToken.kind === SyntaxKind.LessThanToken && contextToken.parent.kind !== SyntaxKind.BinaryExpression; + default: + return Debug.fail(triggerCharacter); + } + } + + function isStringLiteralOrTemplate(node: Node): node is StringLiteralLike | TemplateExpression | TaggedTemplateExpression { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateExpression: + case SyntaxKind.TaggedTemplateExpression: + return true; + default: + return false; + } + } } diff --git a/src/services/services.ts b/src/services/services.ts index a192231405a..d395f1cdbc0 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1409,7 +1409,8 @@ namespace ts { log, getValidSourceFile(fileName), position, - fullPreferences); + fullPreferences, + options.triggerCharacter); } function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = defaultPreferences): CompletionEntryDetails { diff --git a/src/services/shims.ts b/src/services/shims.ts index b8beccab20b..d0735cc1717 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -912,7 +912,7 @@ namespace ts { * to provide at the given source position and providing a member completion * list if requested. */ - public getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined) { + public getCompletionsAtPosition(fileName: string, position: number, preferences: GetCompletionsAtPositionOptions | undefined) { return this.forwardJSONCall( `getCompletionsAtPosition('${fileName}', ${position}, ${preferences})`, () => this.languageService.getCompletionsAtPosition(fileName, position, preferences) diff --git a/src/services/types.ts b/src/services/types.ts index 01b8b5ad4d4..1c4a38b10c6 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -354,8 +354,9 @@ namespace ts { export type OrganizeImportsScope = CombinedCodeFixScope; - /** @deprecated Use UserPreferences */ export interface GetCompletionsAtPositionOptions extends UserPreferences { + /** If the editor is asking for completions because a certain character was typed, and not because the user explicitly requested them, this should be set. */ + triggerCharacter?: string; /** @deprecated Use includeCompletionsForModuleExports */ includeExternalModuleExports?: boolean; /** @deprecated Use includeCompletionsWithInsertText */ diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index b9cf50f14d9..0fc77ba2afa 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4455,8 +4455,9 @@ declare namespace ts { fileName: string; } type OrganizeImportsScope = CombinedCodeFixScope; - /** @deprecated Use UserPreferences */ interface GetCompletionsAtPositionOptions extends UserPreferences { + /** If the editor is asking for completions because a certain character was typed, and not because the user explicitly requested them, this should be set. */ + triggerCharacter?: string; /** @deprecated Use includeCompletionsForModuleExports */ includeExternalModuleExports?: boolean; /** @deprecated Use includeCompletionsWithInsertText */ @@ -6667,6 +6668,7 @@ declare namespace ts.server.protocol { * Optional prefix to apply to possible completions. */ prefix?: string; + triggerCharacter?: string; /** * @deprecated Use UserPreferences.includeCompletionsForModuleExports */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index f5114e2397c..94ece008de1 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4455,8 +4455,9 @@ declare namespace ts { fileName: string; } type OrganizeImportsScope = CombinedCodeFixScope; - /** @deprecated Use UserPreferences */ interface GetCompletionsAtPositionOptions extends UserPreferences { + /** If the editor is asking for completions because a certain character was typed, and not because the user explicitly requested them, this should be set. */ + triggerCharacter?: string; /** @deprecated Use includeCompletionsForModuleExports */ includeExternalModuleExports?: boolean; /** @deprecated Use includeCompletionsWithInsertText */ diff --git a/tests/cases/fourslash/completionsTriggerCharacter.ts b/tests/cases/fourslash/completionsTriggerCharacter.ts new file mode 100644 index 00000000000..06e2a123c52 --- /dev/null +++ b/tests/cases/fourslash/completionsTriggerCharacter.ts @@ -0,0 +1,39 @@ +/// + +// @jsx: preserve + +////const x: "a" | "b" = "/*openQuote*/"/*closeQuote*/; +////const y: 'a' | 'b' = '/*openSingleQuote*/'/*closeSingleQuote*/; +////const z: 'a' | 'b' = `/*openTemplate*/`/*closeTemplate*/; +////const q: "`a`" | "`b`" = "`/*openTemplateInQuote*/a`/*closeTemplateInQuote*/"; + +////// "/*quoteInComment*/