From 377eabe614740c071bd062fd2c3a9ac0e885b80e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 8 Sep 2017 16:21:50 -0700 Subject: [PATCH] Add optional CompletionContext to provideCompletionItems Fixes #752 Adds a new overload of `provideCompletionItems` that takes a context argument. This context is currently used to provide the trigger character of the suggestion --- src/vs/editor/common/modes.ts | 9 ++- .../editor/contrib/suggest/browser/suggest.ts | 6 +- .../suggest/browser/suggestController.ts | 2 +- .../contrib/suggest/browser/suggestModel.ts | 24 +++++--- .../suggest/test/browser/suggestModel.test.ts | 56 +++++++++++++++---- .../standalone/browser/standaloneLanguages.ts | 34 +++++++++-- src/vs/monaco.d.ts | 19 ++++++- src/vs/vscode.d.ts | 18 +++++- .../mainThreadLanguageFeatures.ts | 4 +- src/vs/workbench/api/node/extHost.protocol.ts | 2 +- .../api/node/extHostLanguageFeatures.ts | 13 +++-- .../parts/debug/electron-browser/repl.ts | 2 +- 12 files changed, 148 insertions(+), 41 deletions(-) diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index a67046cf0e4..b711553c164 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -247,6 +247,13 @@ export interface ISuggestResult { dispose?(): void; } +/** + * @internal + */ +export interface SuggestContext { + triggerCharacter?: string; +} + /** * @internal */ @@ -254,7 +261,7 @@ export interface ISuggestSupport { triggerCharacters?: string[]; - provideCompletionItems(model: editorCommon.IModel, position: Position, token: CancellationToken): ISuggestResult | Thenable; + provideCompletionItems(model: editorCommon.IModel, position: Position, context: SuggestContext, token: CancellationToken): ISuggestResult | Thenable; resolveCompletionItem?(model: editorCommon.IModel, position: Position, item: ISuggestion, token: CancellationToken): ISuggestion | Thenable; } diff --git a/src/vs/editor/contrib/suggest/browser/suggest.ts b/src/vs/editor/contrib/suggest/browser/suggest.ts index 3831064a10c..b899c5ffd34 100644 --- a/src/vs/editor/contrib/suggest/browser/suggest.ts +++ b/src/vs/editor/contrib/suggest/browser/suggest.ts @@ -12,7 +12,7 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { IModel, IEditorContribution, ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; -import { ISuggestResult, ISuggestSupport, ISuggestion, SuggestRegistry } from 'vs/editor/common/modes'; +import { ISuggestResult, ISuggestSupport, ISuggestion, SuggestRegistry, SuggestContext } from 'vs/editor/common/modes'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -42,7 +42,7 @@ export function setSnippetSuggestSupport(support: ISuggestSupport): ISuggestSupp return old; } -export function provideSuggestionItems(model: IModel, position: Position, snippetConfig: SnippetConfig = 'bottom', onlyFrom?: ISuggestSupport[]): TPromise { +export function provideSuggestionItems(model: IModel, position: Position, snippetConfig: SnippetConfig = 'bottom', onlyFrom?: ISuggestSupport[], context?: SuggestContext): TPromise { const allSuggestions: ISuggestionItem[] = []; const acceptSuggestion = createSuggesionFilter(snippetConfig); @@ -73,7 +73,7 @@ export function provideSuggestionItems(model: IModel, position: Position, snippe return undefined; } - return asWinJsPromise(token => support.provideCompletionItems(model, position, token)).then(container => { + return asWinJsPromise(token => support.provideCompletionItems(model, position, context || {}, token)).then(container => { const len = allSuggestions.length; diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 37a5ad50ff3..6f7fb7e9add 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -209,7 +209,7 @@ export class SuggestController implements IEditorContribution { } triggerSuggest(onlyFrom?: ISuggestSupport[]): void { - this._model.trigger(false, false, onlyFrom); + this._model.trigger({ auto: false }, false, onlyFrom); this._editor.revealLine(this._editor.getPosition().lineNumber, ScrollType.Smooth); this._editor.focus(); } diff --git a/src/vs/editor/contrib/suggest/browser/suggestModel.ts b/src/vs/editor/contrib/suggest/browser/suggestModel.ts index 69b20642956..49e513c551c 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestModel.ts @@ -31,6 +31,11 @@ export interface ISuggestEvent { auto: boolean; } +export interface SuggestTriggerContext { + auto: boolean; + triggerCharacter?: string; +} + export class LineContext { static shouldAutoTrigger(editor: ICommonCodeEditor): boolean { @@ -201,7 +206,7 @@ export class SuggestModel implements IDisposable { } } } - this.trigger(true, Boolean(this.completionModel), supports, items); + this.trigger({ auto: true, triggerCharacter: lastChar }, Boolean(this.completionModel), supports, items); } }); } @@ -237,7 +242,7 @@ export class SuggestModel implements IDisposable { if (!SuggestRegistry.has(this.editor.getModel())) { this.cancel(); } else { - this.trigger(this._state === State.Auto, true); + this.trigger({ auto: this._state === State.Auto }, true); } } } @@ -311,7 +316,7 @@ export class SuggestModel implements IDisposable { } this.triggerAutoSuggestPromise = null; - this.trigger(true); + this.trigger({ auto: true }); }); } } @@ -326,14 +331,14 @@ export class SuggestModel implements IDisposable { } } - public trigger(auto: boolean, retrigger: boolean = false, onlyFrom?: ISuggestSupport[], existingItems?: ISuggestionItem[]): void { + public trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: ISuggestSupport[], existingItems?: ISuggestionItem[]): void { const model = this.editor.getModel(); if (!model) { return; } - + const auto = context.auto; const ctx = new LineContext(model, this.editor.getPosition(), auto); if (!LineContext.isInEditableRange(this.editor)) { @@ -350,7 +355,8 @@ export class SuggestModel implements IDisposable { this.requestPromise = provideSuggestionItems(model, this.editor.getPosition(), this.editor.getConfiguration().contribInfo.snippetSuggestions, - onlyFrom + onlyFrom, + context ).then(items => { this.requestPromise = null; @@ -394,7 +400,7 @@ export class SuggestModel implements IDisposable { if (ctx.column < this.context.column) { // typed -> moved cursor LEFT -> retrigger if still on a word if (ctx.leadingWord.word) { - this.trigger(this.context.auto, true); + this.trigger({ auto: this.context.auto }, true); } else { this.cancel(); } @@ -409,7 +415,7 @@ export class SuggestModel implements IDisposable { if (ctx.column > this.context.column && this.completionModel.incomplete && ctx.leadingWord.word.length !== 0) { // typed -> moved cursor RIGHT & incomple model & still on a word -> retrigger const { complete, incomplete } = this.completionModel.resolveIncompleteInfo(); - this.trigger(this._state === State.Auto, true, incomplete, complete); + this.trigger({ auto: this._state === State.Auto }, true, incomplete, complete); } else { // typed -> moved cursor RIGHT -> update UI @@ -425,7 +431,7 @@ export class SuggestModel implements IDisposable { if (LineContext.shouldAutoTrigger(this.editor) && this.context.leadingWord.endColumn < ctx.leadingWord.startColumn) { // retrigger when heading into a new word - this.trigger(this.context.auto, true); + this.trigger({ auto: this.context.auto }, true); return; } diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index 6bba3555808..9ba1e83a9a1 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -149,25 +149,25 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { // cancel on trigger assertEvent(model.onDidCancel, function () { - model.trigger(false); + model.trigger({ auto: false }); }, function (event) { assert.equal(event.retrigger, false); }), assertEvent(model.onDidCancel, function () { - model.trigger(false, true); + model.trigger({ auto: false }, true); }, function (event) { assert.equal(event.retrigger, true); }), assertEvent(model.onDidTrigger, function () { - model.trigger(true); + model.trigger({ auto: true }); }, function (event) { assert.equal(event.auto, true); }), assertEvent(model.onDidTrigger, function () { - model.trigger(false); + model.trigger({ auto: false }); }, function (event) { assert.equal(event.auto, false); }) @@ -183,12 +183,12 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { return withOracle(model => { return TPromise.join([ assertEvent(model.onDidCancel, function () { - model.trigger(true); + model.trigger({ auto: true }); }, function (event) { assert.equal(event.retrigger, false); }), assertEvent(model.onDidSuggest, function () { - model.trigger(false); + model.trigger({ auto: false }); }, function (event) { assert.equal(event.auto, false); assert.equal(event.isFrozen, false); @@ -239,7 +239,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { return assertEvent(model.onDidSuggest, () => { // make sure completionModel starts here! - model.trigger(true); + model.trigger({ auto: true }); }, event => { return assertEvent(model.onDidSuggest, () => { @@ -338,7 +338,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { editor.setPosition({ lineNumber: 1, column: 3 }); return assertEvent(model.onDidSuggest, () => { - model.trigger(false); + model.trigger({ auto: false }); }, event => { assert.equal(event.auto, false); assert.equal(event.isFrozen, false); @@ -363,7 +363,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { editor.setPosition({ lineNumber: 1, column: 3 }); return assertEvent(model.onDidSuggest, () => { - model.trigger(false); + model.trigger({ auto: false }); }, event => { assert.equal(event.auto, false); assert.equal(event.isFrozen, false); @@ -400,7 +400,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { editor.setPosition({ lineNumber: 1, column: 4 }); return assertEvent(model.onDidSuggest, () => { - model.trigger(false); + model.trigger({ auto: false }); }, event => { assert.equal(event.auto, false); assert.equal(event.completionModel.incomplete, true); @@ -437,7 +437,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { editor.setPosition({ lineNumber: 1, column: 4 }); return assertEvent(model.onDidSuggest, () => { - model.trigger(false); + model.trigger({ auto: false }); }, event => { assert.equal(event.auto, false); assert.equal(event.completionModel.incomplete, true); @@ -457,4 +457,38 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { }); }); }); + + test('Trigger characters is provided in suggest context', function () { + let triggerCharacter = ''; + disposables.push(SuggestRegistry.register({ scheme: 'test' }, { + triggerCharacters: ['.'], + provideCompletionItems(doc, pos, context) { + triggerCharacter = context.triggerCharacter; + return { + currentWord: '', + incomplete: false, + suggestions: [ + { + label: 'foo.bar', + type: 'property', + insertText: 'foo.bar', + overwriteBefore: pos.column - 1 + } + ] + }; + } + })); + + model.setValue(''); + + return withOracle((model, editor) => { + + return assertEvent(model.onDidSuggest, () => { + editor.setPosition({ lineNumber: 1, column: 1 }); + editor.trigger('keyboard', Handler.Type, { text: 'foo.' }); + }, event => { + assert.equal(triggerCharacter, '.'); + }); + }); + }); }); diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index d97e850f41c..41c1b68becc 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -373,8 +373,8 @@ export function registerCompletionItemProvider(languageId: string, provider: Com let adapter = new SuggestAdapter(provider); return modes.SuggestRegistry.register(languageId, { triggerCharacters: provider.triggerCharacters, - provideCompletionItems: (model: editorCommon.IReadOnlyModel, position: Position, token: CancellationToken): Thenable => { - return adapter.provideCompletionItems(model, position, token); + provideCompletionItems: (model: editorCommon.IReadOnlyModel, position: Position, context: modes.SuggestContext, token: CancellationToken): Thenable => { + return adapter.provideCompletionItems(model, position, context, token); }, resolveCompletionItem: (model: editorCommon.IReadOnlyModel, position: Position, suggestion: modes.ISuggestion, token: CancellationToken): Thenable => { return adapter.resolveCompletionItem(model, position, suggestion, token); @@ -537,6 +537,23 @@ export interface CompletionList { */ items: CompletionItem[]; } + +/** + * Contains additional information about the context in which + * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + */ +export interface CompletionContext { + /** + * Character that triggered the completion item provider. + * + * Undefined if provider was not triggered by a character. + */ + triggerCharacter?: string; +} + +export type ProviderCompletionItems = (document: editorCommon.IReadOnlyModel, position: Position, token: CancellationToken) => CompletionItem[] | Thenable | CompletionList | Thenable; +export type ProviderCompletionItemsForContext = (document: editorCommon.IReadOnlyModel, position: Position, context: CompletionContext, token: CancellationToken) => CompletionItem[] | Thenable | CompletionList | Thenable; + /** * The completion item provider interface defines the contract between extensions and * the [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). @@ -553,7 +570,7 @@ export interface CompletionItemProvider { /** * Provide completion items for the given position and document. */ - provideCompletionItems(model: editorCommon.IReadOnlyModel, position: Position, token: CancellationToken): CompletionItem[] | Thenable | CompletionList | Thenable; + provideCompletionItems: ProviderCompletionItems | ProviderCompletionItemsForContext; /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) * or [details](#CompletionItem.detail). @@ -639,9 +656,14 @@ class SuggestAdapter { return suggestion; } - provideCompletionItems(model: editorCommon.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { - - return toThenable(this._provider.provideCompletionItems(model, position, token)).then(value => { + provideCompletionItems(model: editorCommon.IReadOnlyModel, position: Position, context: modes.SuggestContext, token: CancellationToken): Thenable { + let request: any; + if (this._provider.provideCompletionItems.length <= 3) { + request = (this._provider.provideCompletionItems as ProviderCompletionItems)(model, position, token); + } else { + request = (this._provider.provideCompletionItems as ProviderCompletionItemsForContext)(model, position, context, token); + } + return toThenable(request).then(value => { const result: modes.ISuggestResult = { suggestions: [] }; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 09311d8c968..58eafd8e633 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4229,6 +4229,23 @@ declare module monaco.languages { items: CompletionItem[]; } + /** + * Contains additional information about the context in which + * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + */ + export interface CompletionContext { + /** + * Character that triggered the completion item provider. + * + * Undefined if provider was not triggered by a character. + */ + triggerCharacter?: string; + } + + export type ProviderCompletionItems = (document: editor.IReadOnlyModel, position: Position, token: CancellationToken) => CompletionItem[] | Thenable | CompletionList | Thenable; + + export type ProviderCompletionItemsForContext = (document: editor.IReadOnlyModel, position: Position, context: CompletionContext, token: CancellationToken) => CompletionItem[] | Thenable | CompletionList | Thenable; + /** * The completion item provider interface defines the contract between extensions and * the [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). @@ -4245,7 +4262,7 @@ declare module monaco.languages { /** * Provide completion items for the given position and document. */ - provideCompletionItems(model: editor.IReadOnlyModel, position: Position, token: CancellationToken): CompletionItem[] | Thenable | CompletionList | Thenable; + provideCompletionItems: ProviderCompletionItems | ProviderCompletionItemsForContext; /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) * or [details](#CompletionItem.detail). diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 47bcdcb64a5..49d7378c9ed 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2718,6 +2718,22 @@ declare module 'vscode' { constructor(items?: CompletionItem[], isIncomplete?: boolean); } + /** + * Contains additional information about the context in which + * [completion provider](#CompletionItemProvider.provideCompletionItems) is triggered. + */ + export interface CompletionContext { + /** + * Character that triggered the completion item provider. + * + * Undefined if provider was not triggered by a character. + */ + readonly triggerCharacter?: string; + } + + export type ProviderCompletionItems = (document: TextDocument, position: Position, token: CancellationToken) => ProviderResult; + export type ProviderCompletionItemsForContext = (document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken) => ProviderResult; + /** * The completion item provider interface defines the contract between extensions and * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense). @@ -2743,7 +2759,7 @@ declare module 'vscode' { * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ - provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideCompletionItems: ProviderCompletionItems | ProviderCompletionItemsForContext; /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 525ded68a57..757bab38cea 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -233,8 +233,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations[handle] = modes.SuggestRegistry.register(selector, { triggerCharacters, - provideCompletionItems: (model: IReadOnlyModel, position: EditorPosition, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideCompletionItems(handle, model.uri, position)).then(result => { + provideCompletionItems: (model: IReadOnlyModel, position: EditorPosition, context: modes.SuggestContext, token: CancellationToken): Thenable => { + return wireCancellationToken(token, this._proxy.$provideCompletionItems(handle, model.uri, position, context)).then(result => { if (!result) { return result; } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 8c981201c27..61ae052f117 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -552,7 +552,7 @@ export interface ExtHostLanguageFeaturesShape { $provideWorkspaceSymbols(handle: number, search: string): TPromise; $resolveWorkspaceSymbol(handle: number, symbol: modes.SymbolInformation): TPromise; $provideRenameEdits(handle: number, resource: URI, position: IPosition, newName: string): TPromise; - $provideCompletionItems(handle: number, resource: URI, position: IPosition): TPromise; + $provideCompletionItems(handle: number, resource: URI, position: IPosition, context: modes.SuggestContext): TPromise; $resolveCompletionItem(handle: number, resource: URI, position: IPosition, suggestion: modes.ISuggestion): TPromise; $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: URI, position: IPosition): TPromise; diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index a6733cc7695..132af499967 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -486,12 +486,17 @@ class SuggestAdapter { this._provider = provider; } - provideCompletionItems(resource: URI, position: IPosition): TPromise { + provideCompletionItems(resource: URI, position: IPosition, context: modes.SuggestContext): TPromise { const doc = this._documents.getDocumentData(resource).document; const pos = TypeConverters.toPosition(position); - return asWinJsPromise(token => this._provider.provideCompletionItems(doc, pos, token)).then(value => { + return asWinJsPromise(token => { + if (this._provider.provideCompletionItems.length <= 3) { + return (this._provider.provideCompletionItems as vscode.ProviderCompletionItems)(doc, pos, token); + } + return (this._provider.provideCompletionItems as vscode.ProviderCompletionItemsForContext)(doc, pos, context, token); + }).then(value => { const _id = this._idPool++; @@ -1001,8 +1006,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideCompletionItems(handle: number, resource: URI, position: IPosition): TPromise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(resource, position)); + $provideCompletionItems(handle: number, resource: URI, position: IPosition, context: modes.SuggestContext): TPromise { + return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(resource, position, context)); } $resolveCompletionItem(handle: number, resource: URI, position: IPosition, suggestion: modes.ISuggestion): TPromise { diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index b097d3a3652..a1a9c8250a5 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -180,7 +180,7 @@ export class Repl extends Panel implements IPrivateReplService { modes.SuggestRegistry.register({ scheme: debug.DEBUG_SCHEME }, { triggerCharacters: ['.'], - provideCompletionItems: (model: IReadOnlyModel, position: Position, token: CancellationToken): Thenable => { + provideCompletionItems: (model: IReadOnlyModel, position: Position, _context: modes.SuggestContext, token: CancellationToken): Thenable => { const word = this.replInput.getModel().getWordAtPosition(position); const overwriteBefore = word ? word.word.length : 0; const text = this.replInput.getModel().getLineContent(position.lineNumber); -- GitLab