Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverRendered.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 { IEditorHoverContext, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverParts, RenderedHoverParts } from './hoverTypes.js';
7
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { EditorHoverStatusBar } from './contentHoverStatusBar.js';
9
import { HoverStartSource } from './hoverOperation.js';
10
import { HoverCopyButton } from './hoverCopyButton.js';
11
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
12
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
13
import { ICodeEditor } from '../../../browser/editorBrowser.js';
14
import { Position } from '../../../common/core/position.js';
15
import { Range } from '../../../common/core/range.js';
16
import { ContentHoverResult } from './contentHoverTypes.js';
17
import * as dom from '../../../../base/browser/dom.js';
18
import { HoverVerbosityAction } from '../../../common/languages.js';
19
import { MarkdownHoverParticipant } from './markdownHoverParticipant.js';
20
import { HoverColorPickerParticipant } from '../../colorPicker/browser/hoverColorPicker/hoverColorPickerParticipant.js';
21
import { localize } from '../../../../nls.js';
22
import { InlayHintsHover } from '../../inlayHints/browser/inlayHintsHover.js';
23
import { BugIndicatingError } from '../../../../base/common/errors.js';
24
import { HoverAction } from '../../../../base/browser/ui/hover/hoverWidget.js';
25
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
26
import { IOffsetRange } from '../../../common/core/ranges/offsetRange.js';
27
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
28
import { MarkerHover } from './markerHoverParticipant.js';
29
30
export class RenderedContentHover extends Disposable {
31
32
public closestMouseDistance: number | undefined;
33
public initialMousePosX: number | undefined;
34
public initialMousePosY: number | undefined;
35
36
public readonly showAtPosition: Position;
37
public readonly showAtSecondaryPosition: Position;
38
public readonly shouldFocus: boolean;
39
public readonly source: HoverStartSource;
40
public readonly shouldAppearBeforeContent: boolean;
41
42
private readonly _renderedHoverParts: RenderedContentHoverParts;
43
44
constructor(
45
editor: ICodeEditor,
46
hoverResult: ContentHoverResult,
47
participants: IEditorHoverParticipant<IHoverPart>[],
48
context: IEditorHoverContext,
49
@IKeybindingService keybindingService: IKeybindingService,
50
@IHoverService hoverService: IHoverService,
51
@IClipboardService clipboardService: IClipboardService
52
) {
53
super();
54
const parts = hoverResult.hoverParts;
55
this._renderedHoverParts = this._register(new RenderedContentHoverParts(
56
editor,
57
participants,
58
parts,
59
context,
60
keybindingService,
61
hoverService,
62
clipboardService
63
));
64
const contentHoverComputerOptions = hoverResult.options;
65
const anchor = contentHoverComputerOptions.anchor;
66
const { showAtPosition, showAtSecondaryPosition } = RenderedContentHover.computeHoverPositions(editor, anchor.range, parts);
67
this.shouldAppearBeforeContent = parts.some(m => m.isBeforeContent);
68
this.showAtPosition = showAtPosition;
69
this.showAtSecondaryPosition = showAtSecondaryPosition;
70
this.initialMousePosX = anchor.initialMousePosX;
71
this.initialMousePosY = anchor.initialMousePosY;
72
this.shouldFocus = contentHoverComputerOptions.shouldFocus;
73
this.source = contentHoverComputerOptions.source;
74
}
75
76
public get domNode(): DocumentFragment {
77
return this._renderedHoverParts.domNode;
78
}
79
80
public get domNodeHasChildren(): boolean {
81
return this._renderedHoverParts.domNodeHasChildren;
82
}
83
84
public get focusedHoverPartIndex(): number {
85
return this._renderedHoverParts.focusedHoverPartIndex;
86
}
87
88
public get hoverPartsCount(): number {
89
return this._renderedHoverParts.hoverPartsCount;
90
}
91
92
public focusHoverPartWithIndex(index: number): void {
93
this._renderedHoverParts.focusHoverPartWithIndex(index);
94
}
95
96
public getAccessibleWidgetContent(): string {
97
return this._renderedHoverParts.getAccessibleContent();
98
}
99
100
public getAccessibleWidgetContentAtIndex(index: number): string {
101
return this._renderedHoverParts.getAccessibleHoverContentAtIndex(index);
102
}
103
104
public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<void> {
105
this._renderedHoverParts.updateHoverVerbosityLevel(action, index, focus);
106
}
107
108
public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
109
return this._renderedHoverParts.doesHoverAtIndexSupportVerbosityAction(index, action);
110
}
111
112
public isColorPickerVisible(): boolean {
113
return this._renderedHoverParts.isColorPickerVisible();
114
}
115
116
public static computeHoverPositions(editor: ICodeEditor, anchorRange: Range, hoverParts: IHoverPart[]): { showAtPosition: Position; showAtSecondaryPosition: Position } {
117
118
let startColumnBoundary = 1;
119
if (editor.hasModel()) {
120
// Ensure the range is on the current view line
121
const viewModel = editor._getViewModel();
122
const coordinatesConverter = viewModel.coordinatesConverter;
123
const anchorViewRange = coordinatesConverter.convertModelRangeToViewRange(anchorRange);
124
const anchorViewMinColumn = viewModel.getLineMinColumn(anchorViewRange.startLineNumber);
125
const anchorViewRangeStart = new Position(anchorViewRange.startLineNumber, anchorViewMinColumn);
126
startColumnBoundary = coordinatesConverter.convertViewPositionToModelPosition(anchorViewRangeStart).column;
127
}
128
129
// The anchor range is always on a single line
130
const anchorStartLineNumber = anchorRange.startLineNumber;
131
let secondaryPositionColumn = anchorRange.startColumn;
132
let forceShowAtRange: Range | undefined;
133
134
for (const hoverPart of hoverParts) {
135
const hoverPartRange = hoverPart.range;
136
const hoverPartRangeOnAnchorStartLine = hoverPartRange.startLineNumber === anchorStartLineNumber;
137
const hoverPartRangeOnAnchorEndLine = hoverPartRange.endLineNumber === anchorStartLineNumber;
138
const hoverPartRangeIsOnAnchorLine = hoverPartRangeOnAnchorStartLine && hoverPartRangeOnAnchorEndLine;
139
if (hoverPartRangeIsOnAnchorLine) {
140
// this message has a range that is completely sitting on the line of the anchor
141
const hoverPartStartColumn = hoverPartRange.startColumn;
142
const minSecondaryPositionColumn = Math.min(secondaryPositionColumn, hoverPartStartColumn);
143
secondaryPositionColumn = Math.max(minSecondaryPositionColumn, startColumnBoundary);
144
}
145
if (hoverPart.forceShowAtRange) {
146
forceShowAtRange = hoverPartRange;
147
}
148
}
149
150
let showAtPosition: Position;
151
let showAtSecondaryPosition: Position;
152
if (forceShowAtRange) {
153
const forceShowAtPosition = forceShowAtRange.getStartPosition();
154
showAtPosition = forceShowAtPosition;
155
showAtSecondaryPosition = forceShowAtPosition;
156
} else {
157
showAtPosition = anchorRange.getStartPosition();
158
showAtSecondaryPosition = new Position(anchorStartLineNumber, secondaryPositionColumn);
159
}
160
return {
161
showAtPosition,
162
showAtSecondaryPosition,
163
};
164
}
165
}
166
167
interface IRenderedContentHoverPart {
168
/**
169
* Type of rendered part
170
*/
171
type: 'hoverPart';
172
/**
173
* Participant of the rendered hover part
174
*/
175
participant: IEditorHoverParticipant<IHoverPart>;
176
/**
177
* The rendered hover part
178
*/
179
hoverPart: IHoverPart;
180
/**
181
* The HTML element containing the hover status bar.
182
*/
183
hoverElement: HTMLElement;
184
}
185
186
interface IRenderedContentStatusBar {
187
/**
188
* Type of rendered part
189
*/
190
type: 'statusBar';
191
/**
192
* The HTML element containing the hover status bar.
193
*/
194
hoverElement: HTMLElement;
195
/**
196
* The actions of the hover status bar.
197
*/
198
actions: HoverAction[];
199
}
200
201
type IRenderedContentHoverPartOrStatusBar = IRenderedContentHoverPart | IRenderedContentStatusBar;
202
203
class RenderedStatusBar implements IDisposable {
204
205
constructor(fragment: DocumentFragment, private readonly _statusBar: EditorHoverStatusBar) {
206
fragment.appendChild(this._statusBar.hoverElement);
207
}
208
209
get hoverElement(): HTMLElement {
210
return this._statusBar.hoverElement;
211
}
212
213
get actions(): HoverAction[] {
214
return this._statusBar.actions;
215
}
216
217
dispose() {
218
this._statusBar.dispose();
219
}
220
}
221
222
class RenderedContentHoverParts extends Disposable {
223
224
private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({
225
description: 'content-hover-highlight',
226
className: 'hoverHighlight'
227
});
228
229
private readonly _renderedParts: IRenderedContentHoverPartOrStatusBar[] = [];
230
private readonly _fragment: DocumentFragment;
231
private readonly _context: IEditorHoverContext;
232
233
private _markdownHoverParticipant: MarkdownHoverParticipant | undefined;
234
private _colorHoverParticipant: HoverColorPickerParticipant | undefined;
235
private _focusedHoverPartIndex: number = -1;
236
237
constructor(
238
editor: ICodeEditor,
239
participants: IEditorHoverParticipant<IHoverPart>[],
240
hoverParts: IHoverPart[],
241
context: IEditorHoverContext,
242
@IKeybindingService keybindingService: IKeybindingService,
243
@IHoverService private readonly _hoverService: IHoverService,
244
@IClipboardService private readonly _clipboardService: IClipboardService
245
) {
246
super();
247
this._context = context;
248
this._fragment = document.createDocumentFragment();
249
this._register(this._renderParts(participants, hoverParts, context, keybindingService, this._hoverService));
250
this._register(this._registerListenersOnRenderedParts());
251
this._register(this._createEditorDecorations(editor, hoverParts));
252
this._updateMarkdownAndColorParticipantInfo(participants);
253
}
254
255
private _createEditorDecorations(editor: ICodeEditor, hoverParts: IHoverPart[]): IDisposable {
256
if (hoverParts.length === 0) {
257
return Disposable.None;
258
}
259
let highlightRange = hoverParts[0].range;
260
for (const hoverPart of hoverParts) {
261
const hoverPartRange = hoverPart.range;
262
highlightRange = Range.plusRange(highlightRange, hoverPartRange);
263
}
264
const highlightDecoration = editor.createDecorationsCollection();
265
highlightDecoration.set([{
266
range: highlightRange,
267
options: RenderedContentHoverParts._DECORATION_OPTIONS
268
}]);
269
return toDisposable(() => {
270
highlightDecoration.clear();
271
});
272
}
273
274
private _renderParts(participants: IEditorHoverParticipant<IHoverPart>[], hoverParts: IHoverPart[], hoverContext: IEditorHoverContext, keybindingService: IKeybindingService, hoverService: IHoverService): IDisposable {
275
const statusBar = new EditorHoverStatusBar(keybindingService, hoverService);
276
const hoverRenderingContext: IEditorHoverRenderContext = {
277
fragment: this._fragment,
278
statusBar,
279
...hoverContext
280
};
281
const disposables = new DisposableStore();
282
disposables.add(statusBar);
283
for (const participant of participants) {
284
const renderedHoverParts = this._renderHoverPartsForParticipant(hoverParts, participant, hoverRenderingContext);
285
disposables.add(renderedHoverParts);
286
for (const renderedHoverPart of renderedHoverParts.renderedHoverParts) {
287
this._renderedParts.push({
288
type: 'hoverPart',
289
participant,
290
hoverPart: renderedHoverPart.hoverPart,
291
hoverElement: renderedHoverPart.hoverElement,
292
});
293
}
294
}
295
const renderedStatusBar = this._renderStatusBar(this._fragment, statusBar);
296
if (renderedStatusBar) {
297
disposables.add(renderedStatusBar);
298
this._renderedParts.push({
299
type: 'statusBar',
300
hoverElement: renderedStatusBar.hoverElement,
301
actions: renderedStatusBar.actions,
302
});
303
}
304
return disposables;
305
}
306
307
private _renderHoverPartsForParticipant(hoverParts: IHoverPart[], participant: IEditorHoverParticipant<IHoverPart>, hoverRenderingContext: IEditorHoverRenderContext): IRenderedHoverParts<IHoverPart> {
308
const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant);
309
const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0;
310
if (!hasHoverPartsForParticipant) {
311
return new RenderedHoverParts([]);
312
}
313
return participant.renderHoverParts(hoverRenderingContext, hoverPartsForParticipant);
314
}
315
316
private _renderStatusBar(fragment: DocumentFragment, statusBar: EditorHoverStatusBar): RenderedStatusBar | undefined {
317
if (!statusBar.hasContent) {
318
return undefined;
319
}
320
return new RenderedStatusBar(fragment, statusBar);
321
}
322
323
private _registerListenersOnRenderedParts(): IDisposable {
324
const disposables = new DisposableStore();
325
this._renderedParts.forEach((renderedPart: IRenderedContentHoverPartOrStatusBar, index: number) => {
326
const element = renderedPart.hoverElement;
327
element.tabIndex = 0;
328
disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_IN, (event: Event) => {
329
event.stopPropagation();
330
this._focusedHoverPartIndex = index;
331
}));
332
disposables.add(dom.addDisposableListener(element, dom.EventType.FOCUS_OUT, (event: Event) => {
333
event.stopPropagation();
334
this._focusedHoverPartIndex = -1;
335
}));
336
// Add copy button for marker hovers
337
if (renderedPart.type === 'hoverPart' && renderedPart.hoverPart instanceof MarkerHover) {
338
disposables.add(new HoverCopyButton(
339
element,
340
() => renderedPart.participant.getAccessibleContent(renderedPart.hoverPart),
341
this._clipboardService,
342
this._hoverService
343
));
344
}
345
});
346
return disposables;
347
}
348
349
private _updateMarkdownAndColorParticipantInfo(participants: IEditorHoverParticipant<IHoverPart>[]) {
350
const markdownHoverParticipant = participants.find(p => {
351
return (p instanceof MarkdownHoverParticipant) && !(p instanceof InlayHintsHover);
352
});
353
if (markdownHoverParticipant) {
354
this._markdownHoverParticipant = markdownHoverParticipant as MarkdownHoverParticipant;
355
}
356
this._colorHoverParticipant = participants.find(p => p instanceof HoverColorPickerParticipant);
357
}
358
359
public focusHoverPartWithIndex(index: number): void {
360
if (index < 0 || index >= this._renderedParts.length) {
361
return;
362
}
363
this._renderedParts[index].hoverElement.focus();
364
}
365
366
public getAccessibleContent(): string {
367
const content: string[] = [];
368
for (let i = 0; i < this._renderedParts.length; i++) {
369
content.push(this.getAccessibleHoverContentAtIndex(i));
370
}
371
return content.join('\n\n');
372
}
373
374
public getAccessibleHoverContentAtIndex(index: number): string {
375
const renderedPart = this._renderedParts[index];
376
if (!renderedPart) {
377
return '';
378
}
379
if (renderedPart.type === 'statusBar') {
380
const statusBarDescription = [localize('hoverAccessibilityStatusBar', "This is a hover status bar.")];
381
for (const action of renderedPart.actions) {
382
const keybinding = action.actionKeybindingLabel;
383
if (keybinding) {
384
statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithKeybinding', "It has an action with label {0} and keybinding {1}.", action.actionLabel, keybinding));
385
} else {
386
statusBarDescription.push(localize('hoverAccessibilityStatusBarActionWithoutKeybinding', "It has an action with label {0}.", action.actionLabel));
387
}
388
}
389
return statusBarDescription.join('\n');
390
}
391
return renderedPart.participant.getAccessibleContent(renderedPart.hoverPart);
392
}
393
394
public async updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): Promise<void> {
395
if (!this._markdownHoverParticipant) {
396
return;
397
}
398
let rangeOfIndicesToUpdate: IOffsetRange;
399
if (index >= 0) {
400
rangeOfIndicesToUpdate = { start: index, endExclusive: index + 1 };
401
} else {
402
rangeOfIndicesToUpdate = this._findRangeOfMarkdownHoverParts(this._markdownHoverParticipant);
403
}
404
for (let i = rangeOfIndicesToUpdate.start; i < rangeOfIndicesToUpdate.endExclusive; i++) {
405
const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, i);
406
if (normalizedMarkdownHoverIndex === undefined) {
407
continue;
408
}
409
const renderedPart = await this._markdownHoverParticipant.updateMarkdownHoverVerbosityLevel(action, normalizedMarkdownHoverIndex);
410
if (!renderedPart) {
411
continue;
412
}
413
this._renderedParts[i] = {
414
type: 'hoverPart',
415
participant: this._markdownHoverParticipant,
416
hoverPart: renderedPart.hoverPart,
417
hoverElement: renderedPart.hoverElement,
418
};
419
}
420
if (focus) {
421
if (index >= 0) {
422
this.focusHoverPartWithIndex(index);
423
} else {
424
this._context.focus();
425
}
426
}
427
this._context.onContentsChanged();
428
}
429
430
public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
431
if (!this._markdownHoverParticipant) {
432
return false;
433
}
434
const normalizedMarkdownHoverIndex = this._normalizedIndexToMarkdownHoverIndexRange(this._markdownHoverParticipant, index);
435
if (normalizedMarkdownHoverIndex === undefined) {
436
return false;
437
}
438
return this._markdownHoverParticipant.doesMarkdownHoverAtIndexSupportVerbosityAction(normalizedMarkdownHoverIndex, action);
439
}
440
441
public isColorPickerVisible(): boolean {
442
return this._colorHoverParticipant?.isColorPickerVisible() ?? false;
443
}
444
445
private _normalizedIndexToMarkdownHoverIndexRange(markdownHoverParticipant: MarkdownHoverParticipant, index: number): number | undefined {
446
const renderedPart = this._renderedParts[index];
447
if (!renderedPart || renderedPart.type !== 'hoverPart') {
448
return undefined;
449
}
450
const isHoverPartMarkdownHover = renderedPart.participant === markdownHoverParticipant;
451
if (!isHoverPartMarkdownHover) {
452
return undefined;
453
}
454
const firstIndexOfMarkdownHovers = this._renderedParts.findIndex(renderedPart =>
455
renderedPart.type === 'hoverPart'
456
&& renderedPart.participant === markdownHoverParticipant
457
);
458
if (firstIndexOfMarkdownHovers === -1) {
459
throw new BugIndicatingError();
460
}
461
return index - firstIndexOfMarkdownHovers;
462
}
463
464
private _findRangeOfMarkdownHoverParts(markdownHoverParticipant: MarkdownHoverParticipant): IOffsetRange {
465
const copiedRenderedParts = this._renderedParts.slice();
466
const firstIndexOfMarkdownHovers = copiedRenderedParts.findIndex(renderedPart => renderedPart.type === 'hoverPart' && renderedPart.participant === markdownHoverParticipant);
467
const inversedLastIndexOfMarkdownHovers = copiedRenderedParts.reverse().findIndex(renderedPart => renderedPart.type === 'hoverPart' && renderedPart.participant === markdownHoverParticipant);
468
const lastIndexOfMarkdownHovers = inversedLastIndexOfMarkdownHovers >= 0 ? copiedRenderedParts.length - inversedLastIndexOfMarkdownHovers : inversedLastIndexOfMarkdownHovers;
469
return { start: firstIndexOfMarkdownHovers, endExclusive: lastIndexOfMarkdownHovers + 1 };
470
}
471
472
public get domNode(): DocumentFragment {
473
return this._fragment;
474
}
475
476
public get domNodeHasChildren(): boolean {
477
return this._fragment.hasChildNodes();
478
}
479
480
public get focusedHoverPartIndex(): number {
481
return this._focusedHoverPartIndex;
482
}
483
484
public get hoverPartsCount(): number {
485
return this._renderedParts.length;
486
}
487
}
488
489