Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/hover/browser/contentHoverController.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 { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from './hoverActionIds.js';
7
import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from '../../../browser/editorBrowser.js';
10
import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';
11
import { Range } from '../../../common/core/range.js';
12
import { IEditorContribution, IScrollEvent } from '../../../common/editorCommon.js';
13
import { HoverStartMode, HoverStartSource } from './hoverOperation.js';
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
15
import { InlineSuggestionHintsContentWidget } from '../../inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.js';
16
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
17
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
18
import { HoverVerbosityAction } from '../../../common/languages.js';
19
import { RunOnceScheduler } from '../../../../base/common/async.js';
20
import { isMousePositionWithinElement, shouldShowHover } from './hoverUtils.js';
21
import { ContentHoverWidgetWrapper } from './contentHoverWidgetWrapper.js';
22
import './hover.css';
23
import { Emitter } from '../../../../base/common/event.js';
24
import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js';
25
import { isModifierKey, KeyCode } from '../../../../base/common/keyCodes.js';
26
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
27
28
// sticky hover widget which doesn't disappear on focus out and such
29
const _sticky = false
30
// || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this
31
;
32
33
interface IHoverSettings {
34
readonly enabled: 'on' | 'off' | 'onKeyboardModifier';
35
readonly sticky: boolean;
36
readonly hidingDelay: number;
37
}
38
39
export class ContentHoverController extends Disposable implements IEditorContribution {
40
41
private readonly _onHoverContentsChanged = this._register(new Emitter<void>());
42
public readonly onHoverContentsChanged = this._onHoverContentsChanged.event;
43
44
public static readonly ID = 'editor.contrib.contentHover';
45
46
public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false;
47
48
private readonly _listenersStore = new DisposableStore();
49
50
private _contentWidget: ContentHoverWidgetWrapper | undefined;
51
52
private _mouseMoveEvent: IEditorMouseEvent | undefined;
53
private _reactToEditorMouseMoveRunner: RunOnceScheduler;
54
55
private _hoverSettings!: IHoverSettings;
56
private _isMouseDown: boolean = false;
57
58
private _ignoreMouseEvents: boolean = false;
59
60
constructor(
61
private readonly _editor: ICodeEditor,
62
@IContextMenuService _contextMenuService: IContextMenuService,
63
@IInstantiationService private readonly _instantiationService: IInstantiationService,
64
@IKeybindingService private readonly _keybindingService: IKeybindingService
65
) {
66
super();
67
this._reactToEditorMouseMoveRunner = this._register(new RunOnceScheduler(
68
() => {
69
if (this._mouseMoveEvent) {
70
this._reactToEditorMouseMove(this._mouseMoveEvent);
71
}
72
}, 0
73
));
74
this._register(_contextMenuService.onDidShowContextMenu(() => {
75
this.hideContentHover();
76
this._ignoreMouseEvents = true;
77
}));
78
this._register(_contextMenuService.onDidHideContextMenu(() => {
79
this._ignoreMouseEvents = false;
80
}));
81
this._hookListeners();
82
this._register(this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
83
if (e.hasChanged(EditorOption.hover)) {
84
this._unhookListeners();
85
this._hookListeners();
86
}
87
}));
88
}
89
90
static get(editor: ICodeEditor): ContentHoverController | null {
91
return editor.getContribution<ContentHoverController>(ContentHoverController.ID);
92
}
93
94
private _hookListeners(): void {
95
const hoverOpts = this._editor.getOption(EditorOption.hover);
96
this._hoverSettings = {
97
enabled: hoverOpts.enabled,
98
sticky: hoverOpts.sticky,
99
hidingDelay: hoverOpts.hidingDelay
100
};
101
if (hoverOpts.enabled === 'off') {
102
this._cancelSchedulerAndHide();
103
}
104
this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));
105
this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp()));
106
this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e)));
107
this._listenersStore.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e)));
108
this._listenersStore.add(this._editor.onMouseLeave((e) => this._onEditorMouseLeave(e)));
109
this._listenersStore.add(this._editor.onDidChangeModel(() => this._cancelSchedulerAndHide()));
110
this._listenersStore.add(this._editor.onDidChangeModelContent(() => this._cancelScheduler()));
111
this._listenersStore.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e)));
112
}
113
114
private _unhookListeners(): void {
115
this._listenersStore.clear();
116
}
117
118
private _cancelSchedulerAndHide(): void {
119
this._cancelScheduler();
120
this.hideContentHover();
121
}
122
123
private _cancelScheduler() {
124
this._mouseMoveEvent = undefined;
125
this._reactToEditorMouseMoveRunner.cancel();
126
}
127
128
private _onEditorScrollChanged(e: IScrollEvent): void {
129
if (this._ignoreMouseEvents) {
130
return;
131
}
132
if (e.scrollTopChanged || e.scrollLeftChanged) {
133
this.hideContentHover();
134
}
135
}
136
137
private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
138
if (this._ignoreMouseEvents) {
139
return;
140
}
141
this._isMouseDown = true;
142
const shouldKeepHoverWidgetVisible = this._shouldKeepHoverWidgetVisible(mouseEvent);
143
if (shouldKeepHoverWidgetVisible) {
144
return;
145
}
146
this.hideContentHover();
147
}
148
149
private _shouldKeepHoverWidgetVisible(mouseEvent: IPartialEditorMouseEvent): boolean {
150
return this._isMouseOnContentHoverWidget(mouseEvent) || this._isContentWidgetResizing() || isOnColorDecorator(mouseEvent);
151
}
152
153
private _isMouseOnContentHoverWidget(mouseEvent: IPartialEditorMouseEvent): boolean {
154
if (!this._contentWidget) {
155
return false;
156
}
157
return isMousePositionWithinElement(this._contentWidget.getDomNode(), mouseEvent.event.posx, mouseEvent.event.posy);
158
}
159
160
private _onEditorMouseUp(): void {
161
if (this._ignoreMouseEvents) {
162
return;
163
}
164
this._isMouseDown = false;
165
}
166
167
private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void {
168
if (this._ignoreMouseEvents) {
169
return;
170
}
171
if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) {
172
return;
173
}
174
this._cancelScheduler();
175
const shouldKeepHoverWidgetVisible = this._shouldKeepHoverWidgetVisible(mouseEvent);
176
if (shouldKeepHoverWidgetVisible) {
177
return;
178
}
179
if (_sticky) {
180
return;
181
}
182
this.hideContentHover();
183
}
184
185
private _shouldKeepCurrentHover(mouseEvent: IEditorMouseEvent): boolean {
186
const contentWidget = this._contentWidget;
187
if (!contentWidget) {
188
return false;
189
}
190
const isHoverSticky = this._hoverSettings.sticky;
191
const isMouseOnStickyContentHoverWidget = (mouseEvent: IEditorMouseEvent, isHoverSticky: boolean): boolean => {
192
const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent);
193
return isHoverSticky && isMouseOnContentHoverWidget;
194
};
195
const isMouseOnColorPickerOrChoosingColor = (mouseEvent: IEditorMouseEvent): boolean => {
196
const isColorPickerVisible = contentWidget.isColorPickerVisible;
197
const isMouseOnContentHoverWidget = this._isMouseOnContentHoverWidget(mouseEvent);
198
const isMouseOnHoverWithColorPicker = isColorPickerVisible && isMouseOnContentHoverWidget;
199
const isMaybeChoosingColor = isColorPickerVisible && this._isMouseDown;
200
return isMouseOnHoverWithColorPicker || isMaybeChoosingColor;
201
};
202
// TODO@aiday-mar verify if the following is necessary code
203
const isTextSelectedWithinContentHoverWidget = (mouseEvent: IEditorMouseEvent, sticky: boolean): boolean => {
204
const view = mouseEvent.event.browserEvent.view;
205
if (!view) {
206
return false;
207
}
208
return sticky && contentWidget.containsNode(view.document.activeElement) && !view.getSelection()?.isCollapsed;
209
};
210
const isFocused = contentWidget.isFocused;
211
const isResizing = contentWidget.isResizing;
212
const isStickyAndVisibleFromKeyboard = this._hoverSettings.sticky && contentWidget.isVisibleFromKeyboard;
213
214
return this.shouldKeepOpenOnEditorMouseMoveOrLeave
215
|| isFocused
216
|| isResizing
217
|| isStickyAndVisibleFromKeyboard
218
|| isMouseOnStickyContentHoverWidget(mouseEvent, isHoverSticky)
219
|| isMouseOnColorPickerOrChoosingColor(mouseEvent)
220
|| isTextSelectedWithinContentHoverWidget(mouseEvent, isHoverSticky);
221
}
222
223
private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
224
if (this._ignoreMouseEvents) {
225
return;
226
}
227
this._mouseMoveEvent = mouseEvent;
228
const shouldKeepCurrentHover = this._shouldKeepCurrentHover(mouseEvent);
229
if (shouldKeepCurrentHover) {
230
this._reactToEditorMouseMoveRunner.cancel();
231
return;
232
}
233
const shouldRescheduleHoverComputation = this._shouldRescheduleHoverComputation();
234
if (shouldRescheduleHoverComputation) {
235
if (!this._reactToEditorMouseMoveRunner.isScheduled()) {
236
this._reactToEditorMouseMoveRunner.schedule(this._hoverSettings.hidingDelay);
237
}
238
return;
239
}
240
this._reactToEditorMouseMove(mouseEvent);
241
}
242
243
private _shouldRescheduleHoverComputation(): boolean {
244
const hidingDelay = this._hoverSettings.hidingDelay;
245
const isContentHoverWidgetVisible = this._contentWidget?.isVisible ?? false;
246
// If the mouse is not over the widget, and if sticky is on,
247
// then give it a grace period before reacting to the mouse event
248
return isContentHoverWidgetVisible && this._hoverSettings.sticky && hidingDelay > 0;
249
}
250
251
private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
252
if (shouldShowHover(
253
this._hoverSettings.enabled,
254
this._editor.getOption(EditorOption.multiCursorModifier),
255
mouseEvent
256
)) {
257
const contentWidget: ContentHoverWidgetWrapper = this._getOrCreateContentWidget();
258
if (contentWidget.showsOrWillShow(mouseEvent)) {
259
return;
260
}
261
}
262
if (_sticky) {
263
return;
264
}
265
this.hideContentHover();
266
}
267
268
private _onKeyDown(e: IKeyboardEvent): void {
269
if (this._ignoreMouseEvents) {
270
return;
271
}
272
if (!this._contentWidget) {
273
return;
274
}
275
const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e);
276
const isModifierKeyPressed = isModifierKey(e.keyCode);
277
if (isPotentialKeyboardShortcut || isModifierKeyPressed) {
278
return;
279
}
280
if (this._contentWidget.isFocused && e.keyCode === KeyCode.Tab) {
281
return;
282
}
283
this.hideContentHover();
284
}
285
286
private _isPotentialKeyboardShortcut(e: IKeyboardEvent): boolean {
287
if (!this._editor.hasModel() || !this._contentWidget) {
288
return false;
289
}
290
const resolvedKeyboardEvent = this._keybindingService.softDispatch(e, this._editor.getDomNode());
291
const moreChordsAreNeeded = resolvedKeyboardEvent.kind === ResultKind.MoreChordsNeeded;
292
const isHoverAction = resolvedKeyboardEvent.kind === ResultKind.KbFound
293
&& (resolvedKeyboardEvent.commandId === SHOW_OR_FOCUS_HOVER_ACTION_ID
294
|| resolvedKeyboardEvent.commandId === INCREASE_HOVER_VERBOSITY_ACTION_ID
295
|| resolvedKeyboardEvent.commandId === DECREASE_HOVER_VERBOSITY_ACTION_ID)
296
&& this._contentWidget.isVisible;
297
return moreChordsAreNeeded || isHoverAction;
298
}
299
300
public hideContentHover(): void {
301
if (_sticky) {
302
return;
303
}
304
if (InlineSuggestionHintsContentWidget.dropDownVisible) {
305
return;
306
}
307
this._contentWidget?.hide();
308
}
309
310
private _getOrCreateContentWidget(): ContentHoverWidgetWrapper {
311
if (!this._contentWidget) {
312
this._contentWidget = this._instantiationService.createInstance(ContentHoverWidgetWrapper, this._editor);
313
this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire()));
314
}
315
return this._contentWidget;
316
}
317
318
public showContentHover(
319
range: Range,
320
mode: HoverStartMode,
321
source: HoverStartSource,
322
focus: boolean
323
): void {
324
this._getOrCreateContentWidget().startShowingAtRange(range, mode, source, focus);
325
}
326
327
private _isContentWidgetResizing(): boolean {
328
return this._contentWidget?.widget.isResizing || false;
329
}
330
331
public focusedHoverPartIndex(): number {
332
return this._getOrCreateContentWidget().focusedHoverPartIndex();
333
}
334
335
public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
336
return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action);
337
}
338
339
public updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): void {
340
this._getOrCreateContentWidget().updateHoverVerbosityLevel(action, index, focus);
341
}
342
343
public focus(): void {
344
this._contentWidget?.focus();
345
}
346
347
public focusHoverPartWithIndex(index: number): void {
348
this._contentWidget?.focusHoverPartWithIndex(index);
349
}
350
351
public scrollUp(): void {
352
this._contentWidget?.scrollUp();
353
}
354
355
public scrollDown(): void {
356
this._contentWidget?.scrollDown();
357
}
358
359
public scrollLeft(): void {
360
this._contentWidget?.scrollLeft();
361
}
362
363
public scrollRight(): void {
364
this._contentWidget?.scrollRight();
365
}
366
367
public pageUp(): void {
368
this._contentWidget?.pageUp();
369
}
370
371
public pageDown(): void {
372
this._contentWidget?.pageDown();
373
}
374
375
public goToTop(): void {
376
this._contentWidget?.goToTop();
377
}
378
379
public goToBottom(): void {
380
this._contentWidget?.goToBottom();
381
}
382
383
public getWidgetContent(): string | undefined {
384
return this._contentWidget?.getWidgetContent();
385
}
386
387
public getAccessibleWidgetContent(): string | undefined {
388
return this._contentWidget?.getAccessibleWidgetContent();
389
}
390
391
public getAccessibleWidgetContentAtIndex(index: number): string | undefined {
392
return this._contentWidget?.getAccessibleWidgetContentAtIndex(index);
393
}
394
395
public get isColorPickerVisible(): boolean | undefined {
396
return this._contentWidget?.isColorPickerVisible;
397
}
398
399
public get isHoverVisible(): boolean | undefined {
400
return this._contentWidget?.isVisible;
401
}
402
403
public override dispose(): void {
404
super.dispose();
405
this._unhookListeners();
406
this._listenersStore.dispose();
407
this._contentWidget?.dispose();
408
}
409
}
410
411