Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.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 * as dom from '../../../../base/browser/dom.js';6import { KeyCode } from '../../../../base/common/keyCodes.js';7import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';8import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../browser/editorBrowser.js';9import { EditorOption } from '../../../common/config/editorOptions.js';10import { Range } from '../../../common/core/range.js';11import { TokenizationRegistry } from '../../../common/languages.js';12import { HoverOperation, HoverResult, HoverStartMode, HoverStartSource } from './hoverOperation.js';13import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverContext, IEditorHoverParticipant, IHoverPart, IHoverWidget } from './hoverTypes.js';14import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';15import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';16import { HoverVerbosityAction } from '../../../common/standalone/standaloneEnums.js';17import { ContentHoverWidget } from './contentHoverWidget.js';18import { ContentHoverComputer, ContentHoverComputerOptions } from './contentHoverComputer.js';19import { ContentHoverResult } from './contentHoverTypes.js';20import { Emitter } from '../../../../base/common/event.js';21import { RenderedContentHover } from './contentHoverRendered.js';22import { isMousePositionWithinElement } from './hoverUtils.js';23import { IHoverService } from '../../../../platform/hover/browser/hover.js';24import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';2526export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidget {2728private _currentResult: ContentHoverResult | null = null;29private readonly _renderedContentHover = this._register(new MutableDisposable<RenderedContentHover>());3031private readonly _contentHoverWidget: ContentHoverWidget;32private readonly _participants: IEditorHoverParticipant[];33private readonly _hoverOperation: HoverOperation<ContentHoverComputerOptions, IHoverPart>;3435private readonly _onContentsChanged = this._register(new Emitter<void>());36public readonly onContentsChanged = this._onContentsChanged.event;3738constructor(39private readonly _editor: ICodeEditor,40@IInstantiationService private readonly _instantiationService: IInstantiationService,41@IKeybindingService private readonly _keybindingService: IKeybindingService,42@IHoverService private readonly _hoverService: IHoverService,43@IClipboardService private readonly _clipboardService: IClipboardService44) {45super();46this._contentHoverWidget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));47this._participants = this._initializeHoverParticipants();48this._hoverOperation = this._register(new HoverOperation(this._editor, new ContentHoverComputer(this._editor, this._participants)));49this._registerListeners();50}5152private _initializeHoverParticipants(): IEditorHoverParticipant[] {53const participants: IEditorHoverParticipant[] = [];54for (const participant of HoverParticipantRegistry.getAll()) {55const participantInstance = this._instantiationService.createInstance(participant, this._editor);56participants.push(participantInstance);57}58participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal);59this._register(this._contentHoverWidget.onDidResize(() => {60this._participants.forEach(participant => participant.handleResize?.());61}));62this._register(this._contentHoverWidget.onDidScroll((e) => {63this._participants.forEach(participant => participant.handleScroll?.(e));64}));65this._register(this._contentHoverWidget.onContentsChanged(() => {66this._participants.forEach(participant => participant.handleContentsChanged?.());67}));68return participants;69}7071private _registerListeners(): void {72this._register(this._hoverOperation.onResult((result) => {73const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result) : result.value);74this._withResult(new ContentHoverResult(messages, result.isComplete, result.options));75}));76const contentHoverWidgetNode = this._contentHoverWidget.getDomNode();77this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'keydown', (e) => {78if (e.equals(KeyCode.Escape)) {79this.hide();80}81}));82this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'mouseleave', (e) => {83this._onMouseLeave(e);84}));85this._register(TokenizationRegistry.onDidChange(() => {86if (this._contentHoverWidget.position && this._currentResult) {87this._setCurrentResult(this._currentResult); // render again88}89}));90this._register(this._contentHoverWidget.onContentsChanged(() => {91this._onContentsChanged.fire();92}));93}9495/**96* Returns true if the hover shows now or will show.97*/98private _startShowingOrUpdateHover(99anchor: HoverAnchor | null,100mode: HoverStartMode,101source: HoverStartSource,102focus: boolean,103mouseEvent: IEditorMouseEvent | null104): boolean {105const contentHoverIsVisible = this._contentHoverWidget.position && this._currentResult;106if (!contentHoverIsVisible) {107if (anchor) {108this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);109return true;110}111return false;112}113const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky;114const isMouseGettingCloser = mouseEvent && this._contentHoverWidget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy);115const isHoverStickyAndIsMouseGettingCloser = isHoverSticky && isMouseGettingCloser;116// The mouse is getting closer to the hover, so we will keep the hover untouched117// But we will kick off a hover update at the new anchor, insisting on keeping the hover visible.118if (isHoverStickyAndIsMouseGettingCloser) {119if (anchor) {120this._startHoverOperationIfNecessary(anchor, mode, source, focus, true);121}122return true;123}124// If mouse is not getting closer and anchor not defined, hide the hover125if (!anchor) {126this._setCurrentResult(null);127return false;128}129// If mouse if not getting closer and anchor is defined, and the new anchor is the same as the previous anchor130const currentAnchorEqualsPreviousAnchor = this._currentResult && this._currentResult.options.anchor.equals(anchor);131if (currentAnchorEqualsPreviousAnchor) {132return true;133}134// If mouse if not getting closer and anchor is defined, and the new anchor is not compatible with the previous anchor135const currentAnchorCompatibleWithPreviousAnchor = this._currentResult && anchor.canAdoptVisibleHover(this._currentResult.options.anchor, this._contentHoverWidget.position);136if (!currentAnchorCompatibleWithPreviousAnchor) {137this._setCurrentResult(null);138this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);139return true;140}141// We aren't getting any closer to the hover, so we will filter existing results142// and keep those which also apply to the new anchor.143if (this._currentResult) {144this._setCurrentResult(this._currentResult.filter(anchor));145}146this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);147return true;148}149150private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, shouldFocus: boolean, insistOnKeepingHoverVisible: boolean): void {151const currentAnchorEqualToPreviousHover = this._hoverOperation.options && this._hoverOperation.options.anchor.equals(anchor);152if (currentAnchorEqualToPreviousHover) {153return;154}155this._hoverOperation.cancel();156const contentHoverComputerOptions: ContentHoverComputerOptions = {157anchor,158source,159shouldFocus,160insistOnKeepingHoverVisible161};162this._hoverOperation.start(mode, contentHoverComputerOptions);163}164165private _setCurrentResult(hoverResult: ContentHoverResult | null): void {166let currentHoverResult = hoverResult;167const currentResultEqualToPreviousResult = this._currentResult === currentHoverResult;168if (currentResultEqualToPreviousResult) {169return;170}171const currentHoverResultIsEmpty = currentHoverResult && currentHoverResult.hoverParts.length === 0;172if (currentHoverResultIsEmpty) {173currentHoverResult = null;174}175this._currentResult = currentHoverResult;176if (this._currentResult) {177this._showHover(this._currentResult);178} else {179this._hideHover();180}181}182183private _addLoadingMessage(hoverResult: HoverResult<ContentHoverComputerOptions, IHoverPart>): IHoverPart[] {184for (const participant of this._participants) {185if (!participant.createLoadingMessage) {186continue;187}188const loadingMessage = participant.createLoadingMessage(hoverResult.options.anchor);189if (!loadingMessage) {190continue;191}192return hoverResult.value.slice(0).concat([loadingMessage]);193}194return hoverResult.value;195}196197private _withResult(hoverResult: ContentHoverResult): void {198const previousHoverIsVisibleWithCompleteResult = this._contentHoverWidget.position && this._currentResult && this._currentResult.isComplete;199if (!previousHoverIsVisibleWithCompleteResult) {200this._setCurrentResult(hoverResult);201}202// The hover is visible with a previous complete result.203const isCurrentHoverResultComplete = hoverResult.isComplete;204if (!isCurrentHoverResultComplete) {205// Instead of rendering the new partial result, we wait for the result to be complete.206return;207}208const currentHoverResultIsEmpty = hoverResult.hoverParts.length === 0;209const insistOnKeepingPreviousHoverVisible = hoverResult.options.insistOnKeepingHoverVisible;210const shouldKeepPreviousHoverVisible = currentHoverResultIsEmpty && insistOnKeepingPreviousHoverVisible;211if (shouldKeepPreviousHoverVisible) {212// The hover would now hide normally, so we'll keep the previous messages213return;214}215this._setCurrentResult(hoverResult);216}217218private _showHover(hoverResult: ContentHoverResult): void {219const context = this._getHoverContext();220this._renderedContentHover.value = new RenderedContentHover(this._editor, hoverResult, this._participants, context, this._keybindingService, this._hoverService, this._clipboardService);221if (this._renderedContentHover.value.domNodeHasChildren) {222this._contentHoverWidget.show(this._renderedContentHover.value);223} else {224this._renderedContentHover.clear();225}226}227228private _hideHover(): void {229this._contentHoverWidget.hide();230this._participants.forEach(participant => participant.handleHide?.());231}232233private _getHoverContext(): IEditorHoverContext {234const hide = () => {235this.hide();236};237const onContentsChanged = () => {238this._contentHoverWidget.handleContentsChanged();239};240const setMinimumDimensions = (dimensions: dom.Dimension) => {241this._contentHoverWidget.setMinimumDimensions(dimensions);242};243const focus = () => this.focus();244return { hide, onContentsChanged, setMinimumDimensions, focus };245}246247248public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean {249const isContentWidgetResizing = this._contentHoverWidget.isResizing;250if (isContentWidgetResizing) {251return true;252}253const anchorCandidates: HoverAnchor[] = this._findHoverAnchorCandidates(mouseEvent);254const anchorCandidatesExist = anchorCandidates.length > 0;255if (!anchorCandidatesExist) {256return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);257}258const anchor = anchorCandidates[0];259return this._startShowingOrUpdateHover(anchor, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);260}261262private _findHoverAnchorCandidates(mouseEvent: IEditorMouseEvent): HoverAnchor[] {263const anchorCandidates: HoverAnchor[] = [];264for (const participant of this._participants) {265if (!participant.suggestHoverAnchor) {266continue;267}268const anchor = participant.suggestHoverAnchor(mouseEvent);269if (!anchor) {270continue;271}272anchorCandidates.push(anchor);273}274const target = mouseEvent.target;275switch (target.type) {276case MouseTargetType.CONTENT_TEXT: {277anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));278break;279}280case MouseTargetType.CONTENT_EMPTY: {281const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2;282// Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough283const mouseIsWithinLinesAndCloseToHover = !target.detail.isAfterLines284&& typeof target.detail.horizontalDistanceToText === 'number'285&& target.detail.horizontalDistanceToText < epsilon;286if (!mouseIsWithinLinesAndCloseToHover) {287break;288}289anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));290break;291}292}293anchorCandidates.sort((a, b) => b.priority - a.priority);294return anchorCandidates;295}296297private _onMouseLeave(e: MouseEvent): void {298const editorDomNode = this._editor.getDomNode();299const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y);300if (isMousePositionOutsideOfEditor) {301this.hide();302}303}304305public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void {306this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null);307}308309public getWidgetContent(): string | undefined {310const node = this._contentHoverWidget.getDomNode();311if (!node.textContent) {312return undefined;313}314return node.textContent;315}316317public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<void> {318this._renderedContentHover.value?.updateHoverVerbosityLevel(action, index, focus);319}320321public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {322return this._renderedContentHover.value?.doesHoverAtIndexSupportVerbosityAction(index, action) ?? false;323}324325public getAccessibleWidgetContent(): string | undefined {326return this._renderedContentHover.value?.getAccessibleWidgetContent();327}328329public getAccessibleWidgetContentAtIndex(index: number): string | undefined {330return this._renderedContentHover.value?.getAccessibleWidgetContentAtIndex(index);331}332333public focusedHoverPartIndex(): number {334return this._renderedContentHover.value?.focusedHoverPartIndex ?? -1;335}336337public containsNode(node: Node | null | undefined): boolean {338return (node ? this._contentHoverWidget.getDomNode().contains(node) : false);339}340341public focus(): void {342const hoverPartsCount = this._renderedContentHover.value?.hoverPartsCount;343if (hoverPartsCount === 1) {344this.focusHoverPartWithIndex(0);345return;346}347this._contentHoverWidget.focus();348}349350public focusHoverPartWithIndex(index: number): void {351this._renderedContentHover.value?.focusHoverPartWithIndex(index);352}353354public scrollUp(): void {355this._contentHoverWidget.scrollUp();356}357358public scrollDown(): void {359this._contentHoverWidget.scrollDown();360}361362public scrollLeft(): void {363this._contentHoverWidget.scrollLeft();364}365366public scrollRight(): void {367this._contentHoverWidget.scrollRight();368}369370public pageUp(): void {371this._contentHoverWidget.pageUp();372}373374public pageDown(): void {375this._contentHoverWidget.pageDown();376}377378public goToTop(): void {379this._contentHoverWidget.goToTop();380}381382public goToBottom(): void {383this._contentHoverWidget.goToBottom();384}385386public hide(): void {387this._hoverOperation.cancel();388this._setCurrentResult(null);389}390391public getDomNode(): HTMLElement {392return this._contentHoverWidget.getDomNode();393}394395public get isColorPickerVisible(): boolean {396return this._renderedContentHover.value?.isColorPickerVisible() ?? false;397}398399public get isVisibleFromKeyboard(): boolean {400return this._contentHoverWidget.isVisibleFromKeyboard;401}402403public get isVisible(): boolean {404return this._contentHoverWidget.isVisible;405}406407public get isFocused(): boolean {408return this._contentHoverWidget.isFocused;409}410411public get isResizing(): boolean {412return this._contentHoverWidget.isResizing;413}414415public get widget() {416return this._contentHoverWidget;417}418}419420421