Path: blob/main/src/vs/editor/contrib/suggest/browser/suggestWidget.ts
5334 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();321this._onDidSelect.dispose();322this._onDidFocus.dispose();323this._onDidHide.dispose();324this._onDidShow.dispose();325this._onDetailsKeydown.dispose();326}327328private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {329if (this._details.widget.domNode.contains(mouseEvent.target.element)) {330// Clicking inside details331this._details.widget.domNode.focus();332} else {333// Clicking outside details and inside suggest334if (this.element.domNode.contains(mouseEvent.target.element)) {335this.editor.focus();336}337}338}339340private _onCursorSelectionChanged(): void {341if (this._state !== State.Hidden) {342this._contentWidget.layout();343}344}345346private _onListMouseDownOrTap(e: IListMouseEvent<CompletionItem> | IListGestureEvent<CompletionItem>): void {347if (typeof e.element === 'undefined' || typeof e.index === 'undefined') {348return;349}350351// prevent stealing browser focus from the editor352e.browserEvent.preventDefault();353e.browserEvent.stopPropagation();354355this._select(e.element, e.index);356}357358private _onListSelection(e: IListEvent<CompletionItem>): void {359if (e.elements.length) {360this._select(e.elements[0], e.indexes[0]);361}362}363364private _select(item: CompletionItem, index: number): void {365const completionModel = this._completionModel;366if (completionModel) {367this._onDidSelect.fire({ item, index, model: completionModel });368this.editor.focus();369}370}371372private _onListFocus(e: IListEvent<CompletionItem>): void {373if (this._ignoreFocusEvents) {374return;375}376377if (this._state === State.Details) {378// This can happen when focus is in the details-panel and when379// arrow keys are pressed to select next/prev items380this._setState(State.Open);381}382383if (!e.elements.length) {384if (this._currentSuggestionDetails) {385this._currentSuggestionDetails.cancel();386this._currentSuggestionDetails = undefined;387this._focusedItem = undefined;388}389390this.editor.setAriaOptions({ activeDescendant: undefined });391this._ctxSuggestWidgetHasFocusedSuggestion.set(false);392return;393}394395if (!this._completionModel) {396return;397}398399this._ctxSuggestWidgetHasFocusedSuggestion.set(true);400const item = e.elements[0];401const index = e.indexes[0];402403if (item !== this._focusedItem) {404405this._currentSuggestionDetails?.cancel();406this._currentSuggestionDetails = undefined;407408this._focusedItem = item;409410this._list.reveal(index);411412this._currentSuggestionDetails = createCancelablePromise(async token => {413const loading = disposableTimeout(() => {414if (this._isDetailsVisible()) {415this._showDetails(true, false);416}417}, 250);418const sub = token.onCancellationRequested(() => loading.dispose());419try {420return await item.resolve(token);421} finally {422loading.dispose();423sub.dispose();424}425});426427this._currentSuggestionDetails.then(() => {428if (index >= this._list.length || item !== this._list.element(index)) {429return;430}431432// item can have extra information, so re-render433this._ignoreFocusEvents = true;434this._list.splice(index, 1, [item]);435this._list.setFocus([index]);436this._ignoreFocusEvents = false;437438if (this._isDetailsVisible()) {439this._showDetails(false, false);440} else {441this.element.domNode.classList.remove('docs-side');442}443444this.editor.setAriaOptions({ activeDescendant: this._list.getElementID(index) });445}).catch(onUnexpectedError);446}447448// emit an event449this._onDidFocus.fire({ item, index, model: this._completionModel });450}451452private _setState(state: State): void {453454if (this._state === state) {455return;456}457this._state = state;458459this.element.domNode.classList.toggle('frozen', state === State.Frozen);460this.element.domNode.classList.remove('message');461462switch (state) {463case State.Hidden:464dom.hide(this._messageElement, this._listElement, this._status.element);465this._details.hide(true);466this._status.hide();467this._contentWidget.hide();468this._ctxSuggestWidgetVisible.reset();469this._ctxSuggestWidgetMultipleSuggestions.reset();470this._ctxSuggestWidgetHasFocusedSuggestion.reset();471this._showTimeout.cancel();472this.element.domNode.classList.remove('visible');473this._list.splice(0, this._list.length);474this._focusedItem = undefined;475this._cappedHeight = undefined;476this._explainMode = false;477break;478case State.Loading:479this.element.domNode.classList.add('message');480this._messageElement.textContent = SuggestWidget.LOADING_MESSAGE;481dom.hide(this._listElement, this._status.element);482dom.show(this._messageElement);483this._details.hide();484this._show();485this._focusedItem = undefined;486status(SuggestWidget.LOADING_MESSAGE);487break;488case State.Empty:489this.element.domNode.classList.add('message');490this._messageElement.textContent = SuggestWidget.NO_SUGGESTIONS_MESSAGE;491dom.hide(this._listElement, this._status.element);492dom.show(this._messageElement);493this._details.hide();494this._show();495this._focusedItem = undefined;496status(SuggestWidget.NO_SUGGESTIONS_MESSAGE);497break;498case State.Open:499dom.hide(this._messageElement);500dom.show(this._listElement, this._status.element);501this._show();502break;503case State.Frozen:504dom.hide(this._messageElement);505dom.show(this._listElement, this._status.element);506this._show();507break;508case State.Details:509dom.hide(this._messageElement);510dom.show(this._listElement, this._status.element);511this._details.show();512this._show();513this._details.widget.focus();514break;515}516}517518private _show(): void {519this._status.show();520this._contentWidget.show();521this._layout(this._persistedSize.restore());522this._ctxSuggestWidgetVisible.set(true);523524this._showTimeout.cancelAndSet(() => {525this.element.domNode.classList.add('visible');526this._onDidShow.fire(this);527}, 100);528}529530showTriggered(auto: boolean, delay: number) {531if (this._state !== State.Hidden) {532return;533}534this._contentWidget.setPosition(this.editor.getPosition());535this._isAuto = !!auto;536537if (!this._isAuto) {538this._loadingTimeout = disposableTimeout(() => this._setState(State.Loading), delay);539}540}541542showSuggestions(completionModel: CompletionModel, selectionIndex: number, isFrozen: boolean, isAuto: boolean, noFocus: boolean): void {543544this._contentWidget.setPosition(this.editor.getPosition());545this._loadingTimeout?.dispose();546547this._currentSuggestionDetails?.cancel();548this._currentSuggestionDetails = undefined;549550if (this._completionModel !== completionModel) {551this._completionModel = completionModel;552}553554if (isFrozen && this._state !== State.Empty && this._state !== State.Hidden) {555this._setState(State.Frozen);556return;557}558559const visibleCount = this._completionModel.items.length;560const isEmpty = visibleCount === 0;561this._ctxSuggestWidgetMultipleSuggestions.set(visibleCount > 1);562563if (isEmpty) {564this._setState(isAuto ? State.Hidden : State.Empty);565this._completionModel = undefined;566return;567}568569this._focusedItem = undefined;570571// calling list.splice triggers focus event which this widget forwards. That can lead to572// suggestions being cancelled and the widget being cleared (and hidden). All this happens573// before revealing and focusing is done which means revealing and focusing will fail when574// they get run.575this._onDidFocus.pause();576this._onDidSelect.pause();577try {578this._list.splice(0, this._list.length, this._completionModel.items);579this._setState(isFrozen ? State.Frozen : State.Open);580this._list.reveal(selectionIndex, 0, selectionIndex === 0 ? 0 : this.getLayoutInfo().itemHeight * 0.33);581this._list.setFocus(noFocus ? [] : [selectionIndex]);582} finally {583this._onDidFocus.resume();584this._onDidSelect.resume();585}586587this._pendingLayout.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.element.domNode), () => {588this._pendingLayout.clear();589this._layout(this.element.size);590// Reset focus border591this._details.widget.domNode.classList.remove('focused');592});593}594595focusSelected(): void {596if (this._list.length > 0) {597this._list.setFocus([0]);598}599}600601selectNextPage(): boolean {602switch (this._state) {603case State.Hidden:604return false;605case State.Details:606this._details.widget.pageDown();607return true;608case State.Loading:609return !this._isAuto;610default:611this._list.focusNextPage();612return true;613}614}615616selectNext(): boolean {617switch (this._state) {618case State.Hidden:619return false;620case State.Loading:621return !this._isAuto;622default:623this._list.focusNext(1, true);624return true;625}626}627628selectLast(): boolean {629switch (this._state) {630case State.Hidden:631return false;632case State.Details:633this._details.widget.scrollBottom();634return true;635case State.Loading:636return !this._isAuto;637default:638this._list.focusLast();639return true;640}641}642643selectPreviousPage(): boolean {644switch (this._state) {645case State.Hidden:646return false;647case State.Details:648this._details.widget.pageUp();649return true;650case State.Loading:651return !this._isAuto;652default:653this._list.focusPreviousPage();654return true;655}656}657658selectPrevious(): boolean {659switch (this._state) {660case State.Hidden:661return false;662case State.Loading:663return !this._isAuto;664default:665this._list.focusPrevious(1, true);666return false;667}668}669670selectFirst(): boolean {671switch (this._state) {672case State.Hidden:673return false;674case State.Details:675this._details.widget.scrollTop();676return true;677case State.Loading:678return !this._isAuto;679default:680this._list.focusFirst();681return true;682}683}684685getFocusedItem(): ISelectedSuggestion | undefined {686if (this._state !== State.Hidden687&& this._state !== State.Empty688&& this._state !== State.Loading689&& this._completionModel690&& this._list.getFocus().length > 0691) {692693return {694item: this._list.getFocusedElements()[0],695index: this._list.getFocus()[0],696model: this._completionModel697};698}699return undefined;700}701702toggleDetailsFocus(): void {703if (this._state === State.Details) {704// Should return the focus to the list item.705this._list.setFocus(this._list.getFocus());706this._setState(State.Open);707} else if (this._state === State.Open) {708this._setState(State.Details);709if (!this._isDetailsVisible()) {710this.toggleDetails(true);711} else {712this._details.widget.focus();713}714}715}716717toggleDetails(focused: boolean = false): void {718if (this._isDetailsVisible()) {719// hide details widget720this._pendingShowDetails.clear();721this._ctxSuggestWidgetDetailsVisible.set(false);722this._setDetailsVisible(false);723this._details.hide();724this.element.domNode.classList.remove('shows-details');725726} else if ((canExpandCompletionItem(this._list.getFocusedElements()[0]) || this._explainMode) && (this._state === State.Open || this._state === State.Details || this._state === State.Frozen)) {727// show details widget (iff possible)728this._ctxSuggestWidgetDetailsVisible.set(true);729this._setDetailsVisible(true);730this._showDetails(false, focused);731}732}733734private _showDetails(loading: boolean, focused: boolean): void {735this._pendingShowDetails.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.element.domNode), () => {736this._pendingShowDetails.clear();737this._details.show();738let didFocusDetails = false;739if (loading) {740this._details.widget.renderLoading();741} else {742this._details.widget.renderItem(this._list.getFocusedElements()[0], this._explainMode);743}744if (!this._details.widget.isEmpty) {745this._positionDetails();746this.element.domNode.classList.add('shows-details');747if (focused) {748this._details.widget.focus();749didFocusDetails = true;750}751} else {752this._details.hide();753}754if (!didFocusDetails) {755this.editor.focus();756}757});758}759760toggleExplainMode(): void {761if (this._list.getFocusedElements()[0]) {762this._explainMode = !this._explainMode;763if (!this._isDetailsVisible()) {764this.toggleDetails();765} else {766this._showDetails(false, false);767}768}769}770771resetPersistedSize(): void {772this._persistedSize.reset();773}774775hideWidget(): void {776this._pendingLayout.clear();777this._pendingShowDetails.clear();778this._loadingTimeout?.dispose();779780this._setState(State.Hidden);781this._onDidHide.fire(this);782this.element.clearSashHoverState();783784// ensure that a reasonable widget height is persisted so that785// accidential "resize-to-single-items" cases aren't happening786const dim = this._persistedSize.restore();787const minPersistedHeight = Math.ceil(this.getLayoutInfo().itemHeight * 4.3);788if (dim && dim.height < minPersistedHeight) {789this._persistedSize.store(dim.with(undefined, minPersistedHeight));790}791}792793isFrozen(): boolean {794return this._state === State.Frozen;795}796797_afterRender(position: ContentWidgetPositionPreference | null) {798if (position === null) {799if (this._isDetailsVisible()) {800this._details.hide(); //todo@jrieken soft-hide801}802return;803}804if (this._state === State.Empty || this._state === State.Loading) {805// no special positioning when widget isn't showing list806return;807}808if (this._isDetailsVisible() && !this._details.widget.isEmpty) {809this._details.show();810}811this._positionDetails();812}813814private _layout(size: dom.Dimension | undefined): void {815if (!this.editor.hasModel()) {816return;817}818if (!this.editor.getDomNode()) {819// happens when running tests820return;821}822823const bodyBox = dom.getClientArea(this.element.domNode.ownerDocument.body);824const info = this.getLayoutInfo();825826if (!size) {827size = info.defaultSize;828}829830let height = size.height;831let width = size.width;832833// status bar834this._status.element.style.height = `${info.itemHeight}px`;835836if (this._state === State.Empty || this._state === State.Loading) {837// showing a message only838height = info.itemHeight + info.borderHeight;839width = info.defaultSize.width / 2;840this.element.enableSashes(false, false, false, false);841this.element.minSize = this.element.maxSize = new dom.Dimension(width, height);842this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW);843844} else {845// showing items846847// width math848const maxWidth = bodyBox.width - info.borderHeight - 2 * info.horizontalPadding;849if (width > maxWidth) {850width = maxWidth;851}852const preferredWidth = this._completionModel ? this._completionModel.stats.pLabelLen * info.typicalHalfwidthCharacterWidth : width;853854// height math855const fullHeight = info.statusBarHeight + this._list.contentHeight + info.borderHeight;856const minHeight = info.itemHeight + info.statusBarHeight;857const editorBox = dom.getDomNodePagePosition(this.editor.getDomNode());858const cursorBox = this.editor.getScrolledVisiblePosition(this.editor.getPosition());859const cursorBottom = editorBox.top + cursorBox.top + cursorBox.height;860const maxHeightBelow = Math.min(bodyBox.height - cursorBottom - info.verticalPadding, fullHeight);861const availableSpaceAbove = editorBox.top + cursorBox.top - info.verticalPadding;862const maxHeightAbove = Math.min(availableSpaceAbove, fullHeight);863let maxHeight = Math.min(Math.max(maxHeightAbove, maxHeightBelow) + info.borderHeight, fullHeight);864865if (height === this._cappedHeight?.capped) {866// Restore the old (wanted) height when the current867// height is capped to fit868height = this._cappedHeight.wanted;869}870871if (height < minHeight) {872height = minHeight;873}874if (height > maxHeight) {875height = maxHeight;876}877878const forceRenderingAboveRequiredSpace = 150;879if ((height > maxHeightBelow && maxHeightAbove > maxHeightBelow) || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) {880this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE);881this.element.enableSashes(true, true, false, false);882maxHeight = maxHeightAbove;883} else {884this._contentWidget.setPreference(ContentWidgetPositionPreference.BELOW);885this.element.enableSashes(false, true, true, false);886maxHeight = maxHeightBelow;887}888this.element.preferredSize = new dom.Dimension(preferredWidth, info.defaultSize.height);889this.element.maxSize = new dom.Dimension(maxWidth, maxHeight);890this.element.minSize = new dom.Dimension(220, minHeight);891892// Know when the height was capped to fit and remember893// the wanted height for later. This is required when going894// left to widen suggestions.895this._cappedHeight = height === fullHeight896? { wanted: this._cappedHeight?.wanted ?? size.height, capped: height }897: undefined;898}899this._resize(width, height);900}901902private _resize(width: number, height: number): void {903904const { width: maxWidth, height: maxHeight } = this.element.maxSize;905width = Math.min(maxWidth, width);906height = Math.min(maxHeight, height);907908const { statusBarHeight } = this.getLayoutInfo();909this._list.layout(height - statusBarHeight, width);910this._listElement.style.height = `${height - statusBarHeight}px`;911this.element.layout(height, width);912this._contentWidget.layout();913914this._positionDetails();915}916917private _positionDetails(): void {918if (this._isDetailsVisible()) {919this._details.placeAtAnchor(this.element.domNode, this._contentWidget.getPosition()?.preference[0] === ContentWidgetPositionPreference.BELOW);920}921}922923getLayoutInfo() {924const fontInfo = this.editor.getOption(EditorOption.fontInfo);925const itemHeight = clamp(this.editor.getOption(EditorOption.suggestLineHeight) || fontInfo.lineHeight, 8, 1000);926const statusBarHeight = !this.editor.getOption(EditorOption.suggest).showStatusBar || this._state === State.Empty || this._state === State.Loading ? 0 : itemHeight;927const borderWidth = this._details.widget.getLayoutInfo().borderWidth;928const borderHeight = 2 * borderWidth;929930return {931itemHeight,932statusBarHeight,933borderWidth,934borderHeight,935typicalHalfwidthCharacterWidth: fontInfo.typicalHalfwidthCharacterWidth,936verticalPadding: 22,937horizontalPadding: 14,938defaultSize: new dom.Dimension(430, statusBarHeight + 12 * itemHeight)939};940}941942private _isDetailsVisible(): boolean {943return this._storageService.getBoolean('expandSuggestionDocs', StorageScope.PROFILE, false);944}945946private _setDetailsVisible(value: boolean) {947this._storageService.store('expandSuggestionDocs', value, StorageScope.PROFILE, StorageTarget.USER);948}949950forceRenderingAbove() {951if (!this._forceRenderingAbove) {952this._forceRenderingAbove = true;953this._layout(this._persistedSize.restore());954}955}956957stopForceRenderingAbove() {958this._forceRenderingAbove = false;959}960}961962export class SuggestContentWidget implements IContentWidget {963964readonly allowEditorOverflow = true;965readonly suppressMouseDown = false;966967private _position?: IPosition | null;968private _preference?: ContentWidgetPositionPreference;969private _preferenceLocked = false;970971private _added: boolean = false;972private _hidden: boolean = false;973974constructor(975private readonly _widget: SuggestWidget,976private readonly _editor: ICodeEditor977) { }978979dispose(): void {980if (this._added) {981this._added = false;982this._editor.removeContentWidget(this);983}984}985986getId(): string {987return 'editor.widget.suggestWidget';988}989990getDomNode(): HTMLElement {991return this._widget.element.domNode;992}993994show(): void {995this._hidden = false;996if (!this._added) {997this._added = true;998this._editor.addContentWidget(this);999}1000}10011002hide(): void {1003if (!this._hidden) {1004this._hidden = true;1005this.layout();1006}1007}10081009layout(): void {1010this._editor.layoutContentWidget(this);1011}10121013getPosition(): IContentWidgetPosition | null {1014if (this._hidden || !this._position || !this._preference) {1015return null;1016}1017return {1018position: this._position,1019preference: [this._preference]1020};1021}10221023beforeRender() {1024const { height, width } = this._widget.element.size;1025const { borderWidth, horizontalPadding } = this._widget.getLayoutInfo();1026return new dom.Dimension(width + 2 * borderWidth + horizontalPadding, height + 2 * borderWidth);1027}10281029afterRender(position: ContentWidgetPositionPreference | null) {1030this._widget._afterRender(position);1031}10321033setPreference(preference: ContentWidgetPositionPreference) {1034if (!this._preferenceLocked) {1035this._preference = preference;1036}1037}10381039lockPreference() {1040this._preferenceLocked = true;1041}10421043unlockPreference() {1044this._preferenceLocked = false;1045}10461047setPosition(position: IPosition | null): void {1048this._position = position;1049}1050}105110521053