diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 62e5bb8d593..9e4f4bbf394 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -214,6 +214,7 @@ export interface ISuggestion { filterText?: string; sortText?: string; noAutoAccept?: boolean; + commitCharacters?: string[]; overwriteBefore?: number; overwriteAfter?: number; additionalTextEdits?: editorCommon.ISingleEditOperation[]; diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 31697b381f3..1b9332f32aa 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -7,6 +7,7 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -25,6 +26,47 @@ import { SuggestModel } from '../common/suggestModel'; import { ICompletionItem } from '../common/completionModel'; import { SuggestWidget } from './suggestWidget'; +class AcceptOnCharacterOracle { + + private _disposables: IDisposable[] = []; + + private _activeAcceptCharacters = new Set(); + private _activeItem: ICompletionItem; + + constructor(editor: ICodeEditor, widget: SuggestWidget, accept: (item: ICompletionItem) => any) { + + this._disposables.push(widget.onDidFocus(item => { + if (!item || isFalsyOrEmpty(item.suggestion.commitCharacters)) { + this._activeItem = undefined; + return; + } + + this._activeItem = item; + this._activeAcceptCharacters.clear(); + for (const ch of item.suggestion.commitCharacters) { + this._activeAcceptCharacters.add(ch[0]); + } + })); + + this._disposables.push(editor.onWillType(text => { + if (this._activeItem) { + const ch = text[text.length - 1]; + if (this._activeAcceptCharacters.has(ch)) { + accept(this._activeItem); + } + } + })); + } + + reset(): void { + this._activeItem = undefined; + } + + dispose() { + dispose(this._disposables); + } +} + @editorContribution export class SuggestController implements IEditorContribution { private static ID: string = 'editor.contrib.suggestController'; @@ -59,6 +101,14 @@ export class SuggestController implements IEditorContribution { this.widget = instantiationService.createInstance(SuggestWidget, this.editor); this.toDispose.push(this.widget.onDidSelect(this.onDidSelectItem, this)); + + // Wire up logic to accept a suggestion on certain characters + const autoAcceptOracle = new AcceptOnCharacterOracle(editor, this.widget, item => this.onDidSelectItem(item)); + this.toDispose.push( + this.model.onDidCancel(autoAcceptOracle.reset, autoAcceptOracle), + this.model.onDidTrigger(autoAcceptOracle.reset, autoAcceptOracle), + autoAcceptOracle + ); } getId(): string { diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index b9ec550e7eb..6353559e052 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -293,7 +293,7 @@ export class SuggestWidget implements IContentWidget, IDelegate static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions."); // Editor.IContentWidget.allowEditorOverflow - allowEditorOverflow = true; + readonly allowEditorOverflow = true; private state: State; private isAuto: boolean; @@ -312,12 +312,16 @@ export class SuggestWidget implements IContentWidget, IDelegate private suggestWidgetMultipleSuggestions: IContextKey; private suggestionSupportsAutoAccept: IContextKey; - private onDidSelectEmitter = new Emitter(); - private editorBlurTimeout: TPromise; private showTimeout: TPromise; private toDispose: IDisposable[]; + private onDidSelectEmitter = new Emitter(); + private onDidFocusEmitter = new Emitter(); + + readonly onDidSelect: Event = this.onDidSelectEmitter.event; + readonly onDidFocus: Event = this.onDidFocusEmitter.event; + constructor( private editor: ICodeEditor, @ITelemetryService private telemetryService: ITelemetryService, @@ -472,6 +476,9 @@ export class SuggestWidget implements IContentWidget, IDelegate }) .then(null, err => !isPromiseCanceledError(err) && onUnexpectedError(err)) .then(() => this.currentSuggestionDetails = null); + + // emit an event + this.onDidFocusEmitter.fire(item); } private setState(state: State): void { @@ -528,10 +535,6 @@ export class SuggestWidget implements IContentWidget, IDelegate } } - get onDidSelect(): Event { - return this.onDidSelectEmitter.event; - } - showTriggered(auto: boolean) { if (this.state !== State.Hidden) { return; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 5a7cc0c5389..0c7d9af1f2a 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2396,6 +2396,13 @@ declare module 'vscode' { */ range?: Range; + /** + * An optional set of characters that when pressed while this completion is active will accept it first and + * then insert type that character. *Note* that all commit characters should have `length=1` and that superfluous + * characters will be ignored. + */ + commitCharacters?: string[]; + /** * @deprecated **Deprecated** in favor of `CompletionItem.insertText` and `CompletionItem.range`. * @@ -2814,7 +2821,7 @@ declare module 'vscode' { /** * Readable dictionary that backs this configuration. */ - readonly [key: string]: any; + readonly[key: string]: any; } /** diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 38706f8abb2..6128e8165c2 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -514,7 +514,8 @@ class SuggestAdapter { // insertText: undefined, additionalTextEdits: item.additionalTextEdits && item.additionalTextEdits.map(TypeConverters.TextEdit.from), - command: this._commands.toInternal(item.command) + command: this._commands.toInternal(item.command), + commitCharacters: item.commitCharacters }; // 'insertText'-logic