Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestWidget.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 * as dom from '../../../../base/browser/dom.js';6import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import '../../../../base/browser/ui/codicons/codiconStyles.js'; // The codicon symbol styles are defined here and must be loaded8import { IListEvent, IListGestureEvent, IListMouseEvent } from '../../../../base/browser/ui/list/list.js';9import { List } from '../../../../base/browser/ui/list/listWidget.js';10import { CancelablePromise, createCancelablePromise, disposableTimeout, TimeoutTimer } from '../../../../base/common/async.js';11import { onUnexpectedError } from '../../../../base/common/errors.js';12import { Emitter, Event, PauseableEmitter } from '../../../../base/common/event.js';13import { DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';14import { clamp } from '../../../../base/common/numbers.js';15import * as strings from '../../../../base/common/strings.js';16import './media/suggest.css';17import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from '../../../browser/editorBrowser.js';18import { EmbeddedCodeEditorWidget } from '../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js';19import { EditorOption } from '../../../common/config/editorOptions.js';20import { IPosition } from '../../../common/core/position.js';21import { SuggestWidgetStatus } from './suggestWidgetStatus.js';22import '../../symbolIcons/browser/symbolIcons.js'; // The codicon symbol colors are defined here and must be loaded to get colors23import * as nls from '../../../../nls.js';24import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';25import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';26import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';27import { activeContrastBorder, editorForeground, editorWidgetBackground, editorWidgetBorder, listFocusHighlightForeground, listHighlightForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';28import { IThemeService } from '../../../../platform/theme/common/themeService.js';29import { CompletionModel } from './completionModel.js';30import { ResizableHTMLElement } from '../../../../base/browser/ui/resizable/resizable.js';31import { CompletionItem, Context as SuggestContext, suggestWidgetStatusbarMenu } from './suggest.js';32import { canExpandCompletionItem, SuggestDetailsOverlay, SuggestDetailsWidget } from './suggestWidgetDetails.js';33import { ItemRenderer } from './suggestWidgetRenderer.js';34import { getListStyles } from '../../../../platform/theme/browser/defaultStyles.js';35import { status } from '../../../../base/browser/ui/aria/aria.js';36import { CompletionItemKinds } from '../../../common/languages.js';37import { isWindows } from '../../../../base/common/platform.js';3839/**40* Suggest widget colors41*/42registerColor('editorSuggestWidget.background', editorWidgetBackground, nls.localize('editorSuggestWidgetBackground', 'Background color of the suggest widget.'));43registerColor('editorSuggestWidget.border', editorWidgetBorder, nls.localize('editorSuggestWidgetBorder', 'Border color of the suggest widget.'));44export const editorSuggestWidgetForeground = registerColor('editorSuggestWidget.foreground', editorForeground, nls.localize('editorSuggestWidgetForeground', 'Foreground color of the suggest widget.'));45registerColor('editorSuggestWidget.selectedForeground', quickInputListFocusForeground, nls.localize('editorSuggestWidgetSelectedForeground', 'Foreground color of the selected entry in the suggest widget.'));46registerColor('editorSuggestWidget.selectedIconForeground', quickInputListFocusIconForeground, nls.localize('editorSuggestWidgetSelectedIconForeground', 'Icon foreground color of the selected entry in the suggest widget.'));47export const editorSuggestWidgetSelectedBackground = registerColor('editorSuggestWidget.selectedBackground', quickInputListFocusBackground, nls.localize('editorSuggestWidgetSelectedBackground', 'Background color of the selected entry in the suggest widget.'));48registerColor('editorSuggestWidget.highlightForeground', listHighlightForeground, nls.localize('editorSuggestWidgetHighlightForeground', 'Color of the match highlights in the suggest widget.'));49registerColor('editorSuggestWidget.focusHighlightForeground', listFocusHighlightForeground, nls.localize('editorSuggestWidgetFocusHighlightForeground', 'Color of the match highlights in the suggest widget when an item is focused.'));50registerColor('editorSuggestWidgetStatus.foreground', transparent(editorSuggestWidgetForeground, .5), nls.localize('editorSuggestWidgetStatusForeground', 'Foreground color of the suggest widget status.'));5152const enum State {53Hidden,54Loading,55Empty,56Open,57Frozen,58Details,59onDetailsKeyDown60}6162export interface ISelectedSuggestion {63item: CompletionItem;64index: number;65model: CompletionModel;66}6768class PersistedWidgetSize {6970private readonly _key: string;7172constructor(73private readonly _service: IStorageService,74editor: ICodeEditor75) {76this._key = `suggestWidget.size/${editor.getEditorType()}/${editor instanceof EmbeddedCodeEditorWidget}`;77}7879restore(): dom.Dimension | undefined {80const raw = this._service.get(this._key, StorageScope.PROFILE) ?? '';81try {82const obj = JSON.parse(raw);83if (dom.Dimension.is(obj)) {84return dom.Dimension.lift(obj);85}86} catch {87// ignore88}89return undefined;90}9192store(size: dom.Dimension) {93this._service.store(this._key, JSON.stringify(size), StorageScope.PROFILE, StorageTarget.MACHINE);94}9596reset(): void {97this._service.remove(this._key, StorageScope.PROFILE);98}99}100101export class SuggestWidget implements IDisposable {102103private static LOADING_MESSAGE: string = nls.localize('suggestWidget.loading', "Loading...");104private static NO_SUGGESTIONS_MESSAGE: string = nls.localize('suggestWidget.noSuggestions', "No suggestions.");105106private _state: State = State.Hidden;107private _isAuto: boolean = false;108private _loadingTimeout?: IDisposable;109private readonly _pendingLayout = new MutableDisposable();110private readonly _pendingShowDetails = new MutableDisposable();111private _currentSuggestionDetails?: CancelablePromise<void>;112private _focusedItem?: CompletionItem;113private _ignoreFocusEvents: boolean = false;114private _completionModel?: CompletionModel;115private _cappedHeight?: { wanted: number; capped: number };116private _forceRenderingAbove: boolean = false;117private _explainMode: boolean = false;118119readonly element: ResizableHTMLElement;120private readonly _messageElement: HTMLElement;121private readonly _listElement: HTMLElement;122private readonly _list: List<CompletionItem>;123private readonly _status: SuggestWidgetStatus;124private readonly _details: SuggestDetailsOverlay;125private readonly _contentWidget: SuggestContentWidget;126private readonly _persistedSize: PersistedWidgetSize;127128private readonly _ctxSuggestWidgetVisible: IContextKey<boolean>;129private readonly _ctxSuggestWidgetDetailsVisible: IContextKey<boolean>;130private readonly _ctxSuggestWidgetMultipleSuggestions: IContextKey<boolean>;131private readonly _ctxSuggestWidgetHasFocusedSuggestion: IContextKey<boolean>;132private readonly _ctxSuggestWidgetDetailsFocused: IContextKey<boolean>;133134private readonly _showTimeout = new TimeoutTimer();135private readonly _disposables = new DisposableStore();136137138private readonly _onDidSelect = new PauseableEmitter<ISelectedSuggestion>();139private readonly _onDidFocus = new PauseableEmitter<ISelectedSuggestion>();140private readonly _onDidHide = new Emitter<this>();141private readonly _onDidShow = new Emitter<this>();142143readonly onDidSelect: Event<ISelectedSuggestion> = this._onDidSelect.event;144readonly onDidFocus: Event<ISelectedSuggestion> = this._onDidFocus.event;145readonly onDidHide: Event<this> = this._onDidHide.event;146readonly onDidShow: Event<this> = this._onDidShow.event;147148private readonly _onDetailsKeydown = new Emitter<IKeyboardEvent>();149readonly onDetailsKeyDown: Event<IKeyboardEvent> = this._onDetailsKeydown.event;150151constructor(152private readonly editor: ICodeEditor,153@IStorageService private readonly _storageService: IStorageService,154@IContextKeyService _contextKeyService: IContextKeyService,155@IThemeService _themeService: IThemeService,156@IInstantiationService instantiationService: IInstantiationService,157) {158this.element = new ResizableHTMLElement();159this.element.domNode.classList.add('editor-widget', 'suggest-widget');160161this._contentWidget = new SuggestContentWidget(this, editor);162this._persistedSize = new PersistedWidgetSize(_storageService, editor);163164class ResizeState {165constructor(166readonly persistedSize: dom.Dimension | undefined,167readonly currentSize: dom.Dimension,168public persistHeight = false,169public persistWidth = false,170) { }171}172173let state: ResizeState | undefined;174this._disposables.add(this.element.onDidWillResize(() => {175this._contentWidget.lockPreference();176state = new ResizeState(this._persistedSize.restore(), this.element.size);177}));178this._disposables.add(this.element.onDidResize(e => {179180this._resize(e.dimension.width, e.dimension.height);181182if (state) {183state.persistHeight = state.persistHeight || !!e.north || !!e.south;184state.persistWidth = state.persistWidth || !!e.east || !!e.west;185}186187if (!e.done) {188return;189}190191if (state) {192// only store width or height value that have changed and also193// only store changes that are above a certain threshold194const { itemHeight, defaultSize } = this.getLayoutInfo();195const threshold = Math.round(itemHeight / 2);196let { width, height } = this.element.size;197if (!state.persistHeight || Math.abs(state.currentSize.height - height) <= threshold) {198height = state.persistedSize?.height ?? defaultSize.height;199}200if (!state.persistWidth || Math.abs(state.currentSize.width - width) <= threshold) {201width = state.persistedSize?.width ?? defaultSize.width;202}203this._persistedSize.store(new dom.Dimension(width, height));204}205206// reset working state207this._contentWidget.unlockPreference();208state = undefined;209}));210211this._messageElement = dom.append(this.element.domNode, dom.$('.message'));212this._listElement = dom.append(this.element.domNode, dom.$('.tree'));213214const details = this._disposables.add(instantiationService.createInstance(SuggestDetailsWidget, this.editor));215details.onDidClose(() => this.toggleDetails(), this, this._disposables);216this._details = new SuggestDetailsOverlay(details, this.editor);217218const applyIconStyle = () => this.element.domNode.classList.toggle('no-icons', !this.editor.getOption(EditorOption.suggest).showIcons);219applyIconStyle();220221const renderer = instantiationService.createInstance(ItemRenderer, this.editor);222this._disposables.add(renderer);223this._disposables.add(renderer.onDidToggleDetails(() => this.toggleDetails()));224225this._list = new List('SuggestWidget', this._listElement, {226getHeight: (_element: CompletionItem): number => this.getLayoutInfo().itemHeight,227getTemplateId: (_element: CompletionItem): string => 'suggestion'228}, [renderer], {229alwaysConsumeMouseWheel: true,230useShadows: false,231mouseSupport: false,232multipleSelectionSupport: false,233accessibilityProvider: {234getRole: () => isWindows ? 'listitem' : 'option',235getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"),236getWidgetRole: () => 'listbox',237getAriaLabel: (item: CompletionItem) => {238239let label = item.textLabel;240const kindLabel = CompletionItemKinds.toLabel(item.completion.kind);241if (typeof item.completion.label !== 'string') {242const { detail, description } = item.completion.label;243if (detail && description) {244label = nls.localize('label.full', '{0} {1}, {2}, {3}', label, detail, description, kindLabel);245} else if (detail) {246label = nls.localize('label.detail', '{0} {1}, {2}', label, detail, kindLabel);247} else if (description) {248label = nls.localize('label.desc', '{0}, {1}, {2}', label, description, kindLabel);249}250} else {251label = nls.localize('label', '{0}, {1}', label, kindLabel);252}253if (!item.isResolved || !this._isDetailsVisible()) {254return label;255}256257const { documentation, detail } = item.completion;258const docs = strings.format(259'{0}{1}',260detail || '',261documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : '');262263return nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, docs: {1}", label, docs);264},265}266});267this._list.style(getListStyles({268listInactiveFocusBackground: editorSuggestWidgetSelectedBackground,269listInactiveFocusOutline: activeContrastBorder270}));271272this._status = instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, suggestWidgetStatusbarMenu, undefined);273const applyStatusBarStyle = () => this.element.domNode.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).showStatusBar);274applyStatusBarStyle();275276this._disposables.add(this._list.onMouseDown(e => this._onListMouseDownOrTap(e)));277this._disposables.add(this._list.onTap(e => this._onListMouseDownOrTap(e)));278this._disposables.add(this._list.onDidChangeSelection(e => this._onListSelection(e)));279this._disposables.add(this._list.onDidChangeFocus(e => this._onListFocus(e)));280this._disposables.add(this.editor.onDidChangeCursorSelection(() => this._onCursorSelectionChanged()));281this._disposables.add(this.editor.onDidChangeConfiguration(e => {282if (e.hasChanged(EditorOption.suggest)) {283applyStatusBarStyle();284applyIconStyle();285}286if (this._completionModel && (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.suggestFontSize) || e.hasChanged(EditorOption.suggestLineHeight))) {287this._list.splice(0, this._list.length, this._completionModel.items);288}289}));290291this._ctxSuggestWidgetVisible = SuggestContext.Visible.bindTo(_contextKeyService);292this._ctxSuggestWidgetDetailsVisible = SuggestContext.DetailsVisible.bindTo(_contextKeyService);293this._ctxSuggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(_contextKeyService);294this._ctxSuggestWidgetHasFocusedSuggestion = SuggestContext.HasFocusedSuggestion.bindTo(_contextKeyService);295this._ctxSuggestWidgetDetailsFocused = SuggestContext.DetailsFocused.bindTo(_contextKeyService);296297const detailsFocusTracker = dom.trackFocus(this._details.widget.domNode);298this._disposables.add(detailsFocusTracker);299this._disposables.add(detailsFocusTracker.onDidFocus(() => this._ctxSuggestWidgetDetailsFocused.set(true)));300this._disposables.add(detailsFocusTracker.onDidBlur(() => this._ctxSuggestWidgetDetailsFocused.set(false)));301302this._disposables.add(dom.addStandardDisposableListener(this._details.widget.domNode, 'keydown', e => {303this._onDetailsKeydown.fire(e);304}));305306this._disposables.add(this.editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));307}308309dispose(): void {310this._details.widget.dispose();311this._details.dispose();312this._list.dispose();313this._status.dispose();314this._disposables.dispose();315this._loadingTimeout?.dispose();316this._pendingLayout.dispose();317this._pendingShowDetails.dispose();318this._showTimeout.dispose();319this._contentWidget.dispose();320this.element.dispose();321}322323private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {324if (this._details.widget.domNode.contains(mouseEvent.target.element)) {325// Clicking inside details326this._details.widget.domNode.focus();327} else {328// Clicking outside details and inside suggest329if (this.element.domNode.contains(mouseEvent.target.element)) {330this.editor.focus();331}332}333}334335private _onCursorSelectionChanged(): void {336if (this._state !== State.Hidden) {337this._contentWidget.layout();338}339}340341private _onListMouseDownOrTap(e: IListMouseEvent<CompletionItem> | IListGestureEvent<CompletionItem>): void {342if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {343return;344}345346// prevent stealing browser focus from the editor347e.browserEvent.preventDefault();348e.browserEvent.stopPropagation();349350this._select(e.element, e.index);351}352353private _onListSelection(e: IListEvent<CompletionItem>): void {354if (e.elements.length) {355this._select(e.elements[0], e.indexes[0]);356}357}358359private _select(item: CompletionItem, index: number): void {360const completionModel = this._completionModel;361if (completionModel) {362this._onDidSelect.fire({ item, index, model: completionModel });363this.editor.focus();364}365}366367private _onListFocus(e: IListEvent<CompletionItem>): void {368if (this._ignoreFocusEvents) {369return;370}371372if (this._state === State.Details) {373// This can happen when focus is in the details-panel and when374// arrow keys are pressed to select next/prev items375this._setState(State.Open);376}377378if (!e.elements.length) {379if (this._currentSuggestionDetails) {380this._currentSuggestionDetails.cancel();381this._currentSuggestionDetails = undefined;382this._focusedItem = undefined;383}384385this.editor.setAriaOptions({ activeDescendant: undefined });386this._ctxSuggestWidgetHasFocusedSuggestion.set(false);387return;388}389390if (!this._completionModel) {391return;392}393394this._ctxSuggestWidgetHasFocusedSuggestion.set(true);395const item = e.elements[0];396const index = e.indexes[0];397398if (item !== this._focusedItem) {399400this._currentSuggestionDetails?.cancel();401this._currentSuggestionDetails = undefined;402403this._focusedItem = item;404405this._list.reveal(index);406407this._currentSuggestionDetails = createCancelablePromise(async token => {408const loading = disposableTimeout(() => {409if (this._isDetailsVisible()) {410this._showDetails(true, false);411}412}, 250);413const sub = token.onCancellationRequested(() => loading.dispose());414try {415return await item.resolve(token);416} finally {417loading.dispose();418sub.dispose();419}420});421422this._currentSuggestionDetails.then(() => {423if (index >= this._list.length || item !== this._list.element(index)) {424return;425}426427// item can have extra information, so re-render428this._ignoreFocusEvents = true;429this._list.splice(index, 1, [item]);430this._list.setFocus([index]);431this._ignoreFocusEvents = false;432433if (this._isDetailsVisible()) {434this._showDetails(false, false);435} else {436this.element.domNode.classList.remove('docs-side');437}438439this.editor.setAriaOptions({ activeDescendant: this._list.getElementID(index) });440}).catch(onUnexpectedError);441}442443// emit an event444this._onDidFocus.fire({ item, index, model: this._completionModel });445}446447private _setState(state: State): void {448449if (this._state === state) {450return;451}452this._state = state;453454this.element.domNode.classList.toggle('frozen', state === State.Frozen);455this.element.domNode.classList.remove('message');456457switch (state) {458case State.Hidden:459dom.hide(this._messageElement, this._listElement, this._status.element);460this._details.hide(true);461this._status.hide();462this._contentWidget.hide();463this._ctxSuggestWidgetVisible.reset();464this._ctxSuggestWidgetMultipleSuggestions.reset();465this._ctxSuggestWidgetHasFocusedSuggestion.reset();466this._showTimeout.cancel();467this.element.domNode.classList.remove('visible');468this._list.splice(0, this._list.length);469this._focusedItem = undefined;470this._cappedHeight = undefined;471this._explainMode = false;472break;473case State.Loading:474this.element.domNode.classList.add('message');475this._messageElement.textContent = SuggestWidget.LOADING_MESSAGE;476dom.hide(this._listElement, this._status.element);477dom.show(this._messageElement);478this._details.hide();479this._show();480this._focusedItem = undefined;481status(SuggestWidget.LOADING_MESSAGE);482break;483case State.Empty:484this.element.domNode.classList.add('message');485this._messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;486dom.hide(this._listElement, this._status.element);487dom.show(this._messageElement);488this._details.hide();489this._show();490this._focusedItem = undefined;491status(SuggestWidget.NO_SUGGESTIONS_MESSAGE);492break;493case State.Open:494dom.hide(this._messageElement);495dom.show(this._listElement, this._status.element);496this._show();497break;498case State.Frozen:499dom.hide(this._messageElement);500dom.show(this._listElement, this._status.element);501this._show();502break;503case State.Details:504dom.hide(this._messageElement);505dom.show(this._listElement, this._status.element);506this._details.show();507this._show();508this._details.widget.focus();509break;510}511}512513private _show(): void {514this._status.show();515this._contentWidget.show();516this._layout(this._persistedSize.restore());517this._ctxSuggestWidgetVisible.set(true);518519this._showTimeout.cancelAndSet(() => {520this.element.domNode.classList.add('visible');521this._onDidShow.fire(this);522}, 100);523}524525showTriggered(auto: boolean, delay: number) {526if (this._state !== State.Hidden) {527return;528}529this._contentWidget.setPosition(this.editor.getPosition());530this._isAuto = !!auto;531532if (!this._isAuto) {533this._loadingTimeout = disposableTimeout(() => this._setState(State.Loading), delay);534}535}536537showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean, noFocus: boolean): void {538539this._contentWidget.setPosition(this.editor.getPosition());540this._loadingTimeout?.dispose();541542this._currentSuggestionDetails?.cancel();543this._currentSuggestionDetails = undefined;544545if (this._completionModel !== completionModel) {546this._completionModel = completionModel;547}548549if (isFrozen && this._state !== State.Empty && this._state !== State.Hidden) {550this._setState(State.Frozen);551return;552}553554const visibleCount = this._completionModel.items.length;555const isEmpty = visibleCount === 0;556this._ctxSuggestWidgetMultipleSuggestions.set(visibleCount > 1);557558if (isEmpty) {559this._setState(isAuto ? State.Hidden : State.Empty);560this._completionModel = undefined;561return;562}563564this._focusedItem = undefined;565566// calling list.splice triggers focus event which this widget forwards. That can lead to567// suggestions being cancelled and the widget being cleared (and hidden). All this happens568// before revealing and focusing is done which means revealing and focusing will fail when569// they get run.570this._onDidFocus.pause();571this._onDidSelect.pause();572try {573this._list.splice(0, this._list.length, this._completionModel.items);574this._setState(isFrozen ? State.Frozen : State.Open);575this._list.reveal(selectionIndex, 0, selectionIndex === 0 ? 0 : this.getLayoutInfo().itemHeight * 0.33);576this._list.setFocus(noFocus ? [] : [selectionIndex]);577} finally {578this._onDidFocus.resume();579this._onDidSelect.resume();580}581582this._pendingLayout.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.element.domNode), () => {583this._pendingLayout.clear();584this._layout(this.element.size);585// Reset focus border586this._details.widget.domNode.classList.remove('focused');587});588}589590focusSelected(): void {591if (this._list.length > 0) {592this._list.setFocus([0]);593}594}595596selectNextPage(): boolean {597switch (this._state) {598case State.Hidden:599return false;600case State.Details:601this._details.widget.pageDown();602return true;603case State.Loading:604return !this._isAuto;605default:606this._list.focusNextPage();607return true;608}609}610611selectNext(): boolean {612switch (this._state) {613case State.Hidden:614return false;615case State.Loading:616return !this._isAuto;617default:618this._list.focusNext(1, true);619return true;620}621}622623selectLast(): boolean {624switch (this._state) {625case State.Hidden:626return false;627case State.Details:628this._details.widget.scrollBottom();629return true;630case State.Loading:631return !this._isAuto;632default:633this._list.focusLast();634return true;635}636}637638selectPreviousPage(): boolean {639switch (this._state) {640case State.Hidden:641return false;642case State.Details:643this._details.widget.pageUp();644return true;645case State.Loading:646return !this._isAuto;647default:648this._list.focusPreviousPage();649return true;650}651}652653selectPrevious(): boolean {654switch (this._state) {655case State.Hidden:656return false;657case State.Loading:658return !this._isAuto;659default:660this._list.focusPrevious(1, true);661return false;662}663}664665selectFirst(): boolean {666switch (this._state) {667case State.Hidden:668return false;669case State.Details:670this._details.widget.scrollTop();671return true;672case State.Loading:673return !this._isAuto;674default:675this._list.focusFirst();676return true;677}678}679680getFocusedItem(): ISelectedSuggestion | undefined {681if (this._state !== State.Hidden682&& this._state !== State.Empty683&& this._state !== State.Loading684&& this._completionModel685&& this._list.getFocus().length > 0686) {687688return {689item: this._list.getFocusedElements()[0],690index: this._list.getFocus()[0],691model: this._completionModel692};693}694return undefined;695}696697toggleDetailsFocus(): void {698if (this._state === State.Details) {699// Should return the focus to the list item.700this._list.setFocus(this._list.getFocus());701this._setState(State.Open);702} else if (this._state === State.Open) {703this._setState(State.Details);704if (!this._isDetailsVisible()) {705this.toggleDetails(true);706} else {707this._details.widget.focus();708}709}710}711712toggleDetails(focused: boolean = false): void {713if (this._isDetailsVisible()) {714// hide details widget715this._pendingShowDetails.clear();716this._ctxSuggestWidgetDetailsVisible.set(false);717this._setDetailsVisible(false);718this._details.hide();719this.element.domNode.classList.remove('shows-details');720721} else if ((canExpandCompletionItem(this._list.getFocusedElements()[0]) || this._explainMode) && (this._state === State.Open || this._state === State.Details || this._state === State.Frozen)) {722// show details widget (iff possible)723this._ctxSuggestWidgetDetailsVisible.set(true);724this._setDetailsVisible(true);725this._showDetails(false, focused);726}727}728729private _showDetails(loading: boolean, focused: boolean): void {730this._pendingShowDetails.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.element.domNode), () => {731this._pendingShowDetails.clear();732this._details.show();733let didFocusDetails = false;734if (loading) {735this._details.widget.renderLoading();736} else {737this._details.widget.renderItem(this._list.getFocusedElements()[0], this._explainMode);738}739if (!this._details.widget.isEmpty) {740this._positionDetails();741this.element.domNode.classList.add('shows-details');742if (focused) {743this._details.widget.focus();744didFocusDetails = true;745}746} else {747this._details.hide();748}749if (!didFocusDetails) {750this.editor.focus();751}752});753}754755toggleExplainMode(): void {756if (this._list.getFocusedElements()[0]) {757this._explainMode = !this._explainMode;758if (!this._isDetailsVisible()) {759this.toggleDetails();760} else {761this._showDetails(false, false);762}763}764}765766resetPersistedSize(): void {767this._persistedSize.reset();768}769770hideWidget(): void {771this._pendingLayout.clear();772this._pendingShowDetails.clear();773this._loadingTimeout?.dispose();774775this._setState(State.Hidden);776this._onDidHide.fire(this);777this.element.clearSashHoverState();778779// ensure that a reasonable widget height is persisted so that780// accidential "resize-to-single-items" cases aren't happening781const dim = this._persistedSize.restore();782const minPersistedHeight = Math.ceil(this.getLayoutInfo().itemHeight * 4.3);783if (dim && dim.height < minPersistedHeight) {784this._persistedSize.store(dim.with(undefined, minPersistedHeight));785}786}787788isFrozen(): boolean {789return this._state === State.Frozen;790}791792_afterRender(position: ContentWidgetPositionPreference | null) {793if (position === null) {794if (this._isDetailsVisible()) {795this._details.hide(); //todo@jrieken soft-hide796}797return;798}799if (this._state === State.Empty || this._state === State.Loading) {800// no special positioning when widget isn't showing list801return;802}803if (this._isDetailsVisible() && !this._details.widget.isEmpty) {804this._details.show();805}806this._positionDetails();807}808809private _layout(size: dom.Dimension | undefined): void {810if (!this.editor.hasModel()) {811return;812}813if (!this.editor.getDomNode()) {814// happens when running tests815return;816}817818const bodyBox = dom.getClientArea(this.element.domNode.ownerDocument.body);819const info = this.getLayoutInfo();820821if (!size) {822size = info.defaultSize;823}824825let height = size.height;826let width = size.width;827828// status bar829this._status.element.style.height = `${info.itemHeight}px`;830831if (this._state === State.Empty || this._state === State.Loading) {832// showing a message only833height = info.itemHeight + info.borderHeight;834width = info.defaultSize.width / 2;835this.element.enableSashes(false, false, false, false);836this.element.minSize = this.element.maxSize = new dom.Dimension(width, height);837this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW);838839} else {840// showing items841842// width math843const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding;844if (width > maxWidth) {845width = maxWidth;846}847const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width;848849// height math850const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight;851const minHeight = info.itemHeight + info.statusBarHeight;852const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode());853const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition());854const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height;855const maxHeightBelow = Math.min(bodyBox.height - cursorBottom - info.verticalPadding, fullHeight);856const availableSpaceAbove = editorBox.top + cursorBox.top - info.verticalPadding;857const maxHeightAbove = Math.min(availableSpaceAbove, fullHeight);858let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight);859860if (height === this._cappedHeight?.capped) {861// Restore the old (wanted) height when the current862// height is capped to fit863height = this._cappedHeight.wanted;864}865866if (height < minHeight) {867height = minHeight;868}869if (height > maxHeight) {870height = maxHeight;871}872873const forceRenderingAboveRequiredSpace = 150;874if ((height > maxHeightBelow && maxHeightAbove > maxHeightBelow) || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) {875this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE);876this.element.enableSashes(true, true, false, false);877maxHeight = maxHeightAbove;878} else {879this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW);880this.element.enableSashes(false, true, true, false);881maxHeight = maxHeightBelow;882}883this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height);884this.element.maxSize = new dom.Dimension(maxWidth, maxHeight);885this.element.minSize = new dom.Dimension(220, minHeight);886887// Know when the height was capped to fit and remember888// the wanted height for later. This is required when going889// left to widen suggestions.890this._cappedHeight = height === fullHeight891? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height }892: undefined;893}894this._resize(width, height);895}896897private _resize(width: number, height: number): void {898899const { width: maxWidth, height: maxHeight } = this.element.maxSize;900width = Math.min(maxWidth, width);901height = Math.min(maxHeight, height);902903const { statusBarHeight } = this.getLayoutInfo();904this._list.layout(height - statusBarHeight, width);905this._listElement.style.height = `${height - statusBarHeight}px`;906this.element.layout(height, width);907this._contentWidget.layout();908909this._positionDetails();910}911912private _positionDetails(): void {913if (this._isDetailsVisible()) {914this._details.placeAtAnchor(this.element.domNode, this._contentWidget.getPosition()?.preference[0] === ContentWidgetPositionPreference.BELOW);915}916}917918getLayoutInfo() {919const fontInfo = this.editor.getOption(EditorOption.fontInfo);920const itemHeight = clamp(this.editor.getOption(EditorOption.suggestLineHeight) || fontInfo.lineHeight, 8, 1000);921const statusBarHeight = !this.editor.getOption(EditorOption.suggest).showStatusBar || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight;922const borderWidth = this._details.widget.getLayoutInfo().borderWidth;923const borderHeight = 2 * borderWidth;924925return {926itemHeight,927statusBarHeight,928borderWidth,929borderHeight,930typicalHalfwidthCharacterWidth: fontInfo.typicalHalfwidthCharacterWidth,931verticalPadding: 22,932horizontalPadding: 14,933defaultSize: new dom.Dimension(430, statusBarHeight + 12 * itemHeight)934};935}936937private _isDetailsVisible(): boolean {938return this._storageService.getBoolean('expandSuggestionDocs', StorageScope.PROFILE, false);939}940941private _setDetailsVisible(value: boolean) {942this._storageService.store('expandSuggestionDocs', value, StorageScope.PROFILE, StorageTarget.USER);943}944945forceRenderingAbove() {946if (!this._forceRenderingAbove) {947this._forceRenderingAbove = true;948this._layout(this._persistedSize.restore());949}950}951952stopForceRenderingAbove() {953this._forceRenderingAbove = false;954}955}956957export class SuggestContentWidget implements IContentWidget {958959readonly allowEditorOverflow = true;960readonly suppressMouseDown = false;961962private _position?: IPosition | null;963private _preference?: ContentWidgetPositionPreference;964private _preferenceLocked = false;965966private _added: boolean = false;967private _hidden: boolean = false;968969constructor(970private readonly _widget: SuggestWidget,971private readonly _editor: ICodeEditor972) { }973974dispose(): void {975if (this._added) {976this._added = false;977this._editor.removeContentWidget(this);978}979}980981getId(): string {982return 'editor.widget.suggestWidget';983}984985getDomNode(): HTMLElement {986return this._widget.element.domNode;987}988989show(): void {990this._hidden = false;991if (!this._added) {992this._added = true;993this._editor.addContentWidget(this);994}995}996997hide(): void {998if (!this._hidden) {999this._hidden = true;1000this.layout();1001}1002}10031004layout(): void {1005this._editor.layoutContentWidget(this);1006}10071008getPosition(): IContentWidgetPosition | null {1009if (this._hidden || !this._position || !this._preference) {1010return null;1011}1012return {1013position: this._position,1014preference: [this._preference]1015};1016}10171018beforeRender() {1019const { height, width } = this._widget.element.size;1020const { borderWidth, horizontalPadding } = this._widget.getLayoutInfo();1021return new dom.Dimension(width + 2 * borderWidth + horizontalPadding, height + 2 * borderWidth);1022}10231024afterRender(position: ContentWidgetPositionPreference | null) {1025this._widget._afterRender(position);1026}10271028setPreference(preference: ContentWidgetPositionPreference) {1029if (!this._preferenceLocked) {1030this._preference = preference;1031}1032}10331034lockPreference() {1035this._preferenceLocked = true;1036}10371038unlockPreference() {1039this._preferenceLocked = false;1040}10411042setPosition(position: IPosition | null): void {1043this._position = position;1044}1045}104610471048