UNPKG

monaco-editor-core

Version:

A browser based code editor

261 lines (260 loc) • 12.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { RenderedHoverParts } from './hoverTypes.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { EditorHoverStatusBar } from './contentHoverStatusBar.js'; import { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; import * as dom from '../../../../base/browser/dom.js'; import { MarkdownHoverParticipant } from './markdownHoverParticipant.js'; import { ColorHoverParticipant } from '../../colorPicker/browser/colorHoverParticipant.js'; import { InlayHintsHover } from '../../inlayHints/browser/inlayHintsHover.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; export class RenderedContentHover extends Disposable { constructor(editor, hoverResult, participants, computer, context, keybindingService) { super(); const anchor = hoverResult.anchor; const parts = hoverResult.hoverParts; this._renderedHoverParts = this._register(new RenderedContentHoverParts(editor, participants, parts, keybindingService, context)); const { showAtPosition, showAtSecondaryPosition } = RenderedContentHover.computeHoverPositions(editor, anchor.range, parts); this.shouldAppearBeforeContent = parts.some(m => m.isBeforeContent); this.showAtPosition = showAtPosition; this.showAtSecondaryPosition = showAtSecondaryPosition; this.initialMousePosX = anchor.initialMousePosX; this.initialMousePosY = anchor.initialMousePosY; this.shouldFocus = computer.shouldFocus; this.source = computer.source; } get domNode() { return this._renderedHoverParts.domNode; } get domNodeHasChildren() { return this._renderedHoverParts.domNodeHasChildren; } get focusedHoverPartIndex() { return this._renderedHoverParts.focusedHoverPartIndex; } async updateHoverVerbosityLevel(action, index, focus) { this._renderedHoverParts.updateHoverVerbosityLevel(action, index, focus); } isColorPickerVisible() { return this._renderedHoverParts.isColorPickerVisible(); } static computeHoverPositions(editor, anchorRange, hoverParts) { let startColumnBoundary = 1; if (editor.hasModel()) { // Ensure the range is on the current view line const viewModel = editor._getViewModel(); const coordinatesConverter = viewModel.coordinatesConverter; const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange); const anchorViewMinColumn = viewModel.getLineMinColumn(anchorViewRange.startLineNumber); const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, anchorViewMinColumn); startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column; } // The anchor range is always on a single line const anchorStartLineNumber = anchorRange.startLineNumber; let secondaryPositionColumn = anchorRange.startColumn; let forceShowAtRange; for (const hoverPart of hoverParts) { const hoverPartRange = hoverPart.range; const hoverPartRangeOnAnchorStartLine = hoverPartRange.startLineNumber === anchorStartLineNumber; const hoverPartRangeOnAnchorEndLine = hoverPartRange.endLineNumber === anchorStartLineNumber; const hoverPartRangeIsOnAnchorLine = hoverPartRangeOnAnchorStartLine && hoverPartRangeOnAnchorEndLine; if (hoverPartRangeIsOnAnchorLine) { // this message has a range that is completely sitting on the line of the anchor const hoverPartStartColumn = hoverPartRange.startColumn; const minSecondaryPositionColumn = Math.min(secondaryPositionColumn, hoverPartStartColumn); secondaryPositionColumn = Math.max(minSecondaryPositionColumn, startColumnBoundary); } if (hoverPart.forceShowAtRange) { forceShowAtRange = hoverPartRange; } } let showAtPosition; let showAtSecondaryPosition; if (forceShowAtRange) { const forceShowAtPosition = forceShowAtRange.getStartPosition(); showAtPosition = forceShowAtPosition; showAtSecondaryPosition = forceShowAtPosition; } else { showAtPosition = anchorRange.getStartPosition(); showAtSecondaryPosition = new Position(anchorStartLineNumber, secondaryPositionColumn); } return { showAtPosition, showAtSecondaryPosition, }; } } class RenderedStatusBar { constructor(fragment, _statusBar) { this._statusBar = _statusBar; fragment.appendChild(this._statusBar.hoverElement); } get hoverElement() { return this._statusBar.hoverElement; } get actions() { return this._statusBar.actions; } dispose() { this._statusBar.dispose(); } } class RenderedContentHoverParts extends Disposable { static { this._DECORATION_OPTIONS = ModelDecorationOptions.register({ description: 'content-hover-highlight', className: 'hoverHighlight' }); } constructor(editor, participants, hoverParts, keybindingService, context) { super(); this._renderedParts = []; this._focusedHoverPartIndex = -1; this._context = context; this._fragment = document.createDocumentFragment(); this._register(this._renderParts(participants, hoverParts, context, keybindingService)); this._register(this._registerListenersOnRenderedParts()); this._register(this._createEditorDecorations(editor, hoverParts)); this._updateMarkdownAndColorParticipantInfo(participants); } _createEditorDecorations(editor, hoverParts) { if (hoverParts.length === 0) { return Disposable.None; } let highlightRange = hoverParts[0].range; for (const hoverPart of hoverParts) { const hoverPartRange = hoverPart.range; highlightRange = Range.plusRange(highlightRange, hoverPartRange); } const highlightDecoration = editor.createDecorationsCollection(); highlightDecoration.set([{ range: highlightRange, options: RenderedContentHoverParts._DECORATION_OPTIONS }]); return toDisposable(() => { highlightDecoration.clear(); }); } _renderParts(participants, hoverParts, hoverContext, keybindingService) { const statusBar = new EditorHoverStatusBar(keybindingService); const hoverRenderingContext = { fragment: this._fragment, statusBar, ...hoverContext }; const disposables = new DisposableStore(); for (const participant of participants) { const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext); disposables.add(renderedHoverParts); for (const renderedHoverPart of renderedHoverParts.renderedHoverParts) { this._renderedParts.push({ type: 'hoverPart', participant, hoverPart: renderedHoverPart.hoverPart, hoverElement: renderedHoverPart.hoverElement, }); } } const renderedStatusBar = this._renderStatusBar(this._fragment, statusBar); if (renderedStatusBar) { disposables.add(renderedStatusBar); this._renderedParts.push({ type: 'statusBar', hoverElement: renderedStatusBar.hoverElement, actions: renderedStatusBar.actions, }); } return toDisposable(() => { disposables.dispose(); }); } _renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext) { const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant); const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0; if (!hasHoverPartsForParticipant) { return new RenderedHoverParts([]); } return participant.renderHoverParts(hoverRenderingContext, hoverPartsForParticipant); } _renderStatusBar(fragment, statusBar) { if (!statusBar.hasContent) { return undefined; } return new RenderedStatusBar(fragment, statusBar); } _registerListenersOnRenderedParts() { const disposables = new DisposableStore(); this._renderedParts.forEach((renderedPart, index) => { const element = renderedPart.hoverElement; element.tabIndex = 0; disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event) => { event.stopPropagation(); this._focusedHoverPartIndex = index; })); disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event) => { event.stopPropagation(); this._focusedHoverPartIndex = -1; })); }); return disposables; } _updateMarkdownAndColorParticipantInfo(participants) { const markdownHoverParticipant = participants.find(p => { return (p instanceof MarkdownHoverParticipant) && !(p instanceof InlayHintsHover); }); if (markdownHoverParticipant) { this._markdownHoverParticipant = markdownHoverParticipant; } this._colorHoverParticipant = participants.find(p => p instanceof ColorHoverParticipant); } async updateHoverVerbosityLevel(action, index, focus) { if (!this._markdownHoverParticipant) { return; } const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index); if (normalizedMarkdownHoverIndex === undefined) { return; } const renderedPart = await this._markdownHoverParticipant.updateMarkdownHoverVerbosityLevel(action, normalizedMarkdownHoverIndex, focus); if (!renderedPart) { return; } this._renderedParts[index] = { type: 'hoverPart', participant: this._markdownHoverParticipant, hoverPart: renderedPart.hoverPart, hoverElement: renderedPart.hoverElement, }; this._context.onContentsChanged(); } isColorPickerVisible() { return this._colorHoverParticipant?.isColorPickerVisible() ?? false; } _normalizedIndexToMarkdownHoverIndexRange(markdownHoverParticipant, index) { const renderedPart = this._renderedParts[index]; if (!renderedPart || renderedPart.type !== 'hoverPart') { return undefined; } const isHoverPartMarkdownHover = renderedPart.participant === markdownHoverParticipant; if (!isHoverPartMarkdownHover) { return undefined; } const firstIndexOfMarkdownHovers = this._renderedParts.findIndex(renderedPart => renderedPart.type === 'hoverPart' && renderedPart.participant === markdownHoverParticipant); if (firstIndexOfMarkdownHovers === -1) { throw new BugIndicatingError(); } return index - firstIndexOfMarkdownHovers; } get domNode() { return this._fragment; } get domNodeHasChildren() { return this._fragment.hasChildNodes(); } get focusedHoverPartIndex() { return this._focusedHoverPartIndex; } }