diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 5b51df074b5..fa8f6fc1feb 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -90,6 +90,10 @@ "name": "vs/workbench/contrib/files", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/folding", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/html", "project": "vscode-workbench" diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 87131049763..b33e9c59601 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -21,7 +21,7 @@ import { import { hash } from './utils/hash'; -import { createDocumentColorsLimitItem, createDocumentSymbolsLimitItem, createFoldingRangeLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus'; +import { createDocumentColorsLimitItem, createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus'; namespace VSCodeContentRequest { export const type: RequestType = new RequestType('vscode/content'); @@ -60,6 +60,8 @@ type Settings = { keepLines?: { enable?: boolean }; validate?: { enable?: boolean }; resultLimit?: number; + jsonFoldingLimit?: number; + jsoncFoldingLimit?: number; }; http?: { proxy?: string; @@ -79,6 +81,9 @@ export namespace SettingIds { export const enableValidation = 'json.validate.enable'; export const enableSchemaDownload = 'json.schemaDownload.enable'; export const maxItemsComputed = 'json.maxItemsComputed'; + + export const editorSection = 'editor'; + export const foldingMaximumRegions = 'foldingMaximumRegions'; } export interface TelemetryReporter { @@ -104,6 +109,8 @@ export interface SchemaRequestService { export const languageServerDescription = localize('jsonserver.name', 'JSON Language Server'); let resultLimit = 5000; +let jsonFoldingLimit = 5000; +let jsoncFoldingLimit = 5000; export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { @@ -123,10 +130,9 @@ export async function startClient(context: ExtensionContext, newLanguageClient: let isClientReady = false; - const foldingRangeLimitStatusBarItem = createLimitStatusItem((limit: number) => createFoldingRangeLimitItem(documentSelector, SettingIds.maxItemsComputed, limit)); const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit)); const documentColorsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentColorsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit)); - toDispose.push(foldingRangeLimitStatusBarItem, documentSymbolsLimitStatusbarItem, documentColorsLimitStatusbarItem); + toDispose.push(documentSymbolsLimitStatusbarItem, documentColorsLimitStatusbarItem); toDispose.push(commands.registerCommand('json.clearCache', async () => { if (isClientReady && runtime.schemaRequests.clearCache) { @@ -214,20 +220,11 @@ export async function startClient(context: ExtensionContext, newLanguageClient: return updateHover(r); }, provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature) { - function checkLimit(r: FoldingRange[] | null | undefined): FoldingRange[] | null | undefined { - if (Array.isArray(r) && r.length > resultLimit) { - r.length = resultLimit; // truncate - foldingRangeLimitStatusBarItem.update(document, resultLimit); - } else { - foldingRangeLimitStatusBarItem.update(document, false); - } - return r; - } const r = next(document, context, token); if (isThenable(r)) { - return r.then(checkLimit); + return r; } - return checkLimit(r); + return r; }, provideDocumentColors(document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature) { function checkLimit(r: ColorInformation[] | null | undefined): ColorInformation[] | null | undefined { @@ -472,7 +469,11 @@ function getSettings(): Settings { const configuration = workspace.getConfiguration(); const httpSettings = workspace.getConfiguration('http'); - resultLimit = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000; + const normalizeLimit = (settingValue: any) => Math.trunc(Math.max(0, Number(settingValue))) || 5000; + + resultLimit = normalizeLimit(workspace.getConfiguration().get(SettingIds.maxItemsComputed)); + jsonFoldingLimit = normalizeLimit(workspace.getConfiguration(SettingIds.editorSection, { languageId: 'json' }).get(SettingIds.foldingMaximumRegions)); + jsoncFoldingLimit = normalizeLimit(workspace.getConfiguration(SettingIds.editorSection, { languageId: 'jsonc' }).get(SettingIds.foldingMaximumRegions)); const settings: Settings = { http: { @@ -484,7 +485,9 @@ function getSettings(): Settings { format: { enable: configuration.get(SettingIds.enableFormatter) }, keepLines: { enable: configuration.get(SettingIds.enableKeepLines) }, schemas: [], - resultLimit: resultLimit + 1 // ask for one more so we can detect if the limit has been exceeded + resultLimit: resultLimit + 1, // ask for one more so we can detect if the limit has been exceeded + jsonFoldingLimit: jsonFoldingLimit + 1, + jsoncFoldingLimit: jsoncFoldingLimit + 1 } }; const schemaSettingsById: { [schemaId: string]: JSONSchemaSettings } = Object.create(null); diff --git a/extensions/json-language-features/client/src/languageStatus.ts b/extensions/json-language-features/client/src/languageStatus.ts index c9e45894af2..a200da53d29 100644 --- a/extensions/json-language-features/client/src/languageStatus.ts +++ b/extensions/json-language-features/client/src/languageStatus.ts @@ -181,7 +181,7 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque async function updateLanguageStatus() { const document = window.activeTextEditor?.document; - if (document && documentSelector.indexOf(document.languageId) !== -1) { + if (document) { try { statusItem.text = '$(loading~spin)'; statusItem.detail = localize('pending.detail', 'Loading JSON info'); @@ -205,12 +205,12 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque arguments: [{ schemas, uri: document.uri.toString() } as ShowSchemasInput] }; } catch (e) { - statusItem.text = localize('status.error', 'Unable to compute used schemas'); + statusItem.text = localize('status.error1', 'Unable to compute used schemas: {0}', e.message); statusItem.detail = undefined; statusItem.command = undefined; } } else { - statusItem.text = localize('status.notJSON', 'Not a JSON editor'); + statusItem.text = localize('status.error2', 'Unable to compute used schemas: No document'); statusItem.detail = undefined; statusItem.command = undefined; } diff --git a/extensions/json-language-features/server/README.md b/extensions/json-language-features/server/README.md index bc9b22e4ffe..1285255a607 100644 --- a/extensions/json-language-features/server/README.md +++ b/extensions/json-language-features/server/README.md @@ -68,8 +68,9 @@ The server supports the following settings: - `fileMatch`: an array of file names or paths (separated by `/`). `*` can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern. - `url`: The URL of the schema, optional when also a schema is provided. - `schema`: The schema content. - - `resultLimit`: The max number folding ranges and outline symbols to be computed (for performance reasons) - + - `resultLimit`: The max number of color decorators and outline symbols to be computed (for performance reasons) + - `jsonFoldingLimit`: The max number of folding ranges to be computed for json documents (for performance reasons) + - `jsoncFoldingLimit`: The max number of folding ranges to be computed for jsonc documents (for performance reasons) ```json { "http": { @@ -187,7 +188,8 @@ Notification: ### Item Limit -If the setting `resultLimit` is set, the JSON language server will limit the number of folding ranges and document symbols computed. +If the setting `resultLimit` is set, the JSON language server will limit the number of color symbols and document symbols computed. +If the setting `jsonFoldingLimit` or `jsoncFoldingLimit` is set, the JSON language server will limit the number of folding ranges computed. ## Try diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index 05910009b3b..39055e0965b 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -106,8 +106,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let hierarchicalDocumentSymbolSupport = false; let foldingRangeLimitDefault = Number.MAX_VALUE; - let foldingRangeLimit = Number.MAX_VALUE; let resultLimit = Number.MAX_VALUE; + let jsonFoldingRangeLimit = Number.MAX_VALUE; + let jsoncFoldingRangeLimit = Number.MAX_VALUE; let formatterMaxNumberOfEdits = Number.MAX_VALUE; let diagnosticsSupport: DiagnosticsSupport | undefined; @@ -187,6 +188,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) keepLines?: { enable?: boolean }; validate?: { enable?: boolean }; resultLimit?: number; + jsonFoldingLimit?: number; + jsoncFoldingLimit?: number; }; http?: { proxy?: string; @@ -217,8 +220,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) keepLinesEnabled = settings.json?.keepLines?.enable || false; updateConfiguration(); - foldingRangeLimit = Math.trunc(Math.max(settings.json?.resultLimit || foldingRangeLimitDefault, 0)); - resultLimit = Math.trunc(Math.max(settings.json?.resultLimit || Number.MAX_VALUE, 0)); + const sanitizeLimitSetting = (settingValue: any) => Math.trunc(Math.max(settingValue, 0)); + resultLimit = sanitizeLimitSetting(settings.json?.resultLimit || Number.MAX_VALUE); + jsonFoldingRangeLimit = sanitizeLimitSetting(settings.json?.jsonFoldingLimit || foldingRangeLimitDefault); + jsoncFoldingRangeLimit = sanitizeLimitSetting(settings.json?.jsoncFoldingLimit || foldingRangeLimitDefault); // dynamically enable & disable the formatter if (dynamicFormatterRegistration) { @@ -437,7 +442,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return runSafe(runtime, () => { const document = documents.get(params.textDocument.uri); if (document) { - return languageService.getFoldingRanges(document, { rangeLimit: foldingRangeLimit }); + const rangeLimit = document.languageId === 'jsonc' ? jsoncFoldingRangeLimit : jsonFoldingRangeLimit; + return languageService.getFoldingRanges(document, { rangeLimit }); } return null; }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 4f1e00e9223..4b6e24e48cd 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -35,17 +35,16 @@ import { foldingCollapsedIcon, FoldingDecorationProvider, foldingExpandedIcon, f import { FoldingRegion, FoldingRegions, FoldRange, FoldSource, ILineRange } from './foldingRanges'; import { SyntaxRangeProvider } from './syntaxRangeProvider'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import Severity from 'vs/base/common/severity'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { StopWatch } from 'vs/base/common/stopwatch'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; - +import { Emitter, Event } from 'vs/base/common/event'; const CONTEXT_FOLDING_ENABLED = new RawContextKey('foldingEnabled', false); export interface RangeProvider { readonly id: string; - compute(cancelationToken: CancellationToken, notifyTooMany: (max: number) => void): Promise; + compute(cancelationToken: CancellationToken): Promise; dispose(): void; } @@ -56,6 +55,16 @@ interface FoldingStateMemento { foldedImports?: boolean; } +export interface FoldingLimitReporter { + readonly limit: number; + report(limitInfo: FoldingLimitInfo): void; +} + +export interface FoldingLimitInfo { + computed: number; + limited: number | false; +} + export class FoldingController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.folding'; @@ -71,9 +80,7 @@ export class FoldingController extends Disposable implements IEditorContribution private _restoringViewState: boolean; private _foldingImportsByDefault: boolean; private _currentModelHasFoldedImports: boolean; - private _maxFoldingRegions: number; - private _notifyTooManyRegions: (m: number) => void; - private _tooManyRegionsNotified = false; + private _foldingLimitReporter: FoldingLimitReporter; private readonly foldingDecorationProvider: FoldingDecorationProvider; @@ -93,6 +100,14 @@ export class FoldingController extends Disposable implements IEditorContribution private readonly localToDispose = this._register(new DisposableStore()); private mouseDownInfo: { lineNumber: number; iconClicked: boolean } | null; + private _onDidChangeFoldingLimit = new Emitter(); + public readonly onDidChangeFoldingLimit: Event = this._onDidChangeFoldingLimit.event; + + private _foldingLimitInfo: FoldingLimitInfo | undefined; + public get foldingLimitInfo() { + return this._foldingLimitInfo; + } + constructor( editor: ICodeEditor, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -110,7 +125,17 @@ export class FoldingController extends Disposable implements IEditorContribution this._restoringViewState = false; this._currentModelHasFoldedImports = false; this._foldingImportsByDefault = options.get(EditorOption.foldingImportsByDefault); - this._maxFoldingRegions = options.get(EditorOption.foldingMaximumRegions); + this._foldingLimitReporter = { + get limit() { + return editor.getOptions().get(EditorOption.foldingMaximumRegions); + }, + report: (info: FoldingLimitInfo) => { + if (!this._foldingLimitInfo || (info.limited !== this._foldingLimitInfo.limited)) { + this._foldingLimitInfo = info; + this._onDidChangeFoldingLimit.fire(info); + } + } + }; this.updateDebounceInfo = languageFeatureDebounceService.for(languageFeaturesService.foldingRangeProvider, 'Folding', { min: 200 }); this.foldingModel = null; @@ -128,18 +153,6 @@ export class FoldingController extends Disposable implements IEditorContribution this.foldingEnabled = CONTEXT_FOLDING_ENABLED.bindTo(this.contextKeyService); this.foldingEnabled.set(this._isEnabled); - this._notifyTooManyRegions = (maxFoldingRegions: number) => { - // Message will display once per time vscode runs. Once per file would be tricky. - if (!this._tooManyRegionsNotified) { - notificationService.notify({ - severity: Severity.Warning, - sticky: true, - message: nls.localize('maximum fold ranges', "The number of foldable regions is limited to a maximum of {0}. Increase configuration option ['Folding Maximum Regions'](command:workbench.action.openSettings?[\"editor.foldingMaximumRegions\"]) to enable more.", maxFoldingRegions) - }); - this._tooManyRegionsNotified = true; - } - }; - this._register(this.editor.onDidChangeModel(() => this.onModelChanged())); this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { @@ -149,8 +162,6 @@ export class FoldingController extends Disposable implements IEditorContribution this.onModelChanged(); } if (e.hasChanged(EditorOption.foldingMaximumRegions)) { - this._maxFoldingRegions = this.editor.getOptions().get(EditorOption.foldingMaximumRegions); - this._tooManyRegionsNotified = false; this.onModelChanged(); } if (e.hasChanged(EditorOption.showFoldingControls) || e.hasChanged(EditorOption.foldingHighlight)) { @@ -268,17 +279,17 @@ export class FoldingController extends Disposable implements IEditorContribution if (this.rangeProvider) { return this.rangeProvider; } - this.rangeProvider = new IndentRangeProvider(editorModel, this.languageConfigurationService, this._maxFoldingRegions); // fallback + this.rangeProvider = new IndentRangeProvider(editorModel, this.languageConfigurationService, this._foldingLimitReporter); // fallback if (this._useFoldingProviders && this.foldingModel) { const foldingProviders = this.languageFeaturesService.foldingRangeProvider.ordered(this.foldingModel.textModel); if (foldingProviders.length > 0) { - this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders, () => this.triggerFoldingModelChanged(), this._maxFoldingRegions); + this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders, () => this.triggerFoldingModelChanged(), this._foldingLimitReporter); } } return this.rangeProvider; } - public getFoldingModel() { + public getFoldingModel(): Promise | null { return this.foldingModelPromise; } @@ -287,6 +298,7 @@ export class FoldingController extends Disposable implements IEditorContribution this.triggerFoldingModelChanged(); } + public triggerFoldingModelChanged() { if (this.updateScheduler) { if (this.foldingRegionPromise) { @@ -300,7 +312,7 @@ export class FoldingController extends Disposable implements IEditorContribution } const sw = new StopWatch(true); const provider = this.getRangeProvider(foldingModel.textModel); - const foldingRegionPromise = this.foldingRegionPromise = createCancelablePromise(token => provider.compute(token, this._notifyTooManyRegions)); + const foldingRegionPromise = this.foldingRegionPromise = createCancelablePromise(token => provider.compute(token)); return foldingRegionPromise.then(foldingRanges => { if (foldingRanges && foldingRegionPromise === this.foldingRegionPromise) { // new request or cancelled in the meantime? let scrollState: StableEditorScrollState | undefined; diff --git a/src/vs/editor/contrib/folding/browser/indentRangeProvider.ts b/src/vs/editor/contrib/folding/browser/indentRangeProvider.ts index b79c3651a86..77e52059f65 100644 --- a/src/vs/editor/contrib/folding/browser/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/browser/indentRangeProvider.ts @@ -9,7 +9,7 @@ import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { FoldingRegions, MAX_LINE_NUMBER } from 'vs/editor/contrib/folding/browser/foldingRanges'; -import { RangeProvider } from './folding'; +import { FoldingLimitReporter, RangeProvider } from './folding'; const MAX_FOLDING_REGIONS_FOR_INDENT_DEFAULT = 5000; @@ -21,16 +21,16 @@ export class IndentRangeProvider implements RangeProvider { constructor( private readonly editorModel: ITextModel, private readonly languageConfigurationService: ILanguageConfigurationService, - private readonly maxFoldingRegions: number + private readonly foldingRangesLimit: FoldingLimitReporter ) { } dispose() { } - compute(cancelationToken: CancellationToken, notifyTooManyRegions: (maxRegions: number) => void): Promise { + compute(cancelationToken: CancellationToken,): Promise { const foldingRules = this.languageConfigurationService.getLanguageConfiguration(this.editorModel.getLanguageId()).foldingRules; const offSide = foldingRules && !!foldingRules.offSide; const markers = foldingRules && foldingRules.markers; - return Promise.resolve(computeRanges(this.editorModel, offSide, markers, this.maxFoldingRegions, notifyTooManyRegions)); + return Promise.resolve(computeRanges(this.editorModel, offSide, markers, this.foldingRangesLimit)); } } @@ -40,9 +40,9 @@ export class RangesCollector { private readonly _endIndexes: number[]; private readonly _indentOccurrences: number[]; private _length: number; - private readonly _foldingRangesLimit: number; + private readonly _foldingRangesLimit: FoldingLimitReporter; - constructor(foldingRangesLimit: number, private readonly _notifyTooManyRegions?: (maxRegions: number) => void) { + constructor(foldingRangesLimit: FoldingLimitReporter) { this._startIndexes = []; this._endIndexes = []; this._indentOccurrences = []; @@ -64,7 +64,10 @@ export class RangesCollector { } public toIndentRanges(model: ITextModel) { - if (this._length <= this._foldingRangesLimit) { + const limit = this._foldingRangesLimit.limit; + if (this._length <= limit) { + this._foldingRangesLimit.report({ limited: false, computed: this._length }); + // reverse and create arrays of the exact length const startIndexes = new Uint32Array(this._length); const endIndexes = new Uint32Array(this._length); @@ -74,13 +77,14 @@ export class RangesCollector { } return new FoldingRegions(startIndexes, endIndexes); } else { - this._notifyTooManyRegions?.(this._foldingRangesLimit); + this._foldingRangesLimit.report({ limited: limit, computed: this._length }); + let entries = 0; let maxIndent = this._indentOccurrences.length; for (let i = 0; i < this._indentOccurrences.length; i++) { const n = this._indentOccurrences[i]; if (n) { - if (n + entries > this._foldingRangesLimit) { + if (n + entries > limit) { maxIndent = i; break; } @@ -89,13 +93,13 @@ export class RangesCollector { } const tabSize = model.getOptions().tabSize; // reverse and create arrays of the exact length - const startIndexes = new Uint32Array(this._foldingRangesLimit); - const endIndexes = new Uint32Array(this._foldingRangesLimit); + const startIndexes = new Uint32Array(limit); + const endIndexes = new Uint32Array(limit); for (let i = this._length - 1, k = 0; i >= 0; i--) { const startIndex = this._startIndexes[i]; const lineContent = model.getLineContent(startIndex); const indent = computeIndentLevel(lineContent, tabSize); - if (indent < maxIndent || (indent === maxIndent && entries++ < this._foldingRangesLimit)) { + if (indent < maxIndent || (indent === maxIndent && entries++ < limit)) { startIndexes[k] = startIndex; endIndexes[k] = this._endIndexes[i]; k++; @@ -114,10 +118,14 @@ interface PreviousRegion { line: number; // start line of the region. Only used for marker regions. } -export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, foldingRangesLimit?: number, notifyTooManyRegions?: (maxRegions: number) => void): FoldingRegions { +const foldingRangesLimitDefault: FoldingLimitReporter = { + limit: MAX_FOLDING_REGIONS_FOR_INDENT_DEFAULT, + report: () => { } +}; + +export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, foldingRangesLimit: FoldingLimitReporter = foldingRangesLimitDefault): FoldingRegions { const tabSize = model.getOptions().tabSize; - foldingRangesLimit = foldingRangesLimit ?? MAX_FOLDING_REGIONS_FOR_INDENT_DEFAULT; - const result = new RangesCollector(foldingRangesLimit, notifyTooManyRegions); + const result = new RangesCollector(foldingRangesLimit); let pattern: RegExp | undefined = undefined; if (markers) { diff --git a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts index 1e8a0ac06b9..47192b6701f 100644 --- a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts @@ -8,7 +8,7 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; import { FoldingContext, FoldingRange, FoldingRangeProvider } from 'vs/editor/common/languages'; -import { RangeProvider } from './folding'; +import { FoldingLimitReporter, RangeProvider } from './folding'; import { FoldingRegions, MAX_LINE_NUMBER } from './foldingRanges'; export interface IFoldingRangeData extends FoldingRange { @@ -26,7 +26,12 @@ export class SyntaxRangeProvider implements RangeProvider { readonly disposables: DisposableStore | undefined; - constructor(private readonly editorModel: ITextModel, private providers: FoldingRangeProvider[], handleFoldingRangesChange: () => void, private limit: number) { + constructor( + private readonly editorModel: ITextModel, + private readonly providers: FoldingRangeProvider[], + readonly handleFoldingRangesChange: () => void, + private readonly foldingRangesLimit: FoldingLimitReporter + ) { for (const provider of providers) { if (typeof provider.onDidChange === 'function') { if (!this.disposables) { @@ -37,10 +42,10 @@ export class SyntaxRangeProvider implements RangeProvider { } } - compute(cancellationToken: CancellationToken, notifyTooManyRegions?: (maxRegions: number) => void): Promise { + compute(cancellationToken: CancellationToken): Promise { return collectSyntaxRanges(this.providers, this.editorModel, cancellationToken).then(ranges => { if (ranges) { - const res = sanitizeRanges(ranges, this.limit, notifyTooManyRegions); + const res = sanitizeRanges(ranges, this.foldingRangesLimit); return res; } return null; @@ -84,9 +89,9 @@ export class RangesCollector { private readonly _nestingLevelCounts: number[]; private readonly _types: Array; private _length: number; - private readonly _foldingRangesLimit: number; + private readonly _foldingRangesLimit: FoldingLimitReporter; - constructor(foldingRangesLimit: number, private readonly _notifyTooManyRegions?: (maxRegions: number) => void) { + constructor(foldingRangesLimit: FoldingLimitReporter) { this._startIndexes = []; this._endIndexes = []; this._nestingLevels = []; @@ -112,7 +117,10 @@ export class RangesCollector { } public toIndentRanges() { - if (this._length <= this._foldingRangesLimit) { + const limit = this._foldingRangesLimit.limit; + if (this._length <= limit) { + this._foldingRangesLimit.report({ limited: false, computed: this._length }); + const startIndexes = new Uint32Array(this._length); const endIndexes = new Uint32Array(this._length); for (let i = 0; i < this._length; i++) { @@ -121,13 +129,14 @@ export class RangesCollector { } return new FoldingRegions(startIndexes, endIndexes, this._types); } else { - this._notifyTooManyRegions?.(this._foldingRangesLimit); + this._foldingRangesLimit.report({ limited: limit, computed: this._length }); + let entries = 0; let maxLevel = this._nestingLevelCounts.length; for (let i = 0; i < this._nestingLevelCounts.length; i++) { const n = this._nestingLevelCounts[i]; if (n) { - if (n + entries > this._foldingRangesLimit) { + if (n + entries > limit) { maxLevel = i; break; } @@ -135,12 +144,12 @@ export class RangesCollector { } } - const startIndexes = new Uint32Array(this._foldingRangesLimit); - const endIndexes = new Uint32Array(this._foldingRangesLimit); + const startIndexes = new Uint32Array(limit); + const endIndexes = new Uint32Array(limit); const types: Array = []; for (let i = 0, k = 0; i < this._length; i++) { const level = this._nestingLevels[i]; - if (level < maxLevel || (level === maxLevel && entries++ < this._foldingRangesLimit)) { + if (level < maxLevel || (level === maxLevel && entries++ < limit)) { startIndexes[k] = this._startIndexes[i]; endIndexes[k] = this._endIndexes[i]; types[k] = this._types[i]; @@ -154,7 +163,7 @@ export class RangesCollector { } -export function sanitizeRanges(rangeData: IFoldingRangeData[], limit: number, notifyTooManyRegions?: (maxRegions: number) => void): FoldingRegions { +export function sanitizeRanges(rangeData: IFoldingRangeData[], foldingRangesLimit: FoldingLimitReporter): FoldingRegions { const sorted = rangeData.sort((d1, d2) => { let diff = d1.start - d2.start; if (diff === 0) { @@ -162,7 +171,7 @@ export function sanitizeRanges(rangeData: IFoldingRangeData[], limit: number, no } return diff; }); - const collector = new RangesCollector(limit, notifyTooManyRegions); + const collector = new RangesCollector(foldingRangesLimit); let top: IFoldingRangeData | undefined = undefined; const previous: IFoldingRangeData[] = []; diff --git a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts index 9f6a381afbd..734613f4d13 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -42,7 +42,7 @@ suite('FoldingRanges', () => { lines.push('#endregion'); } const model = createTextModel(lines.join('\n')); - const actual = computeRanges(model, false, markers, MAX_FOLDING_REGIONS); + const actual = computeRanges(model, false, markers, { limit: MAX_FOLDING_REGIONS, report: () => { } }); assert.strictEqual(actual.length, nRegions, 'len'); for (let i = 0; i < nRegions; i++) { assert.strictEqual(actual.getStartLineNumber(i), i + 1, 'start' + i); @@ -108,7 +108,7 @@ suite('FoldingRanges', () => { lines.push('#endregion'); } const model = createTextModel(lines.join('\n')); - const actual = computeRanges(model, false, markers, MAX_FOLDING_REGIONS); + const actual = computeRanges(model, false, markers); assert.strictEqual(actual.length, nRegions, 'len'); for (let i = 0; i < nRegions; i++) { actual.setCollapsed(i, i % 3 === 0); diff --git a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts index 67be46f2747..3d37b2bde9f 100644 --- a/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/indentFold.test.ts @@ -50,13 +50,15 @@ suite('Indentation Folding', () => { const model = createTextModel(lines.join('\n')); function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) { - const indentRanges = computeRanges(model, true, undefined, maxEntries); + let reported: number | false = false; + const indentRanges = computeRanges(model, true, undefined, { limit: maxEntries, report: r => reported = r.limited }); assert.ok(indentRanges.length <= maxEntries, 'max ' + message); const actual: IndentRange[] = []; for (let i = 0; i < indentRanges.length; i++) { actual.push({ start: indentRanges.getStartLineNumber(i), end: indentRanges.getEndLineNumber(i) }); } assert.deepStrictEqual(actual, expectedRanges, message); + assert.equal(reported, 9 <= maxEntries ? false : maxEntries, 'limited'); } assertLimit(1000, [r1, r2, r3, r4, r5, r6, r7, r8, r9], '1000'); diff --git a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts index ea6eb31c270..92b29d1a1c4 100644 --- a/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/syntaxFold.test.ts @@ -8,6 +8,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { FoldingContext, FoldingRange, FoldingRangeProvider, ProviderResult } from 'vs/editor/common/languages'; import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/browser/syntaxRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { FoldingLimitReporter } from 'vs/editor/contrib/folding/browser/folding'; interface IndentRange { start: number; @@ -74,14 +75,18 @@ suite('Syntax folding', () => { const providers = [new TestFoldingRangeProvider(model, ranges)]; async function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) { - const indentRanges = await new SyntaxRangeProvider(model, providers, () => { }, maxEntries).compute(CancellationToken.None); + let reported: number | false = false; + const foldingRangesLimit: FoldingLimitReporter = { limit: maxEntries, report: r => reported = r.limited }; + const indentRanges = await new SyntaxRangeProvider(model, providers, () => { }, foldingRangesLimit).compute(CancellationToken.None); const actual: IndentRange[] = []; if (indentRanges) { for (let i = 0; i < indentRanges.length; i++) { actual.push({ start: indentRanges.getStartLineNumber(i), end: indentRanges.getEndLineNumber(i) }); } + assert.equal(reported, 9 <= maxEntries ? false : maxEntries, 'limited'); } assert.deepStrictEqual(actual, expectedRanges, message); + } await assertLimit(1000, [r1, r2, r3, r4, r5, r6, r7, r8, r9], '1000'); diff --git a/src/vs/workbench/contrib/folding/browser/folding.contribution.ts b/src/vs/workbench/contrib/folding/browser/folding.contribution.ts new file mode 100644 index 00000000000..37a568faff4 --- /dev/null +++ b/src/vs/workbench/contrib/folding/browser/folding.contribution.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import Severity from 'vs/base/common/severity'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { FoldingController, FoldingLimitInfo } from 'vs/editor/contrib/folding/browser/folding'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ILanguageStatus, ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; +import * as nls from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +const openSettingsCommand = 'workbench.action.openSettings'; +const configureSettingsLabel = nls.localize('status.button.configure', "Configure"); + +const foldingMaximumRegionsSettingsId = 'editor.foldingMaximumRegions'; + +export class FoldingLimitIndicatorContribution extends Disposable implements IWorkbenchContribution { + + constructor( + @IEditorService private readonly editorService: IEditorService, + @ILanguageStatusService private readonly languageStatusService: ILanguageStatusService + ) { + super(); + + let changeListener: IDisposable | undefined; + let control: any; + + const onActiveEditorChanged = () => { + const activeControl = editorService.activeTextEditorControl; + if (activeControl === control) { + return; + } + control = undefined; + if (changeListener) { + changeListener.dispose(); + changeListener = undefined; + } + const editor = getCodeEditor(activeControl); + if (editor) { + const controller = FoldingController.get(editor); + if (controller) { + const info = controller.foldingLimitInfo; + this.updateLimitInfo(info); + control = activeControl; + changeListener = controller.onDidChangeFoldingLimit(info => { + this.updateLimitInfo(info); + }); + } else { + this.updateLimitInfo(undefined); + } + } else { + this.updateLimitInfo(undefined); + } + }; + + this._register(this.editorService.onDidActiveEditorChange(onActiveEditorChanged)); + + onActiveEditorChanged(); + } + + private _limitStatusItem: IDisposable | undefined; + + private updateLimitInfo(info: FoldingLimitInfo | undefined) { + if (this._limitStatusItem) { + this._limitStatusItem.dispose(); + this._limitStatusItem = undefined; + } + if (info && info.limited !== false) { + const status: ILanguageStatus = { + id: 'foldingLimitInfo', + selector: '*', + name: nls.localize('foldingRangesStatusItem.name', 'Folding Status'), + severity: Severity.Warning, + label: nls.localize('status.limitedFoldingRanges.short', 'Folding Ranges Limited'), + detail: nls.localize('status.limitedFoldingRanges.details', 'only {0} folding ranges shown for performance reasons', info.limited), + command: { id: openSettingsCommand, arguments: [foldingMaximumRegionsSettingsId], title: configureSettingsLabel }, + accessibilityInfo: undefined, + source: nls.localize('foldingRangesStatusItem.source', 'Folding'), + busy: false + }; + this._limitStatusItem = this.languageStatusService.addStatus(status); + } + + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + FoldingLimitIndicatorContribution, + LifecyclePhase.Restored +); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts index 4f8950963cf..1c769a63697 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts @@ -6,6 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; +import { FoldingLimitReporter } from 'vs/editor/contrib/folding/browser/folding'; import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/browser/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/browser/syntaxRangeProvider'; import { INotebookViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -15,6 +16,10 @@ import { cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/c type RegionFilter = (r: FoldingRegion) => boolean; type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; +const foldingRangeLimit: FoldingLimitReporter = { + limit: 5000, + report: () => { } +}; export class FoldingModel implements IDisposable { private _viewModel: INotebookViewModel | null = null; @@ -200,7 +205,7 @@ export class FoldingModel implements IDisposable { }; }).filter(range => range.start !== range.end); - const newRegions = sanitizeRanges(rawFoldingRanges, 5000); + const newRegions = sanitizeRanges(rawFoldingRanges, foldingRangeLimit); // restore collased state let i = 0; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 01588f36305..1d03361f7cb 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -270,6 +270,9 @@ import 'vs/workbench/contrib/snippets/browser/snippets.contribution'; // Formatter Help import 'vs/workbench/contrib/format/browser/format.contribution'; +// Folding Limit Indicator +import 'vs/workbench/contrib/folding/browser/folding.contribution'; + // Inlay Hint Accessibility import 'vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty';