Path: blob/main/src/vs/editor/contrib/stickyScroll/browser/stickyScrollModelProvider.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, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';6import { IActiveCodeEditor } from '../../../browser/editorBrowser.js';7import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';8import { OutlineElement, OutlineGroup, OutlineModel } from '../../documentSymbols/browser/outlineModel.js';9import { CancellationToken } from '../../../../base/common/cancellation.js';10import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js';11import { FoldingController, RangesLimitReporter } from '../../folding/browser/folding.js';12import { SyntaxRangeProvider } from '../../folding/browser/syntaxRangeProvider.js';13import { IndentRangeProvider } from '../../folding/browser/indentRangeProvider.js';14import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';15import { FoldingRegions } from '../../folding/browser/foldingRanges.js';16import { onUnexpectedError } from '../../../../base/common/errors.js';17import { StickyElement, StickyModel, StickyRange } from './stickyScrollElement.js';18import { Iterable } from '../../../../base/common/iterator.js';19import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';20import { EditorOption } from '../../../common/config/editorOptions.js';2122enum ModelProvider {23OUTLINE_MODEL = 'outlineModel',24FOLDING_PROVIDER_MODEL = 'foldingProviderModel',25INDENTATION_MODEL = 'indentationModel'26}2728enum Status {29VALID,30INVALID,31CANCELED32}3334export interface IStickyModelProvider extends IDisposable {3536/**37* Method which updates the sticky model38* @param token cancellation token39* @returns the sticky model40*/41update(token: CancellationToken): Promise<StickyModel | null>;42}4344export class StickyModelProvider extends Disposable implements IStickyModelProvider {4546private _modelProviders: IStickyModelCandidateProvider<any>[] = [];47private _modelPromise: CancelablePromise<any | null> | null = null;48private _updateScheduler: Delayer<StickyModel | null> = this._register(new Delayer<StickyModel | null>(300));49private readonly _updateOperation: DisposableStore = this._register(new DisposableStore());5051constructor(52private readonly _editor: IActiveCodeEditor,53onProviderUpdate: () => void,54@IInstantiationService _languageConfigurationService: ILanguageConfigurationService,55@ILanguageFeaturesService _languageFeaturesService: ILanguageFeaturesService,56) {57super();5859switch (this._editor.getOption(EditorOption.stickyScroll).defaultModel) {60case ModelProvider.OUTLINE_MODEL:61this._modelProviders.push(new StickyModelFromCandidateOutlineProvider(this._editor, _languageFeaturesService));62// fall through63case ModelProvider.FOLDING_PROVIDER_MODEL:64this._modelProviders.push(new StickyModelFromCandidateSyntaxFoldingProvider(this._editor, onProviderUpdate, _languageFeaturesService));65// fall through66case ModelProvider.INDENTATION_MODEL:67this._modelProviders.push(new StickyModelFromCandidateIndentationFoldingProvider(this._editor, _languageConfigurationService));68break;69}70}7172public override dispose(): void {73this._modelProviders.forEach(provider => provider.dispose());74this._updateOperation.clear();75this._cancelModelPromise();76super.dispose();77}7879private _cancelModelPromise(): void {80if (this._modelPromise) {81this._modelPromise.cancel();82this._modelPromise = null;83}84}8586public async update(token: CancellationToken): Promise<StickyModel | null> {8788this._updateOperation.clear();89this._updateOperation.add({90dispose: () => {91this._cancelModelPromise();92this._updateScheduler.cancel();93}94});95this._cancelModelPromise();9697return await this._updateScheduler.trigger(async () => {9899for (const modelProvider of this._modelProviders) {100const { statusPromise, modelPromise } = modelProvider.computeStickyModel(token);101this._modelPromise = modelPromise;102const status = await statusPromise;103if (this._modelPromise !== modelPromise) {104return null;105}106switch (status) {107case Status.CANCELED:108this._updateOperation.clear();109return null;110case Status.VALID:111return modelProvider.stickyModel;112}113}114return null;115}).catch((error) => {116onUnexpectedError(error);117return null;118});119}120}121122interface IStickyModelCandidateProvider<T> extends IDisposable {123get stickyModel(): StickyModel | null;124125/**126* Method which computes the sticky model and returns a status to signal whether the sticky model has been successfully found127* @param token cancellation token128* @returns a promise of a status indicating whether the sticky model has been successfully found as well as the model promise129*/130computeStickyModel(token: CancellationToken): { statusPromise: Promise<Status> | Status; modelPromise: CancelablePromise<T | null> | null };131}132133abstract class StickyModelCandidateProvider<T> extends Disposable implements IStickyModelCandidateProvider<T> {134135protected _stickyModel: StickyModel | null = null;136137constructor(protected readonly _editor: IActiveCodeEditor) {138super();139}140141get stickyModel(): StickyModel | null {142return this._stickyModel;143}144145private _invalid(): Status {146this._stickyModel = null;147return Status.INVALID;148}149150public computeStickyModel(token: CancellationToken): { statusPromise: Promise<Status> | Status; modelPromise: CancelablePromise<T | null> | null } {151if (token.isCancellationRequested || !this.isProviderValid()) {152return { statusPromise: this._invalid(), modelPromise: null };153}154const providerModelPromise = createCancelablePromise(token => this.createModelFromProvider(token));155156return {157statusPromise: providerModelPromise.then(providerModel => {158if (!this.isModelValid(providerModel)) {159return this._invalid();160161}162if (token.isCancellationRequested) {163return Status.CANCELED;164}165this._stickyModel = this.createStickyModel(token, providerModel);166return Status.VALID;167}).then(undefined, (err) => {168onUnexpectedError(err);169return Status.CANCELED;170}),171modelPromise: providerModelPromise172};173}174175/**176* Method which checks whether the model returned by the provider is valid and can be used to compute a sticky model.177* This method by default returns true.178* @param model model returned by the provider179* @returns boolean indicating whether the model is valid180*/181protected isModelValid(model: T): boolean {182return true;183}184185/**186* Method which checks whether the provider is valid before applying it to find the provider model.187* This method by default returns true.188* @returns boolean indicating whether the provider is valid189*/190protected isProviderValid(): boolean {191return true;192}193194/**195* Abstract method which creates the model from the provider and returns the provider model196* @param token cancellation token197* @returns the model returned by the provider198*/199protected abstract createModelFromProvider(token: CancellationToken): Promise<T>;200201/**202* Abstract method which computes the sticky model from the model returned by the provider and returns the sticky model203* @param token cancellation token204* @param model model returned by the provider205* @returns the sticky model206*/207protected abstract createStickyModel(token: CancellationToken, model: T): StickyModel;208}209210class StickyModelFromCandidateOutlineProvider extends StickyModelCandidateProvider<OutlineModel> {211212constructor(_editor: IActiveCodeEditor, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService) {213super(_editor);214}215216protected createModelFromProvider(token: CancellationToken): Promise<OutlineModel> {217return OutlineModel.create(this._languageFeaturesService.documentSymbolProvider, this._editor.getModel(), token);218}219220protected createStickyModel(token: CancellationToken, model: OutlineModel): StickyModel {221const { stickyOutlineElement, providerID } = this._stickyModelFromOutlineModel(model, this._stickyModel?.outlineProviderId);222const textModel = this._editor.getModel();223return new StickyModel(textModel.uri, textModel.getVersionId(), stickyOutlineElement, providerID);224}225226protected override isModelValid(model: OutlineModel): boolean {227return model && model.children.size > 0;228}229230private _stickyModelFromOutlineModel(outlineModel: OutlineModel, preferredProvider: string | undefined): { stickyOutlineElement: StickyElement; providerID: string | undefined } {231232let outlineElements: Map<string, OutlineElement>;233// When several possible outline providers234if (Iterable.first(outlineModel.children.values()) instanceof OutlineGroup) {235const provider = Iterable.find(outlineModel.children.values(), outlineGroupOfModel => outlineGroupOfModel.id === preferredProvider);236if (provider) {237outlineElements = provider.children;238} else {239let tempID = '';240let maxTotalSumOfRanges = -1;241let optimalOutlineGroup = undefined;242for (const [_key, outlineGroup] of outlineModel.children.entries()) {243const totalSumRanges = this._findSumOfRangesOfGroup(outlineGroup);244if (totalSumRanges > maxTotalSumOfRanges) {245optimalOutlineGroup = outlineGroup;246maxTotalSumOfRanges = totalSumRanges;247tempID = outlineGroup.id;248}249}250preferredProvider = tempID;251outlineElements = optimalOutlineGroup!.children;252}253} else {254outlineElements = outlineModel.children as Map<string, OutlineElement>;255}256const stickyChildren: StickyElement[] = [];257const outlineElementsArray = Array.from(outlineElements.values()).sort((element1, element2) => {258const range1: StickyRange = new StickyRange(element1.symbol.range.startLineNumber, element1.symbol.range.endLineNumber);259const range2: StickyRange = new StickyRange(element2.symbol.range.startLineNumber, element2.symbol.range.endLineNumber);260return this._comparator(range1, range2);261});262for (const outlineElement of outlineElementsArray) {263stickyChildren.push(this._stickyModelFromOutlineElement(outlineElement, outlineElement.symbol.selectionRange.startLineNumber));264}265const stickyOutlineElement = new StickyElement(undefined, stickyChildren, undefined);266267return {268stickyOutlineElement: stickyOutlineElement,269providerID: preferredProvider270};271}272273private _stickyModelFromOutlineElement(outlineElement: OutlineElement, previousStartLine: number): StickyElement {274const children: StickyElement[] = [];275for (const child of outlineElement.children.values()) {276if (child.symbol.selectionRange.startLineNumber !== child.symbol.range.endLineNumber) {277if (child.symbol.selectionRange.startLineNumber !== previousStartLine) {278children.push(this._stickyModelFromOutlineElement(child, child.symbol.selectionRange.startLineNumber));279} else {280for (const subchild of child.children.values()) {281children.push(this._stickyModelFromOutlineElement(subchild, child.symbol.selectionRange.startLineNumber));282}283}284}285}286children.sort((child1, child2) => this._comparator(child1.range!, child2.range!));287const range = new StickyRange(outlineElement.symbol.selectionRange.startLineNumber, outlineElement.symbol.range.endLineNumber);288return new StickyElement(range, children, undefined);289}290291private _comparator(range1: StickyRange, range2: StickyRange): number {292if (range1.startLineNumber !== range2.startLineNumber) {293return range1.startLineNumber - range2.startLineNumber;294} else {295return range2.endLineNumber - range1.endLineNumber;296}297}298299private _findSumOfRangesOfGroup(outline: OutlineGroup | OutlineElement): number {300let res = 0;301for (const child of outline.children.values()) {302res += this._findSumOfRangesOfGroup(child);303}304if (outline instanceof OutlineElement) {305return res + outline.symbol.range.endLineNumber - outline.symbol.selectionRange.startLineNumber;306} else {307return res;308}309}310}311312abstract class StickyModelFromCandidateFoldingProvider extends StickyModelCandidateProvider<FoldingRegions | null> {313314protected _foldingLimitReporter: RangesLimitReporter;315316constructor(editor: IActiveCodeEditor) {317super(editor);318this._foldingLimitReporter = this._register(new RangesLimitReporter(editor));319}320321protected createStickyModel(token: CancellationToken, model: FoldingRegions): StickyModel {322const foldingElement = this._fromFoldingRegions(model);323const textModel = this._editor.getModel();324return new StickyModel(textModel.uri, textModel.getVersionId(), foldingElement, undefined);325}326327protected override isModelValid(model: FoldingRegions): boolean {328return model !== null;329}330331332private _fromFoldingRegions(foldingRegions: FoldingRegions): StickyElement {333const length = foldingRegions.length;334const orderedStickyElements: StickyElement[] = [];335336// The root sticky outline element337const stickyOutlineElement = new StickyElement(338undefined,339[],340undefined341);342343for (let i = 0; i < length; i++) {344// Finding the parent index of the current range345const parentIndex = foldingRegions.getParentIndex(i);346347let parentNode;348if (parentIndex !== -1) {349// Access the reference of the parent node350parentNode = orderedStickyElements[parentIndex];351} else {352// In that case the parent node is the root node353parentNode = stickyOutlineElement;354}355356const child = new StickyElement(357new StickyRange(foldingRegions.getStartLineNumber(i), foldingRegions.getEndLineNumber(i) + 1),358[],359parentNode360);361parentNode.children.push(child);362orderedStickyElements.push(child);363}364return stickyOutlineElement;365}366}367368class StickyModelFromCandidateIndentationFoldingProvider extends StickyModelFromCandidateFoldingProvider {369370private readonly provider: IndentRangeProvider;371372constructor(373editor: IActiveCodeEditor,374@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService) {375super(editor);376377this.provider = this._register(new IndentRangeProvider(editor.getModel(), this._languageConfigurationService, this._foldingLimitReporter));378}379380protected override async createModelFromProvider(token: CancellationToken): Promise<FoldingRegions> {381return this.provider.compute(token);382}383}384385class StickyModelFromCandidateSyntaxFoldingProvider extends StickyModelFromCandidateFoldingProvider {386387private readonly provider: MutableDisposable<SyntaxRangeProvider> = this._register(new MutableDisposable<SyntaxRangeProvider>());388389constructor(390editor: IActiveCodeEditor,391onProviderUpdate: () => void,392@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService393) {394super(editor);395this._register(this._languageFeaturesService.foldingRangeProvider.onDidChange(() => {396this._updateProvider(editor, onProviderUpdate);397}));398this._updateProvider(editor, onProviderUpdate);399}400401private _updateProvider(editor: IActiveCodeEditor, onProviderUpdate: () => void): void {402const selectedProviders = FoldingController.getFoldingRangeProviders(this._languageFeaturesService, editor.getModel());403if (selectedProviders.length === 0) {404return;405}406this.provider.value = new SyntaxRangeProvider(editor.getModel(), selectedProviders, onProviderUpdate, this._foldingLimitReporter, undefined);407}408409protected override isProviderValid(): boolean {410return this.provider !== undefined;411}412413protected override async createModelFromProvider(token: CancellationToken): Promise<FoldingRegions | null> {414return this.provider.value?.compute(token) ?? null;415}416}417418419