Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/hover/browser/hoverOperation.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 { AsyncIterableProducer, CancelableAsyncIterableProducer, createCancelableAsyncIterableProducer, RunOnceScheduler } from '../../../../base/common/async.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { onUnexpectedError } from '../../../../base/common/errors.js';
9
import { Emitter } from '../../../../base/common/event.js';
10
import { Disposable } from '../../../../base/common/lifecycle.js';
11
import { ICodeEditor } from '../../../browser/editorBrowser.js';
12
import { EditorOption } from '../../../common/config/editorOptions.js';
13
14
export interface IHoverComputer<TArgs, TResult> {
15
/**
16
* This is called after half the hover time
17
*/
18
computeAsync?: (args: TArgs, token: CancellationToken) => AsyncIterableProducer<TResult>;
19
/**
20
* This is called after all the hover time
21
*/
22
computeSync?: (args: TArgs) => TResult[];
23
}
24
25
const enum HoverOperationState {
26
Idle,
27
FirstWait,
28
SecondWait,
29
WaitingForAsync = 3,
30
WaitingForAsyncShowingLoading = 4,
31
}
32
33
export const enum HoverStartMode {
34
Delayed = 0,
35
Immediate = 1
36
}
37
38
export const enum HoverStartSource {
39
Mouse = 0,
40
Click = 1,
41
Keyboard = 2
42
}
43
44
export class HoverResult<TArgs, TResult> {
45
constructor(
46
public readonly value: TResult[],
47
public readonly isComplete: boolean,
48
public readonly hasLoadingMessage: boolean,
49
public readonly options: TArgs
50
) { }
51
}
52
53
/**
54
* Computing the hover is very fine tuned.
55
*
56
* Suppose the hover delay is 300ms (the default). Then, when resting the mouse at an anchor:
57
* - at 150ms, the async computation is triggered (i.e. semantic hover)
58
* - if async results already come in, they are not rendered yet.
59
* - at 300ms, the sync computation is triggered (i.e. decorations, markers)
60
* - if there are sync or async results, they are rendered.
61
* - at 900ms, if the async computation hasn't finished, a "Loading..." result is added.
62
*/
63
export class HoverOperation<TArgs, TResult> extends Disposable {
64
65
private readonly _onResult = this._register(new Emitter<HoverResult<TArgs, TResult>>());
66
public readonly onResult = this._onResult.event;
67
68
private readonly _asyncComputationScheduler = this._register(new Debouncer((options: TArgs) => this._triggerAsyncComputation(options), 0));
69
private readonly _syncComputationScheduler = this._register(new Debouncer((options: TArgs) => this._triggerSyncComputation(options), 0));
70
private readonly _loadingMessageScheduler = this._register(new Debouncer((options: TArgs) => this._triggerLoadingMessage(options), 0));
71
72
private _state = HoverOperationState.Idle;
73
private _asyncIterable: CancelableAsyncIterableProducer<TResult> | null = null;
74
private _asyncIterableDone: boolean = false;
75
private _result: TResult[] = [];
76
private _options: TArgs | undefined;
77
78
constructor(
79
private readonly _editor: ICodeEditor,
80
private readonly _computer: IHoverComputer<TArgs, TResult>
81
) {
82
super();
83
}
84
85
public override dispose(): void {
86
if (this._asyncIterable) {
87
this._asyncIterable.cancel();
88
this._asyncIterable = null;
89
}
90
this._options = undefined;
91
super.dispose();
92
}
93
94
private get _hoverTime(): number {
95
return this._editor.getOption(EditorOption.hover).delay;
96
}
97
98
private get _firstWaitTime(): number {
99
return this._hoverTime / 2;
100
}
101
102
private get _secondWaitTime(): number {
103
return this._hoverTime - this._firstWaitTime;
104
}
105
106
private get _loadingMessageTime(): number {
107
return 3 * this._hoverTime;
108
}
109
110
private _setState(state: HoverOperationState, options: TArgs): void {
111
this._options = options;
112
this._state = state;
113
this._fireResult(options);
114
}
115
116
private _triggerAsyncComputation(options: TArgs): void {
117
this._setState(HoverOperationState.SecondWait, options);
118
this._syncComputationScheduler.schedule(options, this._secondWaitTime);
119
120
if (this._computer.computeAsync) {
121
this._asyncIterableDone = false;
122
this._asyncIterable = createCancelableAsyncIterableProducer(token => this._computer.computeAsync!(options, token));
123
124
(async () => {
125
try {
126
for await (const item of this._asyncIterable!) {
127
if (item) {
128
this._result.push(item);
129
this._fireResult(options);
130
}
131
}
132
this._asyncIterableDone = true;
133
134
if (this._state === HoverOperationState.WaitingForAsync || this._state === HoverOperationState.WaitingForAsyncShowingLoading) {
135
this._setState(HoverOperationState.Idle, options);
136
}
137
138
} catch (e) {
139
onUnexpectedError(e);
140
}
141
})();
142
143
} else {
144
this._asyncIterableDone = true;
145
}
146
}
147
148
private _triggerSyncComputation(options: TArgs): void {
149
if (this._computer.computeSync) {
150
this._result = this._result.concat(this._computer.computeSync(options));
151
}
152
this._setState(this._asyncIterableDone ? HoverOperationState.Idle : HoverOperationState.WaitingForAsync, options);
153
}
154
155
private _triggerLoadingMessage(options: TArgs): void {
156
if (this._state === HoverOperationState.WaitingForAsync) {
157
this._setState(HoverOperationState.WaitingForAsyncShowingLoading, options);
158
}
159
}
160
161
private _fireResult(options: TArgs): void {
162
if (this._state === HoverOperationState.FirstWait || this._state === HoverOperationState.SecondWait) {
163
// Do not send out results before the hover time
164
return;
165
}
166
const isComplete = (this._state === HoverOperationState.Idle);
167
const hasLoadingMessage = (this._state === HoverOperationState.WaitingForAsyncShowingLoading);
168
this._onResult.fire(new HoverResult(this._result.slice(0), isComplete, hasLoadingMessage, options));
169
}
170
171
public start(mode: HoverStartMode, options: TArgs): void {
172
if (mode === HoverStartMode.Delayed) {
173
if (this._state === HoverOperationState.Idle) {
174
this._setState(HoverOperationState.FirstWait, options);
175
this._asyncComputationScheduler.schedule(options, this._firstWaitTime);
176
this._loadingMessageScheduler.schedule(options, this._loadingMessageTime);
177
}
178
} else {
179
switch (this._state) {
180
case HoverOperationState.Idle:
181
this._triggerAsyncComputation(options);
182
this._syncComputationScheduler.cancel();
183
this._triggerSyncComputation(options);
184
break;
185
case HoverOperationState.SecondWait:
186
this._syncComputationScheduler.cancel();
187
this._triggerSyncComputation(options);
188
break;
189
}
190
}
191
}
192
193
public cancel(): void {
194
this._asyncComputationScheduler.cancel();
195
this._syncComputationScheduler.cancel();
196
this._loadingMessageScheduler.cancel();
197
if (this._asyncIterable) {
198
this._asyncIterable.cancel();
199
this._asyncIterable = null;
200
}
201
this._result = [];
202
this._options = undefined;
203
this._state = HoverOperationState.Idle;
204
}
205
206
public get options(): TArgs | undefined {
207
return this._options;
208
}
209
}
210
211
class Debouncer<TArgs> extends Disposable {
212
213
private readonly _scheduler: RunOnceScheduler;
214
215
private _options: TArgs | undefined;
216
217
constructor(runner: (options: TArgs) => void, debounceTimeMs: number) {
218
super();
219
this._scheduler = this._register(new RunOnceScheduler(() => runner(this._options!), debounceTimeMs));
220
}
221
222
schedule(options: TArgs, debounceTimeMs: number): void {
223
this._options = options;
224
this._scheduler.schedule(debounceTimeMs);
225
}
226
227
cancel(): void {
228
this._scheduler.cancel();
229
}
230
}
231
232