Path: blob/main/src/vs/editor/contrib/hover/browser/hoverOperation.ts
4779 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { AsyncIterableProducer, CancelableAsyncIterableProducer, createCancelableAsyncIterableProducer, RunOnceScheduler } from '../../../../base/common/async.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { onUnexpectedError } from '../../../../base/common/errors.js';8import { Emitter } from '../../../../base/common/event.js';9import { Disposable } from '../../../../base/common/lifecycle.js';10import { ICodeEditor } from '../../../browser/editorBrowser.js';11import { EditorOption } from '../../../common/config/editorOptions.js';1213export interface IHoverComputer<TArgs, TResult> {14/**15* This is called after half the hover time16*/17computeAsync?: (args: TArgs, token: CancellationToken) => AsyncIterableProducer<TResult>;18/**19* This is called after all the hover time20*/21computeSync?: (args: TArgs) => TResult[];22}2324const enum HoverOperationState {25Idle,26FirstWait,27SecondWait,28WaitingForAsync = 3,29WaitingForAsyncShowingLoading = 4,30}3132export const enum HoverStartMode {33Delayed = 0,34Immediate = 135}3637export const enum HoverStartSource {38Mouse = 0,39Click = 1,40Keyboard = 241}4243export class HoverResult<TArgs, TResult> {44constructor(45public readonly value: TResult[],46public readonly isComplete: boolean,47public readonly hasLoadingMessage: boolean,48public readonly options: TArgs49) { }50}5152/**53* Computing the hover is very fine tuned.54*55* Suppose the hover delay is 300ms (the default). Then, when resting the mouse at an anchor:56* - at 150ms, the async computation is triggered (i.e. semantic hover)57* - if async results already come in, they are not rendered yet.58* - at 300ms, the sync computation is triggered (i.e. decorations, markers)59* - if there are sync or async results, they are rendered.60* - at 900ms, if the async computation hasn't finished, a "Loading..." result is added.61*/62export class HoverOperation<TArgs, TResult> extends Disposable {6364private readonly _onResult = this._register(new Emitter<HoverResult<TArgs, TResult>>());65public readonly onResult = this._onResult.event;6667private readonly _asyncComputationScheduler = this._register(new Debouncer((options: TArgs) => this._triggerAsyncComputation(options), 0));68private readonly _syncComputationScheduler = this._register(new Debouncer((options: TArgs) => this._triggerSyncComputation(options), 0));69private readonly _loadingMessageScheduler = this._register(new Debouncer((options: TArgs) => this._triggerLoadingMessage(options), 0));7071private _state = HoverOperationState.Idle;72private _asyncIterable: CancelableAsyncIterableProducer<TResult> | null = null;73private _asyncIterableDone: boolean = false;74private _result: TResult[] = [];75private _options: TArgs | undefined;7677constructor(78private readonly _editor: ICodeEditor,79private readonly _computer: IHoverComputer<TArgs, TResult>80) {81super();82}8384public override dispose(): void {85if (this._asyncIterable) {86this._asyncIterable.cancel();87this._asyncIterable = null;88}89this._options = undefined;90super.dispose();91}9293private get _hoverTime(): number {94return this._editor.getOption(EditorOption.hover).delay;95}9697private get _firstWaitTime(): number {98return this._hoverTime / 2;99}100101private get _secondWaitTime(): number {102return this._hoverTime - this._firstWaitTime;103}104105private get _loadingMessageTime(): number {106return 3 * this._hoverTime;107}108109private _setState(state: HoverOperationState, options: TArgs): void {110this._options = options;111this._state = state;112this._fireResult(options);113}114115private _triggerAsyncComputation(options: TArgs): void {116this._setState(HoverOperationState.SecondWait, options);117this._syncComputationScheduler.schedule(options, this._secondWaitTime);118119if (this._computer.computeAsync) {120this._asyncIterableDone = false;121this._asyncIterable = createCancelableAsyncIterableProducer(token => this._computer.computeAsync!(options, token));122123(async () => {124try {125for await (const item of this._asyncIterable!) {126if (item) {127this._result.push(item);128this._fireResult(options);129}130}131this._asyncIterableDone = true;132133if (this._state === HoverOperationState.WaitingForAsync || this._state === HoverOperationState.WaitingForAsyncShowingLoading) {134this._setState(HoverOperationState.Idle, options);135}136137} catch (e) {138onUnexpectedError(e);139}140})();141142} else {143this._asyncIterableDone = true;144}145}146147private _triggerSyncComputation(options: TArgs): void {148if (this._computer.computeSync) {149this._result = this._result.concat(this._computer.computeSync(options));150}151this._setState(this._asyncIterableDone ? HoverOperationState.Idle : HoverOperationState.WaitingForAsync, options);152}153154private _triggerLoadingMessage(options: TArgs): void {155if (this._state === HoverOperationState.WaitingForAsync) {156this._setState(HoverOperationState.WaitingForAsyncShowingLoading, options);157}158}159160private _fireResult(options: TArgs): void {161if (this._state === HoverOperationState.FirstWait || this._state === HoverOperationState.SecondWait) {162// Do not send out results before the hover time163return;164}165const isComplete = (this._state === HoverOperationState.Idle);166const hasLoadingMessage = (this._state === HoverOperationState.WaitingForAsyncShowingLoading);167this._onResult.fire(new HoverResult(this._result.slice(0), isComplete, hasLoadingMessage, options));168}169170public start(mode: HoverStartMode, options: TArgs): void {171if (mode === HoverStartMode.Delayed) {172if (this._state === HoverOperationState.Idle) {173this._setState(HoverOperationState.FirstWait, options);174this._asyncComputationScheduler.schedule(options, this._firstWaitTime);175this._loadingMessageScheduler.schedule(options, this._loadingMessageTime);176}177} else {178switch (this._state) {179case HoverOperationState.Idle:180this._triggerAsyncComputation(options);181this._syncComputationScheduler.cancel();182this._triggerSyncComputation(options);183break;184case HoverOperationState.SecondWait:185this._syncComputationScheduler.cancel();186this._triggerSyncComputation(options);187break;188}189}190}191192public cancel(): void {193this._asyncComputationScheduler.cancel();194this._syncComputationScheduler.cancel();195this._loadingMessageScheduler.cancel();196if (this._asyncIterable) {197this._asyncIterable.cancel();198this._asyncIterable = null;199}200this._result = [];201this._options = undefined;202this._state = HoverOperationState.Idle;203}204205public get options(): TArgs | undefined {206return this._options;207}208}209210class Debouncer<TArgs> extends Disposable {211212private readonly _scheduler: RunOnceScheduler;213214private _options: TArgs | undefined;215216constructor(runner: (options: TArgs) => void, debounceTimeMs: number) {217super();218this._scheduler = this._register(new RunOnceScheduler(() => runner(this._options!), debounceTimeMs));219}220221schedule(options: TArgs, debounceTimeMs: number): void {222this._options = options;223this._scheduler.schedule(debounceTimeMs);224}225226cancel(): void {227this._scheduler.cancel();228}229}230231232