Path: blob/main/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts
5292 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';17import { Range } from '../../../common/core/range.js';1819export class StickyLineCandidate {20constructor(21public readonly startLineNumber: number,22public readonly endLineNumber: number,23public readonly top: number,24public readonly height: number,25) { }26}2728export interface IStickyLineCandidateProvider {29/**30* Dispose resources used by the provider.31*/32dispose(): void;3334/**35* Get the version ID of the sticky model.36*/37getVersionId(): number | undefined;3839/**40* Update the sticky line candidates.41*/42update(): Promise<void>;4344/**45* Get sticky line candidates intersecting a given range.46*/47getCandidateStickyLinesIntersecting(range: StickyRange): StickyLineCandidate[];4849/**50* Event triggered when sticky scroll changes.51*/52readonly onDidChangeStickyScroll: Event<void>;53}5455export class StickyLineCandidateProvider extends Disposable implements IStickyLineCandidateProvider {56static readonly ID = 'store.contrib.stickyScrollController';5758private readonly _onDidChangeStickyScroll = this._register(new Emitter<void>());59public readonly onDidChangeStickyScroll = this._onDidChangeStickyScroll.event;6061private readonly _editor: ICodeEditor;62private readonly _updateSoon: RunOnceScheduler;63private readonly _sessionStore: DisposableStore;6465private _model: StickyModel | null = null;66private _cts: CancellationTokenSource | null = null;67private _stickyModelProvider: IStickyModelProvider | null = null;6869constructor(70editor: ICodeEditor,71@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,72@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,73) {74super();75this._editor = editor;76this._sessionStore = this._register(new DisposableStore());77this._updateSoon = this._register(new RunOnceScheduler(() => this.update(), 50));7879this._register(this._editor.onDidChangeConfiguration(e => {80if (e.hasChanged(EditorOption.stickyScroll)) {81this.readConfiguration();82}83}));84this.readConfiguration();85}8687/**88* Read and apply the sticky scroll configuration.89*/90private readConfiguration() {91this._sessionStore.clear();92const options = this._editor.getOption(EditorOption.stickyScroll);93if (!options.enabled) {94return;95}96this._sessionStore.add(this._editor.onDidChangeModel(() => {97this._model = null;98this.updateStickyModelProvider();99this._onDidChangeStickyScroll.fire();100this.update();101}));102this._sessionStore.add(this._editor.onDidChangeHiddenAreas(() => this.update()));103this._sessionStore.add(this._editor.onDidChangeModelContent(() => this._updateSoon.schedule()));104this._sessionStore.add(this._languageFeaturesService.documentSymbolProvider.onDidChange(() => this.update()));105this._sessionStore.add(toDisposable(() => {106this._stickyModelProvider?.dispose();107this._stickyModelProvider = null;108}));109this.updateStickyModelProvider();110this.update();111}112113/**114* Get the version ID of the sticky model.115*/116public getVersionId(): number | undefined {117return this._model?.version;118}119120/**121* Update the sticky model provider.122*/123private updateStickyModelProvider() {124this._stickyModelProvider?.dispose();125this._stickyModelProvider = null;126if (this._editor.hasModel()) {127this._stickyModelProvider = new StickyModelProvider(128this._editor,129() => this._updateSoon.schedule(),130this._languageConfigurationService,131this._languageFeaturesService132);133}134}135136/**137* Update the sticky line candidates.138*/139public async update(): Promise<void> {140this._cts?.dispose(true);141this._cts = new CancellationTokenSource();142await this.updateStickyModel(this._cts.token);143this._onDidChangeStickyScroll.fire();144}145146/**147* Update the sticky model based on the current editor state.148*/149private async updateStickyModel(token: CancellationToken): Promise<void> {150if (!this._editor.hasModel() || !this._stickyModelProvider || this._editor.getModel().isTooLargeForTokenization()) {151this._model = null;152return;153}154const model = await this._stickyModelProvider.update(token);155if (!token.isCancellationRequested) {156this._model = model;157}158}159160/**161* Get sticky line candidates intersecting a given range.162*/163public getCandidateStickyLinesIntersecting(range: StickyRange): StickyLineCandidate[] {164if (!this._model?.element) {165return [];166}167const stickyLineCandidates: StickyLineCandidate[] = [];168this.getCandidateStickyLinesIntersectingFromStickyModel(range, this._model.element, stickyLineCandidates, 0, 0, -1);169return this.filterHiddenRanges(stickyLineCandidates);170}171172/**173* Get sticky line candidates intersecting a given range from the sticky model.174*/175private getCandidateStickyLinesIntersectingFromStickyModel(176range: StickyRange,177outlineModel: StickyElement,178result: StickyLineCandidate[],179depth: number,180top: number,181lastStartLineNumber: number182): void {183const textModel = this._editor.getModel();184if (!textModel) {185return;186}187if (outlineModel.children.length === 0) {188return;189}190let lastLine = lastStartLineNumber;191const childrenStartLines: number[] = [];192193for (let i = 0; i < outlineModel.children.length; i++) {194const child = outlineModel.children[i];195if (child.range) {196childrenStartLines.push(child.range.startLineNumber);197}198}199const lowerBound = this.updateIndex(binarySearch(childrenStartLines, range.startLineNumber, (a: number, b: number) => { return a - b; }));200const upperBound = this.updateIndex(binarySearch(childrenStartLines, range.endLineNumber, (a: number, b: number) => { return a - b; }));201202for (let i = lowerBound; i <= upperBound; i++) {203const child = outlineModel.children[i];204if (!child || !child.range) {205continue;206}207const { startLineNumber, endLineNumber } = child.range;208if (209endLineNumber > startLineNumber + 1210&& range.startLineNumber <= endLineNumber + 1211&& startLineNumber - 1 <= range.endLineNumber212&& startLineNumber !== lastLine213&& textModel.isValidRange(new Range(startLineNumber, 1, endLineNumber, 1))214) {215lastLine = startLineNumber;216const lineHeight = this._editor.getLineHeightForPosition(new Position(startLineNumber, 1));217result.push(new StickyLineCandidate(startLineNumber, endLineNumber - 1, top, lineHeight));218this.getCandidateStickyLinesIntersectingFromStickyModel(range, child, result, depth + 1, top + lineHeight, startLineNumber);219}220}221}222223/**224* Filter out sticky line candidates that are within hidden ranges.225*/226private filterHiddenRanges(stickyLineCandidates: StickyLineCandidate[]): StickyLineCandidate[] {227const hiddenRanges = this._editor._getViewModel()?.getHiddenAreas();228if (!hiddenRanges) {229return stickyLineCandidates;230}231return stickyLineCandidates.filter(candidate => {232return !hiddenRanges.some(hiddenRange =>233candidate.startLineNumber >= hiddenRange.startLineNumber &&234candidate.endLineNumber <= hiddenRange.endLineNumber + 1235);236});237}238239/**240* Update the binary search index.241*/242private updateIndex(index: number): number {243if (index === -1) {244return 0;245} else if (index < 0) {246return -index - 2;247}248return index;249}250}251252253