Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/hover/browser/glyphHoverWidget.ts
4779 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
8
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
9
import { ICodeEditor, IEditorMouseEvent, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from '../../../browser/editorBrowser.js';
10
import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';
11
import { HoverOperation, HoverResult, HoverStartMode } from './hoverOperation.js';
12
import { HoverWidget } from '../../../../base/browser/ui/hover/hoverWidget.js';
13
import { IHoverWidget } from './hoverTypes.js';
14
import { IHoverMessage, LaneOrLineNumber, GlyphHoverComputer, GlyphHoverComputerOptions } from './glyphHoverComputer.js';
15
import { isMousePositionWithinElement } from './hoverUtils.js';
16
17
const $ = dom.$;
18
19
export class GlyphHoverWidget extends Disposable implements IOverlayWidget, IHoverWidget {
20
21
public static readonly ID = 'editor.contrib.modesGlyphHoverWidget';
22
public readonly allowEditorOverflow = true;
23
24
private readonly _editor: ICodeEditor;
25
private readonly _hover: HoverWidget;
26
27
private _isVisible: boolean;
28
private _messages: IHoverMessage[];
29
30
private readonly _hoverOperation: HoverOperation<GlyphHoverComputerOptions, IHoverMessage>;
31
private readonly _renderDisposeables = this._register(new DisposableStore());
32
33
private _hoverComputerOptions: GlyphHoverComputerOptions | undefined;
34
35
constructor(
36
editor: ICodeEditor,
37
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
38
) {
39
super();
40
this._editor = editor;
41
42
this._isVisible = false;
43
this._messages = [];
44
45
this._hover = this._register(new HoverWidget(true));
46
this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);
47
48
this._hoverOperation = this._register(new HoverOperation(this._editor, new GlyphHoverComputer(this._editor)));
49
this._register(this._hoverOperation.onResult((result) => this._withResult(result)));
50
51
this._register(this._editor.onDidChangeModelDecorations(() => this._onModelDecorationsChanged()));
52
this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
53
if (e.hasChanged(EditorOption.fontInfo)) {
54
this._updateFont();
55
}
56
}));
57
this._register(dom.addStandardDisposableListener(this._hover.containerDomNode, 'mouseleave', (e) => {
58
this._onMouseLeave(e);
59
}));
60
this._editor.addOverlayWidget(this);
61
}
62
63
public override dispose(): void {
64
this._hoverComputerOptions = undefined;
65
this._editor.removeOverlayWidget(this);
66
super.dispose();
67
}
68
69
public getId(): string {
70
return GlyphHoverWidget.ID;
71
}
72
73
public getDomNode(): HTMLElement {
74
return this._hover.containerDomNode;
75
}
76
77
public getPosition(): IOverlayWidgetPosition | null {
78
return null;
79
}
80
81
private _updateFont(): void {
82
// eslint-disable-next-line no-restricted-syntax
83
const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code'));
84
codeClasses.forEach(node => this._editor.applyFontInfo(node));
85
}
86
87
private _onModelDecorationsChanged(): void {
88
if (this._isVisible && this._hoverComputerOptions) {
89
// The decorations have changed and the hover is visible,
90
// we need to recompute the displayed text
91
this._hoverOperation.cancel();
92
this._hoverOperation.start(HoverStartMode.Delayed, this._hoverComputerOptions);
93
}
94
}
95
96
public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean {
97
const target = mouseEvent.target;
98
if (target.type === MouseTargetType.GUTTER_GLYPH_MARGIN && target.detail.glyphMarginLane) {
99
this._startShowingAt(target.position.lineNumber, target.detail.glyphMarginLane);
100
return true;
101
}
102
if (target.type === MouseTargetType.GUTTER_LINE_NUMBERS) {
103
this._startShowingAt(target.position.lineNumber, 'lineNo');
104
return true;
105
}
106
return false;
107
}
108
109
private _startShowingAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void {
110
if (this._hoverComputerOptions
111
&& this._hoverComputerOptions.lineNumber === lineNumber
112
&& this._hoverComputerOptions.laneOrLine === laneOrLine) {
113
// We have to show the widget at the exact same line number as before, so no work is needed
114
return;
115
}
116
this._hoverOperation.cancel();
117
this.hide();
118
this._hoverComputerOptions = { lineNumber, laneOrLine };
119
this._hoverOperation.start(HoverStartMode.Delayed, this._hoverComputerOptions);
120
}
121
122
public hide(): void {
123
this._hoverComputerOptions = undefined;
124
this._hoverOperation.cancel();
125
if (!this._isVisible) {
126
return;
127
}
128
this._isVisible = false;
129
this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);
130
}
131
132
private _withResult(result: HoverResult<GlyphHoverComputerOptions, IHoverMessage>): void {
133
this._messages = result.value;
134
135
if (this._messages.length > 0) {
136
this._renderMessages(result.options.lineNumber, result.options.laneOrLine, this._messages);
137
} else {
138
this.hide();
139
}
140
}
141
142
private _renderMessages(lineNumber: number, laneOrLine: LaneOrLineNumber, messages: IHoverMessage[]): void {
143
this._renderDisposeables.clear();
144
145
const fragment = document.createDocumentFragment();
146
147
for (const msg of messages) {
148
const markdownHoverElement = $('div.hover-row.markdown-hover');
149
const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents'));
150
const renderedContents = this._renderDisposeables.add(this._markdownRendererService.render(msg.value, { context: this._editor }));
151
hoverContentsElement.appendChild(renderedContents.element);
152
fragment.appendChild(markdownHoverElement);
153
}
154
155
this._updateContents(fragment);
156
this._showAt(lineNumber, laneOrLine);
157
}
158
159
private _updateContents(node: Node): void {
160
this._hover.contentsDomNode.textContent = '';
161
this._hover.contentsDomNode.appendChild(node);
162
this._updateFont();
163
}
164
165
private _showAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void {
166
if (!this._isVisible) {
167
this._isVisible = true;
168
this._hover.containerDomNode.classList.toggle('hidden', !this._isVisible);
169
}
170
171
const editorLayout = this._editor.getLayoutInfo();
172
const topForLineNumber = this._editor.getTopForLineNumber(lineNumber);
173
const editorScrollTop = this._editor.getScrollTop();
174
const lineHeight = this._editor.getOption(EditorOption.lineHeight);
175
const nodeHeight = this._hover.containerDomNode.clientHeight;
176
const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2);
177
const left = editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth + (laneOrLine === 'lineNo' ? editorLayout.lineNumbersWidth : 0);
178
179
// Constrain the hover widget to stay within the editor bounds
180
const editorHeight = editorLayout.height;
181
const maxTop = editorHeight - nodeHeight;
182
const constrainedTop = Math.max(0, Math.min(Math.round(top), maxTop));
183
184
const fixedOverflowWidgets = this._editor.getOption(EditorOption.fixedOverflowWidgets);
185
if (fixedOverflowWidgets) {
186
// Use fixed positioning relative to the viewport
187
const editorDomNode = this._editor.getDomNode();
188
if (editorDomNode) {
189
const editorRect = dom.getDomNodePagePosition(editorDomNode);
190
this._hover.containerDomNode.style.position = 'fixed';
191
this._hover.containerDomNode.style.left = `${editorRect.left + left}px`;
192
this._hover.containerDomNode.style.top = `${editorRect.top + constrainedTop}px`;
193
}
194
} else {
195
// Use absolute positioning relative to the editor
196
this._hover.containerDomNode.style.position = 'absolute';
197
this._hover.containerDomNode.style.left = `${left}px`;
198
this._hover.containerDomNode.style.top = `${constrainedTop}px`;
199
}
200
this._hover.containerDomNode.style.zIndex = '11'; // 1 more than the zone widget at 10 (#233819)
201
}
202
203
private _onMouseLeave(e: MouseEvent): void {
204
const editorDomNode = this._editor.getDomNode();
205
const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y);
206
if (isMousePositionOutsideOfEditor) {
207
this.hide();
208
}
209
}
210
}
211
212