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
5242 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, isTriggerModifierPressed } 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 || !this._contentWidget) {
270
return;
271
}
272
273
if (this._hoverSettings.enabled === 'onKeyboardModifier'
274
&& isTriggerModifierPressed(this._editor.getOption(EditorOption.multiCursorModifier), e)
275
&& this._mouseMoveEvent) {
276
if (!this._contentWidget.isVisible) {
277
this._contentWidget.showsOrWillShow(this._mouseMoveEvent);
278
}
279
return;
280
}
281
282
const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e);
283
const isModifierKeyPressed = isModifierKey(e.keyCode);
284
if (isPotentialKeyboardShortcut || isModifierKeyPressed) {
285
return;
286
}
287
if (this._contentWidget.isFocused && e.keyCode === KeyCode.Tab) {
288
return;
289
}
290
this.hideContentHover();
291
}
292
293
private _isPotentialKeyboardShortcut(e: IKeyboardEvent): boolean {
294
if (!this._editor.hasModel() || !this._contentWidget) {
295
return false;
296
}
297
const resolvedKeyboardEvent = this._keybindingService.softDispatch(e, this._editor.getDomNode());
298
const moreChordsAreNeeded = resolvedKeyboardEvent.kind === ResultKind.MoreChordsNeeded;
299
const isHoverAction = resolvedKeyboardEvent.kind === ResultKind.KbFound
300
&& (resolvedKeyboardEvent.commandId === SHOW_OR_FOCUS_HOVER_ACTION_ID
301
|| resolvedKeyboardEvent.commandId === INCREASE_HOVER_VERBOSITY_ACTION_ID
302
|| resolvedKeyboardEvent.commandId === DECREASE_HOVER_VERBOSITY_ACTION_ID)
303
&& this._contentWidget.isVisible;
304
return moreChordsAreNeeded || isHoverAction;
305
}
306
307
public hideContentHover(): void {
308
if (_sticky) {
309
return;
310
}
311
if (InlineSuggestionHintsContentWidget.dropDownVisible) {
312
return;
313
}
314
this._contentWidget?.hide();
315
}
316
317
private _getOrCreateContentWidget(): ContentHoverWidgetWrapper {
318
if (!this._contentWidget) {
319
this._contentWidget = this._instantiationService.createInstance(ContentHoverWidgetWrapper, this._editor);
320
this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire()));
321
}
322
return this._contentWidget;
323
}
324
325
public showContentHover(
326
range: Range,
327
mode: HoverStartMode,
328
source: HoverStartSource,
329
focus: boolean
330
): void {
331
this._getOrCreateContentWidget().startShowingAtRange(range, mode, source, focus);
332
}
333
334
private _isContentWidgetResizing(): boolean {
335
return this._contentWidget?.widget.isResizing || false;
336
}
337
338
public focusedHoverPartIndex(): number {
339
return this._getOrCreateContentWidget().focusedHoverPartIndex();
340
}
341
342
public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
343
return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action);
344
}
345
346
public updateHoverVerbosityLevel(action: HoverVerbosityAction, index: number, focus?: boolean): void {
347
this._getOrCreateContentWidget().updateHoverVerbosityLevel(action, index, focus);
348
}
349
350
public focus(): void {
351
this._contentWidget?.focus();
352
}
353
354
public focusHoverPartWithIndex(index: number): void {
355
this._contentWidget?.focusHoverPartWithIndex(index);
356
}
357
358
public scrollUp(): void {
359
this._contentWidget?.scrollUp();
360
}
361
362
public scrollDown(): void {
363
this._contentWidget?.scrollDown();
364
}
365
366
public scrollLeft(): void {
367
this._contentWidget?.scrollLeft();
368
}
369
370
public scrollRight(): void {
371
this._contentWidget?.scrollRight();
372
}
373
374
public pageUp(): void {
375
this._contentWidget?.pageUp();
376
}
377
378
public pageDown(): void {
379
this._contentWidget?.pageDown();
380
}
381
382
public goToTop(): void {
383
this._contentWidget?.goToTop();
384
}
385
386
public goToBottom(): void {
387
this._contentWidget?.goToBottom();
388
}
389
390
public getWidgetContent(): string | undefined {
391
return this._contentWidget?.getWidgetContent();
392
}
393
394
public getAccessibleWidgetContent(): string | undefined {
395
return this._contentWidget?.getAccessibleWidgetContent();
396
}
397
398
public getAccessibleWidgetContentAtIndex(index: number): string | undefined {
399
return this._contentWidget?.getAccessibleWidgetContentAtIndex(index);
400
}
401
402
public get isColorPickerVisible(): boolean | undefined {
403
return this._contentWidget?.isColorPickerVisible;
404
}
405
406
public get isHoverVisible(): boolean | undefined {
407
return this._contentWidget?.isVisible;
408
}
409
410
public override dispose(): void {
411
super.dispose();
412
this._unhookListeners();
413
this._listenersStore.dispose();
414
this._contentWidget?.dispose();
415
}
416
}
417
418