Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverController.ts
4779 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 { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from './hoverActionIds.js';6import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';8import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from '../../../browser/editorBrowser.js';9import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';10import { Range } from '../../../common/core/range.js';11import { IEditorContribution, IScrollEvent } from '../../../common/editorCommon.js';12import { HoverStartMode, HoverStartSource } from './hoverOperation.js';13import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';14import { InlineSuggestionHintsContentWidget } from '../../inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.js';15import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';16import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';17import { HoverVerbosityAction } from '../../../common/languages.js';18import { RunOnceScheduler } from '../../../../base/common/async.js';19import { isMousePositionWithinElement, shouldShowHover } from './hoverUtils.js';20import { ContentHoverWidgetWrapper } from './contentHoverWidgetWrapper.js';21import './hover.css';22import { Emitter } from '../../../../base/common/event.js';23import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js';24import { isModifierKey, KeyCode } from '../../../../base/common/keyCodes.js';25import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';2627// sticky hover widget which doesn't disappear on focus out and such28const _sticky = false29// || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this30;3132interface IHoverSettings {33readonly enabled: 'on' | 'off' | 'onKeyboardModifier';34readonly sticky: boolean;35readonly hidingDelay: number;36}3738export class ContentHoverController extends Disposable implements IEditorContribution {3940private readonly _onHoverContentsChanged = this._register(new Emitter<void>());41public readonly onHoverContentsChanged = this._onHoverContentsChanged.event;4243public static readonly ID = 'editor.contrib.contentHover';4445public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false;4647private readonly _listenersStore = new DisposableStore();4849private _contentWidget: ContentHoverWidgetWrapper | undefined;5051private _mouseMoveEvent: IEditorMouseEvent | undefined;52private _reactToEditorMouseMoveRunner: RunOnceScheduler;5354private _hoverSettings!: IHoverSettings;55private _isMouseDown: boolean = false;5657private _ignoreMouseEvents: boolean = false;5859constructor(60private readonly _editor: ICodeEditor,61@IContextMenuService _contextMenuService: IContextMenuService,62@IInstantiationService private readonly _instantiationService: IInstantiationService,63@IKeybindingService private readonly _keybindingService: IKeybindingService64) {65super();66this._reactToEditorMouseMoveRunner = this._register(new RunOnceScheduler(67() => {68if (this._mouseMoveEvent) {69this._reactToEditorMouseMove(this._mouseMoveEvent);70}71}, 072));73this._register(_contextMenuService.onDidShowContextMenu(() => {74this.hideContentHover();75this._ignoreMouseEvents = true;76}));77this._register(_contextMenuService.onDidHideContextMenu(() => {78this._ignoreMouseEvents = false;79}));80this._hookListeners();81this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {82if (e.hasChanged(EditorOption.hover)) {83this._unhookListeners();84this._hookListeners();85}86}));87}8889static get(editor: ICodeEditor): ContentHoverController | null {90return editor.getContribution<ContentHoverController>(ContentHoverController.ID);91}9293private _hookListeners(): void {94const hoverOpts = this._editor.getOption(EditorOption.hover);95this._hoverSettings = {96enabled: hoverOpts.enabled,97sticky: hoverOpts.sticky,98hidingDelay: hoverOpts.hidingDelay99};100if (hoverOpts.enabled === 'off') {101this._cancelSchedulerAndHide();102}103this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));104this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp()));105this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e)));106this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e)));107this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e)));108this._listenersStore.add(this._editor.onDidChangeModel(() => this._cancelSchedulerAndHide()));109this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler()));110this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e)));111}112113private _unhookListeners(): void {114this._listenersStore.clear();115}116117private _cancelSchedulerAndHide(): void {118this._cancelScheduler();119this.hideContentHover();120}121122private _cancelScheduler() {123this._mouseMoveEvent = undefined;124this._reactToEditorMouseMoveRunner.cancel();125}126127private _onEditorScrollChanged(e: IScrollEvent): void {128if (this._ignoreMouseEvents) {129return;130}131if (e.scrollTopChanged || e.scrollLeftChanged) {132this.hideContentHover();133}134}135136private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {137if (this._ignoreMouseEvents) {138return;139}140this._isMouseDown = true;141const shouldKeepHoverWidgetVisible = this._shouldKeepHoverWidgetVisible(mouseEvent);142if (shouldKeepHoverWidgetVisible) {143return;144}145this.hideContentHover();146}147148private _shouldKeepHoverWidgetVisible(mouseEvent: IPartialEditorMouseEvent): boolean {149return this._isMouseOnContentHoverWidget(mouseEvent) || this._isContentWidgetResizing() || isOnColorDecorator(mouseEvent);150}151152private _isMouseOnContentHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean {153if (!this._contentWidget) {154return false;155}156return isMousePositionWithinElement(this._contentWidget.getDomNode(), mouseEvent.event.posx, mouseEvent.event.posy);157}158159private _onEditorMouseUp(): void {160if (this._ignoreMouseEvents) {161return;162}163this._isMouseDown = false;164}165166private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void {167if (this._ignoreMouseEvents) {168return;169}170if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) {171return;172}173this._cancelScheduler();174const shouldKeepHoverWidgetVisible = this._shouldKeepHoverWidgetVisible(mouseEvent);175if (shouldKeepHoverWidgetVisible) {176return;177}178if (_sticky) {179return;180}181this.hideContentHover();182}183184private _shouldKeepCurrentHover(mouseEvent: IEditorMouseEvent): boolean {185const contentWidget = this._contentWidget;186if (!contentWidget) {187return false;188}189const isHoverSticky = this._hoverSettings.sticky;190const isMouseOnStickyContentHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean): boolean => {191const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent);192return isHoverSticky && isMouseOnContentHoverWidget;193};194const isMouseOnColorPickerOrChoosingColor = (mouseEvent: IEditorMouseEvent): boolean => {195const isColorPickerVisible = contentWidget.isColorPickerVisible;196const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent);197const isMouseOnHoverWithColorPicker = isColorPickerVisible && isMouseOnContentHoverWidget;198const isMaybeChoosingColor = isColorPickerVisible && this._isMouseDown;199return isMouseOnHoverWithColorPicker || isMaybeChoosingColor;200};201// TODO@aiday-mar verify if the following is necessary code202const isTextSelectedWithinContentHoverWidget = (mouseEvent: IEditorMouseEvent, sticky: boolean): boolean => {203const view = mouseEvent.event.browserEvent.view;204if (!view) {205return false;206}207return sticky && contentWidget.containsNode(view.document.activeElement) && !view.getSelection()?.isCollapsed;208};209const isFocused = contentWidget.isFocused;210const isResizing = contentWidget.isResizing;211const isStickyAndVisibleFromKeyboard = this._hoverSettings.sticky && contentWidget.isVisibleFromKeyboard;212213return this.shouldKeepOpenOnEditorMouseMoveOrLeave214|| isFocused215|| isResizing216|| isStickyAndVisibleFromKeyboard217|| isMouseOnStickyContentHoverWidget(mouseEvent, isHoverSticky)218|| isMouseOnColorPickerOrChoosingColor(mouseEvent)219|| isTextSelectedWithinContentHoverWidget(mouseEvent, isHoverSticky);220}221222private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {223if (this._ignoreMouseEvents) {224return;225}226this._mouseMoveEvent = mouseEvent;227const shouldKeepCurrentHover = this._shouldKeepCurrentHover(mouseEvent);228if (shouldKeepCurrentHover) {229this._reactToEditorMouseMoveRunner.cancel();230return;231}232const shouldRescheduleHoverComputation = this._shouldRescheduleHoverComputation();233if (shouldRescheduleHoverComputation) {234if (!this._reactToEditorMouseMoveRunner.isScheduled()) {235this._reactToEditorMouseMoveRunner.schedule(this._hoverSettings.hidingDelay);236}237return;238}239this._reactToEditorMouseMove(mouseEvent);240}241242private _shouldRescheduleHoverComputation(): boolean {243const hidingDelay = this._hoverSettings.hidingDelay;244const isContentHoverWidgetVisible = this._contentWidget?.isVisible ?? false;245// If the mouse is not over the widget, and if sticky is on,246// then give it a grace period before reacting to the mouse event247return isContentHoverWidgetVisible && this._hoverSettings.sticky && hidingDelay > 0;248}249250private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent): void {251if (shouldShowHover(252this._hoverSettings.enabled,253this._editor.getOption(EditorOption.multiCursorModifier),254mouseEvent255)) {256const contentWidget: ContentHoverWidgetWrapper = this._getOrCreateContentWidget();257if (contentWidget.showsOrWillShow(mouseEvent)) {258return;259}260}261if (_sticky) {262return;263}264this.hideContentHover();265}266267private _onKeyDown(e: IKeyboardEvent): void {268if (this._ignoreMouseEvents) {269return;270}271if (!this._contentWidget) {272return;273}274const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e);275const isModifierKeyPressed = isModifierKey(e.keyCode);276if (isPotentialKeyboardShortcut || isModifierKeyPressed) {277return;278}279if (this._contentWidget.isFocused && e.keyCode === KeyCode.Tab) {280return;281}282this.hideContentHover();283}284285private _isPotentialKeyboardShortcut(e: IKeyboardEvent): boolean {286if (!this._editor.hasModel() || !this._contentWidget) {287return false;288}289const resolvedKeyboardEvent = this._keybindingService.softDispatch(e, this._editor.getDomNode());290const moreChordsAreNeeded = resolvedKeyboardEvent.kind === ResultKind.MoreChordsNeeded;291const isHoverAction = resolvedKeyboardEvent.kind === ResultKind.KbFound292&& (resolvedKeyboardEvent.commandId === SHOW_OR_FOCUS_HOVER_ACTION_ID293|| resolvedKeyboardEvent.commandId === INCREASE_HOVER_VERBOSITY_ACTION_ID294|| resolvedKeyboardEvent.commandId === DECREASE_HOVER_VERBOSITY_ACTION_ID)295&& this._contentWidget.isVisible;296return moreChordsAreNeeded || isHoverAction;297}298299public hideContentHover(): void {300if (_sticky) {301return;302}303if (InlineSuggestionHintsContentWidget.dropDownVisible) {304return;305}306this._contentWidget?.hide();307}308309private _getOrCreateContentWidget(): ContentHoverWidgetWrapper {310if (!this._contentWidget) {311this._contentWidget = this._instantiationService.createInstance(ContentHoverWidgetWrapper, this._editor);312this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire()));313}314return this._contentWidget;315}316317public showContentHover(318range: Range,319mode: HoverStartMode,320source: HoverStartSource,321focus: boolean322): void {323this._getOrCreateContentWidget().startShowingAtRange(range, mode, source, focus);324}325326private _isContentWidgetResizing(): boolean {327return this._contentWidget?.widget.isResizing || false;328}329330public focusedHoverPartIndex(): number {331return this._getOrCreateContentWidget().focusedHoverPartIndex();332}333334public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {335return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action);336}337338public updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): void {339this._getOrCreateContentWidget().updateHoverVerbosityLevel(action, index, focus);340}341342public focus(): void {343this._contentWidget?.focus();344}345346public focusHoverPartWithIndex(index: number): void {347this._contentWidget?.focusHoverPartWithIndex(index);348}349350public scrollUp(): void {351this._contentWidget?.scrollUp();352}353354public scrollDown(): void {355this._contentWidget?.scrollDown();356}357358public scrollLeft(): void {359this._contentWidget?.scrollLeft();360}361362public scrollRight(): void {363this._contentWidget?.scrollRight();364}365366public pageUp(): void {367this._contentWidget?.pageUp();368}369370public pageDown(): void {371this._contentWidget?.pageDown();372}373374public goToTop(): void {375this._contentWidget?.goToTop();376}377378public goToBottom(): void {379this._contentWidget?.goToBottom();380}381382public getWidgetContent(): string | undefined {383return this._contentWidget?.getWidgetContent();384}385386public getAccessibleWidgetContent(): string | undefined {387return this._contentWidget?.getAccessibleWidgetContent();388}389390public getAccessibleWidgetContentAtIndex(index: number): string | undefined {391return this._contentWidget?.getAccessibleWidgetContentAtIndex(index);392}393394public get isColorPickerVisible(): boolean | undefined {395return this._contentWidget?.isColorPickerVisible;396}397398public get isHoverVisible(): boolean | undefined {399return this._contentWidget?.isVisible;400}401402public override dispose(): void {403super.dispose();404this._unhookListeners();405this._listenersStore.dispose();406this._contentWidget?.dispose();407}408}409410411