Path: blob/main/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts
3296 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 { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';6import { ICodeEditor } from '../../../browser/editorBrowser.js';7import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';8import { CancellationToken, CancellationTokenSource, } from '../../../../base/common/cancellation.js';9import { EditorOption } from '../../../common/config/editorOptions.js';10import { RunOnceScheduler } from '../../../../base/common/async.js';11import { binarySearch } from '../../../../base/common/arrays.js';12import { Event, Emitter } from '../../../../base/common/event.js';13import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';14import { StickyModelProvider, IStickyModelProvider } from './stickyScrollModelProvider.js';15import { StickyElement, StickyModel, StickyRange } from './stickyScrollElement.js';16import { Position } from '../../../common/core/position.js';1718export class StickyLineCandidate {19constructor(20public readonly startLineNumber: number,21public readonly endLineNumber: number,22public readonly top: number,23public readonly height: number,24) { }25}2627export interface IStickyLineCandidateProvider {28/**29* Dispose resources used by the provider.30*/31dispose(): void;3233/**34* Get the version ID of the sticky model.35*/36getVersionId(): number | undefined;3738/**39* Update the sticky line candidates.40*/41update(): Promise<void>;4243/**44* Get sticky line candidates intersecting a given range.45*/46getCandidateStickyLinesIntersecting(range: StickyRange): StickyLineCandidate[];4748/**49* Event triggered when sticky scroll changes.50*/51onDidChangeStickyScroll: Event<void>;52}5354export class StickyLineCandidateProvider extends Disposable implements IStickyLineCandidateProvider {55static readonly ID = 'store.contrib.stickyScrollController';5657private readonly _onDidChangeStickyScroll = this._register(new Emitter<void>());58public readonly onDidChangeStickyScroll = this._onDidChangeStickyScroll.event;5960private readonly _editor: ICodeEditor;61private readonly _updateSoon: RunOnceScheduler;62private readonly _sessionStore: DisposableStore;6364private _model: StickyModel | null = null;65private _cts: CancellationTokenSource | null = null;66private _stickyModelProvider: IStickyModelProvider | null = null;6768constructor(69editor: ICodeEditor,70@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,71@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,72) {73super();74this._editor = editor;75this._sessionStore = this._register(new DisposableStore());76this._updateSoon = this._register(new RunOnceScheduler(() => this.update(), 50));7778this._register(this._editor.onDidChangeConfiguration(e => {79if (e.hasChanged(EditorOption.stickyScroll)) {80this.readConfiguration();81}82}));83this.readConfiguration();84}8586/**87* Read and apply the sticky scroll configuration.88*/89private readConfiguration() {90this._sessionStore.clear();91const options = this._editor.getOption(EditorOption.stickyScroll);92if (!options.enabled) {93return;94}95this._sessionStore.add(this._editor.onDidChangeModel(() => {96this._model = null;97this.updateStickyModelProvider();98this._onDidChangeStickyScroll.fire();99this.update();100}));101this._sessionStore.add(this._editor.onDidChangeHiddenAreas(() => this.update()));102this._sessionStore.add(this._editor.onDidChangeModelContent(() => this._updateSoon.schedule()));103this._sessionStore.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => this.update()));104this._sessionStore.add(toDisposable(() => {105this._stickyModelProvider?.dispose();106this._stickyModelProvider = null;107}));108this.updateStickyModelProvider();109this.update();110}111112/**113* Get the version ID of the sticky model.114*/115public getVersionId(): number | undefined {116return this._model?.version;117}118119/**120* Update the sticky model provider.121*/122private updateStickyModelProvider() {123this._stickyModelProvider?.dispose();124this._stickyModelProvider = null;125if (this._editor.hasModel()) {126this._stickyModelProvider = new StickyModelProvider(127this._editor,128() => this._updateSoon.schedule(),129this._languageConfigurationService,130this._languageFeaturesService131);132}133}134135/**136* Update the sticky line candidates.137*/138public async update(): Promise<void> {139this._cts?.dispose(true);140this._cts = new CancellationTokenSource();141await this.updateStickyModel(this._cts.token);142this._onDidChangeStickyScroll.fire();143}144145/**146* Update the sticky model based on the current editor state.147*/148private async updateStickyModel(token: CancellationToken): Promise<void> {149if (!this._editor.hasModel() || !this._stickyModelProvider || this._editor.getModel().isTooLargeForTokenization()) {150this._model = null;151return;152}153const model = await this._stickyModelProvider.update(token);154if (!token.isCancellationRequested) {155this._model = model;156}157}158159/**160* Get sticky line candidates intersecting a given range.161*/162public getCandidateStickyLinesIntersecting(range: StickyRange): StickyLineCandidate[] {163if (!this._model?.element) {164return [];165}166const stickyLineCandidates: StickyLineCandidate[] = [];167this.getCandidateStickyLinesIntersectingFromStickyModel(range, this._model.element, stickyLineCandidates, 0, 0, -1);168return this.filterHiddenRanges(stickyLineCandidates);169}170171/**172* Get sticky line candidates intersecting a given range from the sticky model.173*/174private getCandidateStickyLinesIntersectingFromStickyModel(175range: StickyRange,176outlineModel: StickyElement,177result: StickyLineCandidate[],178depth: number,179top: number,180lastStartLineNumber: number181): void {182if (outlineModel.children.length === 0) {183return;184}185let lastLine = lastStartLineNumber;186const childrenStartLines: number[] = [];187188for (let i = 0; i < outlineModel.children.length; i++) {189const child = outlineModel.children[i];190if (child.range) {191childrenStartLines.push(child.range.startLineNumber);192}193}194const lowerBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber, (a: number, b: number) => { return a - b; }));195const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.endLineNumber, (a: number, b: number) => { return a - b; }));196197for (let i = lowerBound; i <= upperBound; i++) {198const child = outlineModel.children[i];199if (!child || !child.range) {200continue;201}202const { startLineNumber, endLineNumber } = child.range;203if (range.startLineNumber <= endLineNumber + 1 && startLineNumber - 1 <= range.endLineNumber && startLineNumber !== lastLine) {204lastLine = startLineNumber;205const lineHeight = this._editor.getLineHeightForPosition(new Position(startLineNumber, 1));206result.push(new StickyLineCandidate(startLineNumber, endLineNumber - 1, top, lineHeight));207this.getCandidateStickyLinesIntersectingFromStickyModel(range, child, result, depth + 1, top + lineHeight, startLineNumber);208}209}210}211212/**213* Filter out sticky line candidates that are within hidden ranges.214*/215private filterHiddenRanges(stickyLineCandidates: StickyLineCandidate[]): StickyLineCandidate[] {216const hiddenRanges = this._editor._getViewModel()?.getHiddenAreas();217if (!hiddenRanges) {218return stickyLineCandidates;219}220return stickyLineCandidates.filter(candidate => {221return !hiddenRanges.some(hiddenRange =>222candidate.startLineNumber >= hiddenRange.startLineNumber &&223candidate.endLineNumber <= hiddenRange.endLineNumber + 1224);225});226}227228/**229* Update the binary search index.230*/231private updateIndex(index: number): number {232if (index === -1) {233return 0;234} else if (index < 0) {235return -index - 2;236}237return index;238}239}240241242