diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 5866654f0ca90..8f3332c1a910a 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -959,6 +959,12 @@ const editorConfiguration: IConfigurationNode = { 'default': EDITOR_DEFAULTS.contribInfo.showFoldingControls, 'description': nls.localize('showFoldingControls', "Controls whether the fold controls on the gutter are automatically hidden.") }, + 'editor.foldingControls': { + 'type': 'string', + 'enum': ['classic', 'top-bottom'], + 'default': EDITOR_DEFAULTS.contribInfo.foldingControls, + 'description': nls.localize('foldingControls', "Controls the style of folding controls shown when folding is enabled.") + }, 'editor.matchBrackets': { 'type': 'boolean', 'default': EDITOR_DEFAULTS.contribInfo.matchBrackets, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 6c010edc1cf18..361af8650113a 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -655,6 +655,11 @@ export interface IEditorOptions { * Defaults to 'mouseover'. */ showFoldingControls?: 'always' | 'mouseover'; + /** + * Controls the style of folding controls shown when folding is enabled. + * Defaults to 'classic'. + */ + foldingControls?: 'classic' | 'top-bottom'; /** * Enable highlighting of matching brackets. * Defaults to true. @@ -1036,6 +1041,7 @@ export interface EditorContribOptions { readonly folding: boolean; readonly foldingStrategy: 'auto' | 'indentation'; readonly showFoldingControls: 'always' | 'mouseover'; + readonly foldingControls: 'classic' | 'top-bottom'; readonly matchBrackets: boolean; readonly find: InternalEditorFindOptions; readonly colorDecorators: boolean; @@ -1463,6 +1469,7 @@ export class InternalEditorOptions { && a.folding === b.folding && a.foldingStrategy === b.foldingStrategy && a.showFoldingControls === b.showFoldingControls + && a.foldingControls === b.foldingControls && a.matchBrackets === b.matchBrackets && this._equalFindOptions(a.find, b.find) && a.colorDecorators === b.colorDecorators @@ -2118,6 +2125,7 @@ export class EditorOptionsValidator { folding: _boolean(opts.folding, defaults.folding), foldingStrategy: _stringSet<'auto' | 'indentation'>(opts.foldingStrategy, defaults.foldingStrategy, ['auto', 'indentation']), showFoldingControls: _stringSet<'always' | 'mouseover'>(opts.showFoldingControls, defaults.showFoldingControls, ['always', 'mouseover']), + foldingControls: _stringSet<'classic' | 'top-bottom'>(opts.foldingControls, defaults.foldingControls, ['classic', 'top-bottom']), matchBrackets: _boolean(opts.matchBrackets, defaults.matchBrackets), find: find, colorDecorators: _boolean(opts.colorDecorators, defaults.colorDecorators), @@ -2232,6 +2240,7 @@ export class InternalEditorOptionsFactory { folding: (accessibilityIsOn ? false : opts.contribInfo.folding), // DISABLED WHEN SCREEN READER IS ATTACHED foldingStrategy: opts.contribInfo.foldingStrategy, showFoldingControls: opts.contribInfo.showFoldingControls, + foldingControls: opts.contribInfo.foldingControls, matchBrackets: (accessibilityIsOn ? false : opts.contribInfo.matchBrackets), // DISABLED WHEN SCREEN READER IS ATTACHED find: opts.contribInfo.find, colorDecorators: opts.contribInfo.colorDecorators, @@ -2728,6 +2737,7 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { folding: true, foldingStrategy: 'auto', showFoldingControls: 'mouseover', + foldingControls: 'classic', matchBrackets: true, find: { seedSearchStringFromSelection: true, diff --git a/src/vs/editor/contrib/folding/folding.css b/src/vs/editor/contrib/folding/folding.css index 79511b6c0c1f2..23cc87882b1fb 100644 --- a/src/vs/editor/contrib/folding/folding.css +++ b/src/vs/editor/contrib/folding/folding.css @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .margin-view-overlays .folding { +.monaco-editor .margin-view-overlays .folding, +.monaco-editor .margin-view-overlays .foldingEnd { cursor: pointer; background-repeat: no-repeat; background-origin: border-box; @@ -17,25 +18,55 @@ background-image: url('tree-expanded-light.svg'); } +.monaco-editor .margin-view-overlays .foldingEnd { + background-image: url('tree-expanded-end-light.svg'); +} + .monaco-editor.hc-black .margin-view-overlays .folding, .monaco-editor.vs-dark .margin-view-overlays .folding { background-image: url('tree-expanded-dark.svg'); } +.monaco-editor.hc-black .margin-view-overlays .foldingEnd, +.monaco-editor.vs-dark .margin-view-overlays .foldingEnd { + background-image: url('tree-expanded-end-dark.svg'); +} + .monaco-editor.hc-black .margin-view-overlays .folding { background-image: url('tree-expanded-hc.svg'); } +.monaco-editor.hc-black .margin-view-overlays .foldingEnd { + background-image: url('tree-expanded-end-hc.svg'); +} + .monaco-editor .margin-view-overlays:hover .folding, .monaco-editor .margin-view-overlays .folding.alwaysShowFoldIcons { opacity: 1; } +.monaco-editor .margin-view-overlays:hover .foldingEnd, +.monaco-editor .margin-view-overlays .foldingEnd.alwaysShowFoldIcons { + opacity: .50; +} + +.monaco-editor .margin-view-overlays:hover .foldingEnd:hover, +.monaco-editor .margin-view-overlays .foldingEnd.alwaysShowFoldIcons:hover { + opacity: 1; +} + .monaco-editor .margin-view-overlays .folding.collapsed { background-image: url('tree-collapsed-light.svg'); opacity: 1; } +.monaco-editor .margin-view-overlays .folding + .foldingEnd, +.monaco-editor .margin-view-overlays .foldingEnd.collapsed { + opacity: 0; + cursor: default; + z-index: -1; +} + .monaco-editor.vs-dark .margin-view-overlays .folding.collapsed { background-image: url('tree-collapsed-dark.svg'); } @@ -51,4 +82,4 @@ display: inline; line-height: 1em; cursor: pointer; -} \ 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 9a9ea25c5456f..ca799e3f493a1 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -59,6 +59,7 @@ export class FoldingController extends Disposable implements IEditorContribution private readonly editor: ICodeEditor; private _isEnabled: boolean; + private _isFoldFromEndEnabled: boolean; private _autoHideFoldingControls: boolean; private _useFoldingProviders: boolean; @@ -87,6 +88,7 @@ export class FoldingController extends Disposable implements IEditorContribution super(); this.editor = editor; this._isEnabled = this.editor.getConfiguration().contribInfo.folding; + this._isFoldFromEndEnabled = this.editor.getConfiguration().contribInfo.foldingControls === 'top-bottom'; this._autoHideFoldingControls = this.editor.getConfiguration().contribInfo.showFoldingControls === 'mouseover'; this._useFoldingProviders = this.editor.getConfiguration().contribInfo.foldingStrategy !== 'indentation'; @@ -106,6 +108,11 @@ export class FoldingController extends Disposable implements IEditorContribution if (oldIsEnabled !== this._isEnabled) { this.onModelChanged(); } + let oldIsFoldFromEndEnabled = this._isFoldFromEndEnabled; + this._isFoldFromEndEnabled = this.editor.getConfiguration().contribInfo.foldingControls === 'top-bottom'; + if (oldIsFoldFromEndEnabled !== this._isFoldFromEndEnabled) { + this.onModelChanged(); + } let oldShowFoldingControls = this._autoHideFoldingControls; this._autoHideFoldingControls = this.editor.getConfiguration().contribInfo.showFoldingControls === 'mouseover'; if (oldShowFoldingControls !== this._autoHideFoldingControls) { @@ -182,7 +189,7 @@ export class FoldingController extends Disposable implements IEditorContribution return; } - this.foldingModel = new FoldingModel(model, this.foldingDecorationProvider); + this.foldingModel = new FoldingModel(model, this.foldingDecorationProvider, this._isFoldFromEndEnabled); this.localToDispose.add(this.foldingModel); this.hiddenRangeModel = new HiddenRangeModel(this.foldingModel); @@ -412,15 +419,29 @@ export class FoldingController extends Disposable implements IEditorContribution foldingModel.then(foldingModel => { if (foldingModel) { let region = foldingModel.getRegionAtLine(lineNumber); - if (region && region.startLineNumber === lineNumber) { + let isEnd = false; + + if (this._isFoldFromEndEnabled && ((region && region.startLineNumber !== lineNumber) || region === null)) { + lineNumber -= 1; + region = foldingModel.getTopEndRegionAtLine(lineNumber); + + isEnd = region !== null && (region.endLineNumber === lineNumber); + } + + if (region && (region.startLineNumber === lineNumber || isEnd)) { let isCollapsed = region.isCollapsed; + + if (isCollapsed && isEnd) { + return; + } + if (iconClicked || isCollapsed) { let toToggle = [region]; if (e.event.middleButton || e.event.shiftKey) { toToggle.push(...foldingModel.getRegionsInside(region, r => r.isCollapsed === isCollapsed)); } foldingModel.toggleCollapseState(toToggle); - this.reveal({ lineNumber, column: 1 }); + this.reveal({ lineNumber: region.startLineNumber, column: 1 }); } } } diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index 4b55e86f6fa98..308c861ce301e 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -26,18 +26,46 @@ export class FoldingDecorationProvider implements IDecorationProvider { linesDecorationsClassName: 'folding alwaysShowFoldIcons' }); + private static COLLAPSED_VISUAL_DECORATION_END = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + linesDecorationsClassName: 'foldingEnd collapsed' + }); + + private static EXPANDED_AUTO_HIDE_VISUAL_DECORATION_END = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + linesDecorationsClassName: 'foldingEnd' + }); + + private static EXPANDED_VISUAL_DECORATION_END = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + linesDecorationsClassName: 'foldingEnd alwaysShowFoldIcons' + }); + + private static VISUAL_DECORATION_HIDDEN = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + linesDecorationsClassName: '' + }); + public autoHideFoldingControls: boolean = true; constructor(private readonly editor: ICodeEditor) { } - getDecorationOption(isCollapsed: boolean): ModelDecorationOptions { - if (isCollapsed) { - return FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION; + getDecorationOption(isCollapsed: boolean, isEnd: boolean = false, isHidden: boolean = false): ModelDecorationOptions { + if (isHidden) { + return FoldingDecorationProvider.VISUAL_DECORATION_HIDDEN; + } else if (isCollapsed) { + return isEnd ? + FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION_END : + FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION; } else if (this.autoHideFoldingControls) { - return FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; + return isEnd ? + FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION_END : + FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; } else { - return FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION; + return isEnd ? + FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION_END : + FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION; } } diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 70906b653eb43..b80402d92afa3 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges'; export interface IDecorationProvider { - getDecorationOption(isCollapsed: boolean): IModelDecorationOptions; + getDecorationOption(isCollapsed: boolean, isEnd?: boolean, hidden?: boolean): IModelDecorationOptions; deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null; } @@ -23,9 +23,11 @@ export type CollapseMemento = ILineRange[]; export class FoldingModel { private readonly _textModel: ITextModel; private readonly _decorationProvider: IDecorationProvider; + private readonly _isFoldFromEndEnabled: boolean; private _regions: FoldingRegions; private _editorDecorationIds: string[]; + private _editorDecorationIdsEnd: string[]; private _isInitialized: boolean; private _updateEventEmitter = new Emitter(); @@ -35,11 +37,13 @@ export class FoldingModel { public get textModel() { return this._textModel; } public get isInitialized() { return this._isInitialized; } - constructor(textModel: ITextModel, decorationProvider: IDecorationProvider) { + constructor(textModel: ITextModel, decorationProvider: IDecorationProvider, foldFromEndEnabled: boolean = false) { this._textModel = textModel; this._decorationProvider = decorationProvider; + this._isFoldFromEndEnabled = foldFromEndEnabled; this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0)); this._editorDecorationIds = []; + this._editorDecorationIdsEnd = []; this._isInitialized = false; } @@ -52,11 +56,28 @@ export class FoldingModel { for (let region of regions) { let index = region.regionIndex; let editorDecorationId = this._editorDecorationIds[index]; + let editorDecorationIdEnd = this._editorDecorationIdsEnd[index]; if (editorDecorationId && !processed[editorDecorationId]) { processed[editorDecorationId] = true; let newCollapseState = !this._regions.isCollapsed(index); this._regions.setCollapsed(index, newCollapseState); accessor.changeDecorationOptions(editorDecorationId, this._decorationProvider.getDecorationOption(newCollapseState)); + + if (editorDecorationIdEnd && !processed[editorDecorationIdEnd]) { + let regionsInside = this.getRegionsInside(region); + + regionsInside.forEach(insideRegion => { + let insideRegionDecorationIdEnd = this._editorDecorationIdsEnd[insideRegion.regionIndex]; + + if (insideRegionDecorationIdEnd && !processed[insideRegionDecorationIdEnd]) { + processed[insideRegionDecorationIdEnd] = true; + accessor.changeDecorationOptions(insideRegionDecorationIdEnd, this._decorationProvider.getDecorationOption(insideRegion.isCollapsed, true, region.isCollapsed)); + } + }); + + processed[editorDecorationIdEnd] = true; + accessor.changeDecorationOptions(editorDecorationIdEnd, this._decorationProvider.getDecorationOption(newCollapseState, true, region.isCollapsed)); + } } } }); @@ -65,6 +86,7 @@ export class FoldingModel { public update(newRegions: FoldingRegions, blockedLineNumers: number[] = []): void { let newEditorDecorations: IModelDeltaDecoration[] = []; + let newEditorDecorationsEnd: IModelDeltaDecoration[] = []; let isBlocked = (startLineNumber: number, endLineNumber: number) => { for (let blockedLineNumber of blockedLineNumers) { @@ -77,18 +99,32 @@ export class FoldingModel { let initRange = (index: number, isCollapsed: boolean) => { let startLineNumber = newRegions.getStartLineNumber(index); - if (isCollapsed && isBlocked(startLineNumber, newRegions.getEndLineNumber(index))) { + let endLineNumber = newRegions.getEndLineNumber(index) + 1; + if (isCollapsed && isBlocked(startLineNumber, endLineNumber)) { isCollapsed = false; } newRegions.setCollapsed(index, isCollapsed); - let maxColumn = this._textModel.getLineMaxColumn(startLineNumber); - let decorationRange = { + let maxColumnStart = this._textModel.getLineMaxColumn(startLineNumber); + let decorationRangeStart = { startLineNumber: startLineNumber, - startColumn: maxColumn, + startColumn: maxColumnStart, endLineNumber: startLineNumber, - endColumn: maxColumn + endColumn: maxColumnStart + }; + newEditorDecorations.push({ range: decorationRangeStart, options: this._decorationProvider.getDecorationOption(isCollapsed) }); + + if (!this._isFoldFromEndEnabled) { + return; + } + + let maxColumnEnd = this._textModel.getLineMaxColumn(endLineNumber); + let decorationRangeEnd = { + startLineNumber: endLineNumber, + startColumn: maxColumnEnd, + endLineNumber: endLineNumber, + endColumn: maxColumnEnd }; - newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed) }); + newEditorDecorationsEnd.push({ range: decorationRangeEnd, options: this._decorationProvider.getDecorationOption(isCollapsed, true) }); }; let i = 0; @@ -130,6 +166,7 @@ export class FoldingModel { } this._editorDecorationIds = this._decorationProvider.deltaDecorations(this._editorDecorationIds, newEditorDecorations); + this._editorDecorationIdsEnd = this._decorationProvider.deltaDecorations(this._editorDecorationIdsEnd, newEditorDecorationsEnd); this._regions = newRegions; this._isInitialized = true; this._updateEventEmitter.fire({ model: this }); @@ -175,6 +212,7 @@ export class FoldingModel { public dispose() { this._decorationProvider.deltaDecorations(this._editorDecorationIds, []); + this._decorationProvider.deltaDecorations(this._editorDecorationIdsEnd, []); } getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] { @@ -204,6 +242,19 @@ export class FoldingModel { return null; } + getTopEndRegionAtLine(lineNumber: number): FoldingRegion | null { + let resultIndex = -1; + let index = this._regions.findRange(lineNumber); + while (index >= 0 && this._regions.getEndLineNumber(index) === lineNumber) { + resultIndex = index; + index = this._regions.getParentIndex(index); + } + if (resultIndex >= 0) { + return this._regions.toRegion(resultIndex); + } + return null; + } + getRegionsInside(region: FoldingRegion | null, filter?: (r: FoldingRegion, level?: number) => boolean): FoldingRegion[] { let result: FoldingRegion[] = []; let index = region ? region.regionIndex + 1 : 0; diff --git a/src/vs/editor/contrib/folding/tree-expanded-end-dark.svg b/src/vs/editor/contrib/folding/tree-expanded-end-dark.svg new file mode 100644 index 0000000000000..6b30728a0a4ef --- /dev/null +++ b/src/vs/editor/contrib/folding/tree-expanded-end-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/editor/contrib/folding/tree-expanded-end-hc.svg b/src/vs/editor/contrib/folding/tree-expanded-end-hc.svg new file mode 100644 index 0000000000000..e2498ea57c9b5 --- /dev/null +++ b/src/vs/editor/contrib/folding/tree-expanded-end-hc.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/editor/contrib/folding/tree-expanded-end-light.svg b/src/vs/editor/contrib/folding/tree-expanded-end-light.svg new file mode 100644 index 0000000000000..5553f11cba17a --- /dev/null +++ b/src/vs/editor/contrib/folding/tree-expanded-end-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 945d10d7e1469..2895656c70c60 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3022,6 +3022,11 @@ declare namespace monaco.editor { * Defaults to 'mouseover'. */ showFoldingControls?: 'always' | 'mouseover'; + /** + * Controls the style of folding controls shown when folding is enabled. + * Defaults to 'classic'. + */ + foldingControls?: 'classic' | 'top-bottom'; /** * Enable highlighting of matching brackets. * Defaults to true. @@ -3344,6 +3349,7 @@ declare namespace monaco.editor { readonly folding: boolean; readonly foldingStrategy: 'auto' | 'indentation'; readonly showFoldingControls: 'always' | 'mouseover'; + readonly foldingControls: 'classic' | 'top-bottom'; readonly matchBrackets: boolean; readonly find: InternalEditorFindOptions; readonly colorDecorators: boolean; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 37b4f848681b9..8fd879e389a41 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -145,6 +145,7 @@ const configurationValueWhitelist = [ 'editor.codeLens', 'editor.folding', 'editor.showFoldingControls', + 'editor.foldingControls', 'editor.matchBrackets', 'editor.glyphMargin', 'editor.useTabStops',