Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts
4780 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 { h, n } from '../../../../../base/browser/dom.js';6import { renderMarkdown } from '../../../../../base/browser/markdownRenderer.js';7import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';8import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';9import { Action, IAction, Separator } from '../../../../../base/common/actions.js';10import { equals } from '../../../../../base/common/arrays.js';11import { RunOnceScheduler } from '../../../../../base/common/async.js';12import { Codicon } from '../../../../../base/common/codicons.js';13import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js';14import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js';15import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from '../../../../../base/common/observable.js';16import { OS } from '../../../../../base/common/platform.js';17import { ThemeIcon } from '../../../../../base/common/themables.js';18import { localize } from '../../../../../nls.js';19import { MenuEntryActionViewItem, getActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';20import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';21import { IMenuService, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';22import { ICommandService } from '../../../../../platform/commands/common/commands.js';23import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';24import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';25import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';26import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';27import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';28import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js';29import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../browser/editorBrowser.js';30import { EditorOption } from '../../../../common/config/editorOptions.js';31import { Position } from '../../../../common/core/position.js';32import { InlineCompletionCommand, InlineCompletionTriggerKind, InlineCompletionWarning } from '../../../../common/languages.js';33import { PositionAffinity } from '../../../../common/model.js';34import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from '../controller/commandIds.js';35import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js';36import './inlineCompletionsHintsWidget.css';3738export class InlineCompletionsHintsWidget extends Disposable {39private readonly alwaysShowToolbar;4041private sessionPosition: Position | undefined;4243private readonly position;4445constructor(46private readonly editor: ICodeEditor,47private readonly model: IObservable<InlineCompletionsModel | undefined>,48@IInstantiationService private readonly instantiationService: IInstantiationService,49) {50super();51this.alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always');52this.sessionPosition = undefined;53this.position = derived(this, reader => {54const ghostText = this.model.read(reader)?.primaryGhostText.read(reader);5556if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) {57this.sessionPosition = undefined;58return null;59}6061const firstColumn = ghostText.parts[0].column;62if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) {63this.sessionPosition = undefined;64}6566const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER));67this.sessionPosition = position;68return position;69});7071this._register(autorunWithStore((reader, store) => {72/** @description setup content widget */73const model = this.model.read(reader);74if (!model || !this.alwaysShowToolbar.read(reader)) {75return;76}7778const contentWidgetValue = derived((reader) => {79const contentWidget = reader.store.add(this.instantiationService.createInstance(80InlineSuggestionHintsContentWidget.hot.read(reader),81this.editor,82true,83this.position,84model.selectedInlineCompletionIndex,85model.inlineCompletionsCount,86model.activeCommands,87model.warning,88() => { },89));90editor.addContentWidget(contentWidget);91reader.store.add(toDisposable(() => editor.removeContentWidget(contentWidget)));9293reader.store.add(autorun(reader => {94/** @description request explicit */95const position = this.position.read(reader);96if (!position) {97return;98}99if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) {100model.triggerExplicitly();101}102}));103return contentWidget;104});105106const hadPosition = derivedObservableWithCache(this, (reader, lastValue) => !!this.position.read(reader) || !!lastValue);107store.add(autorun(reader => {108if (hadPosition.read(reader)) {109contentWidgetValue.read(reader);110}111}));112}));113}114}115116const inlineSuggestionHintsNextIcon = registerIcon('inline-suggestion-hints-next', Codicon.chevronRight, localize('parameterHintsNextIcon', 'Icon for show next parameter hint.'));117const inlineSuggestionHintsPreviousIcon = registerIcon('inline-suggestion-hints-previous', Codicon.chevronLeft, localize('parameterHintsPreviousIcon', 'Icon for show previous parameter hint.'));118119export class InlineSuggestionHintsContentWidget extends Disposable implements IContentWidget {120public static readonly hot = createHotClass(this);121122private static _dropDownVisible = false;123public static get dropDownVisible() { return this._dropDownVisible; }124125private static id = 0;126127private readonly id;128public readonly allowEditorOverflow;129public readonly suppressMouseDown;130131private readonly _warningMessageContentNode;132133private readonly _warningMessageNode;134135private readonly nodes;136137private createCommandAction(commandId: string, label: string, iconClassName: string): Action {138const action = new Action(139commandId,140label,141iconClassName,142true,143() => this._commandService.executeCommand(commandId),144);145action.tooltip = this.keybindingService.appendKeybinding(label, commandId, this._contextKeyService);146return action;147}148149private readonly previousAction;150private readonly availableSuggestionCountAction;151private readonly nextAction;152153private readonly toolBar: CustomizedMenuWorkbenchToolBar;154155// TODO@hediet: deprecate MenuId.InlineCompletionsActions156private readonly inlineCompletionsActionsMenus;157158private readonly clearAvailableSuggestionCountLabelDebounced;159160private readonly disableButtonsDebounced;161162constructor(163private readonly editor: ICodeEditor,164private readonly withBorder: boolean,165private readonly _position: IObservable<Position | null>,166private readonly _currentSuggestionIdx: IObservable<number>,167private readonly _suggestionCount: IObservable<number | undefined>,168private readonly _extraCommands: IObservable<InlineCompletionCommand[]>,169private readonly _warning: IObservable<InlineCompletionWarning | undefined>,170private readonly _relayout: () => void,171@ICommandService private readonly _commandService: ICommandService,172@IInstantiationService instantiationService: IInstantiationService,173@IKeybindingService private readonly keybindingService: IKeybindingService,174@IContextKeyService private readonly _contextKeyService: IContextKeyService,175@IMenuService private readonly _menuService: IMenuService,176) {177super();178this.id = `InlineSuggestionHintsContentWidget${InlineSuggestionHintsContentWidget.id++}`;179this.allowEditorOverflow = true;180this.suppressMouseDown = false;181this._warningMessageContentNode = derived((reader) => {182const warning = this._warning.read(reader);183if (!warning) {184return undefined;185}186if (typeof warning.message === 'string') {187return warning.message;188}189const markdownElement = reader.store.add(renderMarkdown(warning.message));190return markdownElement.element;191});192this._warningMessageNode = n.div({193class: 'warningMessage',194style: {195maxWidth: 400,196margin: 4,197marginBottom: 4,198display: derived(reader => this._warning.read(reader) ? 'block' : 'none'),199}200}, [201this._warningMessageContentNode,202]).keepUpdated(this._store);203this.nodes = h('div.inlineSuggestionsHints', { className: this.withBorder ? 'monaco-hover monaco-hover-content' : '' }, [204this._warningMessageNode.element,205h('div@toolBar'),206]);207this.previousAction = this._register(this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon)));208this.availableSuggestionCountAction = this._register(new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false));209this.nextAction = this._register(this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon)));210this.inlineCompletionsActionsMenus = this._register(this._menuService.createMenu(211MenuId.InlineCompletionsActions,212this._contextKeyService213));214this.clearAvailableSuggestionCountLabelDebounced = this._register(new RunOnceScheduler(() => {215this.availableSuggestionCountAction.label = '';216}, 100));217this.disableButtonsDebounced = this._register(new RunOnceScheduler(() => {218this.previousAction.enabled = this.nextAction.enabled = false;219}, 100));220221this._register(autorun(reader => {222this._warningMessageContentNode.read(reader);223this._warningMessageNode.readEffect(reader);224// Only update after the warning message node has been rendered225this._relayout();226}));227228this.toolBar = this._register(instantiationService.createInstance(CustomizedMenuWorkbenchToolBar, this.nodes.toolBar, MenuId.InlineSuggestionToolbar, {229menuOptions: { renderShortTitle: true },230toolbarOptions: { primaryGroup: g => g.startsWith('primary') },231actionViewItemProvider: (action, options) => {232if (action instanceof MenuItemAction) {233return instantiationService.createInstance(StatusBarViewItem, action, undefined);234}235if (action === this.availableSuggestionCountAction) {236const a = new ActionViewItemWithClassName(undefined, action, { label: true, icon: false });237a.setClass('availableSuggestionCount');238return a;239}240return undefined;241},242telemetrySource: 'InlineSuggestionToolbar',243}));244245this.toolBar.setPrependedPrimaryActions([246this.previousAction,247this.availableSuggestionCountAction,248this.nextAction,249]);250251this._register(this.toolBar.onDidChangeDropdownVisibility(e => {252InlineSuggestionHintsContentWidget._dropDownVisible = e;253}));254255this._register(autorun(reader => {256/** @description update position */257this._position.read(reader);258this.editor.layoutContentWidget(this);259}));260261this._register(autorun(reader => {262/** @description counts */263const suggestionCount = this._suggestionCount.read(reader);264const currentSuggestionIdx = this._currentSuggestionIdx.read(reader);265266if (suggestionCount !== undefined) {267this.clearAvailableSuggestionCountLabelDebounced.cancel();268this.availableSuggestionCountAction.label = `${currentSuggestionIdx + 1}/${suggestionCount}`;269} else {270this.clearAvailableSuggestionCountLabelDebounced.schedule();271}272273if (suggestionCount !== undefined && suggestionCount > 1) {274this.disableButtonsDebounced.cancel();275this.previousAction.enabled = this.nextAction.enabled = true;276} else {277this.disableButtonsDebounced.schedule();278}279}));280281this._register(autorun(reader => {282/** @description extra commands */283const extraCommands = this._extraCommands.read(reader);284const extraActions = extraCommands.map<IAction>(c => ({285class: undefined,286id: c.command.id,287enabled: true,288tooltip: c.command.tooltip || '',289label: c.command.title,290run: (event) => {291return this._commandService.executeCommand(c.command.id);292},293}));294295for (const [_, group] of this.inlineCompletionsActionsMenus.getActions()) {296for (const action of group) {297if (action instanceof MenuItemAction) {298extraActions.push(action);299}300}301}302303if (extraActions.length > 0) {304extraActions.unshift(new Separator());305}306307this.toolBar.setAdditionalSecondaryActions(extraActions);308}));309}310311getId(): string { return this.id; }312313getDomNode(): HTMLElement {314return this.nodes.root;315}316317getPosition(): IContentWidgetPosition | null {318return {319position: this._position.get(),320preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW],321positionAffinity: PositionAffinity.LeftOfInjectedText,322};323}324}325326class ActionViewItemWithClassName extends ActionViewItem {327private _className: string | undefined = undefined;328329setClass(className: string | undefined): void {330this._className = className;331}332333override render(container: HTMLElement): void {334super.render(container);335if (this._className) {336container.classList.add(this._className);337}338}339340protected override updateTooltip(): void {341// NOOP, disable tooltip342}343}344345class StatusBarViewItem extends MenuEntryActionViewItem {346protected override updateLabel() {347const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService, true);348if (!kb) {349return super.updateLabel();350}351if (this.label) {352const div = h('div.keybinding').root;353354const k = this._register(new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions }));355k.set(kb);356this.label.textContent = this._action.label;357this.label.appendChild(div);358this.label.classList.add('inlineSuggestionStatusBarItemLabel');359}360}361362protected override updateTooltip(): void {363// NOOP, disable tooltip364}365}366367export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar {368private readonly menu;369private additionalActions: IAction[];370private prependedPrimaryActions: IAction[];371private additionalPrimaryActions: IAction[];372373constructor(374container: HTMLElement,375private readonly menuId: MenuId,376private readonly options2: IMenuWorkbenchToolBarOptions | undefined,377@IMenuService private readonly menuService: IMenuService,378@IContextKeyService private readonly contextKeyService: IContextKeyService,379@IContextMenuService contextMenuService: IContextMenuService,380@IKeybindingService keybindingService: IKeybindingService,381@ICommandService commandService: ICommandService,382@ITelemetryService telemetryService: ITelemetryService,383) {384super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService);385this.menu = this._store.add(this.menuService.createMenu(this.menuId, this.contextKeyService, { emitEventsForSubmenuChanges: true }));386this.additionalActions = [];387this.prependedPrimaryActions = [];388this.additionalPrimaryActions = [];389390this._store.add(this.menu.onDidChange(() => this.updateToolbar()));391this.updateToolbar();392}393394private updateToolbar(): void {395const { primary, secondary } = getActionBarActions(396this.menu.getActions(this.options2?.menuOptions),397this.options2?.toolbarOptions?.primaryGroup, this.options2?.toolbarOptions?.shouldInlineSubmenu, this.options2?.toolbarOptions?.useSeparatorsInPrimaryActions398);399400secondary.push(...this.additionalActions);401primary.unshift(...this.prependedPrimaryActions);402primary.push(...this.additionalPrimaryActions);403this.setActions(primary, secondary);404}405406setPrependedPrimaryActions(actions: IAction[]): void {407if (equals(this.prependedPrimaryActions, actions, (a, b) => a === b)) {408return;409}410411this.prependedPrimaryActions = actions;412this.updateToolbar();413}414415setAdditionalPrimaryActions(actions: IAction[]): void {416if (equals(this.additionalPrimaryActions, actions, (a, b) => a === b)) {417return;418}419420this.additionalPrimaryActions = actions;421this.updateToolbar();422}423424setAdditionalSecondaryActions(actions: IAction[]): void {425if (equals(this.additionalActions, actions, (a, b) => a === b)) {426return;427}428429this.additionalActions = actions;430this.updateToolbar();431}432}433434435