diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 554a674f732ad..820c27ee15280 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -659,13 +659,73 @@ const editorConfiguration: IConfigurationNode = { }, 'editor.foldingStrategy': { 'type': 'string', - 'enum': ['auto', 'indentation'], + 'enum': ['auto', 'explicit', 'indentation'], 'enumDescriptions': [ nls.localize('foldingStrategyAuto', 'If available, use a language specific folding strategy, otherwise falls back to the indentation based strategy.'), + nls.localize('foldingStrategyExplicit', 'Always use the explicit based folding strategy'), nls.localize('foldingStrategyIndentation', 'Always use the indentation based folding strategy') ], 'default': EDITOR_DEFAULTS.contribInfo.foldingStrategy, - 'description': nls.localize('foldingStrategy', "Controls the way folding ranges are computed. 'auto' picks uses a language specific folding strategy, if available. 'indentation' forces that the indentation based folding strategy is used.") + 'description': nls.localize('foldingStrategy', "Controls the way folding ranges are computed. 'auto' picks uses a language specific folding strategy, if available. 'explicit' forces that the explicit based folding strategy is used. indentation' forces that the indentation based folding strategy is used.") + }, + 'editor.foldingExplicitMarkers': { + 'anyOf': [ + { + type: 'string', + enum: ['language'], + }, + { + type: 'object', + properties: { + comment: { + 'anyOf': [ + { + type: 'string', + enum: ['language'], + }, + { + type: 'object', + properties: { + enabled: { + type: 'boolean', + }, + start: { + type: 'string', + }, + end: { + type: 'string', + }, + } + } + ] + }, + region: { + 'anyOf': [ + { + type: 'string', + enum: ['language'], + }, + { + type: 'object', + properties: { + enabled: { + type: 'boolean', + }, + start: { + type: 'string', + }, + end: { + type: 'string', + }, + } + } + ] + }, + } + } + ], + 'default': EDITOR_DEFAULTS.contribInfo.foldingExplicitMarkers, + 'description': nls.localize('foldingExplicitMarkers', "Controls the way folding ranges are computed in the explicit based folding strategy.") }, 'editor.showFoldingControls': { 'type': 'string', diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 8849311e709a1..d761404132388 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -165,6 +165,9 @@ export interface ICodeActionsOnSaveOptions { [kind: string]: boolean; } +export type TFoldingExplicitMarkers = string | { comment?: TFoldingMarker, region?: TFoldingMarker }; +export type TFoldingMarker = string | { enabled?: boolean, start?: string, end?: string }; + /** * Configuration options for the editor. */ @@ -546,7 +549,8 @@ export interface IEditorOptions { * Selects the folding strategy. 'auto' uses the strategies contributed for the current document, 'indentation' uses the indentation based folding strategy. * Defaults to 'auto'. */ - foldingStrategy?: 'auto' | 'indentation'; + foldingStrategy?: 'auto' | 'explicit' | 'indentation'; + foldingExplicitMarkers?: TFoldingExplicitMarkers; /** * Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter. * Defaults to 'mouseover'. @@ -906,7 +910,8 @@ export interface EditorContribOptions { readonly occurrencesHighlight: boolean; readonly codeLens: boolean; readonly folding: boolean; - readonly foldingStrategy: 'auto' | 'indentation'; + readonly foldingStrategy: 'auto' | 'explicit' | 'indentation'; + readonly foldingExplicitMarkers: TFoldingExplicitMarkers; readonly showFoldingControls: 'always' | 'mouseover'; readonly matchBrackets: boolean; readonly find: InternalEditorFindOptions; @@ -1271,6 +1276,7 @@ export class InternalEditorOptions { && a.codeLens === b.codeLens && a.folding === b.folding && a.foldingStrategy === b.foldingStrategy + && InternalEditorOptions._equalsFoldingExplicitMarkers(a.foldingExplicitMarkers, b.foldingExplicitMarkers) && a.showFoldingControls === b.showFoldingControls && a.matchBrackets === b.matchBrackets && this._equalFindOptions(a.find, b.find) @@ -1297,6 +1303,38 @@ export class InternalEditorOptions { && a.strings === b.strings ); } + + private static _equalsFoldingExplicitMarkers(a: TFoldingExplicitMarkers, b: TFoldingExplicitMarkers): boolean { + if (typeof a === 'string' && typeof b === 'string') { + return true; + } + if (typeof a !== 'object') { + return typeof b !== 'object'; + } + if (typeof b !== 'object') { + return false; + } + + return this._equalsFoldingMarker(a.comment, b.comment) && this._equalsFoldingMarker(a.region, b.region); + } + + private static _equalsFoldingMarker(a: TFoldingMarker, b: TFoldingMarker): boolean { + if (typeof a === 'string' && typeof b === 'string') { + return true; + } + if (typeof a !== 'object') { + return typeof b !== 'object'; + } + if (typeof b !== 'object') { + return false; + } + + return ( + a.enabled === b.enabled + && a.start === b.start + && a.end === b.end + ); + } } /** @@ -1825,6 +1863,23 @@ export class EditorOptionsValidator { } else { quickSuggestions = _boolean(opts.quickSuggestions, defaults.quickSuggestions); } + let foldingExplicitMarkers: TFoldingExplicitMarkers; + if (typeof opts.foldingExplicitMarkers === 'object') { + foldingExplicitMarkers = { + comment: _foldingExplicitMarker(opts.foldingExplicitMarkers.comment, { + enabled: true, + start: '\\/\\*', + end: '\\*\\/' + }), + region: _foldingExplicitMarker(opts.foldingExplicitMarkers.region, { + enabled: true, + start: '#region', + end: '#endregion' + }) + }; + } else { + foldingExplicitMarkers = _string(opts.foldingExplicitMarkers, 'language'); + } // Compatibility support for acceptSuggestionOnEnter if (typeof opts.acceptSuggestionOnEnter === 'boolean') { opts.acceptSuggestionOnEnter = opts.acceptSuggestionOnEnter ? 'on' : 'off'; @@ -1853,7 +1908,8 @@ export class EditorOptionsValidator { occurrencesHighlight: _boolean(opts.occurrencesHighlight, defaults.occurrencesHighlight), codeLens: _boolean(opts.codeLens, defaults.codeLens), folding: _boolean(opts.folding, defaults.folding), - foldingStrategy: _stringSet<'auto' | 'indentation'>(opts.foldingStrategy, defaults.foldingStrategy, ['auto', 'indentation']), + foldingStrategy: _stringSet<'auto' | 'explicit' | 'indentation'>(opts.foldingStrategy, defaults.foldingStrategy, ['auto', 'explicit', 'indentation']), + foldingExplicitMarkers: foldingExplicitMarkers, showFoldingControls: _stringSet<'always' | 'mouseover'>(opts.showFoldingControls, defaults.showFoldingControls, ['always', 'mouseover']), matchBrackets: _boolean(opts.matchBrackets, defaults.matchBrackets), find: find, @@ -1865,6 +1921,19 @@ export class EditorOptionsValidator { } } +function _foldingExplicitMarker(value, defaults) { + if (typeof value === 'object') { + return { + enabled: _boolean(value.enabled, defaults.enabled), + start: _string(value.start, defaults.start), + end: _string(value.end, defaults.end), + }; + } + else { + return _string(value, 'language'); + } +} + /** * @internal */ @@ -1962,6 +2031,7 @@ export class InternalEditorOptionsFactory { codeLens: (accessibilityIsOn ? false : opts.contribInfo.codeLens), // DISABLED WHEN SCREEN READER IS ATTACHED folding: (accessibilityIsOn ? false : opts.contribInfo.folding), // DISABLED WHEN SCREEN READER IS ATTACHED foldingStrategy: opts.contribInfo.foldingStrategy, + foldingExplicitMarkers: opts.contribInfo.foldingExplicitMarkers, showFoldingControls: opts.contribInfo.showFoldingControls, matchBrackets: (accessibilityIsOn ? false : opts.contribInfo.matchBrackets), // DISABLED WHEN SCREEN READER IS ATTACHED find: opts.contribInfo.find, @@ -2433,6 +2503,18 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { codeLens: true, folding: true, foldingStrategy: 'auto', + foldingExplicitMarkers: { + comment: { + enabled: true, + start: '\\/\\*', + end: '\\*\\/' + }, + region: { + enabled: true, + start: '#region', + end: '#endregion' + } + }, showFoldingControls: 'mouseover', matchBrackets: true, find: { diff --git a/src/vs/editor/contrib/folding/explicitRangeProvider.ts b/src/vs/editor/contrib/folding/explicitRangeProvider.ts new file mode 100644 index 0000000000000..05db134eb9bb5 --- /dev/null +++ b/src/vs/editor/contrib/folding/explicitRangeProvider.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; +import { ITextModel } from 'vs/editor/common/model'; +import { RangeProvider } from './folding'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { TFoldingExplicitMarkers, EDITOR_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { FoldingRangeKind } from 'vs/editor/common/modes'; + +export const ID_EXPLICIT_PROVIDER = 'explicit'; + +export class ExplicitRangeProvider implements RangeProvider { + + readonly id = ID_EXPLICIT_PROVIDER; + + private commentEnabled: boolean; + private commentStart: RegExp; + private commentEnd: RegExp; + + private regionEnabled: boolean; + private regionStart: RegExp; + private regionEnd: RegExp; + + constructor(private editorModel: ITextModel, configuration: TFoldingExplicitMarkers) { + const foldingRules = LanguageConfigurationRegistry.getFoldingRules(this.editorModel.getLanguageIdentifier().id); + + if (typeof configuration === 'object' && typeof configuration.region === 'object') { + this.regionEnabled = configuration.region.enabled; + this.regionStart = configuration.region.start ? new RegExp(configuration.region.start) + : foldingRules.markers.start ? foldingRules.markers.start + : new RegExp((<{ region: { enabled: boolean, start: string, end: string } }>EDITOR_DEFAULTS.contribInfo.foldingExplicitMarkers).region.start) + ; + this.regionEnd = configuration.region.end ? new RegExp(configuration.region.end) + : foldingRules.markers.end ? foldingRules.markers.end + : new RegExp((<{ region: { enabled: boolean, start: string, end: string } }>EDITOR_DEFAULTS.contribInfo.foldingExplicitMarkers).region.end) + ; + } else { + this.regionEnabled = true; + this.regionStart = foldingRules.markers.start; + this.regionEnd = foldingRules.markers.end; + } + + const commentRules = LanguageConfigurationRegistry.getComments(this.editorModel.getLanguageIdentifier().id); + + if (typeof configuration === 'object' && typeof configuration.comment === 'object') { + this.commentEnabled = configuration.comment.enabled; + this.commentStart = new RegExp( + configuration.comment.start ? configuration.comment.start + : commentRules.blockCommentStartToken ? commentRules.blockCommentStartToken.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + : (<{ comment: { enabled: boolean, start: string, end: string } }>EDITOR_DEFAULTS.contribInfo.foldingExplicitMarkers).comment.start + ); + this.commentEnd = new RegExp( + configuration.comment.end ? configuration.comment.end + : commentRules.blockCommentEndToken ? commentRules.blockCommentEndToken.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + : (<{ comment: { enabled: boolean, start: string, end: string } }>EDITOR_DEFAULTS.contribInfo.foldingExplicitMarkers).comment.end + ); + } else { + this.commentEnabled = true; + this.commentStart = new RegExp(commentRules.blockCommentStartToken.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')); + this.commentEnd = new RegExp(commentRules.blockCommentEndToken.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')); + } + } + + compute(cancelationToken: CancellationToken): Thenable { + return TPromise.as(this.computeRanges()); + } + + dispose() { + } + + private computeRanges(): FoldingRegions { + const startIndexes: Array = []; + const endIndexes: Array = []; + const types: Array = []; + + const lineCount = this.editorModel.getLineCount(); + + let i = 1; + let line; + while (i <= lineCount) { + line = this.editorModel.getLineContent(i); + if (this.commentEnabled && this.commentStart.test(line)) { + i = this.computeCommentRange(lineCount, startIndexes, endIndexes, types, i); + } else if (this.regionEnabled && this.regionStart.test(line)) { + i = this.computeRegionRange(lineCount, startIndexes, endIndexes, types, i); + } else { + i++; + } + } + + const length = startIndexes.length; + const startIndexesUINT32 = new Uint32Array(length); + const endIndexesUINT32 = new Uint32Array(length); + for (let i = 0; i < length; i++) { + startIndexesUINT32[i] = startIndexes[i]; + endIndexesUINT32[i] = endIndexes[i]; + } + + return new FoldingRegions(startIndexesUINT32, endIndexesUINT32, types); + } + + private computeCommentRange(lineCount, startIndexes, endIndexes, types, fromIndex) { + let i = fromIndex + 1; + let line; + while (i <= lineCount) { + line = this.editorModel.getLineContent(i); + if (this.commentEnd.test(line)) { + startIndexes.push(fromIndex); + endIndexes.push(i); + types.push(FoldingRangeKind.Comment); + + return i + 1; + } else { + i++; + } + } + + return i; + } + + private computeRegionRange(lineCount, startIndexes, endIndexes, types, fromIndex) { + let i = fromIndex + 1; + let line; + while (i <= lineCount) { + line = this.editorModel.getLineContent(i); + if (this.regionStart.test(line)) { + i = this.computeRegionRange(lineCount, startIndexes, endIndexes, types, i); + } else if (this.regionEnd.test(line)) { + startIndexes.push(fromIndex); + endIndexes.push(i); + types.push(FoldingRangeKind.Region); + + return i + 1; + } else if (this.commentEnabled && this.commentStart.test(line)) { + i = this.computeCommentRange(lineCount, startIndexes, endIndexes, types, i); + } else { + i++; + } + } + + return i; + } +} \ No newline at end of file diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 68151b424c831..6c4add93238e7 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -32,6 +32,7 @@ import { FoldingRangeProviderRegistry, FoldingRangeKind } from 'vs/editor/common import { SyntaxRangeProvider, ID_SYNTAX_PROVIDER } from './syntaxRangeProvider'; import { CancellationToken } from 'vs/base/common/cancellation'; import { InitializingRangeProvider, ID_INIT_PROVIDER } from 'vs/editor/contrib/folding/intializingRangeProvider'; +import { ExplicitRangeProvider } from './explicitRangeProvider'; export const ID = 'editor.contrib.folding'; @@ -59,7 +60,7 @@ export class FoldingController implements IEditorContribution { private editor: ICodeEditor; private _isEnabled: boolean; private _autoHideFoldingControls: boolean; - private _useFoldingProviders: boolean; + private _foldingStrategy: string; private foldingDecorationProvider: FoldingDecorationProvider; @@ -84,7 +85,7 @@ export class FoldingController implements IEditorContribution { this.editor = editor; this._isEnabled = this.editor.getConfiguration().contribInfo.folding; this._autoHideFoldingControls = this.editor.getConfiguration().contribInfo.showFoldingControls === 'mouseover'; - this._useFoldingProviders = this.editor.getConfiguration().contribInfo.foldingStrategy !== 'indentation'; + this._foldingStrategy = this.editor.getConfiguration().contribInfo.foldingStrategy; this.globalToDispose = []; this.localToDispose = []; @@ -108,9 +109,9 @@ export class FoldingController implements IEditorContribution { this.foldingDecorationProvider.autoHideFoldingControls = this._autoHideFoldingControls; this.onModelContentChanged(); } - let oldUseFoldingProviders = this._useFoldingProviders; - this._useFoldingProviders = this.editor.getConfiguration().contribInfo.foldingStrategy !== 'indentation'; - if (oldUseFoldingProviders !== this._useFoldingProviders) { + let oldFoldingStrategy = this._foldingStrategy; + this._foldingStrategy = this.editor.getConfiguration().contribInfo.foldingStrategy; + if (oldFoldingStrategy !== this._foldingStrategy) { this.onFoldingStrategyChanged(); } } @@ -228,9 +229,10 @@ export class FoldingController implements IEditorContribution { if (this.rangeProvider) { return this.rangeProvider; } + this.rangeProvider = new IndentRangeProvider(editorModel); // fallback - if (this._useFoldingProviders) { + if (this._foldingStrategy === 'auto') { let foldingProviders = FoldingRangeProviderRegistry.ordered(this.foldingModel.textModel); if (foldingProviders.length === 0 && this.foldingStateMemento) { this.rangeProvider = new InitializingRangeProvider(editorModel, this.foldingStateMemento.collapsedRegions, () => { @@ -242,7 +244,10 @@ export class FoldingController implements IEditorContribution { } else if (foldingProviders.length > 0) { this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders); } + } else if (this._foldingStrategy === 'explicit') { + this.rangeProvider = new ExplicitRangeProvider(editorModel, this.editor.getConfiguration().contribInfo.foldingExplicitMarkers); } + this.foldingStateMemento = null; return this.rangeProvider; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f39717228d6c2..58fd451757f06 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2516,6 +2516,17 @@ declare namespace monaco.editor { [kind: string]: boolean; } + export type TFoldingExplicitMarkers = string | { + comment?: TFoldingMarker; + region?: TFoldingMarker; + }; + + export type TFoldingMarker = string | { + enabled?: boolean; + start?: string; + end?: string; + }; + /** * Configuration options for the editor. */ @@ -2889,7 +2900,8 @@ declare namespace monaco.editor { * Selects the folding strategy. 'auto' uses the strategies contributed for the current document, 'indentation' uses the indentation based folding strategy. * Defaults to 'auto'. */ - foldingStrategy?: 'auto' | 'indentation'; + foldingStrategy?: 'auto' | 'explicit' | 'indentation'; + foldingExplicitMarkers?: TFoldingExplicitMarkers; /** * Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter. * Defaults to 'mouseover'. @@ -3190,7 +3202,8 @@ declare namespace monaco.editor { readonly occurrencesHighlight: boolean; readonly codeLens: boolean; readonly folding: boolean; - readonly foldingStrategy: 'auto' | 'indentation'; + readonly foldingStrategy: 'auto' | 'explicit' | 'indentation'; + readonly foldingExplicitMarkers: TFoldingExplicitMarkers; readonly showFoldingControls: 'always' | 'mouseover'; readonly matchBrackets: boolean; readonly find: InternalEditorFindOptions;