Path: blob/main/src/vs/editor/contrib/folding/browser/folding.ts
5262 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 { CancelablePromise, createCancelablePromise, Delayer, RunOnceScheduler } from '../../../../base/common/async.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { illegalArgument, onUnexpectedError } from '../../../../base/common/errors.js';8import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';9import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';10import { escapeRegExpCharacters } from '../../../../base/common/strings.js';11import * as types from '../../../../base/common/types.js';12import './folding.css';13import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js';14import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from '../../../browser/editorBrowser.js';15import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, registerInstantiatedEditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js';16import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js';17import { IPosition } from '../../../common/core/position.js';18import { IRange } from '../../../common/core/range.js';19import { Selection } from '../../../common/core/selection.js';20import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';21import { EditorContextKeys } from '../../../common/editorContextKeys.js';22import { ITextModel } from '../../../common/model.js';23import { IModelContentChangedEvent } from '../../../common/textModelEvents.js';24import { FoldingRange, FoldingRangeKind, FoldingRangeProvider } from '../../../common/languages.js';25import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';26import { CollapseMemento, FoldingModel, getNextFoldLine, getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateForType, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp, toggleCollapseState } from './foldingModel.js';27import { HiddenRangeModel } from './hiddenRangeModel.js';28import { IndentRangeProvider } from './indentRangeProvider.js';29import * as nls from '../../../../nls.js';30import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';31import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';32import { FoldingDecorationProvider } from './foldingDecorations.js';33import { FoldingRegion, FoldingRegions, FoldRange, FoldSource, ILineRange } from './foldingRanges.js';34import { SyntaxRangeProvider } from './syntaxRangeProvider.js';35import { INotificationService } from '../../../../platform/notification/common/notification.js';36import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';37import { StopWatch } from '../../../../base/common/stopwatch.js';38import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';39import { Emitter, Event } from '../../../../base/common/event.js';40import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';41import { URI } from '../../../../base/common/uri.js';42import { IModelService } from '../../../common/services/model.js';43import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';4445const CONTEXT_FOLDING_ENABLED = new RawContextKey<boolean>('foldingEnabled', false);4647export interface RangeProvider {48readonly id: string;49compute(cancelationToken: CancellationToken): Promise<FoldingRegions | null>;50dispose(): void;51}5253interface FoldingStateMemento {54collapsedRegions?: CollapseMemento;55lineCount?: number;56provider?: string;57foldedImports?: boolean;58}5960export interface FoldingLimitReporter {61readonly limit: number;62update(computed: number, limited: number | false): void;63}6465export type FoldingRangeProviderSelector = (provider: FoldingRangeProvider[], document: ITextModel) => FoldingRangeProvider[] | undefined;6667export class FoldingController extends Disposable implements IEditorContribution {6869public static readonly ID = 'editor.contrib.folding';7071public static get(editor: ICodeEditor): FoldingController | null {72return editor.getContribution<FoldingController>(FoldingController.ID);73}7475private static _foldingRangeSelector: FoldingRangeProviderSelector | undefined;7677public static getFoldingRangeProviders(languageFeaturesService: ILanguageFeaturesService, model: ITextModel): FoldingRangeProvider[] {78const foldingRangeProviders = languageFeaturesService.foldingRangeProvider.ordered(model);79return (FoldingController._foldingRangeSelector?.(foldingRangeProviders, model)) ?? foldingRangeProviders;80}8182public static setFoldingRangeProviderSelector(foldingRangeSelector: FoldingRangeProviderSelector): IDisposable {83FoldingController._foldingRangeSelector = foldingRangeSelector;84return { dispose: () => { FoldingController._foldingRangeSelector = undefined; } };85}8687private readonly editor: ICodeEditor;88private _isEnabled: boolean;89private _useFoldingProviders: boolean;90private _unfoldOnClickAfterEndOfLine: boolean;91private _restoringViewState: boolean;92private _foldingImportsByDefault: boolean;93private _currentModelHasFoldedImports: boolean;9495private readonly foldingDecorationProvider: FoldingDecorationProvider;9697private foldingModel: FoldingModel | null;98private hiddenRangeModel: HiddenRangeModel | null;99100private rangeProvider: RangeProvider | null;101private foldingRegionPromise: CancelablePromise<FoldingRegions | null> | null;102103private foldingModelPromise: Promise<FoldingModel | null> | null;104private updateScheduler: Delayer<FoldingModel | null> | null;105private readonly updateDebounceInfo: IFeatureDebounceInformation;106107private foldingEnabled: IContextKey<boolean>;108private cursorChangedScheduler: RunOnceScheduler | null;109110private readonly localToDispose = this._register(new DisposableStore());111private mouseDownInfo: { lineNumber: number; iconClicked: boolean } | null;112113public readonly _foldingLimitReporter: RangesLimitReporter;114115constructor(116editor: ICodeEditor,117@IContextKeyService private readonly contextKeyService: IContextKeyService,118@ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService,119@INotificationService notificationService: INotificationService,120@ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService,121@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,122) {123super();124this.editor = editor;125126this._foldingLimitReporter = this._register(new RangesLimitReporter(editor));127128const options = this.editor.getOptions();129this._isEnabled = options.get(EditorOption.folding);130this._useFoldingProviders = options.get(EditorOption.foldingStrategy) !== 'indentation';131this._unfoldOnClickAfterEndOfLine = options.get(EditorOption.unfoldOnClickAfterEndOfLine);132this._restoringViewState = false;133this._currentModelHasFoldedImports = false;134this._foldingImportsByDefault = options.get(EditorOption.foldingImportsByDefault);135this.updateDebounceInfo = languageFeatureDebounceService.for(languageFeaturesService.foldingRangeProvider, 'Folding', { min: 200 });136137this.foldingModel = null;138this.hiddenRangeModel = null;139this.rangeProvider = null;140this.foldingRegionPromise = null;141this.foldingModelPromise = null;142this.updateScheduler = null;143this.cursorChangedScheduler = null;144this.mouseDownInfo = null;145146this.foldingDecorationProvider = new FoldingDecorationProvider(editor);147this.foldingDecorationProvider.showFoldingControls = options.get(EditorOption.showFoldingControls);148this.foldingDecorationProvider.showFoldingHighlights = options.get(EditorOption.foldingHighlight);149this.foldingEnabled = CONTEXT_FOLDING_ENABLED.bindTo(this.contextKeyService);150this.foldingEnabled.set(this._isEnabled);151152this._register(this.editor.onDidChangeModel(() => this.onModelChanged()));153154this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {155if (e.hasChanged(EditorOption.folding)) {156this._isEnabled = this.editor.getOptions().get(EditorOption.folding);157this.foldingEnabled.set(this._isEnabled);158this.onModelChanged();159}160if (e.hasChanged(EditorOption.foldingMaximumRegions)) {161this.onModelChanged();162}163if (e.hasChanged(EditorOption.showFoldingControls) || e.hasChanged(EditorOption.foldingHighlight)) {164const options = this.editor.getOptions();165this.foldingDecorationProvider.showFoldingControls = options.get(EditorOption.showFoldingControls);166this.foldingDecorationProvider.showFoldingHighlights = options.get(EditorOption.foldingHighlight);167this.triggerFoldingModelChanged();168}169if (e.hasChanged(EditorOption.foldingStrategy)) {170this._useFoldingProviders = this.editor.getOptions().get(EditorOption.foldingStrategy) !== 'indentation';171this.onFoldingStrategyChanged();172}173if (e.hasChanged(EditorOption.unfoldOnClickAfterEndOfLine)) {174this._unfoldOnClickAfterEndOfLine = this.editor.getOptions().get(EditorOption.unfoldOnClickAfterEndOfLine);175}176if (e.hasChanged(EditorOption.foldingImportsByDefault)) {177this._foldingImportsByDefault = this.editor.getOptions().get(EditorOption.foldingImportsByDefault);178}179}));180this.onModelChanged();181}182183public get limitReporter() {184return this._foldingLimitReporter;185}186187/**188* Store view state.189*/190public saveViewState(): FoldingStateMemento | undefined {191const model = this.editor.getModel();192if (!model || !this._isEnabled || model.isTooLargeForTokenization()) {193return {};194}195if (this.foldingModel) { // disposed ?196const collapsedRegions = this.foldingModel.getMemento();197const provider = this.rangeProvider ? this.rangeProvider.id : undefined;198return { collapsedRegions, lineCount: model.getLineCount(), provider, foldedImports: this._currentModelHasFoldedImports };199}200return undefined;201}202203/**204* Restore view state.205*/206public restoreViewState(state: FoldingStateMemento): void {207const model = this.editor.getModel();208if (!model || !this._isEnabled || model.isTooLargeForTokenization() || !this.hiddenRangeModel) {209return;210}211if (!state) {212return;213}214215this._currentModelHasFoldedImports = !!state.foldedImports;216if (state.collapsedRegions && state.collapsedRegions.length > 0 && this.foldingModel) {217this._restoringViewState = true;218try {219this.foldingModel.applyMemento(state.collapsedRegions);220} finally {221this._restoringViewState = false;222}223}224}225226private onModelChanged(): void {227this.localToDispose.clear();228229const model = this.editor.getModel();230if (!this._isEnabled || !model || model.isTooLargeForTokenization()) {231// huge files get no view model, so they cannot support hidden areas232return;233}234235this._currentModelHasFoldedImports = false;236this.foldingModel = new FoldingModel(model, this.foldingDecorationProvider);237this.localToDispose.add(this.foldingModel);238239this.hiddenRangeModel = new HiddenRangeModel(this.foldingModel);240this.localToDispose.add(this.hiddenRangeModel);241this.localToDispose.add(this.hiddenRangeModel.onDidChange(hr => this.onHiddenRangesChanges(hr)));242243this.updateScheduler = new Delayer<FoldingModel>(this.updateDebounceInfo.get(model));244this.localToDispose.add(this.updateScheduler);245246this.cursorChangedScheduler = new RunOnceScheduler(() => this.revealCursor(), 200);247this.localToDispose.add(this.cursorChangedScheduler);248this.localToDispose.add(this.languageFeaturesService.foldingRangeProvider.onDidChange(() => this.onFoldingStrategyChanged()));249this.localToDispose.add(this.editor.onDidChangeModelLanguageConfiguration(() => this.onFoldingStrategyChanged())); // covers model language changes as well250this.localToDispose.add(this.editor.onDidChangeModelContent(e => this.onDidChangeModelContent(e)));251this.localToDispose.add(this.editor.onDidChangeCursorPosition(() => this.onCursorPositionChanged()));252this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));253this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));254this.localToDispose.add({255dispose: () => {256if (this.foldingRegionPromise) {257this.foldingRegionPromise.cancel();258this.foldingRegionPromise = null;259}260this.updateScheduler?.cancel();261this.updateScheduler = null;262this.foldingModel = null;263this.foldingModelPromise = null;264this.hiddenRangeModel = null;265this.cursorChangedScheduler = null;266this.rangeProvider?.dispose();267this.rangeProvider = null;268}269});270this.triggerFoldingModelChanged();271}272273private onFoldingStrategyChanged() {274this.rangeProvider?.dispose();275this.rangeProvider = null;276this.triggerFoldingModelChanged();277}278279private getRangeProvider(editorModel: ITextModel): RangeProvider {280if (this.rangeProvider) {281return this.rangeProvider;282}283const indentRangeProvider = new IndentRangeProvider(editorModel, this.languageConfigurationService, this._foldingLimitReporter);284this.rangeProvider = indentRangeProvider; // fallback285if (this._useFoldingProviders && this.foldingModel) {286const selectedProviders = FoldingController.getFoldingRangeProviders(this.languageFeaturesService, editorModel);287if (selectedProviders.length > 0) {288this.rangeProvider = new SyntaxRangeProvider(editorModel, selectedProviders, () => this.triggerFoldingModelChanged(), this._foldingLimitReporter, indentRangeProvider);289}290}291return this.rangeProvider;292}293294public getFoldingModel(): Promise<FoldingModel | null> | null {295return this.foldingModelPromise;296}297298private onDidChangeModelContent(e: IModelContentChangedEvent) {299this.hiddenRangeModel?.notifyChangeModelContent(e);300this.triggerFoldingModelChanged();301}302303304public triggerFoldingModelChanged() {305if (this.updateScheduler) {306if (this.foldingRegionPromise) {307this.foldingRegionPromise.cancel();308this.foldingRegionPromise = null;309}310this.foldingModelPromise = this.updateScheduler.trigger(() => {311const foldingModel = this.foldingModel;312if (!foldingModel) { // null if editor has been disposed, or folding turned off313return null;314}315const sw = new StopWatch();316const provider = this.getRangeProvider(foldingModel.textModel);317const foldingRegionPromise = this.foldingRegionPromise = createCancelablePromise(token => provider.compute(token));318return foldingRegionPromise.then(foldingRanges => {319if (foldingRanges && foldingRegionPromise === this.foldingRegionPromise) { // new request or cancelled in the meantime?320let scrollState: StableEditorScrollState | undefined;321322if (this._foldingImportsByDefault && !this._currentModelHasFoldedImports) {323const hasChanges = foldingRanges.setCollapsedAllOfType(FoldingRangeKind.Imports.value, true);324if (hasChanges) {325scrollState = StableEditorScrollState.capture(this.editor);326this._currentModelHasFoldedImports = hasChanges;327}328}329330// some cursors might have moved into hidden regions, make sure they are in expanded regions331const selections = this.editor.getSelections();332foldingModel.update(foldingRanges, toSelectedLines(selections));333334scrollState?.restore(this.editor);335336// update debounce info337const newValue = this.updateDebounceInfo.update(foldingModel.textModel, sw.elapsed());338if (this.updateScheduler) {339this.updateScheduler.defaultDelay = newValue;340}341}342return foldingModel;343});344}).then(undefined, (err) => {345onUnexpectedError(err);346return null;347});348}349}350351private onHiddenRangesChanges(hiddenRanges: IRange[]) {352if (this.hiddenRangeModel && hiddenRanges.length && !this._restoringViewState) {353const selections = this.editor.getSelections();354if (selections) {355if (this.hiddenRangeModel.adjustSelections(selections)) {356this.editor.setSelections(selections);357}358}359}360this.editor.setHiddenAreas(hiddenRanges, this);361}362363private onCursorPositionChanged() {364if (this.hiddenRangeModel && this.hiddenRangeModel.hasRanges()) {365this.cursorChangedScheduler!.schedule();366}367}368369private revealCursor() {370const foldingModel = this.getFoldingModel();371if (!foldingModel) {372return;373}374foldingModel.then(foldingModel => { // null is returned if folding got disabled in the meantime375if (foldingModel) {376const selections = this.editor.getSelections();377if (selections && selections.length > 0) {378const toToggle: FoldingRegion[] = [];379for (const selection of selections) {380const lineNumber = selection.selectionStartLineNumber;381if (this.hiddenRangeModel && this.hiddenRangeModel.isHidden(lineNumber)) {382toToggle.push(...foldingModel.getAllRegionsAtLine(lineNumber, r => r.isCollapsed && lineNumber > r.startLineNumber));383}384}385if (toToggle.length) {386foldingModel.toggleCollapseState(toToggle);387this.reveal(selections[0].getPosition());388}389}390}391}).then(undefined, onUnexpectedError);392393}394395private onEditorMouseDown(e: IEditorMouseEvent): void {396this.mouseDownInfo = null;397398399if (!this.hiddenRangeModel || !e.target || !e.target.range) {400return;401}402if (!e.event.leftButton && !e.event.middleButton) {403return;404}405const range = e.target.range;406let iconClicked = false;407switch (e.target.type) {408case MouseTargetType.GUTTER_LINE_DECORATIONS: {409const data = e.target.detail;410const offsetLeftInGutter = e.target.element!.offsetLeft;411const gutterOffsetX = data.offsetX - offsetLeftInGutter;412413// const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;414415// TODO@joao TODO@alex TODO@martin this is such that we don't collide with dirty diff416if (gutterOffsetX < 4) { // the whitespace between the border and the real folding icon border is 4px417return;418}419420iconClicked = true;421break;422}423case MouseTargetType.CONTENT_EMPTY: {424if (this._unfoldOnClickAfterEndOfLine && this.hiddenRangeModel.hasRanges()) {425const data = e.target.detail;426if (!data.isAfterLines) {427break;428}429}430return;431}432case MouseTargetType.CONTENT_TEXT: {433if (this.hiddenRangeModel.hasRanges()) {434const model = this.editor.getModel();435if (model && range.startColumn === model.getLineMaxColumn(range.startLineNumber)) {436break;437}438}439return;440}441default:442return;443}444445this.mouseDownInfo = { lineNumber: range.startLineNumber, iconClicked };446}447448private onEditorMouseUp(e: IEditorMouseEvent): void {449const foldingModel = this.foldingModel;450if (!foldingModel || !this.mouseDownInfo || !e.target) {451return;452}453const lineNumber = this.mouseDownInfo.lineNumber;454const iconClicked = this.mouseDownInfo.iconClicked;455456const range = e.target.range;457if (!range || range.startLineNumber !== lineNumber) {458return;459}460461if (iconClicked) {462if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {463return;464}465} else {466const model = this.editor.getModel();467if (!model || range.startColumn !== model.getLineMaxColumn(lineNumber)) {468return;469}470}471472const region = foldingModel.getRegionAtLine(lineNumber);473if (region && region.startLineNumber === lineNumber) {474const isCollapsed = region.isCollapsed;475if (iconClicked || isCollapsed) {476const surrounding = e.event.altKey;477let toToggle = [];478if (surrounding) {479const filter = (otherRegion: FoldingRegion) => !otherRegion.containedBy(region) && !region.containedBy(otherRegion);480const toMaybeToggle = foldingModel.getRegionsInside(null, filter);481for (const r of toMaybeToggle) {482if (r.isCollapsed) {483toToggle.push(r);484}485}486// if any surrounding regions are folded, unfold those. Otherwise, fold all surrounding487if (toToggle.length === 0) {488toToggle = toMaybeToggle;489}490}491else {492const recursive = e.event.middleButton || e.event.shiftKey;493if (recursive) {494for (const r of foldingModel.getRegionsInside(region)) {495if (r.isCollapsed === isCollapsed) {496toToggle.push(r);497}498}499}500// when recursive, first only collapse all children. If all are already folded or there are no children, also fold parent.501if (isCollapsed || !recursive || toToggle.length === 0) {502toToggle.push(region);503}504}505foldingModel.toggleCollapseState(toToggle);506this.reveal({ lineNumber, column: 1 });507}508}509}510511public reveal(position: IPosition): void {512this.editor.revealPositionInCenterIfOutsideViewport(position, ScrollType.Smooth);513}514}515516export class RangesLimitReporter extends Disposable implements FoldingLimitReporter {517constructor(private readonly editor: ICodeEditor) {518super();519}520521public get limit() {522return this.editor.getOptions().get(EditorOption.foldingMaximumRegions);523}524525private _onDidChange = this._register(new Emitter<void>());526public get onDidChange(): Event<void> { return this._onDidChange.event; }527528private _computed: number = 0;529private _limited: number | false = false;530public get computed(): number {531return this._computed;532}533public get limited(): number | false {534return this._limited;535}536public update(computed: number, limited: number | false) {537if (computed !== this._computed || limited !== this._limited) {538this._computed = computed;539this._limited = limited;540this._onDidChange.fire();541}542}543}544545abstract class FoldingAction<T> extends EditorAction {546547abstract invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: T, languageConfigurationService: ILanguageConfigurationService): void;548549public override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: T): void | Promise<void> {550const languageConfigurationService = accessor.get(ILanguageConfigurationService);551const foldingController = FoldingController.get(editor);552if (!foldingController) {553return;554}555const foldingModelPromise = foldingController.getFoldingModel();556if (foldingModelPromise) {557this.reportTelemetry(accessor, editor);558return foldingModelPromise.then(foldingModel => {559if (foldingModel) {560this.invoke(foldingController, foldingModel, editor, args, languageConfigurationService);561const selection = editor.getSelection();562if (selection) {563foldingController.reveal(selection.getStartPosition());564}565}566});567}568}569570protected getSelectedLines(editor: ICodeEditor) {571const selections = editor.getSelections();572return selections ? selections.map(s => s.startLineNumber) : [];573}574575protected getLineNumbers(args: FoldingArguments, editor: ICodeEditor) {576if (args && args.selectionLines) {577return args.selectionLines.map(l => l + 1); // to 0-bases line numbers578}579return this.getSelectedLines(editor);580}581582public run(_accessor: ServicesAccessor, _editor: ICodeEditor): void {583}584}585586export interface SelectedLines {587startsInside(startLine: number, endLine: number): boolean;588}589590export function toSelectedLines(selections: Selection[] | null): SelectedLines {591if (!selections || selections.length === 0) {592return {593startsInside: () => false594};595}596return {597startsInside(startLine: number, endLine: number): boolean {598for (const s of selections) {599const line = s.startLineNumber;600if (line >= startLine && line <= endLine) {601return true;602}603}604return false;605}606};607}608609interface FoldingArguments {610levels?: number;611direction?: 'up' | 'down';612selectionLines?: number[];613}614615function foldingArgumentsConstraint(args: unknown) {616if (!types.isUndefined(args)) {617if (!types.isObject(args)) {618return false;619}620const foldingArgs: FoldingArguments = args;621if (!types.isUndefined(foldingArgs.levels) && !types.isNumber(foldingArgs.levels)) {622return false;623}624if (!types.isUndefined(foldingArgs.direction) && !types.isString(foldingArgs.direction)) {625return false;626}627if (!types.isUndefined(foldingArgs.selectionLines) && (!Array.isArray(foldingArgs.selectionLines) || !foldingArgs.selectionLines.every(types.isNumber))) {628return false;629}630}631return true;632}633634class UnfoldAction extends FoldingAction<FoldingArguments> {635636constructor() {637super({638id: 'editor.unfold',639label: nls.localize2('unfoldAction.label', "Unfold"),640precondition: CONTEXT_FOLDING_ENABLED,641kbOpts: {642kbExpr: EditorContextKeys.editorTextFocus,643primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketRight,644mac: {645primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketRight646},647weight: KeybindingWeight.EditorContrib648},649metadata: {650description: 'Unfold the content in the editor',651args: [652{653name: 'Unfold editor argument',654description: `Property-value pairs that can be passed through this argument:655* 'levels': Number of levels to unfold. If not set, defaults to 1.656* 'direction': If 'up', unfold given number of levels up otherwise unfolds down.657* 'selectionLines': Array of the start lines (0-based) of the editor selections to apply the unfold action to. If not set, the active selection(s) will be used.658`,659constraint: foldingArgumentsConstraint,660schema: {661'type': 'object',662'properties': {663'levels': {664'type': 'number',665'default': 1666},667'direction': {668'type': 'string',669'enum': ['up', 'down'],670'default': 'down'671},672'selectionLines': {673'type': 'array',674'items': {675'type': 'number'676}677}678}679}680}681]682}683});684}685686invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: FoldingArguments): void {687const levels = args && args.levels || 1;688const lineNumbers = this.getLineNumbers(args, editor);689if (args && args.direction === 'up') {690setCollapseStateLevelsUp(foldingModel, false, levels, lineNumbers);691} else {692setCollapseStateLevelsDown(foldingModel, false, levels, lineNumbers);693}694}695}696697class UnFoldRecursivelyAction extends FoldingAction<void> {698699constructor() {700super({701id: 'editor.unfoldRecursively',702label: nls.localize2('unFoldRecursivelyAction.label', "Unfold Recursively"),703precondition: CONTEXT_FOLDING_ENABLED,704kbOpts: {705kbExpr: EditorContextKeys.editorTextFocus,706primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.BracketRight),707weight: KeybindingWeight.EditorContrib708}709});710}711712invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, _args: unknown): void {713setCollapseStateLevelsDown(foldingModel, false, Number.MAX_VALUE, this.getSelectedLines(editor));714}715}716717class FoldAction extends FoldingAction<FoldingArguments> {718719constructor() {720super({721id: 'editor.fold',722label: nls.localize2('foldAction.label', "Fold"),723precondition: CONTEXT_FOLDING_ENABLED,724kbOpts: {725kbExpr: EditorContextKeys.editorTextFocus,726primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketLeft,727mac: {728primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketLeft729},730weight: KeybindingWeight.EditorContrib731},732metadata: {733description: 'Fold the content in the editor',734args: [735{736name: 'Fold editor argument',737description: `Property-value pairs that can be passed through this argument:738* 'levels': Number of levels to fold.739* 'direction': If 'up', folds given number of levels up otherwise folds down.740* 'selectionLines': Array of the start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used.741If no levels or direction is set, folds the region at the locations or if already collapsed, the first uncollapsed parent instead.742`,743constraint: foldingArgumentsConstraint,744schema: {745'type': 'object',746'properties': {747'levels': {748'type': 'number',749},750'direction': {751'type': 'string',752'enum': ['up', 'down'],753},754'selectionLines': {755'type': 'array',756'items': {757'type': 'number'758}759}760}761}762}763]764}765});766}767768invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: FoldingArguments): void {769const lineNumbers = this.getLineNumbers(args, editor);770771const levels = args && args.levels;772const direction = args && args.direction;773774if (typeof levels !== 'number' && typeof direction !== 'string') {775// fold the region at the location or if already collapsed, the first uncollapsed parent instead.776setCollapseStateUp(foldingModel, true, lineNumbers);777} else {778if (direction === 'up') {779setCollapseStateLevelsUp(foldingModel, true, levels || 1, lineNumbers);780} else {781setCollapseStateLevelsDown(foldingModel, true, levels || 1, lineNumbers);782}783}784}785}786787788class ToggleFoldAction extends FoldingAction<void> {789790constructor() {791super({792id: 'editor.toggleFold',793label: nls.localize2('toggleFoldAction.label', "Toggle Fold"),794precondition: CONTEXT_FOLDING_ENABLED,795kbOpts: {796kbExpr: EditorContextKeys.editorTextFocus,797primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyL),798weight: KeybindingWeight.EditorContrib799}800});801}802803invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {804const selectedLines = this.getSelectedLines(editor);805toggleCollapseState(foldingModel, 1, selectedLines);806}807}808809810class FoldRecursivelyAction extends FoldingAction<void> {811812constructor() {813super({814id: 'editor.foldRecursively',815label: nls.localize2('foldRecursivelyAction.label', "Fold Recursively"),816precondition: CONTEXT_FOLDING_ENABLED,817kbOpts: {818kbExpr: EditorContextKeys.editorTextFocus,819primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.BracketLeft),820weight: KeybindingWeight.EditorContrib821}822});823}824825invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {826const selectedLines = this.getSelectedLines(editor);827setCollapseStateLevelsDown(foldingModel, true, Number.MAX_VALUE, selectedLines);828}829}830831832class ToggleFoldRecursivelyAction extends FoldingAction<void> {833834constructor() {835super({836id: 'editor.toggleFoldRecursively',837label: nls.localize2('toggleFoldRecursivelyAction.label', "Toggle Fold Recursively"),838precondition: CONTEXT_FOLDING_ENABLED,839kbOpts: {840kbExpr: EditorContextKeys.editorTextFocus,841primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL),842weight: KeybindingWeight.EditorContrib843}844});845}846847invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {848const selectedLines = this.getSelectedLines(editor);849toggleCollapseState(foldingModel, Number.MAX_VALUE, selectedLines);850}851}852853854class FoldAllBlockCommentsAction extends FoldingAction<void> {855856constructor() {857super({858id: 'editor.foldAllBlockComments',859label: nls.localize2('foldAllBlockComments.label', "Fold All Block Comments"),860precondition: CONTEXT_FOLDING_ENABLED,861kbOpts: {862kbExpr: EditorContextKeys.editorTextFocus,863primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Slash),864weight: KeybindingWeight.EditorContrib865}866});867}868869invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: void, languageConfigurationService: ILanguageConfigurationService): void {870if (foldingModel.regions.hasTypes()) {871setCollapseStateForType(foldingModel, FoldingRangeKind.Comment.value, true);872} else {873const editorModel = editor.getModel();874if (!editorModel) {875return;876}877const comments = languageConfigurationService.getLanguageConfiguration(editorModel.getLanguageId()).comments;878if (comments && comments.blockCommentStartToken) {879const regExp = new RegExp('^\\s*' + escapeRegExpCharacters(comments.blockCommentStartToken));880setCollapseStateForMatchingLines(foldingModel, regExp, true);881}882}883}884}885886class FoldAllRegionsAction extends FoldingAction<void> {887888constructor() {889super({890id: 'editor.foldAllMarkerRegions',891label: nls.localize2('foldAllMarkerRegions.label', "Fold All Regions"),892precondition: CONTEXT_FOLDING_ENABLED,893kbOpts: {894kbExpr: EditorContextKeys.editorTextFocus,895primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Digit8),896weight: KeybindingWeight.EditorContrib897}898});899}900901invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: void, languageConfigurationService: ILanguageConfigurationService): void {902if (foldingModel.regions.hasTypes()) {903setCollapseStateForType(foldingModel, FoldingRangeKind.Region.value, true);904} else {905const editorModel = editor.getModel();906if (!editorModel) {907return;908}909const foldingRules = languageConfigurationService.getLanguageConfiguration(editorModel.getLanguageId()).foldingRules;910if (foldingRules && foldingRules.markers && foldingRules.markers.start) {911const regExp = new RegExp(foldingRules.markers.start);912setCollapseStateForMatchingLines(foldingModel, regExp, true);913}914}915}916}917918class UnfoldAllRegionsAction extends FoldingAction<void> {919920constructor() {921super({922id: 'editor.unfoldAllMarkerRegions',923label: nls.localize2('unfoldAllMarkerRegions.label', "Unfold All Regions"),924precondition: CONTEXT_FOLDING_ENABLED,925kbOpts: {926kbExpr: EditorContextKeys.editorTextFocus,927primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Digit9),928weight: KeybindingWeight.EditorContrib929}930});931}932933invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: void, languageConfigurationService: ILanguageConfigurationService): void {934if (foldingModel.regions.hasTypes()) {935setCollapseStateForType(foldingModel, FoldingRangeKind.Region.value, false);936} else {937const editorModel = editor.getModel();938if (!editorModel) {939return;940}941const foldingRules = languageConfigurationService.getLanguageConfiguration(editorModel.getLanguageId()).foldingRules;942if (foldingRules && foldingRules.markers && foldingRules.markers.start) {943const regExp = new RegExp(foldingRules.markers.start);944setCollapseStateForMatchingLines(foldingModel, regExp, false);945}946}947}948}949950class FoldAllExceptAction extends FoldingAction<void> {951952constructor() {953super({954id: 'editor.foldAllExcept',955label: nls.localize2('foldAllExcept.label', "Fold All Except Selected"),956precondition: CONTEXT_FOLDING_ENABLED,957kbOpts: {958kbExpr: EditorContextKeys.editorTextFocus,959primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Minus),960weight: KeybindingWeight.EditorContrib961}962});963}964965invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {966const selectedLines = this.getSelectedLines(editor);967setCollapseStateForRest(foldingModel, true, selectedLines);968}969970}971972class UnfoldAllExceptAction extends FoldingAction<void> {973974constructor() {975super({976id: 'editor.unfoldAllExcept',977label: nls.localize2('unfoldAllExcept.label', "Unfold All Except Selected"),978precondition: CONTEXT_FOLDING_ENABLED,979kbOpts: {980kbExpr: EditorContextKeys.editorTextFocus,981primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Equal),982weight: KeybindingWeight.EditorContrib983}984});985}986987invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {988const selectedLines = this.getSelectedLines(editor);989setCollapseStateForRest(foldingModel, false, selectedLines);990}991}992993class FoldAllAction extends FoldingAction<void> {994995constructor() {996super({997id: 'editor.foldAll',998label: nls.localize2('foldAllAction.label', "Fold All"),999precondition: CONTEXT_FOLDING_ENABLED,1000kbOpts: {1001kbExpr: EditorContextKeys.editorTextFocus,1002primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Digit0),1003weight: KeybindingWeight.EditorContrib1004}1005});1006}10071008invoke(_foldingController: FoldingController, foldingModel: FoldingModel, _editor: ICodeEditor): void {1009setCollapseStateLevelsDown(foldingModel, true);1010}1011}10121013class UnfoldAllAction extends FoldingAction<void> {10141015constructor() {1016super({1017id: 'editor.unfoldAll',1018label: nls.localize2('unfoldAllAction.label', "Unfold All"),1019precondition: CONTEXT_FOLDING_ENABLED,1020kbOpts: {1021kbExpr: EditorContextKeys.editorTextFocus,1022primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyJ),1023weight: KeybindingWeight.EditorContrib1024}1025});1026}10271028invoke(_foldingController: FoldingController, foldingModel: FoldingModel, _editor: ICodeEditor): void {1029setCollapseStateLevelsDown(foldingModel, false);1030}1031}10321033class FoldLevelAction extends FoldingAction<void> {1034private static readonly ID_PREFIX = 'editor.foldLevel';1035public static readonly ID = (level: number) => FoldLevelAction.ID_PREFIX + level;10361037private getFoldingLevel() {1038return parseInt(this.id.substr(FoldLevelAction.ID_PREFIX.length));1039}10401041invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {1042setCollapseStateAtLevel(foldingModel, this.getFoldingLevel(), true, this.getSelectedLines(editor));1043}1044}10451046/** Action to go to the parent fold of current line */1047class GotoParentFoldAction extends FoldingAction<void> {1048constructor() {1049super({1050id: 'editor.gotoParentFold',1051label: nls.localize2('gotoParentFold.label', "Go to Parent Fold"),1052precondition: CONTEXT_FOLDING_ENABLED,1053kbOpts: {1054kbExpr: EditorContextKeys.editorTextFocus,1055weight: KeybindingWeight.EditorContrib1056}1057});1058}10591060invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {1061const selectedLines = this.getSelectedLines(editor);1062if (selectedLines.length > 0) {1063const startLineNumber = getParentFoldLine(selectedLines[0], foldingModel);1064if (startLineNumber !== null) {1065editor.setSelection({1066startLineNumber: startLineNumber,1067startColumn: 1,1068endLineNumber: startLineNumber,1069endColumn: 11070});1071}1072}1073}1074}10751076/** Action to go to the previous fold of current line */1077class GotoPreviousFoldAction extends FoldingAction<void> {1078constructor() {1079super({1080id: 'editor.gotoPreviousFold',1081label: nls.localize2('gotoPreviousFold.label', "Go to Previous Folding Range"),1082precondition: CONTEXT_FOLDING_ENABLED,1083kbOpts: {1084kbExpr: EditorContextKeys.editorTextFocus,1085weight: KeybindingWeight.EditorContrib1086}1087});1088}10891090invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {1091const selectedLines = this.getSelectedLines(editor);1092if (selectedLines.length > 0) {1093const startLineNumber = getPreviousFoldLine(selectedLines[0], foldingModel);1094if (startLineNumber !== null) {1095editor.setSelection({1096startLineNumber: startLineNumber,1097startColumn: 1,1098endLineNumber: startLineNumber,1099endColumn: 11100});1101}1102}1103}1104}11051106/** Action to go to the next fold of current line */1107class GotoNextFoldAction extends FoldingAction<void> {1108constructor() {1109super({1110id: 'editor.gotoNextFold',1111label: nls.localize2('gotoNextFold.label', "Go to Next Folding Range"),1112precondition: CONTEXT_FOLDING_ENABLED,1113kbOpts: {1114kbExpr: EditorContextKeys.editorTextFocus,1115weight: KeybindingWeight.EditorContrib1116}1117});1118}11191120invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {1121const selectedLines = this.getSelectedLines(editor);1122if (selectedLines.length > 0) {1123const startLineNumber = getNextFoldLine(selectedLines[0], foldingModel);1124if (startLineNumber !== null) {1125editor.setSelection({1126startLineNumber: startLineNumber,1127startColumn: 1,1128endLineNumber: startLineNumber,1129endColumn: 11130});1131}1132}1133}1134}11351136class FoldRangeFromSelectionAction extends FoldingAction<void> {11371138constructor() {1139super({1140id: 'editor.createFoldingRangeFromSelection',1141label: nls.localize2('createManualFoldRange.label', "Create Folding Range from Selection"),1142precondition: CONTEXT_FOLDING_ENABLED,1143kbOpts: {1144kbExpr: EditorContextKeys.editorTextFocus,1145primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Comma),1146weight: KeybindingWeight.EditorContrib1147}1148});1149}11501151invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {1152const collapseRanges: FoldRange[] = [];1153const selections = editor.getSelections();1154if (selections) {1155for (const selection of selections) {1156let endLineNumber = selection.endLineNumber;1157if (selection.endColumn === 1) {1158--endLineNumber;1159}1160if (endLineNumber > selection.startLineNumber) {1161collapseRanges.push({1162startLineNumber: selection.startLineNumber,1163endLineNumber: endLineNumber,1164type: undefined,1165isCollapsed: true,1166source: FoldSource.userDefined1167});1168editor.setSelection({1169startLineNumber: selection.startLineNumber,1170startColumn: 1,1171endLineNumber: selection.startLineNumber,1172endColumn: 11173});1174}1175}1176if (collapseRanges.length > 0) {1177collapseRanges.sort((a, b) => {1178return a.startLineNumber - b.startLineNumber;1179});1180const newRanges = FoldingRegions.sanitizeAndMerge(foldingModel.regions, collapseRanges, editor.getModel()?.getLineCount());1181foldingModel.updatePost(FoldingRegions.fromFoldRanges(newRanges));1182}1183}1184}1185}11861187class RemoveFoldRangeFromSelectionAction extends FoldingAction<void> {11881189constructor() {1190super({1191id: 'editor.removeManualFoldingRanges',1192label: nls.localize2('removeManualFoldingRanges.label', "Remove Manual Folding Ranges"),1193precondition: CONTEXT_FOLDING_ENABLED,1194kbOpts: {1195kbExpr: EditorContextKeys.editorTextFocus,1196primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.Period),1197weight: KeybindingWeight.EditorContrib1198}1199});1200}12011202invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {1203const selections = editor.getSelections();1204if (selections) {1205const ranges: ILineRange[] = [];1206for (const selection of selections) {1207const { startLineNumber, endLineNumber } = selection;1208ranges.push(endLineNumber >= startLineNumber ? { startLineNumber, endLineNumber } : { endLineNumber, startLineNumber });1209}1210foldingModel.removeManualRanges(ranges);1211foldingController.triggerFoldingModelChanged();1212}1213}1214}121512161217class ToggleImportFoldAction extends FoldingAction<void> {12181219constructor() {1220super({1221id: 'editor.toggleImportFold',1222label: nls.localize2('toggleImportFold.label', "Toggle Import Fold"),1223precondition: CONTEXT_FOLDING_ENABLED,1224kbOpts: {1225kbExpr: EditorContextKeys.editorTextFocus,1226weight: KeybindingWeight.EditorContrib1227}1228});1229}12301231async invoke(foldingController: FoldingController, foldingModel: FoldingModel): Promise<void> {1232const regionsToToggle: FoldingRegion[] = [];1233const regions = foldingModel.regions;1234for (let i = regions.length - 1; i >= 0; i--) {1235if (regions.getType(i) === FoldingRangeKind.Imports.value) {1236regionsToToggle.push(regions.toRegion(i));1237}1238}1239foldingModel.toggleCollapseState(regionsToToggle);1240foldingController.triggerFoldingModelChanged();1241}1242}124312441245registerEditorContribution(FoldingController.ID, FoldingController, EditorContributionInstantiation.Eager); // eager because it uses `saveViewState`/`restoreViewState`1246registerEditorAction(UnfoldAction);1247registerEditorAction(UnFoldRecursivelyAction);1248registerEditorAction(FoldAction);1249registerEditorAction(FoldRecursivelyAction);1250registerEditorAction(ToggleFoldRecursivelyAction);1251registerEditorAction(FoldAllAction);1252registerEditorAction(UnfoldAllAction);1253registerEditorAction(FoldAllBlockCommentsAction);1254registerEditorAction(FoldAllRegionsAction);1255registerEditorAction(UnfoldAllRegionsAction);1256registerEditorAction(FoldAllExceptAction);1257registerEditorAction(UnfoldAllExceptAction);1258registerEditorAction(ToggleFoldAction);1259registerEditorAction(GotoParentFoldAction);1260registerEditorAction(GotoPreviousFoldAction);1261registerEditorAction(GotoNextFoldAction);1262registerEditorAction(FoldRangeFromSelectionAction);1263registerEditorAction(RemoveFoldRangeFromSelectionAction);1264registerEditorAction(ToggleImportFoldAction);12651266for (let i = 1; i <= 7; i++) {1267registerInstantiatedEditorAction(1268new FoldLevelAction({1269id: FoldLevelAction.ID(i),1270label: nls.localize2('foldLevelAction.label', "Fold Level {0}", i),1271precondition: CONTEXT_FOLDING_ENABLED,1272kbOpts: {1273kbExpr: EditorContextKeys.editorTextFocus,1274primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | (KeyCode.Digit0 + i)),1275weight: KeybindingWeight.EditorContrib1276}1277})1278);1279}12801281CommandsRegistry.registerCommand('_executeFoldingRangeProvider', async function (accessor, ...args) {1282const [resource] = args;1283if (!(resource instanceof URI)) {1284throw illegalArgument();1285}12861287const languageFeaturesService = accessor.get(ILanguageFeaturesService);12881289const model = accessor.get(IModelService).getModel(resource);1290if (!model) {1291throw illegalArgument();1292}12931294const configurationService = accessor.get(IConfigurationService);1295if (!configurationService.getValue('editor.folding', { resource })) {1296return [];1297}12981299const languageConfigurationService = accessor.get(ILanguageConfigurationService);13001301const strategy = configurationService.getValue('editor.foldingStrategy', { resource });1302const foldingLimitReporter = {1303get limit() {1304return configurationService.getValue<number>('editor.foldingMaximumRegions', { resource });1305},1306update: (computed: number, limited: number | false) => { }1307};13081309const indentRangeProvider = new IndentRangeProvider(model, languageConfigurationService, foldingLimitReporter);1310let rangeProvider: RangeProvider = indentRangeProvider;1311if (strategy !== 'indentation') {1312const providers = FoldingController.getFoldingRangeProviders(languageFeaturesService, model);1313if (providers.length) {1314rangeProvider = new SyntaxRangeProvider(model, providers, () => { }, foldingLimitReporter, indentRangeProvider);1315}1316}1317const ranges = await rangeProvider.compute(CancellationToken.None);1318const result: FoldingRange[] = [];1319try {1320if (ranges) {1321for (let i = 0; i < ranges.length; i++) {1322const type = ranges.getType(i);1323result.push({ start: ranges.getStartLineNumber(i), end: ranges.getEndLineNumber(i), kind: type ? FoldingRangeKind.fromValue(type) : undefined });1324}1325}1326return result;1327} finally {1328rangeProvider.dispose();1329}1330});133113321333