Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestController.ts
4797 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { alert } from '../../../../base/browser/ui/aria/aria.js';6import { isNonEmptyArray } from '../../../../base/common/arrays.js';7import { CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js';9import { Emitter, Event } from '../../../../base/common/event.js';10import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';11import { DisposableStore, dispose, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';12import { StopWatch } from '../../../../base/common/stopwatch.js';13import { assertType, isObject } from '../../../../base/common/types.js';14import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js';15import { ICodeEditor } from '../../../browser/editorBrowser.js';16import { EditorAction, EditorCommand, EditorContributionInstantiation, registerEditorAction, registerEditorCommand, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';17import { EditorOption } from '../../../common/config/editorOptions.js';18import { EditOperation } from '../../../common/core/editOperation.js';19import { IPosition, Position } from '../../../common/core/position.js';20import { Range } from '../../../common/core/range.js';21import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';22import { EditorContextKeys } from '../../../common/editorContextKeys.js';23import { ITextModel, TrackedRangeStickiness } from '../../../common/model.js';24import { CompletionItemInsertTextRule, CompletionItemProvider, CompletionTriggerKind, ProviderId } from '../../../common/languages.js';25import { SnippetController2 } from '../../snippet/browser/snippetController2.js';26import { SnippetParser } from '../../snippet/browser/snippetParser.js';27import { ISuggestMemoryService } from './suggestMemory.js';28import { WordContextKey } from './wordContextKey.js';29import * as nls from '../../../../nls.js';30import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';31import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';32import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';33import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';34import { ILogService } from '../../../../platform/log/common/log.js';35import { CompletionItem, Context as SuggestContext, ISuggestItemPreselector, suggestWidgetStatusbarMenu } from './suggest.js';36import { SuggestAlternatives } from './suggestAlternatives.js';37import { CommitCharacterController } from './suggestCommitCharacters.js';38import { State, SuggestModel } from './suggestModel.js';39import { OvertypingCapturer } from './suggestOvertypingCapturer.js';40import { ISelectedSuggestion, SuggestWidget } from './suggestWidget.js';41import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';42import { basename, extname } from '../../../../base/common/resources.js';43import { hash } from '../../../../base/common/hash.js';44import { WindowIdleValue, getWindow } from '../../../../base/browser/dom.js';45import { ModelDecorationOptions } from '../../../common/model/textModel.js';46import { EditSources } from '../../../common/textModelEditSource.js';4748// sticky suggest widget which doesn't disappear on focus out and such49const _sticky = false50// || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this51;5253class LineSuffix {5455private readonly _decorationOptions = ModelDecorationOptions.register({56description: 'suggest-line-suffix',57stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges58});5960private _marker: string | undefined;6162constructor(private readonly _model: ITextModel, private readonly _position: IPosition) {63// spy on what's happening right of the cursor. two cases:64// 1. end of line -> check that it's still end of line65// 2. mid of line -> add a marker and compute the delta66const maxColumn = _model.getLineMaxColumn(_position.lineNumber);67if (maxColumn !== _position.column) {68const offset = _model.getOffsetAt(_position);69const end = _model.getPositionAt(offset + 1);70_model.changeDecorations(accessor => {71if (this._marker) {72accessor.removeDecoration(this._marker);73}74this._marker = accessor.addDecoration(Range.fromPositions(_position, end), this._decorationOptions);75});76}77}7879dispose(): void {80if (this._marker && !this._model.isDisposed()) {81this._model.changeDecorations(accessor => {82accessor.removeDecoration(this._marker!);83this._marker = undefined;84});85}86}8788delta(position: IPosition): number {89if (this._model.isDisposed() || this._position.lineNumber !== position.lineNumber) {90// bail out early if things seems fishy91return 0;92}93// read the marker (in case suggest was triggered at line end) or compare94// the cursor to the line end.95if (this._marker) {96const range = this._model.getDecorationRange(this._marker);97const end = this._model.getOffsetAt(range!.getStartPosition());98return end - this._model.getOffsetAt(position);99} else {100return this._model.getLineMaxColumn(position.lineNumber) - position.column;101}102}103}104105const enum InsertFlags {106None = 0,107NoBeforeUndoStop = 1,108NoAfterUndoStop = 2,109KeepAlternativeSuggestions = 4,110AlternativeOverwriteConfig = 8111}112113export class SuggestController implements IEditorContribution {114115public static readonly ID: string = 'editor.contrib.suggestController';116117public static get(editor: ICodeEditor): SuggestController | null {118return editor.getContribution<SuggestController>(SuggestController.ID);119}120121readonly editor: ICodeEditor;122readonly model: SuggestModel;123readonly widget: WindowIdleValue<SuggestWidget>;124125private readonly _alternatives: WindowIdleValue<SuggestAlternatives>;126private readonly _lineSuffix = new MutableDisposable<LineSuffix>();127private readonly _toDispose = new DisposableStore();128private readonly _overtypingCapturer: WindowIdleValue<OvertypingCapturer>;129private readonly _selectors = new PriorityRegistry<ISuggestItemPreselector>(s => s.priority);130131private readonly _onWillInsertSuggestItem = new Emitter<{ item: CompletionItem }>();132get onWillInsertSuggestItem() { return this._onWillInsertSuggestItem.event; }133134private _wantsForceRenderingAbove = false;135136137constructor(138editor: ICodeEditor,139@ISuggestMemoryService private readonly _memoryService: ISuggestMemoryService,140@ICommandService private readonly _commandService: ICommandService,141@IContextKeyService private readonly _contextKeyService: IContextKeyService,142@IInstantiationService private readonly _instantiationService: IInstantiationService,143@ILogService private readonly _logService: ILogService,144@ITelemetryService private readonly _telemetryService: ITelemetryService,145) {146this.editor = editor;147this.model = _instantiationService.createInstance(SuggestModel, this.editor,);148149// default selector150this._selectors.register({151priority: 0,152select: (model, pos, items) => this._memoryService.select(model, pos, items)153});154155// context key: update insert/replace mode156const ctxInsertMode = SuggestContext.InsertMode.bindTo(_contextKeyService);157ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode);158this._toDispose.add(this.model.onDidTrigger(() => ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode)));159160this.widget = this._toDispose.add(new WindowIdleValue(getWindow(editor.getDomNode()), () => {161162const widget = this._instantiationService.createInstance(SuggestWidget, this.editor);163164this._toDispose.add(widget);165this._toDispose.add(widget.onDidSelect(item => this._insertSuggestion(item, InsertFlags.None), this));166167// Wire up logic to accept a suggestion on certain characters168const commitCharacterController = new CommitCharacterController(this.editor, widget, this.model, item => this._insertSuggestion(item, InsertFlags.NoAfterUndoStop));169this._toDispose.add(commitCharacterController);170171172// Wire up makes text edit context key173const ctxMakesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService);174const ctxHasInsertAndReplace = SuggestContext.HasInsertAndReplaceRange.bindTo(this._contextKeyService);175const ctxCanResolve = SuggestContext.CanResolve.bindTo(this._contextKeyService);176177this._toDispose.add(toDisposable(() => {178ctxMakesTextEdit.reset();179ctxHasInsertAndReplace.reset();180ctxCanResolve.reset();181}));182183this._toDispose.add(widget.onDidFocus(({ item }) => {184185// (ctx: makesTextEdit)186const position = this.editor.getPosition()!;187const startColumn = item.editStart.column;188const endColumn = position.column;189let value = true;190if (191this.editor.getOption(EditorOption.acceptSuggestionOnEnter) === 'smart'192&& this.model.state === State.Auto193&& !item.completion.additionalTextEdits194&& !(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)195&& endColumn - startColumn === item.completion.insertText.length196) {197const oldText = this.editor.getModel()!.getValueInRange({198startLineNumber: position.lineNumber,199startColumn,200endLineNumber: position.lineNumber,201endColumn202});203value = oldText !== item.completion.insertText;204}205ctxMakesTextEdit.set(value);206207// (ctx: hasInsertAndReplaceRange)208ctxHasInsertAndReplace.set(!Position.equals(item.editInsertEnd, item.editReplaceEnd));209210// (ctx: canResolve)211ctxCanResolve.set(Boolean(item.provider.resolveCompletionItem) || Boolean(item.completion.documentation) || item.completion.detail !== item.completion.label);212}));213214if (this._wantsForceRenderingAbove) {215widget.forceRenderingAbove();216}217218return widget;219}));220221// Wire up text overtyping capture222this._overtypingCapturer = this._toDispose.add(new WindowIdleValue(getWindow(editor.getDomNode()), () => {223return this._toDispose.add(new OvertypingCapturer(this.editor, this.model));224}));225226this._alternatives = this._toDispose.add(new WindowIdleValue(getWindow(editor.getDomNode()), () => {227return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService));228}));229230this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor));231232this._toDispose.add(this.model.onDidTrigger(e => {233this.widget.value.showTriggered(e.auto, e.shy ? 250 : 50);234this._lineSuffix.value = new LineSuffix(this.editor.getModel()!, e.position);235}));236this._toDispose.add(this.model.onDidSuggest(e => {237if (e.triggerOptions.shy) {238return;239}240let index = -1;241for (const selector of this._selectors.itemsOrderedByPriorityDesc) {242index = selector.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items);243if (index !== -1) {244break;245}246}247if (index === -1) {248index = 0;249}250if (this.model.state === State.Idle) {251// selecting an item can "pump" out selection/cursor change events252// which can cancel suggest halfway through this function. therefore253// we need to check again and bail if the session has been canceled254return;255}256let noFocus = false;257if (e.triggerOptions.auto) {258// don't "focus" item when configured to do259const options = this.editor.getOption(EditorOption.suggest);260if (options.selectionMode === 'never' || options.selectionMode === 'always') {261// simple: always or never262noFocus = options.selectionMode === 'never';263264} else if (options.selectionMode === 'whenTriggerCharacter') {265// on with trigger character266noFocus = e.triggerOptions.triggerKind !== CompletionTriggerKind.TriggerCharacter;267268} else if (options.selectionMode === 'whenQuickSuggestion') {269// without trigger character or when refiltering270noFocus = e.triggerOptions.triggerKind === CompletionTriggerKind.TriggerCharacter && !e.triggerOptions.refilter;271}272273}274this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.triggerOptions.auto, noFocus);275}));276this._toDispose.add(this.model.onDidCancel(e => {277if (!e.retrigger) {278this.widget.value.hideWidget();279}280}));281this._toDispose.add(this.editor.onDidBlurEditorWidget(() => {282if (!_sticky) {283this.model.cancel();284this.model.clear();285}286}));287288// Manage the acceptSuggestionsOnEnter context key289const acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService);290const updateFromConfig = () => {291const acceptSuggestionOnEnter = this.editor.getOption(EditorOption.acceptSuggestionOnEnter);292acceptSuggestionsOnEnter.set(acceptSuggestionOnEnter === 'on' || acceptSuggestionOnEnter === 'smart');293};294this._toDispose.add(this.editor.onDidChangeConfiguration(() => updateFromConfig()));295updateFromConfig();296}297298dispose(): void {299this._alternatives.dispose();300this._toDispose.dispose();301this.widget.dispose();302this.model.dispose();303this._lineSuffix.dispose();304this._onWillInsertSuggestItem.dispose();305}306307protected _insertSuggestion(308event: ISelectedSuggestion | undefined,309flags: InsertFlags310): void {311if (!event || !event.item) {312this._alternatives.value.reset();313this.model.cancel();314this.model.clear();315return;316}317if (!this.editor.hasModel()) {318return;319}320const snippetController = SnippetController2.get(this.editor);321if (!snippetController) {322return;323}324325this._onWillInsertSuggestItem.fire({ item: event.item });326327const model = this.editor.getModel();328const modelVersionNow = model.getAlternativeVersionId();329const { item } = event;330331//332const tasks: Promise<unknown>[] = [];333const cts = new CancellationTokenSource();334335// pushing undo stops *before* additional text edits and336// *after* the main edit337if (!(flags & InsertFlags.NoBeforeUndoStop)) {338this.editor.pushUndoStop();339}340341// compute overwrite[Before|After] deltas BEFORE applying extra edits342const info = this.getOverwriteInfo(item, Boolean(flags & InsertFlags.AlternativeOverwriteConfig));343344// keep item in memory345this._memoryService.memorize(model, this.editor.getPosition(), item);346347const isResolved = item.isResolved;348349// telemetry data points: duration of command execution, info about async additional edits (-1=n/a, -2=none, 1=success, 0=failed)350let _commandExectionDuration = -1;351let _additionalEditsAppliedAsync = -1;352353if (Array.isArray(item.completion.additionalTextEdits)) {354355// cancel -> stops all listening and closes widget356this.model.cancel();357358// sync additional edits359const scrollState = StableEditorScrollState.capture(this.editor);360this.editor.executeEdits(361'suggestController.additionalTextEdits.sync',362item.completion.additionalTextEdits.map(edit => {363let range = Range.lift(edit.range);364if (range.startLineNumber === item.position.lineNumber && range.startColumn > item.position.column) {365// shift additional edit when it is "after" the completion insertion position366const columnDelta = this.editor.getPosition()!.column - item.position.column;367const startColumnDelta = columnDelta;368const endColumnDelta = Range.spansMultipleLines(range) ? 0 : columnDelta;369range = new Range(range.startLineNumber, range.startColumn + startColumnDelta, range.endLineNumber, range.endColumn + endColumnDelta);370}371return EditOperation.replaceMove(range, edit.text);372})373);374scrollState.restoreRelativeVerticalPositionOfCursor(this.editor);375376} else if (!isResolved) {377// async additional edits378const sw = new StopWatch();379let position: IPosition | undefined;380381const docListener = model.onDidChangeContent(e => {382if (e.isFlush) {383cts.cancel();384docListener.dispose();385return;386}387for (const change of e.changes) {388const thisPosition = Range.getEndPosition(change.range);389if (!position || Position.isBefore(thisPosition, position)) {390position = thisPosition;391}392}393});394395const oldFlags = flags;396flags |= InsertFlags.NoAfterUndoStop;397let didType = false;398const typeListener = this.editor.onWillType(() => {399typeListener.dispose();400didType = true;401if (!(oldFlags & InsertFlags.NoAfterUndoStop)) {402this.editor.pushUndoStop();403}404});405406tasks.push(item.resolve(cts.token).then(() => {407if (!item.completion.additionalTextEdits || cts.token.isCancellationRequested) {408return undefined;409}410if (position && item.completion.additionalTextEdits.some(edit => Position.isBefore(position!, Range.getStartPosition(edit.range)))) {411return false;412}413if (didType) {414this.editor.pushUndoStop();415}416const scrollState = StableEditorScrollState.capture(this.editor);417this.editor.executeEdits(418'suggestController.additionalTextEdits.async',419item.completion.additionalTextEdits.map(edit => EditOperation.replaceMove(Range.lift(edit.range), edit.text))420);421scrollState.restoreRelativeVerticalPositionOfCursor(this.editor);422if (didType || !(oldFlags & InsertFlags.NoAfterUndoStop)) {423this.editor.pushUndoStop();424}425return true;426}).then(applied => {427this._logService.trace('[suggest] async resolving of edits DONE (ms, applied?)', sw.elapsed(), applied);428_additionalEditsAppliedAsync = applied === true ? 1 : applied === false ? 0 : -2;429}).finally(() => {430docListener.dispose();431typeListener.dispose();432}));433}434435let { insertText } = item.completion;436if (!(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) {437insertText = SnippetParser.escape(insertText);438}439440// cancel -> stops all listening and closes widget441this.model.cancel();442443snippetController.insert(insertText, {444overwriteBefore: info.overwriteBefore,445overwriteAfter: info.overwriteAfter,446undoStopBefore: false,447undoStopAfter: false,448adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace),449clipboardText: event.model.clipboardText,450overtypingCapturer: this._overtypingCapturer.value,451reason: EditSources.suggest({ providerId: ProviderId.fromExtensionId(item.extensionId?.value) }),452});453454if (!(flags & InsertFlags.NoAfterUndoStop)) {455this.editor.pushUndoStop();456}457458if (item.completion.command) {459if (item.completion.command.id === TriggerSuggestAction.id) {460// retigger461this.model.trigger({ auto: true, retrigger: true });462} else {463// exec command, done464const sw = new StopWatch();465tasks.push(this._commandService.executeCommand(item.completion.command.id, ...(item.completion.command.arguments ? [...item.completion.command.arguments] : [])).catch(e => {466if (item.completion.extensionId) {467onUnexpectedExternalError(e);468} else {469onUnexpectedError(e);470}471}).finally(() => {472_commandExectionDuration = sw.elapsed();473}));474}475}476477if (flags & InsertFlags.KeepAlternativeSuggestions) {478this._alternatives.value.set(event, next => {479480// cancel resolving of additional edits481cts.cancel();482483// this is not so pretty. when inserting the 'next'484// suggestion we undo until we are at the state at485// which we were before inserting the previous suggestion...486while (model.canUndo()) {487if (modelVersionNow !== model.getAlternativeVersionId()) {488model.undo();489}490this._insertSuggestion(491next,492InsertFlags.NoBeforeUndoStop | InsertFlags.NoAfterUndoStop | (flags & InsertFlags.AlternativeOverwriteConfig ? InsertFlags.AlternativeOverwriteConfig : 0)493);494break;495}496});497}498499this._alertCompletionItem(item);500501// clear only now - after all tasks are done502Promise.all(tasks).finally(() => {503this._reportSuggestionAcceptedTelemetry(item, model, isResolved, _commandExectionDuration, _additionalEditsAppliedAsync, event.index, event.model.items);504505this.model.clear();506cts.dispose();507});508}509510private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, itemResolved: boolean, commandExectionDuration: number, additionalEditsAppliedAsync: number, index: number, completionItems: CompletionItem[]): void {511if (Math.random() > 0.0001) { // 0.01%512return;513}514515const labelMap = new Map<string, number[]>();516517for (let i = 0; i < Math.min(30, completionItems.length); i++) {518const label = completionItems[i].textLabel;519520if (labelMap.has(label)) {521labelMap.get(label)!.push(i);522} else {523labelMap.set(label, [i]);524}525}526527const firstIndexArray = labelMap.get(item.textLabel);528const hasDuplicates = firstIndexArray && firstIndexArray.length > 1;529const firstIndex = hasDuplicates ? firstIndexArray[0] : -1;530531type AcceptedSuggestion = {532extensionId: string; providerId: string;533fileExtension: string; languageId: string; basenameHash: string; kind: number;534resolveInfo: number; resolveDuration: number;535commandDuration: number;536additionalEditsAsync: number;537index: number; firstIndex: number;538};539type AcceptedSuggestionClassification = {540owner: 'jrieken';541comment: 'Information accepting completion items';542extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Extension contributing the completions item' };543providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Provider of the completions item' };544basenameHash: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Hash of the basename of the file into which the completion was inserted' };545fileExtension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension of the file into which the completion was inserted' };546languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Language type of the file into which the completion was inserted' };547kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The completion item kind' };548resolveInfo: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the item was inserted before resolving was done' };549resolveDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How long resolving took to finish' };550commandDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How long a completion item command took' };551additionalEditsAsync: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Info about asynchronously applying additional edits' };552index: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The index of the completion item in the sorted list.' };553firstIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When there are multiple completions, the index of the first instance.' };554};555556this._telemetryService.publicLog2<AcceptedSuggestion, AcceptedSuggestionClassification>('suggest.acceptedSuggestion', {557extensionId: item.extensionId?.value ?? 'unknown',558providerId: item.provider._debugDisplayName ?? 'unknown',559kind: item.completion.kind,560basenameHash: hash(basename(model.uri)).toString(16),561languageId: model.getLanguageId(),562fileExtension: extname(model.uri),563resolveInfo: !item.provider.resolveCompletionItem ? -1 : itemResolved ? 1 : 0,564resolveDuration: item.resolveDuration,565commandDuration: commandExectionDuration,566additionalEditsAsync: additionalEditsAppliedAsync,567index,568firstIndex,569});570}571572getOverwriteInfo(item: CompletionItem, toggleMode: boolean): { overwriteBefore: number; overwriteAfter: number } {573assertType(this.editor.hasModel());574575let replace = this.editor.getOption(EditorOption.suggest).insertMode === 'replace';576if (toggleMode) {577replace = !replace;578}579const overwriteBefore = item.position.column - item.editStart.column;580const overwriteAfter = (replace ? item.editReplaceEnd.column : item.editInsertEnd.column) - item.position.column;581const columnDelta = this.editor.getPosition().column - item.position.column;582const suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this.editor.getPosition()) : 0;583584return {585overwriteBefore: overwriteBefore + columnDelta,586overwriteAfter: overwriteAfter + suffixDelta587};588}589590private _alertCompletionItem(item: CompletionItem): void {591if (isNonEmptyArray(item.completion.additionalTextEdits)) {592const msg = nls.localize('aria.alert.snippet', "Accepting '{0}' made {1} additional edits", item.textLabel, item.completion.additionalTextEdits.length);593alert(msg);594}595}596597triggerSuggest(onlyFrom?: Set<CompletionItemProvider>, auto?: boolean, noFilter?: boolean): void {598if (this.editor.hasModel()) {599this.model.trigger({600auto: auto ?? false,601completionOptions: { providerFilter: onlyFrom, kindFilter: noFilter ? new Set() : undefined }602});603this.editor.revealPosition(this.editor.getPosition(), ScrollType.Smooth);604this.editor.focus();605}606}607608triggerSuggestAndAcceptBest(arg: { fallback: string }): void {609if (!this.editor.hasModel()) {610return;611612}613const positionNow = this.editor.getPosition();614615const fallback = () => {616if (positionNow.equals(this.editor.getPosition()!)) {617this._commandService.executeCommand(arg.fallback);618}619};620621const makesTextEdit = (item: CompletionItem): boolean => {622if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet || item.completion.additionalTextEdits) {623// snippet, other editor -> makes edit624return true;625}626const position = this.editor.getPosition()!;627const startColumn = item.editStart.column;628const endColumn = position.column;629if (endColumn - startColumn !== item.completion.insertText.length) {630// unequal lengths -> makes edit631return true;632}633const textNow = this.editor.getModel()!.getValueInRange({634startLineNumber: position.lineNumber,635startColumn,636endLineNumber: position.lineNumber,637endColumn638});639// unequal text -> makes edit640return textNow !== item.completion.insertText;641};642643Event.once(this.model.onDidTrigger)(_ => {644// wait for trigger because only then the cancel-event is trustworthy645const listener: IDisposable[] = [];646647Event.any<unknown>(this.model.onDidTrigger, this.model.onDidCancel)(() => {648// retrigger or cancel -> try to type default text649dispose(listener);650fallback();651}, undefined, listener);652653this.model.onDidSuggest(({ completionModel }) => {654dispose(listener);655if (completionModel.items.length === 0) {656fallback();657return;658}659const index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, completionModel.items);660const item = completionModel.items[index];661if (!makesTextEdit(item)) {662fallback();663return;664}665this.editor.pushUndoStop();666this._insertSuggestion({ index, item, model: completionModel }, InsertFlags.KeepAlternativeSuggestions | InsertFlags.NoBeforeUndoStop | InsertFlags.NoAfterUndoStop);667668}, undefined, listener);669});670671this.model.trigger({ auto: false, shy: true });672this.editor.revealPosition(positionNow, ScrollType.Smooth);673this.editor.focus();674}675676acceptSelectedSuggestion(keepAlternativeSuggestions: boolean, alternativeOverwriteConfig: boolean): void {677const item = this.widget.value.getFocusedItem();678let flags = 0;679if (keepAlternativeSuggestions) {680flags |= InsertFlags.KeepAlternativeSuggestions;681}682if (alternativeOverwriteConfig) {683flags |= InsertFlags.AlternativeOverwriteConfig;684}685this._insertSuggestion(item, flags);686}687688acceptNextSuggestion() {689this._alternatives.value.next();690}691692acceptPrevSuggestion() {693this._alternatives.value.prev();694}695696cancelSuggestWidget(): void {697this.model.cancel();698this.model.clear();699this.widget.value.hideWidget();700}701702focusSuggestion(): void {703this.widget.value.focusSelected();704}705706selectNextSuggestion(): void {707this.widget.value.selectNext();708}709710selectNextPageSuggestion(): void {711this.widget.value.selectNextPage();712}713714selectLastSuggestion(): void {715this.widget.value.selectLast();716}717718selectPrevSuggestion(): void {719this.widget.value.selectPrevious();720}721722selectPrevPageSuggestion(): void {723this.widget.value.selectPreviousPage();724}725726selectFirstSuggestion(): void {727this.widget.value.selectFirst();728}729730toggleSuggestionDetails(): void {731this.widget.value.toggleDetails();732}733734toggleExplainMode(): void {735this.widget.value.toggleExplainMode();736}737738toggleSuggestionFocus(): void {739this.widget.value.toggleDetailsFocus();740}741742resetWidgetSize(): void {743this.widget.value.resetPersistedSize();744}745746forceRenderingAbove() {747if (this.widget.isInitialized) {748this.widget.value.forceRenderingAbove();749} else {750// Defer this until the widget is created751this._wantsForceRenderingAbove = true;752}753}754755stopForceRenderingAbove() {756if (this.widget.isInitialized) {757this.widget.value.stopForceRenderingAbove();758} else {759this._wantsForceRenderingAbove = false;760}761}762763registerSelector(selector: ISuggestItemPreselector): IDisposable {764return this._selectors.register(selector);765}766}767768class PriorityRegistry<T> {769private readonly _items = new Array<T>();770771constructor(private readonly prioritySelector: (item: T) => number) { }772773register(value: T): IDisposable {774if (this._items.indexOf(value) !== -1) {775throw new Error('Value is already registered');776}777this._items.push(value);778this._items.sort((s1, s2) => this.prioritySelector(s2) - this.prioritySelector(s1));779780return {781dispose: () => {782const idx = this._items.indexOf(value);783if (idx >= 0) {784this._items.splice(idx, 1);785}786}787};788}789790get itemsOrderedByPriorityDesc(): readonly T[] {791return this._items;792}793}794795export class TriggerSuggestAction extends EditorAction {796797static readonly id = 'editor.action.triggerSuggest';798799constructor() {800super({801id: TriggerSuggestAction.id,802label: nls.localize2('suggest.trigger.label', "Trigger Suggest"),803precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCompletionItemProvider, SuggestContext.Visible.toNegated()),804kbOpts: {805kbExpr: EditorContextKeys.textInputFocus,806primary: KeyMod.CtrlCmd | KeyCode.Space,807secondary: [KeyMod.CtrlCmd | KeyCode.KeyI],808mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.Alt | KeyCode.Escape, KeyMod.CtrlCmd | KeyCode.KeyI] },809weight: KeybindingWeight.EditorContrib810}811});812}813814run(_accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void {815const controller = SuggestController.get(editor);816817if (!controller) {818return;819}820821type TriggerArgs = { auto: boolean };822let auto: boolean | undefined;823if (args && typeof args === 'object') {824if ((<TriggerArgs>args).auto === true) {825auto = true;826}827}828829controller.triggerSuggest(undefined, auto, undefined);830}831}832833registerEditorContribution(SuggestController.ID, SuggestController, EditorContributionInstantiation.BeforeFirstInteraction);834registerEditorAction(TriggerSuggestAction);835836const weight = KeybindingWeight.EditorContrib + 90;837838const SuggestCommand = EditorCommand.bindToContribution<SuggestController>(SuggestController.get);839840841registerEditorCommand(new SuggestCommand({842id: 'acceptSelectedSuggestion',843precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.HasFocusedSuggestion),844handler(x) {845x.acceptSelectedSuggestion(true, false);846},847kbOpts: [{848// normal tab849primary: KeyCode.Tab,850kbExpr: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus),851weight,852}, {853// accept on enter has special rules854primary: KeyCode.Enter,855kbExpr: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus, SuggestContext.AcceptSuggestionsOnEnter, SuggestContext.MakesTextEdit),856weight,857}],858menuOpts: [{859menuId: suggestWidgetStatusbarMenu,860title: nls.localize('accept.insert', "Insert"),861group: 'left',862order: 1,863when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange.toNegated())864}, {865menuId: suggestWidgetStatusbarMenu,866title: nls.localize('accept.insert', "Insert"),867group: 'left',868order: 1,869when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert'))870}, {871menuId: suggestWidgetStatusbarMenu,872title: nls.localize('accept.replace', "Replace"),873group: 'left',874order: 1,875when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace'))876}]877}));878879registerEditorCommand(new SuggestCommand({880id: 'acceptAlternativeSelectedSuggestion',881precondition: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus, SuggestContext.HasFocusedSuggestion),882kbOpts: {883weight: weight,884kbExpr: EditorContextKeys.textInputFocus,885primary: KeyMod.Shift | KeyCode.Enter,886secondary: [KeyMod.Shift | KeyCode.Tab],887},888handler(x) {889x.acceptSelectedSuggestion(false, true);890},891menuOpts: [{892menuId: suggestWidgetStatusbarMenu,893group: 'left',894order: 2,895when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')),896title: nls.localize('accept.replace', "Replace")897}, {898menuId: suggestWidgetStatusbarMenu,899group: 'left',900order: 2,901when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')),902title: nls.localize('accept.insert', "Insert")903}]904}));905906907// continue to support the old command908CommandsRegistry.registerCommandAlias('acceptSelectedSuggestionOnEnter', 'acceptSelectedSuggestion');909910registerEditorCommand(new SuggestCommand({911id: 'hideSuggestWidget',912precondition: SuggestContext.Visible,913handler: x => x.cancelSuggestWidget(),914kbOpts: {915weight: weight,916kbExpr: EditorContextKeys.textInputFocus,917primary: KeyCode.Escape,918secondary: [KeyMod.Shift | KeyCode.Escape]919}920}));921922registerEditorCommand(new SuggestCommand({923id: 'selectNextSuggestion',924precondition: ContextKeyExpr.and(SuggestContext.Visible, ContextKeyExpr.or(SuggestContext.MultipleSuggestions, SuggestContext.HasFocusedSuggestion.negate())),925handler: c => c.selectNextSuggestion(),926kbOpts: {927weight: weight,928kbExpr: EditorContextKeys.textInputFocus,929primary: KeyCode.DownArrow,930secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow],931mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] }932},933menuOpts: {934menuId: suggestWidgetStatusbarMenu,935group: 'left',936order: 0,937when: SuggestContext.HasFocusedSuggestion.toNegated(),938title: nls.localize('focus.suggestion', "Select")939}940}));941942registerEditorCommand(new SuggestCommand({943id: 'selectNextPageSuggestion',944precondition: ContextKeyExpr.and(SuggestContext.Visible, ContextKeyExpr.or(SuggestContext.MultipleSuggestions, SuggestContext.HasFocusedSuggestion.negate())),945handler: c => c.selectNextPageSuggestion(),946kbOpts: {947weight: weight,948kbExpr: EditorContextKeys.textInputFocus,949primary: KeyCode.PageDown,950secondary: [KeyMod.CtrlCmd | KeyCode.PageDown]951}952}));953954registerEditorCommand(new SuggestCommand({955id: 'selectLastSuggestion',956precondition: ContextKeyExpr.and(SuggestContext.Visible, ContextKeyExpr.or(SuggestContext.MultipleSuggestions, SuggestContext.HasFocusedSuggestion.negate())),957handler: c => c.selectLastSuggestion()958}));959960registerEditorCommand(new SuggestCommand({961id: 'selectPrevSuggestion',962precondition: ContextKeyExpr.and(SuggestContext.Visible, ContextKeyExpr.or(SuggestContext.MultipleSuggestions, SuggestContext.HasFocusedSuggestion.negate())),963handler: c => c.selectPrevSuggestion(),964kbOpts: {965weight: weight,966kbExpr: EditorContextKeys.textInputFocus,967primary: KeyCode.UpArrow,968secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow],969mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] }970}971}));972973registerEditorCommand(new SuggestCommand({974id: 'selectPrevPageSuggestion',975precondition: ContextKeyExpr.and(SuggestContext.Visible, ContextKeyExpr.or(SuggestContext.MultipleSuggestions, SuggestContext.HasFocusedSuggestion.negate())),976handler: c => c.selectPrevPageSuggestion(),977kbOpts: {978weight: weight,979kbExpr: EditorContextKeys.textInputFocus,980primary: KeyCode.PageUp,981secondary: [KeyMod.CtrlCmd | KeyCode.PageUp]982}983}));984985registerEditorCommand(new SuggestCommand({986id: 'selectFirstSuggestion',987precondition: ContextKeyExpr.and(SuggestContext.Visible, ContextKeyExpr.or(SuggestContext.MultipleSuggestions, SuggestContext.HasFocusedSuggestion.negate())),988handler: c => c.selectFirstSuggestion()989}));990991registerEditorCommand(new SuggestCommand({992id: 'focusSuggestion',993precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.HasFocusedSuggestion.negate()),994handler: x => x.focusSuggestion(),995kbOpts: {996weight: weight,997kbExpr: EditorContextKeys.textInputFocus,998primary: KeyMod.CtrlCmd | KeyCode.Space,999secondary: [KeyMod.CtrlCmd | KeyCode.KeyI],1000mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] }1001},1002}));10031004registerEditorCommand(new SuggestCommand({1005id: 'focusAndAcceptSuggestion',1006precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.HasFocusedSuggestion.negate()),1007handler: c => {1008c.focusSuggestion();1009c.acceptSelectedSuggestion(true, false);1010}1011}));10121013registerEditorCommand(new SuggestCommand({1014id: 'toggleSuggestionDetails',1015precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.HasFocusedSuggestion),1016handler: x => x.toggleSuggestionDetails(),1017kbOpts: {1018weight: weight,1019kbExpr: EditorContextKeys.textInputFocus,1020primary: KeyMod.CtrlCmd | KeyCode.Space,1021secondary: [KeyMod.CtrlCmd | KeyCode.KeyI],1022mac: { primary: KeyMod.WinCtrl | KeyCode.Space, secondary: [KeyMod.CtrlCmd | KeyCode.KeyI] }1023},1024menuOpts: [{1025menuId: suggestWidgetStatusbarMenu,1026group: 'right',1027order: 1,1028when: ContextKeyExpr.and(SuggestContext.DetailsVisible, SuggestContext.CanResolve),1029title: nls.localize('detail.more', "Show Less")1030}, {1031menuId: suggestWidgetStatusbarMenu,1032group: 'right',1033order: 1,1034when: ContextKeyExpr.and(SuggestContext.DetailsVisible.toNegated(), SuggestContext.CanResolve),1035title: nls.localize('detail.less', "Show More")1036}]1037}));10381039registerEditorCommand(new SuggestCommand({1040id: 'toggleExplainMode',1041precondition: SuggestContext.Visible,1042handler: x => x.toggleExplainMode(),1043kbOpts: {1044weight: KeybindingWeight.EditorContrib,1045primary: KeyMod.CtrlCmd | KeyCode.Slash,1046}1047}));10481049registerEditorCommand(new SuggestCommand({1050id: 'toggleSuggestionFocus',1051precondition: SuggestContext.Visible,1052handler: x => x.toggleSuggestionFocus(),1053kbOpts: {1054weight: weight,1055kbExpr: EditorContextKeys.textInputFocus,1056primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Space,1057mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space }1058}1059}));10601061//#region tab completions10621063registerEditorCommand(new SuggestCommand({1064id: 'insertBestCompletion',1065precondition: ContextKeyExpr.and(1066EditorContextKeys.textInputFocus,1067ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),1068WordContextKey.AtEnd,1069SuggestContext.Visible.toNegated(),1070SuggestAlternatives.OtherSuggestions.toNegated(),1071SnippetController2.InSnippetMode.toNegated()1072),1073handler: (x, arg) => {10741075x.triggerSuggestAndAcceptBest(isObject(arg) ? { fallback: 'tab', ...arg } : { fallback: 'tab' });1076},1077kbOpts: {1078weight,1079primary: KeyCode.Tab1080}1081}));10821083registerEditorCommand(new SuggestCommand({1084id: 'insertNextSuggestion',1085precondition: ContextKeyExpr.and(1086EditorContextKeys.textInputFocus,1087ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),1088SuggestAlternatives.OtherSuggestions,1089SuggestContext.Visible.toNegated(),1090SnippetController2.InSnippetMode.toNegated()1091),1092handler: x => x.acceptNextSuggestion(),1093kbOpts: {1094weight: weight,1095kbExpr: EditorContextKeys.textInputFocus,1096primary: KeyCode.Tab1097}1098}));10991100registerEditorCommand(new SuggestCommand({1101id: 'insertPrevSuggestion',1102precondition: ContextKeyExpr.and(1103EditorContextKeys.textInputFocus,1104ContextKeyExpr.equals('config.editor.tabCompletion', 'on'),1105SuggestAlternatives.OtherSuggestions,1106SuggestContext.Visible.toNegated(),1107SnippetController2.InSnippetMode.toNegated()1108),1109handler: x => x.acceptPrevSuggestion(),1110kbOpts: {1111weight: weight,1112kbExpr: EditorContextKeys.textInputFocus,1113primary: KeyMod.Shift | KeyCode.Tab1114}1115}));111611171118registerEditorCommand(new class extends EditorCommand {1119constructor() {1120super({1121id: 'suggestWidgetCopy',1122precondition: SuggestContext.DetailsFocused,1123kbOpts: {1124weight: weight + 10,1125kbExpr: SuggestContext.DetailsFocused,1126primary: KeyMod.CtrlCmd | KeyCode.KeyC,1127win: { primary: KeyMod.CtrlCmd | KeyCode.KeyC, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] }1128}1129});1130}1131runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {1132getWindow(editor.getDomNode()).document.execCommand('copy');1133}1134}());11351136registerEditorAction(class extends EditorAction {11371138constructor() {1139super({1140id: 'editor.action.resetSuggestSize',1141label: nls.localize2('suggest.reset.label', "Reset Suggest Widget Size"),1142precondition: undefined1143});1144}11451146run(_accessor: ServicesAccessor, editor: ICodeEditor): void {1147SuggestController.get(editor)?.resetWidgetSize();1148}1149});115011511152