Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverRendered.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 { IEditorHoverContext, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts, RenderedHoverParts } from './hoverTypes.js';6import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { EditorHoverStatusBar } from './contentHoverStatusBar.js';8import { HoverStartSource } from './hoverOperation.js';9import { HoverCopyButton } from './hoverCopyButton.js';10import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';11import { ModelDecorationOptions } from '../../../common/model/textModel.js';12import { ICodeEditor } from '../../../browser/editorBrowser.js';13import { Position } from '../../../common/core/position.js';14import { Range } from '../../../common/core/range.js';15import { ContentHoverResult } from './contentHoverTypes.js';16import * as dom from '../../../../base/browser/dom.js';17import { HoverVerbosityAction } from '../../../common/languages.js';18import { MarkdownHoverParticipant } from './markdownHoverParticipant.js';19import { HoverColorPickerParticipant } from '../../colorPicker/browser/hoverColorPicker/hoverColorPickerParticipant.js';20import { localize } from '../../../../nls.js';21import { InlayHintsHover } from '../../inlayHints/browser/inlayHintsHover.js';22import { BugIndicatingError } from '../../../../base/common/errors.js';23import { HoverAction } from '../../../../base/browser/ui/hover/hoverWidget.js';24import { IHoverService } from '../../../../platform/hover/browser/hover.js';25import { IOffsetRange } from '../../../common/core/ranges/offsetRange.js';26import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';27import { MarkerHover } from './markerHoverParticipant.js';2829export class RenderedContentHover extends Disposable {3031public closestMouseDistance: number | undefined;32public initialMousePosX: number | undefined;33public initialMousePosY: number | undefined;3435public readonly showAtPosition: Position;36public readonly showAtSecondaryPosition: Position;37public readonly shouldFocus: boolean;38public readonly source: HoverStartSource;39public readonly shouldAppearBeforeContent: boolean;4041private readonly _renderedHoverParts: RenderedContentHoverParts;4243constructor(44editor: ICodeEditor,45hoverResult: ContentHoverResult,46participants: IEditorHoverParticipant<IHoverPart>[],47context: IEditorHoverContext,48@IKeybindingService keybindingService: IKeybindingService,49@IHoverService hoverService: IHoverService,50@IClipboardService clipboardService: IClipboardService51) {52super();53const parts = hoverResult.hoverParts;54this._renderedHoverParts = this._register(new RenderedContentHoverParts(55editor,56participants,57parts,58context,59keybindingService,60hoverService,61clipboardService62));63const contentHoverComputerOptions = hoverResult.options;64const anchor = contentHoverComputerOptions.anchor;65const { showAtPosition, showAtSecondaryPosition } = RenderedContentHover.computeHoverPositions(editor, anchor.range, parts);66this.shouldAppearBeforeContent = parts.some(m => m.isBeforeContent);67this.showAtPosition = showAtPosition;68this.showAtSecondaryPosition = showAtSecondaryPosition;69this.initialMousePosX = anchor.initialMousePosX;70this.initialMousePosY = anchor.initialMousePosY;71this.shouldFocus = contentHoverComputerOptions.shouldFocus;72this.source = contentHoverComputerOptions.source;73}7475public get domNode(): DocumentFragment {76return this._renderedHoverParts.domNode;77}7879public get domNodeHasChildren(): boolean {80return this._renderedHoverParts.domNodeHasChildren;81}8283public get focusedHoverPartIndex(): number {84return this._renderedHoverParts.focusedHoverPartIndex;85}8687public get hoverPartsCount(): number {88return this._renderedHoverParts.hoverPartsCount;89}9091public focusHoverPartWithIndex(index: number): void {92this._renderedHoverParts.focusHoverPartWithIndex(index);93}9495public getAccessibleWidgetContent(): string {96return this._renderedHoverParts.getAccessibleContent();97}9899public getAccessibleWidgetContentAtIndex(index: number): string {100return this._renderedHoverParts.getAccessibleHoverContentAtIndex(index);101}102103public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<void> {104this._renderedHoverParts.updateHoverVerbosityLevel(action, index, focus);105}106107public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {108return this._renderedHoverParts.doesHoverAtIndexSupportVerbosityAction(index, action);109}110111public isColorPickerVisible(): boolean {112return this._renderedHoverParts.isColorPickerVisible();113}114115public static computeHoverPositions(editor: ICodeEditor, anchorRange: Range, hoverParts: IHoverPart[]): { showAtPosition: Position; showAtSecondaryPosition: Position } {116117let startColumnBoundary = 1;118if (editor.hasModel()) {119// Ensure the range is on the current view line120const viewModel = editor._getViewModel();121const coordinatesConverter = viewModel.coordinatesConverter;122const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange);123const anchorViewMinColumn = viewModel.getLineMinColumn(anchorViewRange.startLineNumber);124const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, anchorViewMinColumn);125startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column;126}127128// The anchor range is always on a single line129const anchorStartLineNumber = anchorRange.startLineNumber;130let secondaryPositionColumn = anchorRange.startColumn;131let forceShowAtRange: Range | undefined;132133for (const hoverPart of hoverParts) {134const hoverPartRange = hoverPart.range;135const hoverPartRangeOnAnchorStartLine = hoverPartRange.startLineNumber === anchorStartLineNumber;136const hoverPartRangeOnAnchorEndLine = hoverPartRange.endLineNumber === anchorStartLineNumber;137const hoverPartRangeIsOnAnchorLine = hoverPartRangeOnAnchorStartLine && hoverPartRangeOnAnchorEndLine;138if (hoverPartRangeIsOnAnchorLine) {139// this message has a range that is completely sitting on the line of the anchor140const hoverPartStartColumn = hoverPartRange.startColumn;141const minSecondaryPositionColumn = Math.min(secondaryPositionColumn, hoverPartStartColumn);142secondaryPositionColumn = Math.max(minSecondaryPositionColumn, startColumnBoundary);143}144if (hoverPart.forceShowAtRange) {145forceShowAtRange = hoverPartRange;146}147}148149let showAtPosition: Position;150let showAtSecondaryPosition: Position;151if (forceShowAtRange) {152const forceShowAtPosition = forceShowAtRange.getStartPosition();153showAtPosition = forceShowAtPosition;154showAtSecondaryPosition = forceShowAtPosition;155} else {156showAtPosition = anchorRange.getStartPosition();157showAtSecondaryPosition = new Position(anchorStartLineNumber, secondaryPositionColumn);158}159return {160showAtPosition,161showAtSecondaryPosition,162};163}164}165166interface IRenderedContentHoverPart {167/**168* Type of rendered part169*/170type: 'hoverPart';171/**172* Participant of the rendered hover part173*/174participant: IEditorHoverParticipant<IHoverPart>;175/**176* The rendered hover part177*/178hoverPart: IHoverPart;179/**180* The HTML element containing the hover status bar.181*/182hoverElement: HTMLElement;183}184185interface IRenderedContentStatusBar {186/**187* Type of rendered part188*/189type: 'statusBar';190/**191* The HTML element containing the hover status bar.192*/193hoverElement: HTMLElement;194/**195* The actions of the hover status bar.196*/197actions: HoverAction[];198}199200type IRenderedContentHoverPartOrStatusBar = IRenderedContentHoverPart | IRenderedContentStatusBar;201202class RenderedStatusBar implements IDisposable {203204constructor(fragment: DocumentFragment, private readonly _statusBar: EditorHoverStatusBar) {205fragment.appendChild(this._statusBar.hoverElement);206}207208get hoverElement(): HTMLElement {209return this._statusBar.hoverElement;210}211212get actions(): HoverAction[] {213return this._statusBar.actions;214}215216dispose() {217this._statusBar.dispose();218}219}220221class RenderedContentHoverParts extends Disposable {222223private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({224description: 'content-hover-highlight',225className: 'hoverHighlight'226});227228private readonly _renderedParts: IRenderedContentHoverPartOrStatusBar[] = [];229private readonly _fragment: DocumentFragment;230private readonly _context: IEditorHoverContext;231232private _markdownHoverParticipant: MarkdownHoverParticipant | undefined;233private _colorHoverParticipant: HoverColorPickerParticipant | undefined;234private _focusedHoverPartIndex: number = -1;235236constructor(237editor: ICodeEditor,238participants: IEditorHoverParticipant<IHoverPart>[],239hoverParts: IHoverPart[],240context: IEditorHoverContext,241@IKeybindingService keybindingService: IKeybindingService,242@IHoverService private readonly _hoverService: IHoverService,243@IClipboardService private readonly _clipboardService: IClipboardService244) {245super();246this._context = context;247this._fragment = document.createDocumentFragment();248this._register(this._renderParts(participants, hoverParts, context, keybindingService, this._hoverService));249this._register(this._registerListenersOnRenderedParts());250this._register(this._createEditorDecorations(editor, hoverParts));251this._updateMarkdownAndColorParticipantInfo(participants);252}253254private _createEditorDecorations(editor: ICodeEditor, hoverParts: IHoverPart[]): IDisposable {255if (hoverParts.length === 0) {256return Disposable.None;257}258let highlightRange = hoverParts[0].range;259for (const hoverPart of hoverParts) {260const hoverPartRange = hoverPart.range;261highlightRange = Range.plusRange(highlightRange, hoverPartRange);262}263const highlightDecoration = editor.createDecorationsCollection();264highlightDecoration.set([{265range: highlightRange,266options: RenderedContentHoverParts._DECORATION_OPTIONS267}]);268return toDisposable(() => {269highlightDecoration.clear();270});271}272273private _renderParts(participants: IEditorHoverParticipant<IHoverPart>[], hoverParts: IHoverPart[], hoverContext: IEditorHoverContext, keybindingService: IKeybindingService, hoverService: IHoverService): IDisposable {274const statusBar = new EditorHoverStatusBar(keybindingService, hoverService);275const hoverRenderingContext: IEditorHoverRenderContext = {276fragment: this._fragment,277statusBar,278...hoverContext279};280const disposables = new DisposableStore();281disposables.add(statusBar);282for (const participant of participants) {283const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext);284disposables.add(renderedHoverParts);285for (const renderedHoverPart of renderedHoverParts.renderedHoverParts) {286this._renderedParts.push({287type: 'hoverPart',288participant,289hoverPart: renderedHoverPart.hoverPart,290hoverElement: renderedHoverPart.hoverElement,291});292}293}294const renderedStatusBar = this._renderStatusBar(this._fragment, statusBar);295if (renderedStatusBar) {296disposables.add(renderedStatusBar);297this._renderedParts.push({298type: 'statusBar',299hoverElement: renderedStatusBar.hoverElement,300actions: renderedStatusBar.actions,301});302}303return disposables;304}305306private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant<IHoverPart>, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts<IHoverPart> {307const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant);308const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0;309if (!hasHoverPartsForParticipant) {310return new RenderedHoverParts([]);311}312return participant.renderHoverParts(hoverRenderingContext, hoverPartsForParticipant);313}314315private _renderStatusBar(fragment: DocumentFragment, statusBar: EditorHoverStatusBar): RenderedStatusBar | undefined {316if (!statusBar.hasContent) {317return undefined;318}319return new RenderedStatusBar(fragment, statusBar);320}321322private _registerListenersOnRenderedParts(): IDisposable {323const disposables = new DisposableStore();324this._renderedParts.forEach((renderedPart: IRenderedContentHoverPartOrStatusBar, index: number) => {325const element = renderedPart.hoverElement;326element.tabIndex = 0;327disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => {328event.stopPropagation();329this._focusedHoverPartIndex = index;330}));331disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => {332event.stopPropagation();333this._focusedHoverPartIndex = -1;334}));335// Add copy button for marker hovers336if (renderedPart.type === 'hoverPart' && renderedPart.hoverPart instanceof MarkerHover) {337disposables.add(new HoverCopyButton(338element,339() => renderedPart.participant.getAccessibleContent(renderedPart.hoverPart),340this._clipboardService,341this._hoverService342));343}344});345return disposables;346}347348private _updateMarkdownAndColorParticipantInfo(participants: IEditorHoverParticipant<IHoverPart>[]) {349const markdownHoverParticipant = participants.find(p => {350return (p instanceof MarkdownHoverParticipant) && !(p instanceof InlayHintsHover);351});352if (markdownHoverParticipant) {353this._markdownHoverParticipant = markdownHoverParticipant as MarkdownHoverParticipant;354}355this._colorHoverParticipant = participants.find(p => p instanceof HoverColorPickerParticipant);356}357358public focusHoverPartWithIndex(index: number): void {359if (index < 0 || index >= this._renderedParts.length) {360return;361}362this._renderedParts[index].hoverElement.focus();363}364365public getAccessibleContent(): string {366const content: string[] = [];367for (let i = 0; i < this._renderedParts.length; i++) {368content.push(this.getAccessibleHoverContentAtIndex(i));369}370return content.join('\n\n');371}372373public getAccessibleHoverContentAtIndex(index: number): string {374const renderedPart = this._renderedParts[index];375if (!renderedPart) {376return '';377}378if (renderedPart.type === 'statusBar') {379const statusBarDescription = [localize('hoverAccessibilityStatusBar', "This is a hover status bar.")];380for (const action of renderedPart.actions) {381const keybinding = action.actionKeybindingLabel;382if (keybinding) {383statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithKeybinding', "It has an action with label {0} and keybinding {1}.", action.actionLabel, keybinding));384} else {385statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithoutKeybinding', "It has an action with label {0}.", action.actionLabel));386}387}388return statusBarDescription.join('\n');389}390return renderedPart.participant.getAccessibleContent(renderedPart.hoverPart);391}392393public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<void> {394if (!this._markdownHoverParticipant) {395return;396}397let rangeOfIndicesToUpdate: IOffsetRange;398if (index >= 0) {399rangeOfIndicesToUpdate = { start: index, endExclusive: index + 1 };400} else {401rangeOfIndicesToUpdate = this._findRangeOfMarkdownHoverParts(this._markdownHoverParticipant);402}403for (let i = rangeOfIndicesToUpdate.start; i < rangeOfIndicesToUpdate.endExclusive; i++) {404const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, i);405if (normalizedMarkdownHoverIndex === undefined) {406continue;407}408const renderedPart = await this._markdownHoverParticipant.updateMarkdownHoverVerbosityLevel(action, normalizedMarkdownHoverIndex);409if (!renderedPart) {410continue;411}412this._renderedParts[i] = {413type: 'hoverPart',414participant: this._markdownHoverParticipant,415hoverPart: renderedPart.hoverPart,416hoverElement: renderedPart.hoverElement,417};418}419if (focus) {420if (index >= 0) {421this.focusHoverPartWithIndex(index);422} else {423this._context.focus();424}425}426this._context.onContentsChanged();427}428429public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {430if (!this._markdownHoverParticipant) {431return false;432}433const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index);434if (normalizedMarkdownHoverIndex === undefined) {435return false;436}437return this._markdownHoverParticipant.doesMarkdownHoverAtIndexSupportVerbosityAction(normalizedMarkdownHoverIndex, action);438}439440public isColorPickerVisible(): boolean {441return this._colorHoverParticipant?.isColorPickerVisible() ?? false;442}443444private _normalizedIndexToMarkdownHoverIndexRange(markdownHoverParticipant: MarkdownHoverParticipant, index: number): number | undefined {445const renderedPart = this._renderedParts[index];446if (!renderedPart || renderedPart.type !== 'hoverPart') {447return undefined;448}449const isHoverPartMarkdownHover = renderedPart.participant === markdownHoverParticipant;450if (!isHoverPartMarkdownHover) {451return undefined;452}453const firstIndexOfMarkdownHovers = this._renderedParts.findIndex(renderedPart =>454renderedPart.type === 'hoverPart'455&& renderedPart.participant === markdownHoverParticipant456);457if (firstIndexOfMarkdownHovers === -1) {458throw new BugIndicatingError();459}460return index - firstIndexOfMarkdownHovers;461}462463private _findRangeOfMarkdownHoverParts(markdownHoverParticipant: MarkdownHoverParticipant): IOffsetRange {464const copiedRenderedParts = this._renderedParts.slice();465const firstIndexOfMarkdownHovers = copiedRenderedParts.findIndex(renderedPart => renderedPart.type === 'hoverPart' && renderedPart.participant === markdownHoverParticipant);466const inversedLastIndexOfMarkdownHovers = copiedRenderedParts.reverse().findIndex(renderedPart => renderedPart.type === 'hoverPart' && renderedPart.participant === markdownHoverParticipant);467const lastIndexOfMarkdownHovers = inversedLastIndexOfMarkdownHovers >= 0 ? copiedRenderedParts.length - inversedLastIndexOfMarkdownHovers : inversedLastIndexOfMarkdownHovers;468return { start: firstIndexOfMarkdownHovers, endExclusive: lastIndexOfMarkdownHovers + 1 };469}470471public get domNode(): DocumentFragment {472return this._fragment;473}474475public get domNodeHasChildren(): boolean {476return this._fragment.hasChildNodes();477}478479public get focusedHoverPartIndex(): number {480return this._focusedHoverPartIndex;481}482483public get hoverPartsCount(): number {484return this._renderedParts.length;485}486}487488489