Path: blob/main/src/vs/editor/contrib/hover/browser/glyphHoverWidget.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 { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';7import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';8import { ICodeEditor, IEditorMouseEvent, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from '../../../browser/editorBrowser.js';9import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';10import { HoverOperation, HoverResult, HoverStartMode } from './hoverOperation.js';11import { HoverWidget } from '../../../../base/browser/ui/hover/hoverWidget.js';12import { IHoverWidget } from './hoverTypes.js';13import { IHoverMessage, LaneOrLineNumber, GlyphHoverComputer, GlyphHoverComputerOptions } from './glyphHoverComputer.js';14import { isMousePositionWithinElement } from './hoverUtils.js';1516const $ = dom.$;1718export class GlyphHoverWidget extends Disposable implements IOverlayWidget, IHoverWidget {1920public static readonly ID = 'editor.contrib.modesGlyphHoverWidget';21public readonly allowEditorOverflow = true;2223private readonly _editor: ICodeEditor;24private readonly _hover: HoverWidget;2526private _isVisible: boolean;27private _messages: IHoverMessage[];2829private readonly _hoverOperation: HoverOperation<GlyphHoverComputerOptions, IHoverMessage>;30private readonly _renderDisposeables = this._register(new DisposableStore());3132private _hoverComputerOptions: GlyphHoverComputerOptions | undefined;3334constructor(35editor: ICodeEditor,36@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,37) {38super();39this._editor = editor;4041this._isVisible = false;42this._messages = [];4344this._hover = this._register(new HoverWidget(true));45this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);4647this._hoverOperation = this._register(new HoverOperation(this._editor, new GlyphHoverComputer(this._editor)));48this._register(this._hoverOperation.onResult((result) => this._withResult(result)));4950this._register(this._editor.onDidChangeModelDecorations(() => this._onModelDecorationsChanged()));51this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {52if (e.hasChanged(EditorOption.fontInfo)) {53this._updateFont();54}55}));56this._register(dom.addStandardDisposableListener(this._hover.containerDomNode, 'mouseleave', (e) => {57this._onMouseLeave(e);58}));59this._editor.addOverlayWidget(this);60}6162public override dispose(): void {63this._hoverComputerOptions = undefined;64this._editor.removeOverlayWidget(this);65super.dispose();66}6768public getId(): string {69return GlyphHoverWidget.ID;70}7172public getDomNode(): HTMLElement {73return this._hover.containerDomNode;74}7576public getPosition(): IOverlayWidgetPosition | null {77return null;78}7980private _updateFont(): void {81// eslint-disable-next-line no-restricted-syntax82const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));83codeClasses.forEach(node => this._editor.applyFontInfo(node));84}8586private _onModelDecorationsChanged(): void {87if (this._isVisible && this._hoverComputerOptions) {88// The decorations have changed and the hover is visible,89// we need to recompute the displayed text90this._hoverOperation.cancel();91this._hoverOperation.start(HoverStartMode.Delayed, this._hoverComputerOptions);92}93}9495public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean {96const target = mouseEvent.target;97if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN && target.detail.glyphMarginLane) {98this._startShowingAt(target.position.lineNumber, target.detail.glyphMarginLane);99return true;100}101if (target.type === MouseTargetType.GUTTER_LINE_NUMBERS) {102this._startShowingAt(target.position.lineNumber, 'lineNo');103return true;104}105return false;106}107108private _startShowingAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void {109if (this._hoverComputerOptions110&& this._hoverComputerOptions.lineNumber === lineNumber111&& this._hoverComputerOptions.laneOrLine === laneOrLine) {112// We have to show the widget at the exact same line number as before, so no work is needed113return;114}115this._hoverOperation.cancel();116this.hide();117this._hoverComputerOptions = { lineNumber, laneOrLine };118this._hoverOperation.start(HoverStartMode.Delayed, this._hoverComputerOptions);119}120121public hide(): void {122this._hoverComputerOptions = undefined;123this._hoverOperation.cancel();124if (!this._isVisible) {125return;126}127this._isVisible = false;128this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);129}130131private _withResult(result: HoverResult<GlyphHoverComputerOptions, IHoverMessage>): void {132this._messages = result.value;133134if (this._messages.length > 0) {135this._renderMessages(result.options.lineNumber, result.options.laneOrLine, this._messages);136} else {137this.hide();138}139}140141private _renderMessages(lineNumber: number, laneOrLine: LaneOrLineNumber, messages: IHoverMessage[]): void {142this._renderDisposeables.clear();143144const fragment = document.createDocumentFragment();145146for (const msg of messages) {147const markdownHoverElement = $('div.hover-row.markdown-hover');148const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents'));149const renderedContents = this._renderDisposeables.add(this._markdownRendererService.render(msg.value, { context: this._editor }));150hoverContentsElement.appendChild(renderedContents.element);151fragment.appendChild(markdownHoverElement);152}153154this._updateContents(fragment);155this._showAt(lineNumber, laneOrLine);156}157158private _updateContents(node: Node): void {159this._hover.contentsDomNode.textContent = '';160this._hover.contentsDomNode.appendChild(node);161this._updateFont();162}163164private _showAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void {165if (!this._isVisible) {166this._isVisible = true;167this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);168}169170const editorLayout = this._editor.getLayoutInfo();171const topForLineNumber = this._editor.getTopForLineNumber(lineNumber);172const editorScrollTop = this._editor.getScrollTop();173const lineHeight = this._editor.getOption(EditorOption.lineHeight);174const nodeHeight = this._hover.containerDomNode.clientHeight;175const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2);176const left = editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth + (laneOrLine === 'lineNo' ? editorLayout.lineNumbersWidth : 0);177178// Constrain the hover widget to stay within the editor bounds179const editorHeight = editorLayout.height;180const maxTop = editorHeight - nodeHeight;181const constrainedTop = Math.max(0, Math.min(Math.round(top), maxTop));182183const fixedOverflowWidgets = this._editor.getOption(EditorOption.fixedOverflowWidgets);184if (fixedOverflowWidgets) {185// Use fixed positioning relative to the viewport186const editorDomNode = this._editor.getDomNode();187if (editorDomNode) {188const editorRect = dom.getDomNodePagePosition(editorDomNode);189this._hover.containerDomNode.style.position = 'fixed';190this._hover.containerDomNode.style.left = `${editorRect.left + left}px`;191this._hover.containerDomNode.style.top = `${editorRect.top + constrainedTop}px`;192}193} else {194// Use absolute positioning relative to the editor195this._hover.containerDomNode.style.position = 'absolute';196this._hover.containerDomNode.style.left = `${left}px`;197this._hover.containerDomNode.style.top = `${constrainedTop}px`;198}199this._hover.containerDomNode.style.zIndex = '11'; // 1 more than the zone widget at 10 (#233819)200}201202private _onMouseLeave(e: MouseEvent): void {203const editorDomNode = this._editor.getDomNode();204const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y);205if (isMousePositionOutsideOfEditor) {206this.hide();207}208}209}210211212