Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverWidgetWrapper.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 { KeyCode } from '../../../../base/common/keyCodes.js';
8
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
9
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../browser/editorBrowser.js';
10
import { EditorOption } from '../../../common/config/editorOptions.js';
11
import { Range } from '../../../common/core/range.js';
12
import { TokenizationRegistry } from '../../../common/languages.js';
13
import { HoverOperation, HoverResult, HoverStartMode, HoverStartSource } from './hoverOperation.js';
14
import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverContext, IEditorHoverParticipant, IHoverPart, IHoverWidget } from './hoverTypes.js';
15
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
16
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
17
import { HoverVerbosityAction } from '../../../common/standalone/standaloneEnums.js';
18
import { ContentHoverWidget } from './contentHoverWidget.js';
19
import { ContentHoverComputer, ContentHoverComputerOptions } from './contentHoverComputer.js';
20
import { ContentHoverResult } from './contentHoverTypes.js';
21
import { Emitter } from '../../../../base/common/event.js';
22
import { RenderedContentHover } from './contentHoverRendered.js';
23
import { isMousePositionWithinElement } from './hoverUtils.js';
24
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
25
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
26
27
export class ContentHoverWidgetWrapper extends Disposable implements IHoverWidget {
28
29
private _currentResult: ContentHoverResult | null = null;
30
private readonly _renderedContentHover = this._register(new MutableDisposable<RenderedContentHover>());
31
32
private readonly _contentHoverWidget: ContentHoverWidget;
33
private readonly _participants: IEditorHoverParticipant[];
34
private readonly _hoverOperation: HoverOperation<ContentHoverComputerOptions, IHoverPart>;
35
36
private readonly _onContentsChanged = this._register(new Emitter<void>());
37
public readonly onContentsChanged = this._onContentsChanged.event;
38
39
constructor(
40
private readonly _editor: ICodeEditor,
41
@IInstantiationService private readonly _instantiationService: IInstantiationService,
42
@IKeybindingService private readonly _keybindingService: IKeybindingService,
43
@IHoverService private readonly _hoverService: IHoverService,
44
@IClipboardService private readonly _clipboardService: IClipboardService
45
) {
46
super();
47
this._contentHoverWidget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));
48
this._participants = this._initializeHoverParticipants();
49
this._hoverOperation = this._register(new HoverOperation(this._editor, new ContentHoverComputer(this._editor, this._participants)));
50
this._registerListeners();
51
}
52
53
private _initializeHoverParticipants(): IEditorHoverParticipant[] {
54
const participants: IEditorHoverParticipant[] = [];
55
for (const participant of HoverParticipantRegistry.getAll()) {
56
const participantInstance = this._instantiationService.createInstance(participant, this._editor);
57
participants.push(participantInstance);
58
}
59
participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal);
60
this._register(this._contentHoverWidget.onDidResize(() => {
61
this._participants.forEach(participant => participant.handleResize?.());
62
}));
63
this._register(this._contentHoverWidget.onDidScroll((e) => {
64
this._participants.forEach(participant => participant.handleScroll?.(e));
65
}));
66
this._register(this._contentHoverWidget.onContentsChanged(() => {
67
this._participants.forEach(participant => participant.handleContentsChanged?.());
68
}));
69
return participants;
70
}
71
72
private _registerListeners(): void {
73
this._register(this._hoverOperation.onResult((result) => {
74
const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result) : result.value);
75
this._withResult(new ContentHoverResult(messages, result.isComplete, result.options));
76
}));
77
const contentHoverWidgetNode = this._contentHoverWidget.getDomNode();
78
this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'keydown', (e) => {
79
if (e.equals(KeyCode.Escape)) {
80
this.hide();
81
}
82
}));
83
this._register(dom.addStandardDisposableListener(contentHoverWidgetNode, 'mouseleave', (e) => {
84
this._onMouseLeave(e);
85
}));
86
this._register(TokenizationRegistry.onDidChange(() => {
87
if (this._contentHoverWidget.position && this._currentResult) {
88
this._setCurrentResult(this._currentResult); // render again
89
}
90
}));
91
this._register(this._contentHoverWidget.onContentsChanged(() => {
92
this._onContentsChanged.fire();
93
}));
94
}
95
96
/**
97
* Returns true if the hover shows now or will show.
98
*/
99
private _startShowingOrUpdateHover(
100
anchor: HoverAnchor | null,
101
mode: HoverStartMode,
102
source: HoverStartSource,
103
focus: boolean,
104
mouseEvent: IEditorMouseEvent | null
105
): boolean {
106
const contentHoverIsVisible = this._contentHoverWidget.position && this._currentResult;
107
if (!contentHoverIsVisible) {
108
if (anchor) {
109
this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);
110
return true;
111
}
112
return false;
113
}
114
const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky;
115
const isMouseGettingCloser = mouseEvent && this._contentHoverWidget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy);
116
const isHoverStickyAndIsMouseGettingCloser = isHoverSticky && isMouseGettingCloser;
117
// The mouse is getting closer to the hover, so we will keep the hover untouched
118
// But we will kick off a hover update at the new anchor, insisting on keeping the hover visible.
119
if (isHoverStickyAndIsMouseGettingCloser) {
120
if (anchor) {
121
this._startHoverOperationIfNecessary(anchor, mode, source, focus, true);
122
}
123
return true;
124
}
125
// If mouse is not getting closer and anchor not defined, hide the hover
126
if (!anchor) {
127
this._setCurrentResult(null);
128
return false;
129
}
130
// If mouse if not getting closer and anchor is defined, and the new anchor is the same as the previous anchor
131
const currentAnchorEqualsPreviousAnchor = this._currentResult && this._currentResult.options.anchor.equals(anchor);
132
if (currentAnchorEqualsPreviousAnchor) {
133
return true;
134
}
135
// If mouse if not getting closer and anchor is defined, and the new anchor is not compatible with the previous anchor
136
const currentAnchorCompatibleWithPreviousAnchor = this._currentResult && anchor.canAdoptVisibleHover(this._currentResult.options.anchor, this._contentHoverWidget.position);
137
if (!currentAnchorCompatibleWithPreviousAnchor) {
138
this._setCurrentResult(null);
139
this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);
140
return true;
141
}
142
// We aren't getting any closer to the hover, so we will filter existing results
143
// and keep those which also apply to the new anchor.
144
if (this._currentResult) {
145
this._setCurrentResult(this._currentResult.filter(anchor));
146
}
147
this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);
148
return true;
149
}
150
151
private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, shouldFocus: boolean, insistOnKeepingHoverVisible: boolean): void {
152
const currentAnchorEqualToPreviousHover = this._hoverOperation.options && this._hoverOperation.options.anchor.equals(anchor);
153
if (currentAnchorEqualToPreviousHover) {
154
return;
155
}
156
this._hoverOperation.cancel();
157
const contentHoverComputerOptions: ContentHoverComputerOptions = {
158
anchor,
159
source,
160
shouldFocus,
161
insistOnKeepingHoverVisible
162
};
163
this._hoverOperation.start(mode, contentHoverComputerOptions);
164
}
165
166
private _setCurrentResult(hoverResult: ContentHoverResult | null): void {
167
let currentHoverResult = hoverResult;
168
const currentResultEqualToPreviousResult = this._currentResult === currentHoverResult;
169
if (currentResultEqualToPreviousResult) {
170
return;
171
}
172
const currentHoverResultIsEmpty = currentHoverResult && currentHoverResult.hoverParts.length === 0;
173
if (currentHoverResultIsEmpty) {
174
currentHoverResult = null;
175
}
176
this._currentResult = currentHoverResult;
177
if (this._currentResult) {
178
this._showHover(this._currentResult);
179
} else {
180
this._hideHover();
181
}
182
}
183
184
private _addLoadingMessage(hoverResult: HoverResult<ContentHoverComputerOptions, IHoverPart>): IHoverPart[] {
185
for (const participant of this._participants) {
186
if (!participant.createLoadingMessage) {
187
continue;
188
}
189
const loadingMessage = participant.createLoadingMessage(hoverResult.options.anchor);
190
if (!loadingMessage) {
191
continue;
192
}
193
return hoverResult.value.slice(0).concat([loadingMessage]);
194
}
195
return hoverResult.value;
196
}
197
198
private _withResult(hoverResult: ContentHoverResult): void {
199
const previousHoverIsVisibleWithCompleteResult = this._contentHoverWidget.position && this._currentResult && this._currentResult.isComplete;
200
if (!previousHoverIsVisibleWithCompleteResult) {
201
this._setCurrentResult(hoverResult);
202
}
203
// The hover is visible with a previous complete result.
204
const isCurrentHoverResultComplete = hoverResult.isComplete;
205
if (!isCurrentHoverResultComplete) {
206
// Instead of rendering the new partial result, we wait for the result to be complete.
207
return;
208
}
209
const currentHoverResultIsEmpty = hoverResult.hoverParts.length === 0;
210
const insistOnKeepingPreviousHoverVisible = hoverResult.options.insistOnKeepingHoverVisible;
211
const shouldKeepPreviousHoverVisible = currentHoverResultIsEmpty && insistOnKeepingPreviousHoverVisible;
212
if (shouldKeepPreviousHoverVisible) {
213
// The hover would now hide normally, so we'll keep the previous messages
214
return;
215
}
216
this._setCurrentResult(hoverResult);
217
}
218
219
private _showHover(hoverResult: ContentHoverResult): void {
220
const context = this._getHoverContext();
221
this._renderedContentHover.value = new RenderedContentHover(this._editor, hoverResult, this._participants, context, this._keybindingService, this._hoverService, this._clipboardService);
222
if (this._renderedContentHover.value.domNodeHasChildren) {
223
this._contentHoverWidget.show(this._renderedContentHover.value);
224
} else {
225
this._renderedContentHover.clear();
226
}
227
}
228
229
private _hideHover(): void {
230
this._contentHoverWidget.hide();
231
this._participants.forEach(participant => participant.handleHide?.());
232
}
233
234
private _getHoverContext(): IEditorHoverContext {
235
const hide = () => {
236
this.hide();
237
};
238
const onContentsChanged = () => {
239
this._contentHoverWidget.handleContentsChanged();
240
};
241
const setMinimumDimensions = (dimensions: dom.Dimension) => {
242
this._contentHoverWidget.setMinimumDimensions(dimensions);
243
};
244
const focus = () => this.focus();
245
return { hide, onContentsChanged, setMinimumDimensions, focus };
246
}
247
248
249
public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean {
250
const isContentWidgetResizing = this._contentHoverWidget.isResizing;
251
if (isContentWidgetResizing) {
252
return true;
253
}
254
const anchorCandidates: HoverAnchor[] = this._findHoverAnchorCandidates(mouseEvent);
255
const anchorCandidatesExist = anchorCandidates.length > 0;
256
if (!anchorCandidatesExist) {
257
return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);
258
}
259
const anchor = anchorCandidates[0];
260
return this._startShowingOrUpdateHover(anchor, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);
261
}
262
263
private _findHoverAnchorCandidates(mouseEvent: IEditorMouseEvent): HoverAnchor[] {
264
const anchorCandidates: HoverAnchor[] = [];
265
for (const participant of this._participants) {
266
if (!participant.suggestHoverAnchor) {
267
continue;
268
}
269
const anchor = participant.suggestHoverAnchor(mouseEvent);
270
if (!anchor) {
271
continue;
272
}
273
anchorCandidates.push(anchor);
274
}
275
const target = mouseEvent.target;
276
switch (target.type) {
277
case MouseTargetType.CONTENT_TEXT: {
278
anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));
279
break;
280
}
281
case MouseTargetType.CONTENT_EMPTY: {
282
const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2;
283
// Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
284
const mouseIsWithinLinesAndCloseToHover = !target.detail.isAfterLines
285
&& typeof target.detail.horizontalDistanceToText === 'number'
286
&& target.detail.horizontalDistanceToText < epsilon;
287
if (!mouseIsWithinLinesAndCloseToHover) {
288
break;
289
}
290
anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));
291
break;
292
}
293
}
294
anchorCandidates.sort((a, b) => b.priority - a.priority);
295
return anchorCandidates;
296
}
297
298
private _onMouseLeave(e: MouseEvent): void {
299
const editorDomNode = this._editor.getDomNode();
300
const isMousePositionOutsideOfEditor = !editorDomNode || !isMousePositionWithinElement(editorDomNode, e.x, e.y);
301
if (isMousePositionOutsideOfEditor) {
302
this.hide();
303
}
304
}
305
306
public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void {
307
this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null);
308
}
309
310
public getWidgetContent(): string | undefined {
311
const node = this._contentHoverWidget.getDomNode();
312
if (!node.textContent) {
313
return undefined;
314
}
315
return node.textContent;
316
}
317
318
public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<void> {
319
this._renderedContentHover.value?.updateHoverVerbosityLevel(action, index, focus);
320
}
321
322
public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
323
return this._renderedContentHover.value?.doesHoverAtIndexSupportVerbosityAction(index, action) ?? false;
324
}
325
326
public getAccessibleWidgetContent(): string | undefined {
327
return this._renderedContentHover.value?.getAccessibleWidgetContent();
328
}
329
330
public getAccessibleWidgetContentAtIndex(index: number): string | undefined {
331
return this._renderedContentHover.value?.getAccessibleWidgetContentAtIndex(index);
332
}
333
334
public focusedHoverPartIndex(): number {
335
return this._renderedContentHover.value?.focusedHoverPartIndex ?? -1;
336
}
337
338
public containsNode(node: Node | null | undefined): boolean {
339
return (node ? this._contentHoverWidget.getDomNode().contains(node) : false);
340
}
341
342
public focus(): void {
343
const hoverPartsCount = this._renderedContentHover.value?.hoverPartsCount;
344
if (hoverPartsCount === 1) {
345
this.focusHoverPartWithIndex(0);
346
return;
347
}
348
this._contentHoverWidget.focus();
349
}
350
351
public focusHoverPartWithIndex(index: number): void {
352
this._renderedContentHover.value?.focusHoverPartWithIndex(index);
353
}
354
355
public scrollUp(): void {
356
this._contentHoverWidget.scrollUp();
357
}
358
359
public scrollDown(): void {
360
this._contentHoverWidget.scrollDown();
361
}
362
363
public scrollLeft(): void {
364
this._contentHoverWidget.scrollLeft();
365
}
366
367
public scrollRight(): void {
368
this._contentHoverWidget.scrollRight();
369
}
370
371
public pageUp(): void {
372
this._contentHoverWidget.pageUp();
373
}
374
375
public pageDown(): void {
376
this._contentHoverWidget.pageDown();
377
}
378
379
public goToTop(): void {
380
this._contentHoverWidget.goToTop();
381
}
382
383
public goToBottom(): void {
384
this._contentHoverWidget.goToBottom();
385
}
386
387
public hide(): void {
388
this._hoverOperation.cancel();
389
this._setCurrentResult(null);
390
}
391
392
public getDomNode(): HTMLElement {
393
return this._contentHoverWidget.getDomNode();
394
}
395
396
public get isColorPickerVisible(): boolean {
397
return this._renderedContentHover.value?.isColorPickerVisible() ?? false;
398
}
399
400
public get isVisibleFromKeyboard(): boolean {
401
return this._contentHoverWidget.isVisibleFromKeyboard;
402
}
403
404
public get isVisible(): boolean {
405
return this._contentHoverWidget.isVisible;
406
}
407
408
public get isFocused(): boolean {
409
return this._contentHoverWidget.isFocused;
410
}
411
412
public get isResizing(): boolean {
413
return this._contentHoverWidget.isResizing;
414
}
415
416
public get widget() {
417
return this._contentHoverWidget;
418
}
419
}
420
421