From 0086d4bbc51911e0b7a3509db46c62c9f8aefc30 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Tue, 22 Nov 2022 22:41:17 -0500 Subject: [PATCH 01/19] support showing collapsed text at the end of the first line of the folded range --- src/vs/editor/common/languages.ts | 6 ++ src/vs/editor/common/model/intervalTree.ts | 6 +- .../contrib/folding/browser/folding.css | 4 +- .../editor/contrib/folding/browser/folding.ts | 1 + .../folding/browser/foldingDecorations.ts | 77 ++++++++++----- .../contrib/folding/browser/foldingModel.ts | 14 ++- .../contrib/folding/browser/foldingRanges.ts | 21 +++- .../folding/browser/syntaxRangeProvider.ts | 19 ++-- .../test/browser/foldingRanges.test.ts | 96 ++++++++++--------- src/vs/monaco.d.ts | 5 + .../api/common/extHostTypeConverters.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 5 +- src/vscode-dts/vscode.d.ts | 7 +- 13 files changed, 170 insertions(+), 93 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 0d25cd047b4a1..4cad67ed3f672 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1371,6 +1371,12 @@ export interface FoldingRange { * {@link FoldingRangeKind} for an enumeration of standardized kinds. */ kind?: FoldingRangeKind; + + /** + * The text to be shown instead of the folded area. + * Will be shown at the end of the start line. + */ + collapsedText?: string; } export class FoldingRangeKind { /** diff --git a/src/vs/editor/common/model/intervalTree.ts b/src/vs/editor/common/model/intervalTree.ts index ce7ffeaa1ca49..d32239e752064 100644 --- a/src/vs/editor/common/model/intervalTree.ts +++ b/src/vs/editor/common/model/intervalTree.ts @@ -200,9 +200,9 @@ export class IntervalNode { } public detach(): void { - this.parent = null!; - this.left = null!; - this.right = null!; + this.parent = SENTINEL; + this.left = SENTINEL; + this.right = SENTINEL; } } diff --git a/src/vs/editor/contrib/folding/browser/folding.css b/src/vs/editor/contrib/folding/browser/folding.css index 98ae93d37ab6c..fff9fa8e33b34 100644 --- a/src/vs/editor/contrib/folding/browser/folding.css +++ b/src/vs/editor/contrib/folding/browser/folding.css @@ -23,12 +23,10 @@ opacity: 1; } -.monaco-editor .inline-folded:after { +.monaco-editor .collapsed-text { color: grey; margin: 0.1em 0.2em 0 0.2em; - content: "\22EF"; /* ellipses unicode character */ display: inline; line-height: 1em; cursor: pointer; } - diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 370d424b6561d..1dd97769d729f 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -1112,6 +1112,7 @@ class FoldRangeFromSelectionAction extends FoldingAction { startLineNumber: selection.startLineNumber, endLineNumber: endLineNumber, type: undefined, + collapsedText: undefined, isCollapsed: true, source: FoldSource.userDefined }); diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index 6a163da1beb1d..5b3ac6c1ec589 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -5,7 +5,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IModelDecorationOptions, IModelDecorationsChangeAccessor, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationOptions, IModelDecorationsChangeAccessor, InjectedTextOptions, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IDecorationProvider } from 'vs/editor/contrib/folding/browser/foldingModel'; import { localize } from 'vs/nls'; @@ -19,92 +19,102 @@ export const foldingManualExpandedIcon = registerIcon('folding-manual-expanded', export class FoldingDecorationProvider implements IDecorationProvider { - private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static readonly DEFAULT_COLLAPSED_TEXT = '\u22EF'; /* ellipses unicode character */ + + //To always keep decoration in injectedTextTree between toggles, otherwise would crash + private static EMPTY_INJECTED_TEXT: InjectedTextOptions = { content: '' }; + + private static readonly COLLAPSED_VISUAL_DECORATION = { description: 'folding-collapsed-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon) - }); + }; - private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = { description: 'folding-collapsed-highlighted-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - afterContentClassName: 'inline-folded', className: 'folded-background', isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon) - }); + }; - private static readonly MANUALLY_COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static readonly MANUALLY_COLLAPSED_VISUAL_DECORATION = { description: 'folding-manually-collapsed-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) - }); + }; - private static readonly MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ + private static readonly MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = { description: 'folding-manually-collapsed-highlighted-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', isWholeLine: true, firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) - }); + }; - private static readonly NO_CONTROLS_COLLAPSED_RANGE_DECORATION = ModelDecorationOptions.register({ + private static readonly NO_CONTROLS_COLLAPSED_RANGE_DECORATION = { description: 'folding-no-controls-range-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', isWholeLine: true - }); + }; - private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION = ModelDecorationOptions.register({ + private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION = { description: 'folding-no-controls-range-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', isWholeLine: true - }); + }; private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-expanded-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, isWholeLine: true, - firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon) + firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon), + before: FoldingDecorationProvider.EMPTY_INJECTED_TEXT }); private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-expanded-auto-hide-visual-decoration', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon), + before: FoldingDecorationProvider.EMPTY_INJECTED_TEXT }); private static readonly MANUALLY_EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-manually-expanded-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, isWholeLine: true, - firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingManualExpandedIcon) + firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingManualExpandedIcon), + before: FoldingDecorationProvider.EMPTY_INJECTED_TEXT }); private static readonly MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ description: 'folding-manually-expanded-auto-hide-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon), + before: FoldingDecorationProvider.EMPTY_INJECTED_TEXT }); private static readonly NO_CONTROLS_EXPANDED_RANGE_DECORATION = ModelDecorationOptions.register({ description: 'folding-no-controls-range-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - isWholeLine: true + isWholeLine: true, + before: FoldingDecorationProvider.EMPTY_INJECTED_TEXT }); private static readonly HIDDEN_RANGE_DECORATION = ModelDecorationOptions.register({ description: 'folding-hidden-range-decoration', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + before: FoldingDecorationProvider.EMPTY_INJECTED_TEXT }); public showFoldingControls: 'always' | 'never' | 'mouseover' = 'mouseover'; @@ -114,20 +124,24 @@ export class FoldingDecorationProvider implements IDecorationProvider { constructor(private readonly editor: ICodeEditor) { } - getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManual: boolean): IModelDecorationOptions { + getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManual: boolean, collapsedText = FoldingDecorationProvider.DEFAULT_COLLAPSED_TEXT): IModelDecorationOptions { if (isHidden) { // is inside another collapsed region return FoldingDecorationProvider.HIDDEN_RANGE_DECORATION; } + if (this.showFoldingControls === 'never') { if (isCollapsed) { - return this.showFoldingHighlights ? FoldingDecorationProvider.NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION : FoldingDecorationProvider.NO_CONTROLS_COLLAPSED_RANGE_DECORATION; + return this.addCollapsedText( + collapsedText, + this.showFoldingHighlights ? FoldingDecorationProvider.NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION : FoldingDecorationProvider.NO_CONTROLS_COLLAPSED_RANGE_DECORATION + ); } return FoldingDecorationProvider.NO_CONTROLS_EXPANDED_RANGE_DECORATION; } if (isCollapsed) { - return isManual ? + return this.addCollapsedText(collapsedText, isManual ? (this.showFoldingHighlights ? FoldingDecorationProvider.MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.MANUALLY_COLLAPSED_VISUAL_DECORATION) - : (this.showFoldingHighlights ? FoldingDecorationProvider.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION); + : (this.showFoldingHighlights ? FoldingDecorationProvider.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION)); } else if (this.showFoldingControls === 'mouseover') { return isManual ? FoldingDecorationProvider.MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION : FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; } else { @@ -135,6 +149,21 @@ export class FoldingDecorationProvider implements IDecorationProvider { } } + private addCollapsedText(collapsedText: string, decorationOption: IModelDecorationOptions): IModelDecorationOptions { + const before: InjectedTextOptions = { + content: this.fixSpace(collapsedText), + inlineClassName: 'collapsed-text', + inlineClassNameAffectsLetterSpacing: true + }; + return ModelDecorationOptions.register({ ...decorationOption, before }); + } + + // Prevents the view from potentially visible whitespace + private fixSpace(str: string): string { + const noBreakWhitespace = '\xa0'; + return str.replace(/[ \t]/g, noBreakWhitespace); + } + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T { return this.editor.changeDecorations(callback); } diff --git a/src/vs/editor/contrib/folding/browser/foldingModel.ts b/src/vs/editor/contrib/folding/browser/foldingModel.ts index 543df6931620f..a1e8cd35a9da3 100644 --- a/src/vs/editor/contrib/folding/browser/foldingModel.ts +++ b/src/vs/editor/contrib/folding/browser/foldingModel.ts @@ -9,7 +9,7 @@ import { FoldingRegion, FoldingRegions, ILineRange, FoldRange, FoldSource } from import { hash } from 'vs/base/common/hash'; export interface IDecorationProvider { - getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManual: boolean): IModelDecorationOptions; + getDecorationOption(isCollapsed: boolean, isHidden: boolean, isManual: boolean, collapsedText?: string): IModelDecorationOptions; changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null; removeDecorations(decorationIds: string[]): void; } @@ -23,6 +23,7 @@ interface ILineMemento extends ILineRange { checksum?: number; isCollapsed?: boolean; source?: FoldSource; + collapsedText?: string; } export type CollapseMemento = ILineMemento[]; @@ -62,10 +63,11 @@ export class FoldingModel { const updateDecorationsUntil = (index: number) => { while (k < index) { const endLineNumber = this._regions.getEndLineNumber(k); + const collapsedText = this._regions.getCollapsedText(k); const isCollapsed = this._regions.isCollapsed(k); if (endLineNumber <= dirtyRegionEndLine) { const isManual = this.regions.getSource(k) !== FoldSource.provider; - accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual)); + accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual, collapsedText)); } if (isCollapsed && endLineNumber > lastHiddenLine) { lastHiddenLine = endLineNumber; @@ -123,6 +125,7 @@ export class FoldingModel { for (let index = 0, limit = newRegions.length; index < limit; index++) { const startLineNumber = newRegions.getStartLineNumber(index); const endLineNumber = newRegions.getEndLineNumber(index); + const collapsedText = newRegions.getCollapsedText(index); const isCollapsed = newRegions.isCollapsed(index); const isManual = newRegions.getSource(index) !== FoldSource.provider; const decorationRange = { @@ -131,7 +134,7 @@ export class FoldingModel { endLineNumber: endLineNumber, endColumn: this._textModel.getLineMaxColumn(endLineNumber) + 1 }; - newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual) }); + newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual, collapsedText) }); if (isCollapsed && endLineNumber > lastHiddenLine) { lastHiddenLine = endLineNumber; } @@ -167,6 +170,7 @@ export class FoldingModel { startLineNumber: decRange.startLineNumber, endLineNumber: decRange.endLineNumber, type: foldRange.type, + collapsedText: foldRange.collapsedText, isCollapsed, source }); @@ -195,7 +199,8 @@ export class FoldingModel { endLineNumber: range.endLineNumber, isCollapsed: range.isCollapsed, source: range.source, - checksum: checksum + checksum: checksum, + collapsedText: range.collapsedText, }); } return (result.length > 0) ? result : undefined; @@ -220,6 +225,7 @@ export class FoldingModel { startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, type: undefined, + collapsedText: range.collapsedText, isCollapsed: range.isCollapsed ?? true, source: range.source ?? FoldSource.provider }); diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index 5c800345b4826..aa419e98eac32 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -24,6 +24,7 @@ export interface FoldRange { startLineNumber: number; endLineNumber: number; type: string | undefined; + collapsedText: string | undefined; isCollapsed: boolean; source: FoldSource; } @@ -67,8 +68,9 @@ export class FoldingRegions { private _parentsComputed: boolean; private readonly _types: Array | undefined; + private readonly _collapsedTexts: Array | undefined; - constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, types?: Array) { + constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, types?: Array, collapsedTexts?: Array) { if (startIndexes.length !== endIndexes.length || startIndexes.length > MAX_FOLDING_REGIONS) { throw new Error('invalid startIndexes or endIndexes size'); } @@ -78,6 +80,7 @@ export class FoldingRegions { this._userDefinedStates = new BitField(startIndexes.length); this._recoveredStates = new BitField(startIndexes.length); this._types = types; + this._collapsedTexts = collapsedTexts; this._parentsComputed = false; } @@ -126,6 +129,10 @@ export class FoldingRegions { return !!this._types; } + public getCollapsedText(index: number): string | undefined { + return this._collapsedTexts ? this._collapsedTexts[index] : undefined; + } + public isCollapsed(index: number): boolean { return this._collapseStates.get(index); } @@ -250,6 +257,7 @@ export class FoldingRegions { startLineNumber: this._startIndexes[index] & MAX_LINE_NUMBER, endLineNumber: this._endIndexes[index] & MAX_LINE_NUMBER, type: this._types ? this._types[index] : undefined, + collapsedText: this._collapsedTexts ? this._collapsedTexts[index] : undefined, isCollapsed: this.isCollapsed(index), source: this.getSource(index) }; @@ -261,6 +269,8 @@ export class FoldingRegions { const endIndexes = new Uint32Array(rangesLength); let types: Array | undefined = []; let gotTypes = false; + let collapsedTexts: Array | undefined = []; + let gotCollapsedTexts = false; for (let i = 0; i < rangesLength; i++) { const range = ranges[i]; startIndexes[i] = range.startLineNumber; @@ -269,11 +279,18 @@ export class FoldingRegions { if (range.type) { gotTypes = true; } + collapsedTexts.push(range.collapsedText); + if (range.collapsedText) { + gotCollapsedTexts = true; + } } if (!gotTypes) { types = undefined; } - const regions = new FoldingRegions(startIndexes, endIndexes, types); + if (!gotCollapsedTexts) { + collapsedTexts = undefined; + } + const regions = new FoldingRegions(startIndexes, endIndexes, types, collapsedTexts); for (let i = 0; i < rangesLength; i++) { if (ranges[i].isCollapsed) { regions.setCollapsed(i, true); diff --git a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts index 93f41829b639e..cbead62085350 100644 --- a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts @@ -71,7 +71,7 @@ function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextMode const nLines = model.getLineCount(); for (const r of ranges) { if (r.start > 0 && r.end > r.start && r.end <= nLines) { - rangeData.push({ start: r.start, end: r.end, rank: i, kind: r.kind }); + rangeData.push({ start: r.start, end: r.end, rank: i, kind: r.kind, collapsedText: r.collapsedText }); } } } @@ -88,6 +88,7 @@ class RangesCollector { private readonly _nestingLevels: number[]; private readonly _nestingLevelCounts: number[]; private readonly _types: Array; + private readonly _collapsedTexts: Array; private _length: number; private readonly _foldingRangesLimit: FoldingLimitReporter; @@ -97,11 +98,12 @@ class RangesCollector { this._nestingLevels = []; this._nestingLevelCounts = []; this._types = []; + this._collapsedTexts = []; this._length = 0; this._foldingRangesLimit = foldingRangesLimit; } - public add(startLineNumber: number, endLineNumber: number, type: string | undefined, nestingLevel: number) { + public add(startLineNumber: number, endLineNumber: number, type: string | undefined, collapsedText: string | undefined, nestingLevel: number) { if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) { return; } @@ -110,6 +112,7 @@ class RangesCollector { this._endIndexes[index] = endLineNumber; this._nestingLevels[index] = nestingLevel; this._types[index] = type; + this._collapsedTexts[index] = collapsedText; this._length++; if (nestingLevel < 30) { this._nestingLevelCounts[nestingLevel] = (this._nestingLevelCounts[nestingLevel] || 0) + 1; @@ -127,7 +130,7 @@ class RangesCollector { startIndexes[i] = this._startIndexes[i]; endIndexes[i] = this._endIndexes[i]; } - return new FoldingRegions(startIndexes, endIndexes, this._types); + return new FoldingRegions(startIndexes, endIndexes, this._types, this._collapsedTexts); } else { this._foldingRangesLimit.report({ limited: limit, computed: this._length }); @@ -147,16 +150,18 @@ class RangesCollector { const startIndexes = new Uint32Array(limit); const endIndexes = new Uint32Array(limit); const types: Array = []; + const collapsedTexts: Array = []; for (let i = 0, k = 0; i < this._length; i++) { const level = this._nestingLevels[i]; if (level < maxLevel || (level === maxLevel && entries++ < limit)) { startIndexes[k] = this._startIndexes[i]; endIndexes[k] = this._endIndexes[i]; types[k] = this._types[i]; + collapsedTexts[k] = this._collapsedTexts[i]; k++; } } - return new FoldingRegions(startIndexes, endIndexes, types); + return new FoldingRegions(startIndexes, endIndexes, types, collapsedTexts); } } @@ -178,13 +183,13 @@ export function sanitizeRanges(rangeData: IFoldingRangeData[], foldingRangesLimi for (const entry of sorted) { if (!top) { top = entry; - collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length); + collector.add(entry.start, entry.end, entry.kind && entry.kind.value, entry.collapsedText, previous.length); } else { if (entry.start > top.start) { if (entry.end <= top.end) { previous.push(top); top = entry; - collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length); + collector.add(entry.start, entry.end, entry.kind && entry.kind.value, entry.collapsedText, previous.length); } else { if (entry.start > top.end) { do { @@ -195,7 +200,7 @@ export function sanitizeRanges(rangeData: IFoldingRangeData[], foldingRangesLimi } top = entry; } - collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length); + collector.add(entry.start, entry.end, entry.kind && entry.kind.value, entry.collapsedText, previous.length); } } } 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 734613f4d1316..b0e968bbd2417 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -16,11 +16,12 @@ const markers: FoldingMarkers = { suite('FoldingRanges', () => { - const foldRange = (from: number, to: number, collapsed: boolean | undefined = undefined, source: FoldSource = FoldSource.provider, type: string | undefined = undefined) => + const foldRange = (from: number, to: number, collapsed: boolean | undefined = undefined, source: FoldSource = FoldSource.provider, type: string | undefined = undefined, collapsedText: string | undefined = undefined) => { startLineNumber: from, endLineNumber: to, type: type, + collapsedText: collapsedText, isCollapsed: collapsed || false, source }; @@ -28,6 +29,7 @@ suite('FoldingRanges', () => { assert.strictEqual(range1.startLineNumber, range2.startLineNumber, msg + ' start'); assert.strictEqual(range1.endLineNumber, range2.endLineNumber, msg + ' end'); assert.strictEqual(range1.type, range2.type, msg + ' type'); + assert.strictEqual(range1.collapsedText, range2.collapsedText, msg + ' collapsedText'); assert.strictEqual(range1.isCollapsed, range2.isCollapsed, msg + ' collapsed'); assert.strictEqual(range1.source, range2.source, msg + ' source'); }; @@ -122,88 +124,88 @@ suite('FoldingRanges', () => { test('sanitizeAndMerge1', () => { const regionSet1: FoldRange[] = [ foldRange(0, 100), // invalid, should be removed - foldRange(1, 100, false, FoldSource.provider, 'A'), // valid - foldRange(1, 100, false, FoldSource.provider, 'Z'), // invalid, duplicate start + foldRange(1, 100, false, FoldSource.provider, 'A', 'A ct'), // valid + foldRange(1, 100, false, FoldSource.provider, 'Z', 'Z ct'), // invalid, duplicate start foldRange(10, 10, false), // invalid, should be removed - foldRange(20, 80, false, FoldSource.provider, 'C1'), // valid inside 'B' - foldRange(22, 80, true, FoldSource.provider, 'D1'), // valid inside 'C1' + foldRange(20, 80, false, FoldSource.provider, 'C1', 'C1 ct'), // valid inside 'B' + foldRange(22, 80, true, FoldSource.provider, 'D1', 'D1 ct'), // valid inside 'C1' foldRange(90, 101), // invalid, should be removed ]; const regionSet2: FoldRange[] = [ foldRange(20, 80, true), // should merge with C1 foldRange(18, 80, true), // invalid, out of order - foldRange(21, 81, true, FoldSource.provider, 'Z'), // invalid, overlapping - foldRange(22, 80, true, FoldSource.provider, 'D2'), // should merge with D1 + foldRange(21, 81, true, FoldSource.provider, 'Z', 'Z ct'), // invalid, overlapping + foldRange(22, 80, true, FoldSource.provider, 'D2', 'D2 ct'), // should merge with D1 ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); assert.strictEqual(result.length, 3, 'result length1'); - assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'A'), 'A1'); - assertEqualRanges(result[1], foldRange(20, 80, true, FoldSource.provider, 'C1'), 'C1'); - assertEqualRanges(result[2], foldRange(22, 80, true, FoldSource.provider, 'D1'), 'D1'); + assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'A', 'A ct'), 'A1'); + assertEqualRanges(result[1], foldRange(20, 80, true, FoldSource.provider, 'C1', 'C1 ct'), 'C1'); + assertEqualRanges(result[2], foldRange(22, 80, true, FoldSource.provider, 'D1', 'D1 ct'), 'D1'); }); test('sanitizeAndMerge2', () => { const regionSet1: FoldRange[] = [ - foldRange(1, 100, false, FoldSource.provider, 'a1'), // valid - foldRange(2, 100, false, FoldSource.provider, 'a2'), // valid - foldRange(3, 19, false, FoldSource.provider, 'a3'), // valid - foldRange(20, 71, false, FoldSource.provider, 'a4'), // overlaps b3 - foldRange(21, 29, false, FoldSource.provider, 'a5'), // valid - foldRange(81, 91, false, FoldSource.provider, 'a6'), // overlaps b4 + foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), // valid + foldRange(2, 100, false, FoldSource.provider, 'a2', 'a2 ct'), // valid + foldRange(3, 19, false, FoldSource.provider, 'a3', 'a3 ct'), // valid + foldRange(20, 71, false, FoldSource.provider, 'a4', 'a4 ct'), // overlaps b3 + foldRange(21, 29, false, FoldSource.provider, 'a5', 'a5 ct'), // valid + foldRange(81, 91, false, FoldSource.provider, 'a6', 'a6 ct'), // overlaps b4 ]; const regionSet2: FoldRange[] = [ - foldRange(30, 39, true, FoldSource.provider, 'b1'), // valid, will be recovered - foldRange(40, 49, true, FoldSource.userDefined, 'b2'), // valid - foldRange(50, 100, true, FoldSource.userDefined, 'b3'), // overlaps a4 - foldRange(80, 90, true, FoldSource.userDefined, 'b4'), // overlaps a6 - foldRange(92, 100, true, FoldSource.userDefined, 'b5'), // valid + foldRange(30, 39, true, FoldSource.provider, 'b1', 'b1 ct'), // valid, will be recovered + foldRange(40, 49, true, FoldSource.userDefined, 'b2', 'b2 ct'), // valid + foldRange(50, 100, true, FoldSource.userDefined, 'b3', 'b3 ct'), // overlaps a4 + foldRange(80, 90, true, FoldSource.userDefined, 'b4', 'b4 ct'), // overlaps a6 + foldRange(92, 100, true, FoldSource.userDefined, 'b5', 'b5 ct'), // valid ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); assert.strictEqual(result.length, 9, 'result length1'); - assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1'), 'P1'); - assertEqualRanges(result[1], foldRange(2, 100, false, FoldSource.provider, 'a2'), 'P2'); - assertEqualRanges(result[2], foldRange(3, 19, false, FoldSource.provider, 'a3'), 'P3'); - assertEqualRanges(result[3], foldRange(21, 29, false, FoldSource.provider, 'a5'), 'P4'); - assertEqualRanges(result[4], foldRange(30, 39, true, FoldSource.recovered, 'b1'), 'P5'); - assertEqualRanges(result[5], foldRange(40, 49, true, FoldSource.userDefined, 'b2'), 'P6'); - assertEqualRanges(result[6], foldRange(50, 100, true, FoldSource.userDefined, 'b3'), 'P7'); - assertEqualRanges(result[7], foldRange(80, 90, true, FoldSource.userDefined, 'b4'), 'P8'); - assertEqualRanges(result[8], foldRange(92, 100, true, FoldSource.userDefined, 'b5'), 'P9'); + assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), 'P1'); + assertEqualRanges(result[1], foldRange(2, 100, false, FoldSource.provider, 'a2', 'a2 ct'), 'P2'); + assertEqualRanges(result[2], foldRange(3, 19, false, FoldSource.provider, 'a3', 'a3 ct'), 'P3'); + assertEqualRanges(result[3], foldRange(21, 29, false, FoldSource.provider, 'a5', 'a5 ct'), 'P4'); + assertEqualRanges(result[4], foldRange(30, 39, true, FoldSource.recovered, 'b1', 'b1 ct'), 'P5'); + assertEqualRanges(result[5], foldRange(40, 49, true, FoldSource.userDefined, 'b2', 'b2 ct'), 'P6'); + assertEqualRanges(result[6], foldRange(50, 100, true, FoldSource.userDefined, 'b3', 'b3 ct'), 'P7'); + assertEqualRanges(result[7], foldRange(80, 90, true, FoldSource.userDefined, 'b4', 'b4 ct'), 'P8'); + assertEqualRanges(result[8], foldRange(92, 100, true, FoldSource.userDefined, 'b5', 'b5 ct'), 'P9'); }); test('sanitizeAndMerge3', () => { const regionSet1: FoldRange[] = [ - foldRange(1, 100, false, FoldSource.provider, 'a1'), // valid - foldRange(10, 29, false, FoldSource.provider, 'a2'), // matches manual hidden - foldRange(35, 39, true, FoldSource.recovered, 'a3'), // valid + foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), // valid + foldRange(10, 29, false, FoldSource.provider, 'a2', 'a2 ct'), // matches manual hidden + foldRange(35, 39, true, FoldSource.recovered, 'a3', 'a3 ct'), // valid ]; const regionSet2: FoldRange[] = [ - foldRange(10, 29, true, FoldSource.recovered, 'b1'), // matches a - foldRange(20, 28, true, FoldSource.provider, 'b2'), // should remain - foldRange(30, 39, true, FoldSource.recovered, 'b3'), // should remain + foldRange(10, 29, true, FoldSource.recovered, 'b1', 'b1 ct'), // matches a + foldRange(20, 28, true, FoldSource.provider, 'b2', 'b2 ct'), // should remain + foldRange(30, 39, true, FoldSource.recovered, 'b3', 'b3 ct'), // should remain ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); assert.strictEqual(result.length, 5, 'result length3'); - assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1'), 'R1'); - assertEqualRanges(result[1], foldRange(10, 29, true, FoldSource.provider, 'a2'), 'R2'); - assertEqualRanges(result[2], foldRange(20, 28, true, FoldSource.recovered, 'b2'), 'R3'); - assertEqualRanges(result[3], foldRange(30, 39, true, FoldSource.recovered, 'b3'), 'R3'); - assertEqualRanges(result[4], foldRange(35, 39, true, FoldSource.recovered, 'a3'), 'R4'); + assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), 'R1'); + assertEqualRanges(result[1], foldRange(10, 29, true, FoldSource.provider, 'a2', 'a2 ct'), 'R2'); + assertEqualRanges(result[2], foldRange(20, 28, true, FoldSource.recovered, 'b2', 'b2 ct'), 'R3'); + assertEqualRanges(result[3], foldRange(30, 39, true, FoldSource.recovered, 'b3', 'b3 ct'), 'R3'); + assertEqualRanges(result[4], foldRange(35, 39, true, FoldSource.recovered, 'a3', 'a3 ct'), 'R4'); }); test('sanitizeAndMerge4', () => { const regionSet1: FoldRange[] = [ - foldRange(1, 100, false, FoldSource.provider, 'a1'), // valid + foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), // valid ]; const regionSet2: FoldRange[] = [ - foldRange(20, 28, true, FoldSource.provider, 'b1'), // hidden - foldRange(30, 38, true, FoldSource.provider, 'b2'), // hidden + foldRange(20, 28, true, FoldSource.provider, 'b1', 'b1 ct'), // hidden + foldRange(30, 38, true, FoldSource.provider, 'b2', 'b2 ct'), // hidden ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); assert.strictEqual(result.length, 3, 'result length4'); - assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1'), 'R1'); - assertEqualRanges(result[1], foldRange(20, 28, true, FoldSource.recovered, 'b1'), 'R2'); - assertEqualRanges(result[2], foldRange(30, 38, true, FoldSource.recovered, 'b2'), 'R3'); + assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), 'R1'); + assertEqualRanges(result[1], foldRange(20, 28, true, FoldSource.recovered, 'b1', 'b1 ct'), 'R2'); + assertEqualRanges(result[2], foldRange(30, 38, true, FoldSource.recovered, 'b2', 'b2 ct'), 'R3'); }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c4d3b9e072a83..70a337133eb43 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7123,6 +7123,11 @@ declare namespace monaco.languages { * {@link FoldingRangeKind} for an enumeration of standardized kinds. */ kind?: FoldingRangeKind; + /** + * The text to be shown instead of the folded area. + * Will be shown at the end of the start line. + */ + collapsedText?: string; } export class FoldingRangeKind { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ff023d1fc3ba7..a2896c1c8211c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1376,7 +1376,7 @@ export namespace ProgressLocation { export namespace FoldingRange { export function from(r: vscode.FoldingRange): languages.FoldingRange { - const range: languages.FoldingRange = { start: r.start + 1, end: r.end + 1 }; + const range: languages.FoldingRange = { start: r.start + 1, end: r.end + 1, collapsedText: r.collapsedText }; if (r.kind) { range.kind = FoldingRangeKind.from(r.kind); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 17aee78078b6a..2901293b66910 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2978,10 +2978,13 @@ export class FoldingRange { kind?: FoldingRangeKind; - constructor(start: number, end: number, kind?: FoldingRangeKind) { + collapsedText?: string; + + constructor(start: number, end: number, kind?: FoldingRangeKind, collapsedText?: string) { this.start = start; this.end = end; this.kind = kind; + this.collapsedText = collapsedText; } } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 7399790e37523..c1918a14bfaa9 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -5187,6 +5187,11 @@ declare module 'vscode' { */ kind?: FoldingRangeKind; + /** + * The text to be shown instead of the folded area. + */ + collapsedText?: string; + /** * Creates a new folding range. * @@ -5194,7 +5199,7 @@ declare module 'vscode' { * @param end The end line of the folded range. * @param kind The kind of the folding range. */ - constructor(start: number, end: number, kind?: FoldingRangeKind); + constructor(start: number, end: number, kind?: FoldingRangeKind, collapsedText?: string); } /** From 93571289fc7435cbf71b8c7c925e3d62c035ee73 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 19 Dec 2022 17:26:43 -0500 Subject: [PATCH 02/19] support startColumn in FoldingRange --- .../browser/view/domLineBreaksComputer.ts | 20 +++-- src/vs/editor/common/languages.ts | 7 +- src/vs/editor/common/model.ts | 15 ++++ src/vs/editor/common/model/textModel.ts | 64 ++++++++++++-- .../editor/common/modelLineProjectionData.ts | 20 ++++- src/vs/editor/common/textModelEvents.ts | 88 ++++++++++++++++++- .../common/viewModel/modelLineProjection.ts | 11 +-- .../viewModel/monospaceLineBreaksComputer.ts | 24 +++-- .../editor/common/viewModel/viewModelImpl.ts | 12 ++- .../editor/common/viewModel/viewModelLines.ts | 10 ++- .../editor/contrib/folding/browser/folding.ts | 1 + .../folding/browser/foldingDecorations.ts | 40 +++++---- .../contrib/folding/browser/foldingModel.ts | 16 ++-- .../contrib/folding/browser/foldingRanges.ts | 84 ++++++++++-------- .../folding/browser/hiddenRangeModel.ts | 5 +- .../folding/browser/syntaxRangeProvider.ts | 63 +++++++------ src/vs/editor/test/common/model/model.test.ts | 14 +-- src/vs/monaco.d.ts | 11 ++- .../api/common/extHostTypeConverters.ts | 11 ++- src/vs/workbench/api/common/extHostTypes.ts | 5 +- src/vscode-dts/vscode.d.ts | 8 +- 21 files changed, 383 insertions(+), 146 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 8c1029e6b67b1..1249716aa80fa 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -9,7 +9,7 @@ import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; -import { LineInjectedText } from 'vs/editor/common/textModelEvents'; +import { InlineFoldRange, LineInjectedText } from 'vs/editor/common/textModelEvents'; import { InjectedTextOptions } from 'vs/editor/common/model'; import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from 'vs/editor/common/modelLineProjectionData'; @@ -27,19 +27,21 @@ export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ILineBreaksComputer { const requests: string[] = []; const injectedTexts: (LineInjectedText[] | null)[] = []; + const inlineFoldsRanges: (InlineFoldRange[] | null)[] = []; return { - addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => { + addRequest: (lineText: string, injectedText: LineInjectedText[] | null, inlineFolds: InlineFoldRange[] | null, previousLineBreakData: ModelLineProjectionData | null) => { requests.push(lineText); injectedTexts.push(injectedText); + inlineFoldsRanges.push(inlineFolds); }, finalize: () => { - return createLineBreaks(requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, injectedTexts); + return createLineBreaks(requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, injectedTexts, inlineFoldsRanges); } }; } } -function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', injectedTextsPerLine: (LineInjectedText[] | null)[]): (ModelLineProjectionData | null)[] { +function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', injectedTextsPerLine: (LineInjectedText[] | null)[], inlineFoldsPerLine: (InlineFoldRange[] | null)[]): (ModelLineProjectionData | null)[] { function createEmptyLineBreakWithPossiblyInjectedText(requestIdx: number): ModelLineProjectionData | null { const injectedTexts = injectedTextsPerLine[requestIdx]; if (injectedTexts) { @@ -50,7 +52,7 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe // creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK // because `breakOffsetsVisibleColumn` will never be used because it contains injected text - return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0); + return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], null, 0); } else { return null; } @@ -79,7 +81,8 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe const allCharOffsets: number[][] = []; const allVisibleColumns: number[][] = []; for (let i = 0; i < requests.length; i++) { - const lineContent = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]); + const contentWithInjectedText = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]); + const lineContent = InlineFoldRange.applyInlineFoldsWithInjectedText(contentWithInjectedText, inlineFoldsPerLine[i], injectedTextsPerLine[i]); let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; @@ -179,7 +182,10 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe injectionOffsets = null; } - result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength); + const curInlineFolds = inlineFoldsPerLine[i]; + const foldingOffset = InlineFoldRange.getFoldingOffset(curInlineFolds, curInjectedTexts); + + result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, foldingOffset, wrappedTextIndentLength); } document.body.removeChild(containerDomNode); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 4cad67ed3f672..51f62429ebd59 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1355,10 +1355,15 @@ export interface FoldingRangeProvider { export interface FoldingRange { /** - * The one-based start line of the range to fold. The folded area starts after the line's last character. + * The one-based start line of the range to fold. */ start: number; + /** + * The one-based start column of the range to fold. If not defined, folded area starts at the end of start line. + */ + startColumn?: number; + /** * The one-based end line of the range to fold. The folded area ends with the line's last character. */ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 861ad5b583252..955484fcb4e6f 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -188,6 +188,15 @@ export interface IModelDecorationOptions { * @internal */ hideInStringTokens?: boolean | null; + + /** + * If set, this decoration inline content will not be rendered. + * Only applies to start line if range is multiline. + * This decorations's injected text will still render normally. + * !!Currently only supports hiding to the end of the line.!! + * @internal + */ + hideContent?: boolean | null; } /** @@ -974,6 +983,12 @@ export interface ITextModel { */ getInjectedTextDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain an inline fold. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getInlineFoldsDecorations(ownerId?: number): IModelDecoration[]; + /** * @internal */ diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 9447ad435700e..4bb7fa661076c 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -38,7 +38,7 @@ import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeText import { SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch'; import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InlineFoldRange, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService'; @@ -1443,10 +1443,20 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const firstEditLineNumber = currentEditStartLineNumber; const lastInsertedLineNumber = currentEditStartLineNumber + insertingLinesCnt; + const currentEditStartOffset = this.getOffsetAt(new Position(firstEditLineNumber, 1)); + const currentEditEndOffset = this.getOffsetAt(new Position(lastInsertedLineNumber, this.getLineMaxColumn(lastInsertedLineNumber))); + const decorationsWithInjectedTextInEditedRange = this._decorationsTree.getInjectedTextInInterval( this, - this.getOffsetAt(new Position(firstEditLineNumber, 1)), - this.getOffsetAt(new Position(lastInsertedLineNumber, this.getLineMaxColumn(lastInsertedLineNumber))), + currentEditStartOffset, + currentEditEndOffset, + 0 + ); + + const decorationsWithInlineFoldsInEditedRange = this._decorationsTree.getInlineFoldsInInterval( + this, + currentEditStartOffset, + currentEditEndOffset, 0 ); @@ -1454,18 +1464,27 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const injectedTextInEditedRange = LineInjectedText.fromDecorations(decorationsWithInjectedTextInEditedRange); const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange); + const inlineFoldsInEditedRange = InlineFoldRange.fromDecorations(decorationsWithInlineFoldsInEditedRange); + const inlineFoldsInEditedRangeQueue = new ArrayQueue(inlineFoldsInEditedRange); + + for (let j = editingLinesCnt; j >= 0; j--) { const editLineNumber = startLineNumber + j; const currentEditLineNumber = currentEditStartLineNumber + j; injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber > currentEditLineNumber); - const decorationsInCurrentLine = injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber === currentEditLineNumber); + const injectedTextInCurrentLine = injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber === currentEditLineNumber); + + + inlineFoldsInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber > currentEditLineNumber); + const inlineFoldsInCurrentLine = inlineFoldsInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber === currentEditLineNumber); rawContentChanges.push( new ModelRawLineChanged( editLineNumber, this.getLineContent(currentEditLineNumber), - decorationsInCurrentLine + injectedTextInCurrentLine, + inlineFoldsInCurrentLine )); } @@ -1477,11 +1496,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (editingLinesCnt < insertingLinesCnt) { const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange); + const inlineFoldsInEditedRangeQueue = new ArrayQueue(inlineFoldsInEditedRange); // Must insert some lines const spliceLineNumber = startLineNumber + editingLinesCnt; const cnt = insertingLinesCnt - editingLinesCnt; const fromLineNumber = newLineCount - lineCount - cnt + spliceLineNumber + 1; const injectedTexts: (LineInjectedText[] | null)[] = []; + const inlineFolds: (InlineFoldRange[] | null)[] = []; const newLines: string[] = []; for (let i = 0; i < cnt; i++) { const lineNumber = fromLineNumber + i; @@ -1489,6 +1510,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber < lineNumber); injectedTexts[i] = injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber === lineNumber); + + inlineFoldsInEditedRangeQueue.takeWhile(r => r.lineNumber < lineNumber); + inlineFolds[i] = inlineFoldsInEditedRangeQueue.takeWhile(r => r.lineNumber === lineNumber); } rawContentChanges.push( @@ -1496,7 +1520,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati spliceLineNumber + 1, startLineNumber + insertingLinesCnt, newLines, - injectedTexts + injectedTexts, + inlineFolds ) ); } @@ -1553,7 +1578,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } const affectedLines = Array.from(affectedInjectedTextLines); - const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); + const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber), this._getInlineFoldsInLine(lineNumber))); this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); } @@ -1738,6 +1763,18 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return LineInjectedText.fromDecorations(result).filter(t => t.lineNumber === lineNumber); } + public getInlineFoldsDecorations(ownerId: number = 0): model.IModelDecoration[] { + return this.getInjectedTextDecorations(ownerId).filter(d => d.options.hideContent); + } + + private _getInlineFoldsInLine(lineNumber: number): InlineFoldRange[] { + const startOffset = this._buffer.getOffsetAt(lineNumber, 1); + const endOffset = startOffset + this._buffer.getLineLength(lineNumber); + + const result = this._decorationsTree.getInjectedTextInInterval(this, startOffset, endOffset, 0).filter(d => d.options.hideContent); + return InlineFoldRange.fromDecorations(result).filter(t => t.lineNumber === lineNumber); + } + public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] { let result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, false); result = result.concat(this._decorationProvider.getAllDecorations(ownerId, filterOutValidation)); @@ -2034,6 +2071,15 @@ class DecorationsTrees { return this._ensureNodesHaveRanges(host, result).filter((i) => i.options.showIfCollapsed || !i.range.isEmpty()); } + public getInlineFoldsInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number): model.IModelDecoration[] { + return this.getInjectedTextInInterval(host, start, end, filterOwnerId).filter((i) => i.options.hideContent); + } + + public getAllInlineFolds(host: IDecorationsTreesHost, filterOwnerId: number): model.IModelDecoration[] { + return this.getAllInjectedText(host, filterOwnerId).filter((i) => i.options.hideContent); + } + + public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean): model.IModelDecoration[] { const versionId = host.getVersionId(); const result = this._search(filterOwnerId, filterOutValidation, overviewRulerOnly, versionId); @@ -2259,6 +2305,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { readonly before: ModelDecorationInjectedTextOptions | null; readonly hideInCommentTokens: boolean | null; readonly hideInStringTokens: boolean | null; + readonly hideContent?: boolean | null | undefined; private constructor(options: model.IModelDecorationOptions) { @@ -2287,6 +2334,9 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.before = options.before ? ModelDecorationInjectedTextOptions.from(options.before) : null; this.hideInCommentTokens = options.hideInCommentTokens ?? false; this.hideInStringTokens = options.hideInStringTokens ?? false; + this.hideContent = options.hideContent ?? false; + console.log({ hideContent: options.hideContent }); + } } ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({ description: 'empty' }); diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index 78d14ef17de64..27211c26da23f 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -8,7 +8,7 @@ import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { Position } from 'vs/editor/common/core/position'; import { InjectedTextCursorStops, InjectedTextOptions, PositionAffinity } from 'vs/editor/common/model'; -import { LineInjectedText } from 'vs/editor/common/textModelEvents'; +import { InlineFoldRange, LineInjectedText } from 'vs/editor/common/textModelEvents'; /** * *input*: @@ -28,15 +28,25 @@ import { LineInjectedText } from 'vs/editor/common/textModelEvents'; * xxxxxx[ii]xxxx * ``` * + * -> folding at offset `$` in `xxxxxx[iiiiiii|iii]xxxxxxxxxxx|xx$xxxx[ii]xxxx|`: + * ``` + * xxxxxx[iiiiiii + * iii]xxxxxxxxxxx + * xx + * ``` + * * -> applying wrappedTextIndentLength, *output*: * ``` * xxxxxx[iiiiiii * iii]xxxxxxxxxxx - * xxxxxx[ii]xxxx + * xx * ``` */ export class ModelLineProjectionData { constructor( + /** + * Offsets after foldingOffset are still intact. + */ public injectionOffsets: number[] | null, /** * `injectionOptions.length` must equal `injectionOffsets.length` @@ -51,6 +61,10 @@ export class ModelLineProjectionData { * Refers to offsets after applying injections */ public breakOffsetsVisibleColumn: number[], + /** + * Refers to folding offset after applying injections to the source. + */ + public foldingOffset: number | null, public wrappedTextIndentLength: number ) { } @@ -336,6 +350,6 @@ export interface ILineBreaksComputer { /** * Pass in `previousLineBreakData` if the only difference is in breaking columns!!! */ - addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null): void; + addRequest(lineText: string, injectedText: LineInjectedText[] | null, inlineFolds: InlineFoldRange[] | null, previousLineBreakData: ModelLineProjectionData | null): void; finalize(): (ModelLineProjectionData | null)[]; } diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index a2f38b12900ac..31684a4aa4dbc 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IModelDecoration, InjectedTextOptions } from 'vs/editor/common/model'; @@ -161,7 +161,7 @@ export class LineInjectedText { decoration.range.startLineNumber, decoration.range.startColumn, decoration.options.before, - 0, + decoration.options.hideContent ? 2 : 0, //collapsedText should always render last )); } if (decoration.options.after && decoration.options.after.content.length > 0) { @@ -199,6 +199,73 @@ export class LineInjectedText { } } +/** + * Represents a hidden range inside a line. + * !!Currently only supports hiding to the end of the line.!! + * @internal + */ +export class InlineFoldRange { + public readonly ownerId: number; + public readonly lineNumber: number; + public readonly startColumn: number; + /** + * If set to null, it will mark the end of the line. + */ + public readonly endColumn: number | null; + + constructor(ownerId: number, lineNumber: number, startColumn: number, endColumn: number | null = null) { + this.ownerId = ownerId; + this.lineNumber = lineNumber; + this.startColumn = startColumn; + this.endColumn = endColumn; + } + + public static fromDecorations(decorations: IModelDecoration[]): InlineFoldRange[] { + return decorations + .sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)) + .map((decoration) => new InlineFoldRange( + decoration.ownerId, + decoration.range.startLineNumber, + decoration.range.startColumn, + )); + } + + /** + * !!Currently only supports hiding to the end of the line.!! + */ + public static applyInlineFoldsWithInjectedText(lineTextAfterInjections: string, inlineFolds: InlineFoldRange[] | null, injectedTexts: LineInjectedText[] | null): string { + const foldingOffset = InlineFoldRange.getFoldingOffset(inlineFolds, injectedTexts); + if (!foldingOffset) { + return lineTextAfterInjections; + } + return lineTextAfterInjections.substring(0, foldingOffset); + } + + /** + * !!Currently only supports hiding to the end of the line.!! + */ + public static getFoldingOffset(inlineFolds: InlineFoldRange[] | null, injectedTexts: LineInjectedText[] | null): number | null { + if (!inlineFolds || inlineFolds.length === 0) { + return null; + } + + const originalFoldingColumn = inlineFolds[0].startColumn; + const originalFoldingIndex = originalFoldingColumn - 1; + if (!injectedTexts || injectedTexts.length === 0) { + return originalFoldingIndex; + } + + let sumUnfoldedInjectedTextLengths = 0; + for (const injectedText of injectedTexts) { + if (injectedText.column > originalFoldingColumn) { + break; + } + sumUnfoldedInjectedTextLengths += injectedText.options.content.length; + } + return originalFoldingIndex + sumUnfoldedInjectedTextLengths; + } +} + /** * An event describing that a line has changed in a model. * @internal @@ -217,11 +284,18 @@ export class ModelRawLineChanged { * The injected text on the line. */ public readonly injectedText: LineInjectedText[] | null; + /** + * The hidden ranges in the line. + * !!Currently only supports hiding to the end of the line.!! + */ + public readonly inlineFolds: InlineFoldRange[] | null; + - constructor(lineNumber: number, detail: string, injectedText: LineInjectedText[] | null) { + constructor(lineNumber: number, detail: string, injectedText: LineInjectedText[] | null, inlineFolds: InlineFoldRange[] | null) { this.lineNumber = lineNumber; this.detail = detail; this.injectedText = injectedText; + this.inlineFolds = inlineFolds; } } @@ -268,9 +342,15 @@ export class ModelRawLinesInserted { * The injected texts for every inserted line. */ public readonly injectedTexts: (LineInjectedText[] | null)[]; + /** + * The hidden ranges in the line. + * !!Currently only supports hiding to the end of the line.!! + */ + public readonly inlineFolds: (InlineFoldRange[] | null)[]; - constructor(fromLineNumber: number, toLineNumber: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) { + constructor(fromLineNumber: number, toLineNumber: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[], inlineFolds: (InlineFoldRange[] | null)[]) { this.injectedTexts = injectedTexts; + this.inlineFolds = inlineFolds; this.fromLineNumber = fromLineNumber; this.toLineNumber = toLineNumber; this.detail = detail; diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index 81be5db0a101f..df630644bcca5 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -63,6 +63,7 @@ export function createModelLineProjection(lineBreakData: ModelLineProjectionData * This projection is used to * * wrap model lines * * inject text + * * hide inline content */ class ModelLineProjection implements IModelLineProjection { private readonly _projectionData: ModelLineProjectionData; @@ -96,8 +97,8 @@ class ModelLineProjection implements IModelLineProjection { public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string { this._assertVisible(); - const startOffsetInInputWithInjections = outputLineIndex > 0 ? this._projectionData.breakOffsets[outputLineIndex - 1] : 0; - const endOffsetInInputWithInjections = this._projectionData.breakOffsets[outputLineIndex]; + const startOffsetInInputInjectedFolded = outputLineIndex > 0 ? this._projectionData.breakOffsets[outputLineIndex - 1] : 0; + const endOffsetInInputInjectedFolded = this._projectionData.breakOffsets[outputLineIndex]; let r: string; if (this._projectionData.injectionOffsets !== null) { @@ -114,13 +115,13 @@ class ModelLineProjection implements IModelLineProjection { model.getLineContent(modelLineNumber), injectedTexts ); - r = lineWithInjections.substring(startOffsetInInputWithInjections, endOffsetInInputWithInjections); + r = lineWithInjections.substring(startOffsetInInputInjectedFolded, endOffsetInInputInjectedFolded); } else { r = model.getValueInRange({ startLineNumber: modelLineNumber, - startColumn: startOffsetInInputWithInjections + 1, + startColumn: startOffsetInInputInjectedFolded + 1, endLineNumber: modelLineNumber, - endColumn: endOffsetInInputWithInjections + 1 + endColumn: endOffsetInInputInjectedFolded + 1 }); } diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 05a54a8ab2979..745cc344608e8 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -8,7 +8,7 @@ import * as strings from 'vs/base/common/strings'; import { WrappingIndent, IComputedEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; -import { LineInjectedText } from 'vs/editor/common/textModelEvents'; +import { InlineFoldRange, LineInjectedText } from 'vs/editor/common/textModelEvents'; import { InjectedTextOptions } from 'vs/editor/common/model'; import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData } from 'vs/editor/common/modelLineProjectionData'; @@ -29,11 +29,13 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ILineBreaksComputer { const requests: string[] = []; const injectedTexts: (LineInjectedText[] | null)[] = []; + const inlineFoldsRanges: (InlineFoldRange[] | null)[] = []; const previousBreakingData: (ModelLineProjectionData | null)[] = []; return { - addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => { + addRequest: (lineText: string, injectedText: LineInjectedText[] | null, inlineFolds: InlineFoldRange[] | null, previousLineBreakData: ModelLineProjectionData | null) => { requests.push(lineText); injectedTexts.push(injectedText); + inlineFoldsRanges.push(inlineFolds); previousBreakingData.push(previousLineBreakData); }, finalize: () => { @@ -41,11 +43,12 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa const result: (ModelLineProjectionData | null)[] = []; for (let i = 0, len = requests.length; i < len; i++) { const injectedText = injectedTexts[i]; + const inlineFolds = inlineFoldsRanges[i]; const previousLineBreakData = previousBreakingData[i]; - if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText) { + if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText && previousLineBreakData.foldingOffset === null && !inlineFolds) { result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); } else { - result[i] = createLineBreaks(this.classifier, requests[i], injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); + result[i] = createLineBreaks(this.classifier, requests[i], injectedText, inlineFolds, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); } } arrPool1.length = 0; @@ -355,8 +358,9 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla return previousBreakingData; } -function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ModelLineProjectionData | null { - const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts); +function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, inlineFolds: InlineFoldRange[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ModelLineProjectionData | null { + const lineTextWithInjections = LineInjectedText.applyInjectedText(_lineText, injectedTexts); + const lineText = InlineFoldRange.applyInlineFoldsWithInjectedText(lineTextWithInjections, inlineFolds, injectedTexts); let injectionOptions: InjectedTextOptions[] | null; let injectionOffsets: number[] | null; @@ -368,13 +372,15 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st injectionOffsets = null; } + const foldingOffset = InlineFoldRange.getFoldingOffset(inlineFolds, injectedTexts); + if (firstLineBreakColumn === -1) { if (!injectionOptions) { return null; } // creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK // because `breakOffsetsVisibleColumn` will never be used because it contains injected text - return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0); + return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], foldingOffset, 0); } const len = lineText.length; @@ -384,7 +390,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st } // creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK // because `breakOffsetsVisibleColumn` will never be used because it contains injected text - return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0); + return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], foldingOffset, 0); } const isKeepAll = (wordBreak === 'keepAll'); @@ -463,7 +469,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st breakingOffsets[breakingOffsetsCount] = len; breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn; - return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); + return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, foldingOffset, wrappedTextIndentLength); } function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 9033f5e84db17..33deb50401dea 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -285,7 +285,11 @@ export class ViewModel extends Disposable implements IViewModel { if (injectedText) { injectedText = injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); } - lineBreaksComputer.addRequest(line, injectedText, null); + let inlineFolds = change.inlineFolds[lineIdx]; + if (inlineFolds) { + inlineFolds = inlineFolds.filter(element => (!element.ownerId || element.ownerId === this._editorId)); + } + lineBreaksComputer.addRequest(line, injectedText, inlineFolds, null); } break; } @@ -294,7 +298,11 @@ export class ViewModel extends Disposable implements IViewModel { if (change.injectedText) { injectedText = change.injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId)); } - lineBreaksComputer.addRequest(change.detail, injectedText, null); + let inlineFolds: textModelEvents.InlineFoldRange[] | null = null; + if (change.inlineFolds) { + inlineFolds = change.inlineFolds.filter(element => (!element.ownerId || element.ownerId === this._editorId)); + } + lineBreaksComputer.addRequest(change.detail, injectedText, inlineFolds, null); break; } } diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 96746d3bb9f90..10bfbea0aa3c1 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from 'vs/editor/common/model'; import { IActiveIndentGuideInfo, BracketGuideOptions, IndentGuide, IndentGuideHorizontalLine } from 'vs/editor/common/textModelGuides'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { LineInjectedText } from 'vs/editor/common/textModelEvents'; +import { InlineFoldRange, LineInjectedText } from 'vs/editor/common/textModelEvents'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { createModelLineProjection, IModelLineProjection } from 'vs/editor/common/viewModel/modelLineProjection'; import { ILineBreaksComputer, ModelLineProjectionData, InjectedText, ILineBreaksComputerFactory } from 'vs/editor/common/modelLineProjectionData'; @@ -125,13 +125,17 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { const linesContent = this.model.getLinesContent(); const injectedTextDecorations = this.model.getInjectedTextDecorations(this._editorId); + const inlineFoldsDecorations = this.model.getInlineFoldsDecorations(this._editorId); const lineCount = linesContent.length; const lineBreaksComputer = this.createLineBreaksComputer(); const injectedTextQueue = new arrays.ArrayQueue(LineInjectedText.fromDecorations(injectedTextDecorations)); + const inlineFoldsQueue = new arrays.ArrayQueue(InlineFoldRange.fromDecorations(inlineFoldsDecorations)); for (let i = 0; i < lineCount; i++) { const lineInjectedText = injectedTextQueue.takeWhile(t => t.lineNumber === i + 1); - lineBreaksComputer.addRequest(linesContent[i], lineInjectedText, previousLineBreaks ? previousLineBreaks[i] : null); + const inlineFolds = inlineFoldsQueue.takeWhile(f => f.lineNumber === i + 1); + + lineBreaksComputer.addRequest(linesContent[i], lineInjectedText, inlineFolds, previousLineBreaks ? previousLineBreaks[i] : null); } const linesBreaks = lineBreaksComputer.finalize(); @@ -1130,7 +1134,7 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { public createLineBreaksComputer(): ILineBreaksComputer { const result: null[] = []; return { - addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null) => { + addRequest: (lineText: string, injectedText: LineInjectedText[] | null, inlineFolds: InlineFoldRange[] | null, previousLineBreakData: ModelLineProjectionData | null) => { result.push(null); }, finalize: () => { diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 1dd97769d729f..f4fc7097233dc 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -1111,6 +1111,7 @@ class FoldRangeFromSelectionAction extends FoldingAction { collapseRanges.push({ startLineNumber: selection.startLineNumber, endLineNumber: endLineNumber, + startColumn: selection.startColumn, type: undefined, collapsedText: undefined, isCollapsed: true, diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index 5b3ac6c1ec589..d6ab291a41294 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -5,7 +5,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IModelDecorationOptions, IModelDecorationsChangeAccessor, InjectedTextOptions, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationOptions, IModelDecorationsChangeAccessor, InjectedTextCursorStops, InjectedTextOptions, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IDecorationProvider } from 'vs/editor/contrib/folding/browser/foldingModel'; import { localize } from 'vs/nls'; @@ -24,52 +24,53 @@ export class FoldingDecorationProvider implements IDecorationProvider { //To always keep decoration in injectedTextTree between toggles, otherwise would crash private static EMPTY_INJECTED_TEXT: InjectedTextOptions = { content: '' }; - private static readonly COLLAPSED_VISUAL_DECORATION = { + private static readonly COLLAPSED_VISUAL_DECORATION: IModelDecorationOptions = { description: 'folding-collapsed-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - afterContentClassName: 'inline-folded', isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon), + hideContent: true }; - private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = { + private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION: IModelDecorationOptions = { description: 'folding-collapsed-highlighted-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, className: 'folded-background', isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon), + hideContent: true }; - private static readonly MANUALLY_COLLAPSED_VISUAL_DECORATION = { + private static readonly MANUALLY_COLLAPSED_VISUAL_DECORATION: IModelDecorationOptions = { description: 'folding-manually-collapsed-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - afterContentClassName: 'inline-folded', isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon), + hideContent: true }; - private static readonly MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = { + private static readonly MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION: IModelDecorationOptions = { description: 'folding-manually-collapsed-highlighted-visual-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - afterContentClassName: 'inline-folded', className: 'folded-background', isWholeLine: true, - firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon) + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon), + hideContent: true }; - private static readonly NO_CONTROLS_COLLAPSED_RANGE_DECORATION = { + private static readonly NO_CONTROLS_COLLAPSED_RANGE_DECORATION: IModelDecorationOptions = { description: 'folding-no-controls-range-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - afterContentClassName: 'inline-folded', - isWholeLine: true + isWholeLine: true, + hideContent: true }; - private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION = { + private static readonly NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION: IModelDecorationOptions = { description: 'folding-no-controls-range-decoration', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - afterContentClassName: 'inline-folded', className: 'folded-background', - isWholeLine: true + isWholeLine: true, + hideContent: true }; private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ @@ -153,7 +154,8 @@ export class FoldingDecorationProvider implements IDecorationProvider { const before: InjectedTextOptions = { content: this.fixSpace(collapsedText), inlineClassName: 'collapsed-text', - inlineClassNameAffectsLetterSpacing: true + inlineClassNameAffectsLetterSpacing: true, + cursorStops: InjectedTextCursorStops.None }; return ModelDecorationOptions.register({ ...decorationOption, before }); } diff --git a/src/vs/editor/contrib/folding/browser/foldingModel.ts b/src/vs/editor/contrib/folding/browser/foldingModel.ts index a1e8cd35a9da3..7238b75c6e454 100644 --- a/src/vs/editor/contrib/folding/browser/foldingModel.ts +++ b/src/vs/editor/contrib/folding/browser/foldingModel.ts @@ -19,14 +19,11 @@ export interface FoldingModelChangeEvent { collapseStateChanged?: FoldingRegion[]; } -interface ILineMemento extends ILineRange { +interface FoldRangeMemento extends FoldRange { checksum?: number; - isCollapsed?: boolean; - source?: FoldSource; - collapsedText?: string; } -export type CollapseMemento = ILineMemento[]; +export type CollapseMemento = FoldRangeMemento[]; export class FoldingModel { private readonly _textModel: ITextModel; @@ -125,12 +122,13 @@ export class FoldingModel { for (let index = 0, limit = newRegions.length; index < limit; index++) { const startLineNumber = newRegions.getStartLineNumber(index); const endLineNumber = newRegions.getEndLineNumber(index); + const startColumn = newRegions.getStartColumn(index); const collapsedText = newRegions.getCollapsedText(index); const isCollapsed = newRegions.isCollapsed(index); const isManual = newRegions.getSource(index) !== FoldSource.provider; const decorationRange = { startLineNumber: startLineNumber, - startColumn: this._textModel.getLineMaxColumn(startLineNumber), + startColumn: startColumn ?? this._textModel.getLineMaxColumn(startLineNumber), endLineNumber: endLineNumber, endColumn: this._textModel.getLineMaxColumn(endLineNumber) + 1 }; @@ -169,6 +167,7 @@ export class FoldingModel { foldedRanges.push({ startLineNumber: decRange.startLineNumber, endLineNumber: decRange.endLineNumber, + startColumn: decRange.startColumn, type: foldRange.type, collapsedText: foldRange.collapsedText, isCollapsed, @@ -186,7 +185,7 @@ export class FoldingModel { */ public getMemento(): CollapseMemento | undefined { const foldedOrManualRanges = this._currentFoldedOrManualRanges(); - const result: ILineMemento[] = []; + const result: FoldRangeMemento[] = []; const maxLineNumber = this._textModel.getLineCount(); for (let i = 0, limit = foldedOrManualRanges.length; i < limit; i++) { const range = foldedOrManualRanges[i]; @@ -197,10 +196,12 @@ export class FoldingModel { result.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, + startColumn: range.startColumn, isCollapsed: range.isCollapsed, source: range.source, checksum: checksum, collapsedText: range.collapsedText, + type: undefined, }); } return (result.length > 0) ? result : undefined; @@ -224,6 +225,7 @@ export class FoldingModel { rangesToRestore.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, + startColumn: range.startColumn, type: undefined, collapsedText: range.collapsedText, isCollapsed: range.isCollapsed ?? true, diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index aa419e98eac32..06313e02f1af7 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -23,6 +23,7 @@ export const foldSourceAbbr = { export interface FoldRange { startLineNumber: number; endLineNumber: number; + startColumn: number | undefined; type: string | undefined; collapsedText: string | undefined; isCollapsed: boolean; @@ -31,6 +32,7 @@ export interface FoldRange { export const MAX_FOLDING_REGIONS = 0xFFFF; export const MAX_LINE_NUMBER = 0xFFFFFF; +export const MAX_COLUMN_NUMBER = 0xFFFFFF; const MASK_INDENT = 0xFF000000; @@ -60,8 +62,9 @@ class BitField { } export class FoldingRegions { - private readonly _startIndexes: Uint32Array; - private readonly _endIndexes: Uint32Array; + private readonly _startLineIndexes: Uint32Array; + private readonly _endLineIndexes: Uint32Array; + private readonly _startColumnIndexes: Array | undefined; private readonly _collapseStates: BitField; private readonly _userDefinedStates: BitField; private readonly _recoveredStates: BitField; @@ -70,15 +73,16 @@ export class FoldingRegions { private readonly _types: Array | undefined; private readonly _collapsedTexts: Array | undefined; - constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, types?: Array, collapsedTexts?: Array) { - if (startIndexes.length !== endIndexes.length || startIndexes.length > MAX_FOLDING_REGIONS) { + constructor(startLineIndexes: Uint32Array, endLineIndexes: Uint32Array, startColumnIndexes?: Array, types?: Array, collapsedTexts?: Array) { + if (startLineIndexes.length !== endLineIndexes.length || startLineIndexes.length > MAX_FOLDING_REGIONS) { throw new Error('invalid startIndexes or endIndexes size'); } - this._startIndexes = startIndexes; - this._endIndexes = endIndexes; - this._collapseStates = new BitField(startIndexes.length); - this._userDefinedStates = new BitField(startIndexes.length); - this._recoveredStates = new BitField(startIndexes.length); + this._startLineIndexes = startLineIndexes; + this._endLineIndexes = endLineIndexes; + this._startColumnIndexes = startColumnIndexes; + this._collapseStates = new BitField(startLineIndexes.length); + this._userDefinedStates = new BitField(startLineIndexes.length); + this._recoveredStates = new BitField(startLineIndexes.length); this._types = types; this._collapsedTexts = collapsedTexts; this._parentsComputed = false; @@ -92,9 +96,9 @@ export class FoldingRegions { const index = parentIndexes[parentIndexes.length - 1]; return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber; }; - for (let i = 0, len = this._startIndexes.length; i < len; i++) { - const startLineNumber = this._startIndexes[i]; - const endLineNumber = this._endIndexes[i]; + for (let i = 0, len = this._startLineIndexes.length; i < len; i++) { + const startLineNumber = this._startLineIndexes[i]; + const endLineNumber = this._endLineIndexes[i]; if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) { throw new Error('startLineNumber or endLineNumber must not exceed ' + MAX_LINE_NUMBER); } @@ -103,22 +107,26 @@ export class FoldingRegions { } const parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1; parentIndexes.push(i); - this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24); - this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16); + this._startLineIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24); + this._endLineIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16); } } } public get length(): number { - return this._startIndexes.length; + return this._startLineIndexes.length; } public getStartLineNumber(index: number): number { - return this._startIndexes[index] & MAX_LINE_NUMBER; + return this._startLineIndexes[index] & MAX_LINE_NUMBER; } public getEndLineNumber(index: number): number { - return this._endIndexes[index] & MAX_LINE_NUMBER; + return this._endLineIndexes[index] & MAX_LINE_NUMBER; + } + + public getStartColumn(index: number): number | undefined { + return this._startColumnIndexes ? this._startColumnIndexes[index] : undefined; } public getType(index: number): string | undefined { @@ -198,7 +206,7 @@ export class FoldingRegions { public getParentIndex(index: number) { this.ensureParentIndices(); - const parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16); + const parent = ((this._startLineIndexes[index] & MASK_INDENT) >>> 24) + ((this._endLineIndexes[index] & MASK_INDENT) >>> 16); if (parent === MAX_FOLDING_REGIONS) { return -1; } @@ -210,7 +218,7 @@ export class FoldingRegions { } private findIndex(line: number) { - let low = 0, high = this._startIndexes.length; + let low = 0, high = this._startLineIndexes.length; if (high === 0) { return -1; // no children } @@ -254,8 +262,9 @@ export class FoldingRegions { public toFoldRange(index: number): FoldRange { return { - startLineNumber: this._startIndexes[index] & MAX_LINE_NUMBER, - endLineNumber: this._endIndexes[index] & MAX_LINE_NUMBER, + startLineNumber: this._startLineIndexes[index] & MAX_LINE_NUMBER, + endLineNumber: this._endLineIndexes[index] & MAX_LINE_NUMBER, + startColumn: this._startColumnIndexes ? this._startColumnIndexes[index] : undefined, type: this._types ? this._types[index] : undefined, collapsedText: this._collapsedTexts ? this._collapsedTexts[index] : undefined, isCollapsed: this.isCollapsed(index), @@ -265,32 +274,29 @@ export class FoldingRegions { public static fromFoldRanges(ranges: FoldRange[]): FoldingRegions { const rangesLength = ranges.length; - const startIndexes = new Uint32Array(rangesLength); - const endIndexes = new Uint32Array(rangesLength); + const startLineIndexes = new Uint32Array(rangesLength); + const endLineIndexes = new Uint32Array(rangesLength); + let startColumnIndexes: Array | undefined = []; let types: Array | undefined = []; - let gotTypes = false; let collapsedTexts: Array | undefined = []; - let gotCollapsedTexts = false; for (let i = 0; i < rangesLength; i++) { const range = ranges[i]; - startIndexes[i] = range.startLineNumber; - endIndexes[i] = range.endLineNumber; + startLineIndexes[i] = range.startLineNumber; + endLineIndexes[i] = range.endLineNumber; + startColumnIndexes.push(range.startColumn); types.push(range.type); - if (range.type) { - gotTypes = true; - } collapsedTexts.push(range.collapsedText); - if (range.collapsedText) { - gotCollapsedTexts = true; - } } - if (!gotTypes) { + if (startColumnIndexes.every(c => c === undefined)) { + startColumnIndexes = undefined; + } + if (types.every(t => t === undefined)) { types = undefined; } - if (!gotCollapsedTexts) { + if (collapsedTexts.every(t => t === undefined)) { collapsedTexts = undefined; } - const regions = new FoldingRegions(startIndexes, endIndexes, types, collapsedTexts); + const regions = new FoldingRegions(startLineIndexes, endLineIndexes, startColumnIndexes, types, collapsedTexts); for (let i = 0; i < rangesLength; i++) { if (ranges[i].isCollapsed) { regions.setCollapsed(i, true); @@ -341,7 +347,7 @@ export class FoldingRegions { let useRange: FoldRange | undefined = undefined; if (nextB && (!nextA || nextA.startLineNumber >= nextB.startLineNumber)) { if (nextA && nextA.startLineNumber === nextB.startLineNumber) { - if (nextB.source === FoldSource.userDefined) { + if (nextB.source === FoldSource.userDefined || (nextA.startColumn ?? MAX_COLUMN_NUMBER) >= (nextB.startColumn ?? MAX_COLUMN_NUMBER)) { // a user defined range (possibly unfolded) useRange = nextB; } else { @@ -412,6 +418,10 @@ export class FoldingRegion { return this.ranges.getStartLineNumber(this.index); } + public get startColumn() { + return this.ranges.getStartColumn(this.index); + } + public get endLineNumber() { return this.ranges.getEndLineNumber(this.index); } diff --git a/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts b/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts index ea8ff076531d8..3a06860de325e 100644 --- a/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts +++ b/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts @@ -58,18 +58,19 @@ export class HiddenRangeModel { const startLineNumber = ranges.getStartLineNumber(i) + 1; // the first line is not hidden const endLineNumber = ranges.getEndLineNumber(i); + const startColumn = ranges.getStartColumn(i); if (lastCollapsedStart <= startLineNumber && endLineNumber <= lastCollapsedEnd) { // ignore ranges contained in collapsed regions continue; } - if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].startLineNumber === startLineNumber && this._hiddenRanges[k].endLineNumber === endLineNumber) { + if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].startLineNumber === startLineNumber && this._hiddenRanges[k].endLineNumber === endLineNumber && this._hiddenRanges[k].startColumn === startColumn) { // reuse the old ranges newHiddenAreas.push(this._hiddenRanges[k]); k++; } else { updateHiddenAreas = true; - newHiddenAreas.push(new Range(startLineNumber, 1, endLineNumber, 1)); + newHiddenAreas.push(new Range(startLineNumber, startColumn ?? 1, endLineNumber, 1)); } lastCollapsedStart = startLineNumber; lastCollapsedEnd = endLineNumber; diff --git a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts index cbead62085350..1cb7db3780d0e 100644 --- a/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts +++ b/src/vs/editor/contrib/folding/browser/syntaxRangeProvider.ts @@ -9,7 +9,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; import { FoldingContext, FoldingRange, FoldingRangeProvider } from 'vs/editor/common/languages'; import { FoldingLimitReporter, RangeProvider } from './folding'; -import { FoldingRegions, MAX_LINE_NUMBER } from './foldingRanges'; +import { FoldingRegions, MAX_COLUMN_NUMBER, MAX_LINE_NUMBER } from './foldingRanges'; export interface IFoldingRangeData extends FoldingRange { rank: number; @@ -71,7 +71,7 @@ function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextMode const nLines = model.getLineCount(); for (const r of ranges) { if (r.start > 0 && r.end > r.start && r.end <= nLines) { - rangeData.push({ start: r.start, end: r.end, rank: i, kind: r.kind, collapsedText: r.collapsedText }); + rangeData.push({ start: r.start, end: r.end, startColumn: r.startColumn, rank: i, kind: r.kind, collapsedText: r.collapsedText }); } } } @@ -83,8 +83,9 @@ function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextMode } class RangesCollector { - private readonly _startIndexes: number[]; - private readonly _endIndexes: number[]; + private readonly _startLineIndexes: number[]; + private readonly _endLineIndexes: number[]; + private readonly _startColumnIndexes: Array; private readonly _nestingLevels: number[]; private readonly _nestingLevelCounts: number[]; private readonly _types: Array; @@ -93,8 +94,9 @@ class RangesCollector { private readonly _foldingRangesLimit: FoldingLimitReporter; constructor(foldingRangesLimit: FoldingLimitReporter) { - this._startIndexes = []; - this._endIndexes = []; + this._startLineIndexes = []; + this._endLineIndexes = []; + this._startColumnIndexes = []; this._nestingLevels = []; this._nestingLevelCounts = []; this._types = []; @@ -103,13 +105,14 @@ class RangesCollector { this._foldingRangesLimit = foldingRangesLimit; } - public add(startLineNumber: number, endLineNumber: number, type: string | undefined, collapsedText: string | undefined, nestingLevel: number) { + public add(startLineNumber: number, endLineNumber: number, startColumn: number | undefined, type: string | undefined, collapsedText: string | undefined, nestingLevel: number) { if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) { return; } const index = this._length; - this._startIndexes[index] = startLineNumber; - this._endIndexes[index] = endLineNumber; + this._startLineIndexes[index] = startLineNumber; + this._endLineIndexes[index] = endLineNumber; + this._startColumnIndexes[index] = startColumn; this._nestingLevels[index] = nestingLevel; this._types[index] = type; this._collapsedTexts[index] = collapsedText; @@ -124,13 +127,15 @@ class RangesCollector { if (this._length <= limit) { this._foldingRangesLimit.report({ limited: false, computed: this._length }); - const startIndexes = new Uint32Array(this._length); - const endIndexes = new Uint32Array(this._length); + const startLineIndexes = new Uint32Array(this._length); + const endLineIndexes = new Uint32Array(this._length); + const startColumnIndexes: Array = []; for (let i = 0; i < this._length; i++) { - startIndexes[i] = this._startIndexes[i]; - endIndexes[i] = this._endIndexes[i]; + startLineIndexes[i] = this._startLineIndexes[i]; + endLineIndexes[i] = this._endLineIndexes[i]; + startColumnIndexes[i] = this._startColumnIndexes[i]; } - return new FoldingRegions(startIndexes, endIndexes, this._types, this._collapsedTexts); + return new FoldingRegions(startLineIndexes, endLineIndexes, startColumnIndexes, this._types, this._collapsedTexts); } else { this._foldingRangesLimit.report({ limited: limit, computed: this._length }); @@ -147,21 +152,23 @@ class RangesCollector { } } - const startIndexes = new Uint32Array(limit); - const endIndexes = new Uint32Array(limit); + const startLineIndexes = new Uint32Array(limit); + const endLineIndexes = new Uint32Array(limit); + const startColumnIndexes: Array = []; const types: Array = []; const collapsedTexts: Array = []; for (let i = 0, k = 0; i < this._length; i++) { const level = this._nestingLevels[i]; if (level < maxLevel || (level === maxLevel && entries++ < limit)) { - startIndexes[k] = this._startIndexes[i]; - endIndexes[k] = this._endIndexes[i]; + startLineIndexes[k] = this._startLineIndexes[i]; + endLineIndexes[k] = this._endLineIndexes[i]; + startColumnIndexes[k] = this._startColumnIndexes[i]; types[k] = this._types[i]; collapsedTexts[k] = this._collapsedTexts[i]; k++; } } - return new FoldingRegions(startIndexes, endIndexes, types, collapsedTexts); + return new FoldingRegions(startLineIndexes, endLineIndexes, startColumnIndexes, types, collapsedTexts); } } @@ -170,11 +177,15 @@ class RangesCollector { export function sanitizeRanges(rangeData: IFoldingRangeData[], foldingRangesLimit: FoldingLimitReporter): FoldingRegions { const sorted = rangeData.sort((d1, d2) => { - let diff = d1.start - d2.start; - if (diff === 0) { - diff = d1.rank - d2.rank; + const lineDiff = d1.start - d2.start; + if (d1.start !== d2.start) { + return lineDiff; } - return diff; + const columnDiff = (d1.startColumn ?? MAX_COLUMN_NUMBER) - (d2.startColumn ?? MAX_COLUMN_NUMBER); + if (columnDiff !== 0) { + return columnDiff; + } + return d1.rank - d2.rank; }); const collector = new RangesCollector(foldingRangesLimit); @@ -183,13 +194,13 @@ export function sanitizeRanges(rangeData: IFoldingRangeData[], foldingRangesLimi for (const entry of sorted) { if (!top) { top = entry; - collector.add(entry.start, entry.end, entry.kind && entry.kind.value, entry.collapsedText, previous.length); + collector.add(entry.start, entry.end, entry.startColumn, entry.kind && entry.kind.value, entry.collapsedText, previous.length); } else { if (entry.start > top.start) { if (entry.end <= top.end) { previous.push(top); top = entry; - collector.add(entry.start, entry.end, entry.kind && entry.kind.value, entry.collapsedText, previous.length); + collector.add(entry.start, entry.end, entry.startColumn, entry.kind && entry.kind.value, entry.collapsedText, previous.length); } else { if (entry.start > top.end) { do { @@ -200,7 +211,7 @@ export function sanitizeRanges(rangeData: IFoldingRangeData[], foldingRangesLimi } top = entry; } - collector.add(entry.start, entry.end, entry.kind && entry.kind.value, entry.collapsedText, previous.length); + collector.add(entry.start, entry.end, entry.startColumn, entry.kind && entry.kind.value, entry.collapsedText, previous.length); } } } diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 9039d6d6b4c31..661d4289bb7c3 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -113,7 +113,7 @@ suite('Editor Model - Model', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 1), 'foo ')]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'foo My First Line', null) + new ModelRawLineChanged(1, 'foo My First Line', null, null) ], 2, false, @@ -132,8 +132,8 @@ suite('Editor Model - Model', () => { thisModel.applyEdits([EditOperation.insert(new Position(1, 3), ' new line\nNo longer')]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My new line', null), - new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null]), + new ModelRawLineChanged(1, 'My new line', null, null), + new ModelRawLinesInserted(2, 2, ['No longer First Line'], [null], [null]), ], 2, false, @@ -209,7 +209,7 @@ suite('Editor Model - Model', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'y First Line', null), + new ModelRawLineChanged(1, 'y First Line', null, null), ], 2, false, @@ -228,7 +228,7 @@ suite('Editor Model - Model', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 14))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, '', null), + new ModelRawLineChanged(1, '', null, null), ], 2, false, @@ -247,7 +247,7 @@ suite('Editor Model - Model', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 2, 6))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My Second Line', null), + new ModelRawLineChanged(1, 'My Second Line', null, null), new ModelRawLinesDeleted(2, 2), ], 2, @@ -267,7 +267,7 @@ suite('Editor Model - Model', () => { thisModel.applyEdits([EditOperation.delete(new Range(1, 4, 3, 5))]); assert.deepStrictEqual(e, new ModelRawContentChangedEvent( [ - new ModelRawLineChanged(1, 'My Third Line', null), + new ModelRawLineChanged(1, 'My Third Line', null, null), new ModelRawLinesDeleted(2, 3), ], 2, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e4c06adff2d4c..a9c3736b8883b 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2091,6 +2091,11 @@ declare namespace monaco.editor { * @param ownerId If set, it will ignore decorations belonging to other owners. */ getInjectedTextDecorations(ownerId?: number): IModelDecoration[]; + /** + * Gets all the decorations that contain an inline fold. + * @param ownerId If set, it will ignore decorations belonging to other owners. + */ + getInlineFoldsDecorations(ownerId?: number): IModelDecoration[]; /** * Normalize a string containing whitespace according to indentation rules (converts to spaces or to tabs). */ @@ -7249,9 +7254,13 @@ declare namespace monaco.languages { export interface FoldingRange { /** - * The one-based start line of the range to fold. The folded area starts after the line's last character. + * The one-based start line of the range to fold. */ start: number; + /** + * The one-based start column of the range to fold. If not defined, folded area starts at the end of start line. + */ + startColumn?: number; /** * The one-based end line of the range to fold. The folded area ends with the line's last character. */ diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 97601b2b93413..d15f1f6455e09 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1376,10 +1376,13 @@ export namespace ProgressLocation { export namespace FoldingRange { export function from(r: vscode.FoldingRange): languages.FoldingRange { - const range: languages.FoldingRange = { start: r.start + 1, end: r.end + 1, collapsedText: r.collapsedText }; - if (r.kind) { - range.kind = FoldingRangeKind.from(r.kind); - } + const range: languages.FoldingRange = { + start: r.start + 1, + end: r.end + 1, + startColumn: r.startColumn !== undefined ? r.startColumn + 1 : undefined, + collapsedText: r.collapsedText, + kind: r.kind ? FoldingRangeKind.from(r.kind) : undefined, + }; return range; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 3a5093f7d05b8..e76b36dbee20d 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2976,11 +2976,14 @@ export class FoldingRange { collapsedText?: string; - constructor(start: number, end: number, kind?: FoldingRangeKind, collapsedText?: string) { + startColumn?: number; + + constructor(start: number, end: number, kind?: FoldingRangeKind, collapsedText?: string, startColumn?: number) { this.start = start; this.end = end; this.kind = kind; this.collapsedText = collapsedText; + this.startColumn = startColumn; } } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 32e233a79d313..a9e9173e32628 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -5169,11 +5169,17 @@ declare module 'vscode' { export class FoldingRange { /** - * The zero-based start line of the range to fold. The folded area starts after the line's last character. + * The zero-based start line of the range to fold. + * The folded area starts at the start column if set, otherwise, after the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. */ start: number; + /** + * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + */ + startColumn?: number; + /** * The zero-based end line of the range to fold. The folded area ends with the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. From 461a54d2f7df588e6ed4dfc9afb11a017ec04061 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 19 Dec 2022 18:01:19 -0500 Subject: [PATCH 03/19] clean up --- src/vs/editor/common/model/textModel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index b8957013dfffa..28b5982ff66b4 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2327,7 +2327,6 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions { this.hideInCommentTokens = options.hideInCommentTokens ?? false; this.hideInStringTokens = options.hideInStringTokens ?? false; this.hideContent = options.hideContent ?? false; - console.log({ hideContent: options.hideContent }); } } From 7b15ee62549329d4ccc8332ab648c88c61aefc49 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 19 Dec 2022 18:25:09 -0500 Subject: [PATCH 04/19] pass missing foldingOffset arguments in couple places --- src/vs/editor/browser/widget/diffEditorWidget.ts | 2 +- .../test/browser/viewModel/modelLineProjection.test.ts | 2 +- src/vs/editor/test/common/viewModel/lineBreakData.test.ts | 8 ++++---- .../common/viewModel/monospaceLineBreaksComputer.test.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index dde0947c63c9d..748df78df0d8a 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -2454,7 +2454,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { }; for (let lineNumber = lineChange.originalStartLineNumber; lineNumber <= lineChange.originalEndLineNumber; lineNumber++) { - this._lineBreaksComputer.addRequest(this._originalModel.getLineContent(lineNumber), null, null); + this._lineBreaksComputer.addRequest(this._originalModel.getLineContent(lineNumber), null, null, null); } this._pendingLineChange.push(lineChange); diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 06de7665c0271..ecc3ac4178ec3 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -987,7 +987,7 @@ function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleCo for (let i = 0; i < breakingLengths.length; i++) { sums[i] = (i > 0 ? sums[i - 1] : 0) + breakingLengths[i]; } - return new ModelLineProjectionData(null, null, sums, breakingOffsetsVisibleColumn, wrappedTextIndentWidth); + return new ModelLineProjectionData(null, null, sums, breakingOffsetsVisibleColumn, null, wrappedTextIndentWidth); } function createModel(text: string): ISimpleModel { diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 9de24985297e3..22a3d36e674d1 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -10,7 +10,7 @@ import { ModelLineProjectionData } from 'vs/editor/common/modelLineProjectionDat suite('Editor ViewModel - LineBreakData', () => { test('Basic', () => { - const data = new ModelLineProjectionData([], [], [100], [0], 10); + const data = new ModelLineProjectionData([], [], [100], [0], null, 10); assert.strictEqual(data.translateToInputOffset(0, 50), 50); assert.strictEqual(data.translateToInputOffset(1, 60), 150); @@ -44,7 +44,7 @@ suite('Editor ViewModel - LineBreakData', () => { } suite('Injected Text 1', () => { - const data = new ModelLineProjectionData([2, 3, 10], mapTextToInjectedTextOptions(['1', '22', '333']), [10, 100], [], 10); + const data = new ModelLineProjectionData([2, 3, 10], mapTextToInjectedTextOptions(['1', '22', '333']), [10, 100], [], null, 10); test('getInputOffsetOfOutputPosition', () => { // For every view model position, what is the model position? @@ -183,7 +183,7 @@ suite('Editor ViewModel - LineBreakData', () => { }); suite('Injected Text 2', () => { - const data = new ModelLineProjectionData([2, 2, 6], mapTextToInjectedTextOptions(['1', '22', '333']), [10, 100], [], 0); + const data = new ModelLineProjectionData([2, 2, 6], mapTextToInjectedTextOptions(['1', '22', '333']), [10, 100], [], null, 0); test('getInputOffsetOfOutputPosition', () => { assert.deepStrictEqual( @@ -205,7 +205,7 @@ suite('Editor ViewModel - LineBreakData', () => { }); suite('Injected Text 3', () => { - const data = new ModelLineProjectionData([2, 2, 7], mapTextToInjectedTextOptions(['1', '22', '333']), [10, 100], [], 0); + const data = new ModelLineProjectionData([2, 2, 7], mapTextToInjectedTextOptions(['1', '22', '333']), [10, 100], [], null, 0); test('getInputOffsetOfOutputPosition', () => { assert.deepStrictEqual( diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index a44c69c543868..6111cd59a78c5 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -63,8 +63,8 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, maxDigitWidth: 7 }, false); const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak); - const previousLineBreakDataClone = previousLineBreakData ? new ModelLineProjectionData(null, null, previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null; - lineBreaksComputer.addRequest(text, null, previousLineBreakDataClone); + const previousLineBreakDataClone = previousLineBreakData ? new ModelLineProjectionData(null, null, previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), null, previousLineBreakData.wrappedTextIndentLength) : null; + lineBreaksComputer.addRequest(text, null, null, previousLineBreakDataClone); return lineBreaksComputer.finalize()[0]; } From 098c709d8c8fe5b67cf88197724492da4e48f3e3 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 19 Dec 2022 19:22:53 -0500 Subject: [PATCH 05/19] unfold hidden ranges if selection is within the column range --- .../editor/contrib/folding/browser/folding.ts | 12 ++++- .../folding/browser/hiddenRangeModel.ts | 48 ++++++++++--------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index b9bc1a28df92d..bc5e50b343d46 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -385,8 +385,16 @@ export class FoldingController extends Disposable implements IEditorContribution const toToggle: FoldingRegion[] = []; for (const selection of selections) { const lineNumber = selection.selectionStartLineNumber; - if (this.hiddenRangeModel && this.hiddenRangeModel.isHidden(lineNumber)) { - toToggle.push(...foldingModel.getAllRegionsAtLine(lineNumber, r => r.isCollapsed && lineNumber > r.startLineNumber)); + const columnNumber = selection.selectionStartColumn; + if (this.hiddenRangeModel && this.hiddenRangeModel.isHidden(lineNumber, columnNumber)) { + toToggle.push(...foldingModel.getAllRegionsAtLine(lineNumber, r => { + if (!r.isCollapsed) { + return false; + } + const withinColumnRange = r.startColumn ? lineNumber === r.startLineNumber && columnNumber > r.startColumn : false; + const withinLineRange = lineNumber > r.startLineNumber; + return withinColumnRange || withinLineRange; + })); } } if (toToggle.length) { diff --git a/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts b/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts index 3a06860de325e..ba2f0702dce10 100644 --- a/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts +++ b/src/vs/editor/contrib/folding/browser/hiddenRangeModel.ts @@ -90,34 +90,24 @@ export class HiddenRangeModel { return this._hiddenRanges.length > 0; } - public isHidden(line: number): boolean { - return findRange(this._hiddenRanges, line) !== null; + public isHidden(line: number, column?: number): boolean { + return this.findContainingHiddenRange(line, column) !== null; } public adjustSelections(selections: Selection[]): boolean { let hasChanges = false; - const editorModel = this._foldingModel.textModel; - let lastRange: IRange | null = null; - - const adjustLine = (line: number) => { - if (!lastRange || !isInside(line, lastRange)) { - lastRange = findRange(this._hiddenRanges, line); - } - if (lastRange) { - return lastRange.startLineNumber - 1; - } - return null; - }; for (let i = 0, len = selections.length; i < len; i++) { let selection = selections[i]; - const adjustedStartLine = adjustLine(selection.startLineNumber); - if (adjustedStartLine) { - selection = selection.setStartPosition(adjustedStartLine, editorModel.getLineMaxColumn(adjustedStartLine)); + let containingRange = this.findContainingHiddenRange(selection.startLineNumber, selection.startColumn); + if (containingRange) { + const adjustedStartLine = containingRange.startLineNumber - 1; + selection = selection.setStartPosition(adjustedStartLine, containingRange.startColumn); hasChanges = true; } - const adjustedEndLine = adjustLine(selection.endLineNumber); - if (adjustedEndLine) { - selection = selection.setEndPosition(adjustedEndLine, editorModel.getLineMaxColumn(adjustedEndLine)); + containingRange = this.findContainingHiddenRange(selection.endLineNumber, selection.endColumn); + if (containingRange) { + const adjustedEndLine = containingRange.startLineNumber - 1; + selection = selection.setEndPosition(adjustedEndLine, containingRange.startColumn); hasChanges = true; } selections[i] = selection; @@ -125,6 +115,10 @@ export class HiddenRangeModel { return hasChanges; } + private findContainingHiddenRange(line: number, column?: number): IRange | null { + const closestRange = findRange(this._hiddenRanges, line); + return closestRange && isInside(line, column, closestRange) ? closestRange : null; + } public dispose() { if (this.hiddenRanges.length > 0) { @@ -138,11 +132,21 @@ export class HiddenRangeModel { } } -function isInside(line: number, range: IRange) { +function isInside(line: number, column: number | undefined, range: IRange) { + if (column !== undefined) { + if ((line === range.startLineNumber - 1) && column > range.startColumn) { + return true; + } + if (line === range.endLineNumber && column < range.endColumn) { + return true; + } + } return line >= range.startLineNumber && line <= range.endLineNumber; } + function findRange(ranges: IRange[], line: number): IRange | null { - const i = findFirstInSorted(ranges, r => line < r.startLineNumber) - 1; + //startLineNumber - 1 to include hidden ranges' starting line as well. + const i = findFirstInSorted(ranges, r => line < r.startLineNumber - 1) - 1; if (i >= 0 && ranges[i].endLineNumber >= line) { return ranges[i]; } From 783c1edfce817c3f8e055782064c9554f1ff47c2 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 19 Dec 2022 21:12:34 -0500 Subject: [PATCH 06/19] support clicking collapsedText to unfold ranges with a set startColumn --- .../editor/contrib/folding/browser/folding.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index bc5e50b343d46..0127e2af7c06f 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -105,7 +105,7 @@ export class FoldingController extends Disposable implements IEditorContribution private cursorChangedScheduler: RunOnceScheduler | null; private readonly localToDispose = this._register(new DisposableStore()); - private mouseDownInfo: { lineNumber: number; iconClicked: boolean } | null; + private mouseDownInfo: { lineNumber: number; columnNumber: number; iconClicked: boolean } | null; private _onDidChangeFoldingLimit = new Emitter(); public readonly onDidChangeFoldingLimit: Event = this._onDidChangeFoldingLimit.event; @@ -446,10 +446,7 @@ export class FoldingController extends Disposable implements IEditorContribution } case MouseTargetType.CONTENT_TEXT: { if (this.hiddenRangeModel.hasRanges()) { - const model = this.editor.getModel(); - if (model && range.startColumn === model.getLineMaxColumn(range.startLineNumber)) { - break; - } + break; } return; } @@ -457,7 +454,7 @@ export class FoldingController extends Disposable implements IEditorContribution return; } - this.mouseDownInfo = { lineNumber: range.startLineNumber, iconClicked }; + this.mouseDownInfo = { lineNumber: range.startLineNumber, columnNumber: range.startColumn, iconClicked }; } private onEditorMouseUp(e: IEditorMouseEvent): void { @@ -466,10 +463,11 @@ export class FoldingController extends Disposable implements IEditorContribution return; } const lineNumber = this.mouseDownInfo.lineNumber; + const columnNumber = this.mouseDownInfo.columnNumber; const iconClicked = this.mouseDownInfo.iconClicked; const range = e.target.range; - if (!range || range.startLineNumber !== lineNumber) { + if (!range || range.startLineNumber !== lineNumber || range.startColumn !== columnNumber) { return; } @@ -477,15 +475,12 @@ export class FoldingController extends Disposable implements IEditorContribution if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { return; } - } else { - const model = this.editor.getModel(); - if (!model || range.startColumn !== model.getLineMaxColumn(lineNumber)) { - return; - } } const region = foldingModel.getRegionAtLine(lineNumber); - if (region && region.startLineNumber === lineNumber) { + const model = this.editor.getModel(); + const regionStartColumn = region?.startColumn ?? model?.getLineMaxColumn(lineNumber); + if (region && region.startLineNumber === lineNumber && (regionStartColumn === columnNumber || iconClicked)) { const isCollapsed = region.isCollapsed; if (iconClicked || isCollapsed) { const surrounding = e.event.altKey; From 373aaa2ca42b994f21450c8072d72c56b090b7d9 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Tue, 20 Dec 2022 20:00:12 -0500 Subject: [PATCH 07/19] hide decorations after the folding offset. --- .../editor/common/modelLineProjectionData.ts | 12 +++++++++ .../common/viewModel/modelLineProjection.ts | 27 ++++++++++++++++++- .../editor/common/viewModel/viewModelLines.ts | 9 +++++-- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index 27211c26da23f..718150ee9a724 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -96,6 +96,18 @@ export class ModelLineProjectionData { return this.getLineLength(outputLineIndex); } + public getInputFoldingOffset(): number | null { + if (this.foldingOffset === null) { + return null; + } + return this.offsetInInputWithInjectionsToInputOffset(this.foldingOffset); + } + + private offsetInInputWithInjectionsToInputOffset(offsetInInputWithInjections: number, affinity: PositionAffinity = PositionAffinity.None): number { + const outputPosition = this.offsetInInputWithInjectionsToOutputPosition(offsetInInputWithInjections, affinity); + return this.translateToInputOffset(outputPosition.outputLineIndex, outputPosition.outputOffset); + } + public translateToInputOffset(outputLineIndex: number, outputOffset: number): number { if (outputLineIndex > 0) { outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength); diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index df630644bcca5..824139ff83ad7 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -5,7 +5,7 @@ import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; import { Position } from 'vs/editor/common/core/position'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference, ITextModel, PositionAffinity } from 'vs/editor/common/model'; import { LineInjectedText } from 'vs/editor/common/textModelEvents'; import { InjectedText, ModelLineProjectionData } from 'vs/editor/common/modelLineProjectionData'; @@ -29,6 +29,10 @@ export interface IModelLineProjection { getViewLinesData(model: ISimpleModel, modelLineNumber: number, outputLineIdx: number, lineCount: number, globalStartIndex: number, needed: boolean[], result: Array): void; getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number; + /** + * Currently assumes only one inline folding range. + */ + getModelVisibleRanges(model: ISimpleModel, modelLineNumber: number): Range[]; getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity?: PositionAffinity): Position; getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number; normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position; @@ -268,6 +272,19 @@ class ModelLineProjection implements IModelLineProjection { return this._projectionData.translateToInputOffset(outputLineIndex, outputColumn - 1) + 1; } + + public getModelVisibleRanges(model: ISimpleModel, modelLineNumber: number): Range[] { + if (!this._isVisible) { + return []; + } + if (this._projectionData.foldingOffset === null) { + return [new Range(modelLineNumber, model.getLineMinColumn(modelLineNumber), modelLineNumber, model.getLineMaxColumn(modelLineNumber))]; + } + + const lastVisibleModelColumn = this._projectionData.getInputFoldingOffset(); + return [new Range(modelLineNumber, model.getLineMinColumn(modelLineNumber), modelLineNumber, lastVisibleModelColumn!)]; + } + public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position { this._assertVisible(); const r = this._projectionData.translateToOutputPosition(inputColumn - 1, affinity); @@ -367,6 +384,10 @@ class IdentityModelLineProjection implements IModelLineProjection { return outputColumn; } + public getModelVisibleRanges(model: ISimpleModel, modelLineNumber: number): Range[] { + return [new Range(modelLineNumber, model.getLineMinColumn(modelLineNumber), modelLineNumber, model.getLineMaxColumn(modelLineNumber))]; + } + public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { return new Position(deltaLineNumber, inputColumn); } @@ -439,6 +460,10 @@ class HiddenModelLineProjection implements IModelLineProjection { throw new Error('Not supported'); } + public getModelVisibleRanges(model: ISimpleModel, modelLineNumber: number): Range[] { + return []; + } + public getViewPositionOfModelPosition(_deltaLineNumber: number, _inputColumn: number): Position { throw new Error('Not supported'); } diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 10bfbea0aa3c1..fcb838565edf3 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -922,8 +922,13 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { } else { // hit invisible line => flush request if (reqStart !== null) { - const maxLineColumn = this.model.getLineMaxColumn(modelLineIndex); - result = result.concat(this.model.getDecorationsInRange(new Range(reqStart.lineNumber, reqStart.column, modelLineIndex, maxLineColumn), ownerId, filterOutValidation, onlyMinimapDecorations)); + //Assuming only one inline folding range for now. + const prevLine = this.modelLineProjections[modelLineIndex - 1]; + const [lastVisibleRange] = prevLine.getModelVisibleRanges(this.model, modelLineIndex); + //endColumn + 1 to include the folding range decoration, then filter out the extra decorations that were added as a result. + const decorationsPlusExtras = this.model.getDecorationsInRange(new Range(reqStart.lineNumber, reqStart.column, modelLineIndex, lastVisibleRange.endColumn + 1), ownerId, filterOutValidation, onlyMinimapDecorations); + const decorationsPlusOnlyFoldingRange = decorationsPlusExtras.filter(d => lastVisibleRange.endLineNumber !== d.range.startLineNumber || d.range.startColumn !== lastVisibleRange.endColumn + 1 || d.options.hideContent); + result = result.concat(decorationsPlusOnlyFoldingRange); reqStart = null; } } From 3a3fe9dcea4d382aafe74d249f4451aeb66c929c Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Tue, 20 Dec 2022 22:09:12 -0500 Subject: [PATCH 08/19] fix startColumn comparison in FoldingRegions.sanitizeAndMerge --- .../contrib/folding/browser/foldingRanges.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index 06313e02f1af7..15bd099c872e4 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -343,11 +343,20 @@ export class FoldingRegions { const resultRanges: FoldRange[] = []; while (nextA || nextB) { + const aStartColumn = nextA?.startColumn ?? MAX_COLUMN_NUMBER; + const bStartColumn = nextB?.startColumn ?? MAX_COLUMN_NUMBER; + + const aStartsAtB = nextA && nextB && + nextA.startLineNumber === nextB.startLineNumber && aStartColumn === bStartColumn; + + const aStartsAfterB = nextA && nextB && ( + nextA.startLineNumber > nextB.startLineNumber || + (nextA.startLineNumber === nextB.startLineNumber && aStartColumn > bStartColumn)); let useRange: FoldRange | undefined = undefined; - if (nextB && (!nextA || nextA.startLineNumber >= nextB.startLineNumber)) { - if (nextA && nextA.startLineNumber === nextB.startLineNumber) { - if (nextB.source === FoldSource.userDefined || (nextA.startColumn ?? MAX_COLUMN_NUMBER) >= (nextB.startColumn ?? MAX_COLUMN_NUMBER)) { + if (nextB && (!nextA || aStartsAtB || aStartsAfterB)) { + if (nextA && aStartsAtB) { + if (nextB.source === FoldSource.userDefined) { // a user defined range (possibly unfolded) useRange = nextB; } else { From 78c3e7f8cd2e8fb89c10108fb85564f4b84c56db Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Wed, 21 Dec 2022 03:39:43 -0500 Subject: [PATCH 09/19] update foldingModel tests to include startColumn cases --- .../folding/test/browser/foldingModel.test.ts | 433 +++++++++++++----- 1 file changed, 327 insertions(+), 106 deletions(-) diff --git a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts index 9f2e3a0344c0c..4d3046b30b0a1 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingModel.test.ts @@ -7,10 +7,12 @@ import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { FoldingMarkers } from 'vs/editor/common/languages/languageConfiguration'; import { IModelDecorationsChangeAccessor, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; +import { FoldingLimitReporter } from 'vs/editor/contrib/folding/browser/folding'; import { FoldingModel, getNextFoldLine, getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp } from 'vs/editor/contrib/folding/browser/foldingModel'; -import { FoldingRegion } from 'vs/editor/contrib/folding/browser/foldingRanges'; +import { FoldingRegion, FoldingRegions, FoldRange } from 'vs/editor/contrib/folding/browser/foldingRanges'; import { computeRanges } from 'vs/editor/contrib/folding/browser/indentRangeProvider'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; @@ -19,11 +21,13 @@ interface ExpectedRegion { startLineNumber: number; endLineNumber: number; isCollapsed: boolean; + startColumn: number | undefined; } interface ExpectedDecoration { line: number; type: 'hidden' | 'collapsed' | 'expanded'; + column: number | undefined; } export class TestDecorationProvider { @@ -74,24 +78,39 @@ export class TestDecorationProvider { const res: ExpectedDecoration[] = []; for (const decoration of decorations) { if (decoration.options === TestDecorationProvider.hiddenDecoration) { - res.push({ line: decoration.range.startLineNumber, type: 'hidden' }); + res.push({ line: decoration.range.startLineNumber, type: 'hidden', column: decoration.range.startColumn }); } else if (decoration.options === TestDecorationProvider.collapsedDecoration) { - res.push({ line: decoration.range.startLineNumber, type: 'collapsed' }); + res.push({ line: decoration.range.startLineNumber, type: 'collapsed', column: decoration.range.startColumn }); } else if (decoration.options === TestDecorationProvider.expandedDecoration) { - res.push({ line: decoration.range.startLineNumber, type: 'expanded' }); + res.push({ line: decoration.range.startLineNumber, type: 'expanded', column: decoration.range.startColumn }); } } return res; } } +function computeRangesWithModifiedStartColumn(model: ITextModel, offSide: boolean, markers?: FoldingMarkers | undefined, foldingRangesLimit?: FoldingLimitReporter): FoldingRegions { + const modifiedRanges: FoldRange[] = []; + const originalRanges = computeRanges(model, offSide, markers, foldingRangesLimit); + for (let i = 0; i < originalRanges.length; i++) { + const originalRange = originalRanges.toFoldRange(i); + const startColumn = originalRange.startColumn ?? model.getLineMaxColumn(originalRange.startLineNumber); + const modifiedRange = { + ...originalRange, + startColumn: startColumn - 1 + }; + modifiedRanges.push(modifiedRange); + } + return FoldingRegions.fromFoldRanges(modifiedRanges); +} + suite('Folding Model', () => { - function r(startLineNumber: number, endLineNumber: number, isCollapsed: boolean = false): ExpectedRegion { - return { startLineNumber, endLineNumber, isCollapsed }; + function r(startLineNumber: number, endLineNumber: number, isCollapsed: boolean = false, startColumn?: number): ExpectedRegion { + return { startLineNumber, endLineNumber, isCollapsed, startColumn }; } - function d(line: number, type: 'hidden' | 'collapsed' | 'expanded'): ExpectedDecoration { - return { line, type }; + function d(line: number, type: 'hidden' | 'collapsed' | 'expanded', column?: number | undefined): ExpectedDecoration { + return { line, type, column }; } function assertRegion(actual: FoldingRegion | null, expected: ExpectedRegion | null, message?: string) { @@ -103,33 +122,46 @@ suite('Folding Model', () => { } } - function assertFoldedRanges(foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) { + function assertFoldedRanges(textModel: TextModel, foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) { + expectedRegions = expectedRegions.map(r => ({ ...r, startColumn: r.startColumn ?? textModel.getLineMaxColumn(r.startLineNumber) })); const actualRanges: ExpectedRegion[] = []; const actual = foldingModel.regions; for (let i = 0; i < actual.length; i++) { if (actual.isCollapsed(i)) { - actualRanges.push(r(actual.getStartLineNumber(i), actual.getEndLineNumber(i))); + const startLineNumber = actual.getStartLineNumber(i); + const startColumn = actual.getStartColumn(i) ?? textModel.getLineMaxColumn(startLineNumber); + actualRanges.push(r(startLineNumber, actual.getEndLineNumber(i), false, startColumn)); } } assert.deepStrictEqual(actualRanges, expectedRegions, message); } - function assertRanges(foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) { + function assertRanges(textModel: TextModel, foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) { + expectedRegions = expectedRegions.map(r => ({ ...r, startColumn: r.startColumn ?? textModel.getLineMaxColumn(r.startLineNumber) })); const actualRanges: ExpectedRegion[] = []; const actual = foldingModel.regions; for (let i = 0; i < actual.length; i++) { - actualRanges.push(r(actual.getStartLineNumber(i), actual.getEndLineNumber(i), actual.isCollapsed(i))); + const startLineNumber = actual.getStartLineNumber(i); + const startColumn = actual.getStartColumn(i) ?? textModel.getLineMaxColumn(startLineNumber); + actualRanges.push(r(startLineNumber, actual.getEndLineNumber(i), actual.isCollapsed(i), startColumn)); } assert.deepStrictEqual(actualRanges, expectedRegions, message); } - function assertDecorations(foldingModel: FoldingModel, expectedDecoration: ExpectedDecoration[], message?: string) { + function assertDecorations(textModel: TextModel, foldingModel: FoldingModel, expectedDecoration: ExpectedDecoration[], message?: string) { + expectedDecoration = expectedDecoration.map(d => ({ ...d, column: d.column ?? textModel.getLineMaxColumn(d.line) })); const decorationProvider = foldingModel.decorationProvider as TestDecorationProvider; assert.deepStrictEqual(decorationProvider.getDecorations(), expectedDecoration, message); } - function assertRegions(actual: FoldingRegion[], expectedRegions: ExpectedRegion[], message?: string) { - assert.deepStrictEqual(actual.map(r => ({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber, isCollapsed: r.isCollapsed })), expectedRegions, message); + function assertRegions(textModel: TextModel, actual: FoldingRegion[], expectedRegions: ExpectedRegion[], message?: string) { + expectedRegions = expectedRegions.map(r => ({ ...r, startColumn: r.startColumn ?? textModel.getLineMaxColumn(r.startLineNumber) })); + assert.deepStrictEqual(actual.map(r => ({ + startLineNumber: r.startLineNumber, + endLineNumber: r.endLineNumber, + isCollapsed: r.isCollapsed, + startColumn: r.startColumn ?? textModel.getLineMaxColumn(r.startLineNumber) + })), expectedRegions, message); } test('getRegionAtLine', () => { @@ -154,7 +186,7 @@ suite('Folding Model', () => { const r2 = r(4, 7, false); const r3 = r(5, 6, false); - assertRanges(foldingModel, [r1, r2, r3]); + assertRanges(textModel, foldingModel, [r1, r2, r3]); assertRegion(foldingModel.getRegionAtLine(1), r1, '1'); assertRegion(foldingModel.getRegionAtLine(2), r1, '2'); @@ -193,22 +225,22 @@ suite('Folding Model', () => { const r2 = r(4, 7, false); const r3 = r(5, 6, false); - assertRanges(foldingModel, [r1, r2, r3]); + assertRanges(textModel, foldingModel, [r1, r2, r3]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!]); foldingModel.update(ranges); - assertRanges(foldingModel, [r(1, 3, true), r2, r3]); + assertRanges(textModel, foldingModel, [r(1, 3, true), r2, r3]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(5)!]); foldingModel.update(ranges); - assertRanges(foldingModel, [r(1, 3, true), r2, r(5, 6, true)]); + assertRanges(textModel, foldingModel, [r(1, 3, true), r2, r(5, 6, true)]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(7)!]); foldingModel.update(ranges); - assertRanges(foldingModel, [r(1, 3, true), r(4, 7, true), r(5, 6, true)]); + assertRanges(textModel, foldingModel, [r(1, 3, true), r(4, 7, true), r(5, 6, true)]); textModel.dispose(); } finally { @@ -217,6 +249,56 @@ suite('Folding Model', () => { }); + test('collapse, with a defined startColumn', () => { + const lines = [ + /* 1*/ '/**', + /* 2*/ ' * Comment', + /* 3*/ ' */', + /* 4*/ 'class A {', + /* 5*/ ' void foo() {', + /* 6*/ ' // comment {', + /* 7*/ ' }', + /* 8*/ '}']; + + const textModel = createTextModel(lines.join('\n')); + try { + const foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel)); + + //returns the same as computeRanges, but with startColumn - 1 + const ranges = computeRangesWithModifiedStartColumn(textModel, false, undefined); + + foldingModel.update(ranges); + + const r1 = r(1, 3, false, 3); + const r2 = r(4, 7, false, 9); + const r3 = r(5, 6, false, 14); + + assertRanges(textModel, foldingModel, [r1, r2, r3]); + + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!]); + foldingModel.update(ranges); + + assertRanges(textModel, foldingModel, [r(1, 3, true, 3), r2, r3]); + + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(5)!]); + foldingModel.update(ranges); + + assertRanges(textModel, foldingModel, [r(1, 3, true, 3), r2, r(5, 6, true, 14)]); + + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(7)!]); + foldingModel.update(ranges); + + assertRanges(textModel, foldingModel, [r(1, 3, true, 3), r(4, 7, true, 9), r(5, 6, true, 14)]); + + textModel.dispose(); + } finally { + textModel.dispose(); + } + + }); + + + test('update', () => { const lines = [ /* 1*/ '/**', @@ -239,14 +321,50 @@ suite('Folding Model', () => { const r2 = r(4, 7, false); const r3 = r(5, 6, false); - assertRanges(foldingModel, [r1, r2, r3]); + assertRanges(textModel, foldingModel, [r1, r2, r3]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(2)!, foldingModel.getRegionAtLine(5)!]); textModel.applyEdits([EditOperation.insert(new Position(4, 1), '//hello\n')]); foldingModel.update(computeRanges(textModel, false, undefined)); - assertRanges(foldingModel, [r(1, 3, true), r(5, 8, false), r(6, 7, true)]); + assertRanges(textModel, foldingModel, [r(1, 3, true), r(5, 8, false), r(6, 7, true)]); + } finally { + textModel.dispose(); + } + }); + + test('update, with a defined startColumn', () => { + const lines = [ + /* 1*/ '/**', + /* 2*/ ' * Comment', + /* 3*/ ' */', + /* 4*/ 'class A {', + /* 5*/ ' void foo() {', + /* 6*/ ' // comment {', + /* 7*/ ' }', + /* 8*/ '}']; + + const textModel = createTextModel(lines.join('\n')); + try { + const foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel)); + + const ranges = computeRangesWithModifiedStartColumn(textModel, false, undefined); + foldingModel.update(ranges); + + + const r1 = r(1, 3, false, 3); + const r2 = r(4, 7, false, 9); + const r3 = r(5, 6, false, 14); + + assertRanges(textModel, foldingModel, [r1, r2, r3]); + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(2)!, foldingModel.getRegionAtLine(5)!]); + + textModel.applyEdits([EditOperation.insert(new Position(4, 1), '//hello\n')]); + + foldingModel.update(computeRangesWithModifiedStartColumn(textModel, false, undefined)); + + assertRanges(textModel, foldingModel, [r(1, 3, true, 3), r(5, 8, false, 9), r(6, 7, true, 14)]); } finally { textModel.dispose(); } @@ -281,14 +399,56 @@ suite('Folding Model', () => { const r4 = r(6, 8, false); const r5 = r(9, 11, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(6)!]); textModel.applyEdits([EditOperation.delete(new Range(6, 11, 9, 0))]); foldingModel.update(computeRanges(textModel, false, undefined)); - assertRanges(foldingModel, [r(1, 9, false), r(2, 8, false), r(3, 5, false), r(6, 8, false)]); + assertRanges(textModel, foldingModel, [r(1, 9, false), r(2, 8, false), r(3, 5, false), r(6, 8, false)]); + } finally { + textModel.dispose(); + } + }); + + test('delete, with a defined startColumn', () => { + const lines = [ + /* 1*/ 'function foo() {', + /* 2*/ ' switch (x) {', + /* 3*/ ' case 1:', + /* 4*/ ' //hello1', + /* 5*/ ' break;', + /* 6*/ ' case 2:', + /* 7*/ ' //hello2', + /* 8*/ ' break;', + /* 9*/ ' case 3:', + /* 10*/ ' //hello3', + /* 11*/ ' break;', + /* 12*/ ' }', + /* 13*/ '}']; + + const textModel = createTextModel(lines.join('\n')); + try { + const foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel)); + + const ranges = computeRangesWithModifiedStartColumn(textModel, false, undefined); + foldingModel.update(ranges); + + const r1 = r(1, 12, false, 16); + const r2 = r(2, 11, false, 14); + const r3 = r(3, 5, false, 11); + const r4 = r(6, 8, false, 11); + const r5 = r(9, 11, false, 11); + + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(6)!]); + + textModel.applyEdits([EditOperation.delete(new Range(6, 11, 9, 0))]); + + foldingModel.update(computeRangesWithModifiedStartColumn(textModel, false, undefined)); + + assertRanges(textModel, foldingModel, [r(1, 9, false, 16), r(2, 8, false, 14), r(3, 5, false, 11), r(6, 8, false, 21)]); } finally { textModel.dispose(); } @@ -316,15 +476,15 @@ suite('Folding Model', () => { const r2 = r(4, 7, false); const r3 = r(5, 6, false); - assertRanges(foldingModel, [r1, r2, r3]); + assertRanges(textModel, foldingModel, [r1, r2, r3]); const region1 = foldingModel.getRegionAtLine(r1.startLineNumber); const region2 = foldingModel.getRegionAtLine(r2.startLineNumber); const region3 = foldingModel.getRegionAtLine(r3.startLineNumber); - assertRegions(foldingModel.getRegionsInside(null), [r1, r2, r3], '1'); - assertRegions(foldingModel.getRegionsInside(region1), [], '2'); - assertRegions(foldingModel.getRegionsInside(region2), [r3], '3'); - assertRegions(foldingModel.getRegionsInside(region3), [], '4'); + assertRegions(textModel, foldingModel.getRegionsInside(null), [r1, r2, r3], '1'); + assertRegions(textModel, foldingModel.getRegionsInside(region1), [], '2'); + assertRegions(textModel, foldingModel.getRegionsInside(region2), [r3], '3'); + assertRegions(textModel, foldingModel.getRegionsInside(region3), [], '4'); } finally { textModel.dispose(); } @@ -364,19 +524,19 @@ suite('Folding Model', () => { const region2 = foldingModel.getRegionAtLine(r2.startLineNumber); const region3 = foldingModel.getRegionAtLine(r3.startLineNumber); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); - assertRegions(foldingModel.getRegionsInside(null, (r, level) => level === 1), [r1, r2], '1'); - assertRegions(foldingModel.getRegionsInside(null, (r, level) => level === 2), [r3], '2'); - assertRegions(foldingModel.getRegionsInside(null, (r, level) => level === 3), [r4, r5], '3'); + assertRegions(textModel, foldingModel.getRegionsInside(null, (r, level) => level === 1), [r1, r2], '1'); + assertRegions(textModel, foldingModel.getRegionsInside(null, (r, level) => level === 2), [r3], '2'); + assertRegions(textModel, foldingModel.getRegionsInside(null, (r, level) => level === 3), [r4, r5], '3'); - assertRegions(foldingModel.getRegionsInside(region2, (r, level) => level === 1), [r3], '4'); - assertRegions(foldingModel.getRegionsInside(region2, (r, level) => level === 2), [r4, r5], '5'); - assertRegions(foldingModel.getRegionsInside(region3, (r, level) => level === 1), [r4, r5], '6'); + assertRegions(textModel, foldingModel.getRegionsInside(region2, (r, level) => level === 1), [r3], '4'); + assertRegions(textModel, foldingModel.getRegionsInside(region2, (r, level) => level === 2), [r4, r5], '5'); + assertRegions(textModel, foldingModel.getRegionsInside(region3, (r, level) => level === 1), [r4, r5], '6'); - assertRegions(foldingModel.getRegionsInside(region2, (r, level) => r.hidesLine(9)), [r3, r5], '7'); + assertRegions(textModel, foldingModel.getRegionsInside(region2, (r, level) => r.hidesLine(9)), [r3, r5], '7'); - assertRegions(foldingModel.getRegionsInside(region1, (r, level) => level === 1), [], '8'); + assertRegions(textModel, foldingModel.getRegionsInside(region1, (r, level) => level === 1), [], '8'); } finally { textModel.dispose(); } @@ -409,19 +569,19 @@ suite('Folding Model', () => { const r3 = r(3, 7, false); const r4 = r(4, 5, false); - assertRanges(foldingModel, [r1, r2, r3, r4]); - - assertRegions(foldingModel.getAllRegionsAtLine(1), [r1], '1'); - assertRegions(foldingModel.getAllRegionsAtLine(2), [r1, r2].reverse(), '2'); - assertRegions(foldingModel.getAllRegionsAtLine(3), [r1, r2, r3].reverse(), '3'); - assertRegions(foldingModel.getAllRegionsAtLine(4), [r1, r2, r3, r4].reverse(), '4'); - assertRegions(foldingModel.getAllRegionsAtLine(5), [r1, r2, r3, r4].reverse(), '5'); - assertRegions(foldingModel.getAllRegionsAtLine(6), [r1, r2, r3].reverse(), '6'); - assertRegions(foldingModel.getAllRegionsAtLine(7), [r1, r2, r3].reverse(), '7'); - assertRegions(foldingModel.getAllRegionsAtLine(8), [r1, r2].reverse(), '8'); - assertRegions(foldingModel.getAllRegionsAtLine(9), [r1], '9'); - assertRegions(foldingModel.getAllRegionsAtLine(10), [r1], '10'); - assertRegions(foldingModel.getAllRegionsAtLine(11), [], '10'); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4]); + + assertRegions(textModel, foldingModel.getAllRegionsAtLine(1), [r1], '1'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(2), [r1, r2].reverse(), '2'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(3), [r1, r2, r3].reverse(), '3'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(4), [r1, r2, r3, r4].reverse(), '4'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(5), [r1, r2, r3, r4].reverse(), '5'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(6), [r1, r2, r3].reverse(), '6'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(7), [r1, r2, r3].reverse(), '7'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(8), [r1, r2].reverse(), '8'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(9), [r1], '9'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(10), [r1], '10'); + assertRegions(textModel, foldingModel.getAllRegionsAtLine(11), [], '10'); } finally { textModel.dispose(); } @@ -455,25 +615,25 @@ suite('Folding Model', () => { const r3 = r(4, 11, false); const r4 = r(5, 6, false); const r5 = r(9, 10, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); setCollapseStateLevelsDown(foldingModel, true, Number.MAX_VALUE, [4]); - assertFoldedRanges(foldingModel, [r3, r4, r5], '1'); + assertFoldedRanges(textModel, foldingModel, [r3, r4, r5], '1'); setCollapseStateLevelsDown(foldingModel, false, Number.MAX_VALUE, [8]); - assertFoldedRanges(foldingModel, [], '2'); + assertFoldedRanges(textModel, foldingModel, [], '2'); setCollapseStateLevelsDown(foldingModel, true, Number.MAX_VALUE, [12]); - assertFoldedRanges(foldingModel, [r2, r3, r4, r5], '1'); + assertFoldedRanges(textModel, foldingModel, [r2, r3, r4, r5], '1'); setCollapseStateLevelsDown(foldingModel, false, Number.MAX_VALUE, [7]); - assertFoldedRanges(foldingModel, [r2], '1'); + assertFoldedRanges(textModel, foldingModel, [r2], '1'); setCollapseStateLevelsDown(foldingModel, false); - assertFoldedRanges(foldingModel, [], '1'); + assertFoldedRanges(textModel, foldingModel, [], '1'); setCollapseStateLevelsDown(foldingModel, true); - assertFoldedRanges(foldingModel, [r1, r2, r3, r4, r5], '1'); + assertFoldedRanges(textModel, foldingModel, [r1, r2, r3, r4, r5], '1'); } finally { textModel.dispose(); } @@ -512,28 +672,28 @@ suite('Folding Model', () => { const r4 = r(5, 6, false); const r5 = r(9, 10, false); const r6 = r(13, 15, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5, r6]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5, r6]); setCollapseStateAtLevel(foldingModel, 1, true, []); - assertFoldedRanges(foldingModel, [r1, r2], '1'); + assertFoldedRanges(textModel, foldingModel, [r1, r2], '1'); setCollapseStateAtLevel(foldingModel, 1, false, [5]); - assertFoldedRanges(foldingModel, [r2], '2'); + assertFoldedRanges(textModel, foldingModel, [r2], '2'); setCollapseStateAtLevel(foldingModel, 1, false, [1]); - assertFoldedRanges(foldingModel, [], '3'); + assertFoldedRanges(textModel, foldingModel, [], '3'); setCollapseStateAtLevel(foldingModel, 2, true, []); - assertFoldedRanges(foldingModel, [r3, r6], '4'); + assertFoldedRanges(textModel, foldingModel, [r3, r6], '4'); setCollapseStateAtLevel(foldingModel, 2, false, [5, 6]); - assertFoldedRanges(foldingModel, [r3], '5'); + assertFoldedRanges(textModel, foldingModel, [r3], '5'); setCollapseStateAtLevel(foldingModel, 3, true, [4, 9]); - assertFoldedRanges(foldingModel, [r3, r4], '6'); + assertFoldedRanges(textModel, foldingModel, [r3, r4], '6'); setCollapseStateAtLevel(foldingModel, 3, false, [4, 9]); - assertFoldedRanges(foldingModel, [r3], '7'); + assertFoldedRanges(textModel, foldingModel, [r3], '7'); } finally { textModel.dispose(); } @@ -567,25 +727,25 @@ suite('Folding Model', () => { const r3 = r(4, 11, false); const r4 = r(5, 6, false); const r5 = r(9, 10, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); setCollapseStateLevelsDown(foldingModel, true, 1, [4]); - assertFoldedRanges(foldingModel, [r3], '1'); + assertFoldedRanges(textModel, foldingModel, [r3], '1'); setCollapseStateLevelsDown(foldingModel, true, 2, [4]); - assertFoldedRanges(foldingModel, [r3, r4, r5], '2'); + assertFoldedRanges(textModel, foldingModel, [r3, r4, r5], '2'); setCollapseStateLevelsDown(foldingModel, false, 2, [3]); - assertFoldedRanges(foldingModel, [r4, r5], '3'); + assertFoldedRanges(textModel, foldingModel, [r4, r5], '3'); setCollapseStateLevelsDown(foldingModel, false, 2, [2]); - assertFoldedRanges(foldingModel, [r4, r5], '4'); + assertFoldedRanges(textModel, foldingModel, [r4, r5], '4'); setCollapseStateLevelsDown(foldingModel, true, 4, [2]); - assertFoldedRanges(foldingModel, [r1, r4, r5], '5'); + assertFoldedRanges(textModel, foldingModel, [r1, r4, r5], '5'); setCollapseStateLevelsDown(foldingModel, false, 4, [2, 3]); - assertFoldedRanges(foldingModel, [], '6'); + assertFoldedRanges(textModel, foldingModel, [], '6'); } finally { textModel.dispose(); } @@ -619,19 +779,19 @@ suite('Folding Model', () => { const r3 = r(4, 11, false); const r4 = r(5, 6, false); const r5 = r(9, 10, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); setCollapseStateLevelsUp(foldingModel, true, 1, [4]); - assertFoldedRanges(foldingModel, [r3], '1'); + assertFoldedRanges(textModel, foldingModel, [r3], '1'); setCollapseStateLevelsUp(foldingModel, true, 2, [4]); - assertFoldedRanges(foldingModel, [r2, r3], '2'); + assertFoldedRanges(textModel, foldingModel, [r2, r3], '2'); setCollapseStateLevelsUp(foldingModel, false, 4, [1, 3, 4]); - assertFoldedRanges(foldingModel, [], '3'); + assertFoldedRanges(textModel, foldingModel, [], '3'); setCollapseStateLevelsUp(foldingModel, true, 2, [10]); - assertFoldedRanges(foldingModel, [r3, r5], '4'); + assertFoldedRanges(textModel, foldingModel, [r3, r5], '4'); } finally { textModel.dispose(); } @@ -666,16 +826,16 @@ suite('Folding Model', () => { const r3 = r(4, 11, false); const r4 = r(5, 6, false); const r5 = r(9, 10, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); setCollapseStateUp(foldingModel, true, [5]); - assertFoldedRanges(foldingModel, [r4], '1'); + assertFoldedRanges(textModel, foldingModel, [r4], '1'); setCollapseStateUp(foldingModel, true, [5]); - assertFoldedRanges(foldingModel, [r3, r4], '2'); + assertFoldedRanges(textModel, foldingModel, [r3, r4], '2'); setCollapseStateUp(foldingModel, true, [4]); - assertFoldedRanges(foldingModel, [r2, r3, r4], '2'); + assertFoldedRanges(textModel, foldingModel, [r2, r3, r4], '2'); } finally { textModel.dispose(); } @@ -711,11 +871,11 @@ suite('Folding Model', () => { const r3 = r(5, 7, false); const r4 = r(8, 11, false); const r5 = r(9, 11, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); const regExp = new RegExp('^\\s*' + escapeRegExpCharacters('/*')); setCollapseStateForMatchingLines(foldingModel, regExp, true); - assertFoldedRanges(foldingModel, [r1, r3, r5], '1'); + assertFoldedRanges(textModel, foldingModel, [r1, r3, r5], '1'); } finally { textModel.dispose(); } @@ -751,19 +911,19 @@ suite('Folding Model', () => { const r3 = r(4, 11, false); const r4 = r(5, 6, false); const r5 = r(9, 10, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5]); setCollapseStateForRest(foldingModel, true, [5]); - assertFoldedRanges(foldingModel, [r1, r5], '1'); + assertFoldedRanges(textModel, foldingModel, [r1, r5], '1'); setCollapseStateForRest(foldingModel, false, [5]); - assertFoldedRanges(foldingModel, [], '2'); + assertFoldedRanges(textModel, foldingModel, [], '2'); setCollapseStateForRest(foldingModel, true, [1]); - assertFoldedRanges(foldingModel, [r2, r3, r4, r5], '3'); + assertFoldedRanges(textModel, foldingModel, [r2, r3, r4, r5], '3'); setCollapseStateForRest(foldingModel, true, [3]); - assertFoldedRanges(foldingModel, [r1, r2, r3, r4, r5], '3'); + assertFoldedRanges(textModel, foldingModel, [r1, r2, r3, r4, r5], '3'); } finally { textModel.dispose(); @@ -793,38 +953,99 @@ suite('Folding Model', () => { const r2 = r(2, 5, false); const r3 = r(3, 4, false); - assertRanges(foldingModel, [r1, r2, r3]); - assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'expanded'), d(3, 'expanded')]); + assertRanges(textModel, foldingModel, [r1, r2, r3]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded'), d(2, 'expanded'), d(3, 'expanded')]); + + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(2)!]); + + assertRanges(textModel, foldingModel, [r1, r(2, 5, true), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + + foldingModel.update(ranges); + + assertRanges(textModel, foldingModel, [r1, r(2, 5, true), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!]); + + assertRanges(textModel, foldingModel, [r(1, 6, true), r(2, 5, true), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'collapsed'), d(2, 'hidden'), d(3, 'hidden')]); + + foldingModel.update(ranges); + + assertRanges(textModel, foldingModel, [r(1, 6, true), r(2, 5, true), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'collapsed'), d(2, 'hidden'), d(3, 'hidden')]); + + foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(3)!]); + + assertRanges(textModel, foldingModel, [r1, r(2, 5, true), r(3, 4, true)]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + + foldingModel.update(ranges); + + assertRanges(textModel, foldingModel, [r1, r(2, 5, true), r(3, 4, true)]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + + textModel.dispose(); + } finally { + textModel.dispose(); + } + + }); + + test('folding decoration, with a defined startColumn', () => { + const lines = [ + /* 1*/ 'class A {', + /* 2*/ ' void foo() {', + /* 3*/ ' if (true) {', + /* 4*/ ' hoo();', + /* 5*/ ' }', + /* 6*/ ' }', + /* 7*/ '}']; + + const textModel = createTextModel(lines.join('\n')); + try { + const foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel)); + + const ranges = computeRangesWithModifiedStartColumn(textModel, false, undefined); + foldingModel.update(ranges); + + const r1 = r(1, 6, false, 9); + const r2 = r(2, 5, false, 14); + const r3 = r(3, 4, false, 15); + + assertRanges(textModel, foldingModel, [r1, r2, r3]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded', 9), d(2, 'expanded', 14), d(3, 'expanded', 15)]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(2)!]); - assertRanges(foldingModel, [r1, r(2, 5, true), r3]); - assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + assertRanges(textModel, foldingModel, [r1, r(2, 5, true, 14), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded', 9), d(2, 'collapsed', 14), d(3, 'hidden', 15)]); foldingModel.update(ranges); - assertRanges(foldingModel, [r1, r(2, 5, true), r3]); - assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + assertRanges(textModel, foldingModel, [r1, r(2, 5, true, 14), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded', 9), d(2, 'collapsed', 14), d(3, 'hidden', 15)]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!]); - assertRanges(foldingModel, [r(1, 6, true), r(2, 5, true), r3]); - assertDecorations(foldingModel, [d(1, 'collapsed'), d(2, 'hidden'), d(3, 'hidden')]); + assertRanges(textModel, foldingModel, [r(1, 6, true, 9), r(2, 5, true, 14), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'collapsed', 9), d(2, 'hidden', 14), d(3, 'hidden', 15)]); foldingModel.update(ranges); - assertRanges(foldingModel, [r(1, 6, true), r(2, 5, true), r3]); - assertDecorations(foldingModel, [d(1, 'collapsed'), d(2, 'hidden'), d(3, 'hidden')]); + assertRanges(textModel, foldingModel, [r(1, 6, true, 9), r(2, 5, true, 14), r3]); + assertDecorations(textModel, foldingModel, [d(1, 'collapsed', 9), d(2, 'hidden', 14), d(3, 'hidden', 15)]); foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(3)!]); - assertRanges(foldingModel, [r1, r(2, 5, true), r(3, 4, true)]); - assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + assertRanges(textModel, foldingModel, [r1, r(2, 5, true, 14), r(3, 4, true, 15)]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded', 9), d(2, 'collapsed', 14), d(3, 'hidden', 15)]); foldingModel.update(ranges); - assertRanges(foldingModel, [r1, r(2, 5, true), r(3, 4, true)]); - assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]); + assertRanges(textModel, foldingModel, [r1, r(2, 5, true, 14), r(3, 4, true, 15)]); + assertDecorations(textModel, foldingModel, [d(1, 'expanded', 9), d(2, 'collapsed', 14), d(3, 'hidden', 15)]); textModel.dispose(); } finally { @@ -863,7 +1084,7 @@ suite('Folding Model', () => { const r4 = r(5, 8, false); const r5 = r(6, 7, false); const r6 = r(9, 10, false); - assertRanges(foldingModel, [r1, r2, r3, r4, r5, r6]); + assertRanges(textModel, foldingModel, [r1, r2, r3, r4, r5, r6]); // Test jump to parent. assert.strictEqual(getParentFoldLine(7, foldingModel), 6); @@ -916,7 +1137,7 @@ suite('Folding Model', () => { const r1 = r(2, 3, false); const r2 = r(4, 6, false); - assertRanges(foldingModel, [r1, r2]); + assertRanges(textModel, foldingModel, [r1, r2]); // Test jump to next. assert.strictEqual(getNextFoldLine(1, foldingModel), 2); From d591451432f333e3e67021008f8650ca2f73ee6c Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Wed, 21 Dec 2022 04:27:55 -0500 Subject: [PATCH 10/19] update sanitizeAndMerge validation to include cases where ranges start at the same line but different columns --- .../contrib/folding/browser/foldingRanges.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index 15bd099c872e4..04ad768735e06 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -340,6 +340,7 @@ export class FoldingRegions { const stackedRanges: FoldRange[] = []; let topStackedRange: FoldRange | undefined; let prevLineNumber = 0; + let prevColumnNumber: number | undefined = 0; const resultRanges: FoldRange[] = []; while (nextA || nextB) { @@ -398,13 +399,22 @@ export class FoldingRegions { && topStackedRange.endLineNumber < useRange.startLineNumber) { topStackedRange = stackedRanges.pop(); } + + const useRangeColumnNumber = useRange.startColumn ?? MAX_COLUMN_NUMBER; + prevColumnNumber = prevColumnNumber ?? MAX_COLUMN_NUMBER; + + const useRangeStartsAfterPrevRange = useRange.startLineNumber > prevLineNumber + || (useRange.startLineNumber === prevLineNumber && useRangeColumnNumber > prevColumnNumber); + const useRangeEndsBeforeOrWithTopStackedRange = !topStackedRange + || topStackedRange.endLineNumber >= useRange.endLineNumber; + if (useRange.endLineNumber > useRange.startLineNumber - && useRange.startLineNumber > prevLineNumber - && useRange.endLineNumber <= maxLineNumber - && (!topStackedRange - || topStackedRange.endLineNumber >= useRange.endLineNumber)) { + && useRangeStartsAfterPrevRange + && useRangeEndsBeforeOrWithTopStackedRange + && useRange.endLineNumber <= maxLineNumber) { resultRanges.push(useRange); prevLineNumber = useRange.startLineNumber; + prevColumnNumber = useRange.startColumn; if (topStackedRange) { stackedRanges.push(topStackedRange); } From c2dbf98853b763af28a03d9f6e4ec82e26613c40 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Wed, 21 Dec 2022 04:36:06 -0500 Subject: [PATCH 11/19] fix sanitizeAndMerge not throwing out ranges starting at line 0 as a result of prevColumnNumber --- src/vs/editor/contrib/folding/browser/foldingRanges.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index 04ad768735e06..86f9214a66fcf 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -340,7 +340,7 @@ export class FoldingRegions { const stackedRanges: FoldRange[] = []; let topStackedRange: FoldRange | undefined; let prevLineNumber = 0; - let prevColumnNumber: number | undefined = 0; + let prevColumnNumber: number | undefined = undefined; const resultRanges: FoldRange[] = []; while (nextA || nextB) { From a5706ed31fb0db36802db4b3c90ab89342dda34c Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Wed, 21 Dec 2022 04:38:47 -0500 Subject: [PATCH 12/19] update foldingRanges tests to include startColumn cases --- .../test/browser/foldingRanges.test.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) 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 b0e968bbd2417..b7bd6855552d6 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -16,14 +16,15 @@ const markers: FoldingMarkers = { suite('FoldingRanges', () => { - const foldRange = (from: number, to: number, collapsed: boolean | undefined = undefined, source: FoldSource = FoldSource.provider, type: string | undefined = undefined, collapsedText: string | undefined = undefined) => + const foldRange = (from: number, to: number, collapsed: boolean | undefined = undefined, source: FoldSource = FoldSource.provider, type: string | undefined = undefined, collapsedText: string | undefined = undefined, startColumn: number | undefined = undefined) => { startLineNumber: from, endLineNumber: to, type: type, collapsedText: collapsedText, isCollapsed: collapsed || false, - source + source, + startColumn }; const assertEqualRanges = (range1: FoldRange, range2: FoldRange, msg: string) => { assert.strictEqual(range1.startLineNumber, range2.startLineNumber, msg + ' start'); @@ -32,6 +33,7 @@ suite('FoldingRanges', () => { assert.strictEqual(range1.collapsedText, range2.collapsedText, msg + ' collapsedText'); assert.strictEqual(range1.isCollapsed, range2.isCollapsed, msg + ' collapsed'); assert.strictEqual(range1.source, range2.source, msg + ' source'); + assert.strictEqual(range1.startColumn, range2.startColumn, msg + ' startColumn'); }; test('test max folding regions', () => { @@ -138,6 +140,7 @@ suite('FoldingRanges', () => { foldRange(22, 80, true, FoldSource.provider, 'D2', 'D2 ct'), // should merge with D1 ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + console.log(JSON.stringify(result, null, '\t')); assert.strictEqual(result.length, 3, 'result length1'); assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'A', 'A ct'), 'A1'); assertEqualRanges(result[1], foldRange(20, 80, true, FoldSource.provider, 'C1', 'C1 ct'), 'C1'); @@ -208,4 +211,27 @@ suite('FoldingRanges', () => { assertEqualRanges(result[2], foldRange(30, 38, true, FoldSource.recovered, 'b2', 'b2 ct'), 'R3'); }); + test('sanitizeAndMerge5', () => { + const regionSet1: FoldRange[] = [ + foldRange(0, 100, false, FoldSource.provider, 'Z1', 'Z1 ct', 1), // invalid, invalid start + foldRange(2, 100, false, FoldSource.provider, 'A', 'A ct', 2), // valid + foldRange(2, 100, false, FoldSource.provider, 'B', 'B ct', 3), // valid inside 'A' + foldRange(2, 100, false, FoldSource.provider, 'Z2', 'Z2 ct', 3), // invalid, duplicate start + foldRange(2, 100, false, FoldSource.provider, 'C', 'C ct'), // valid inside 'B', no startColumn => end of line + foldRange(3, 80, true, FoldSource.provider, 'D', 'D ct', 1), // valid inside 'C' + ]; + const regionSet2: FoldRange[] = [ + foldRange(2, 100, true, undefined, undefined, undefined, 3), // should merge with 'B' + foldRange(1, 80, true), // invalid, out of order + foldRange(2, 80, true, undefined, undefined, undefined, 2), // invalid, out of order + foldRange(3, 81, true, FoldSource.provider, 'Z3', 'Z3 ct'), // invalid, overlapping + ]; + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + assert.strictEqual(result.length, 4, 'result length1'); + assertEqualRanges(result[0], foldRange(2, 100, false, FoldSource.provider, 'A', 'A ct', 2), 'A'); + assertEqualRanges(result[1], foldRange(2, 100, true, FoldSource.provider, 'B', 'B ct', 3), 'B'); + assertEqualRanges(result[2], foldRange(2, 100, false, FoldSource.provider, 'C', 'C ct'), 'C'); + assertEqualRanges(result[3], foldRange(3, 80, true, FoldSource.provider, 'D', 'D ct', 1), 'D'); + }); + }); From 001846b806d74791767a3ee13e4c9896b77e9b79 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Wed, 21 Dec 2022 20:35:46 -0500 Subject: [PATCH 13/19] test foldingDecorations --- .../folding/browser/foldingDecorations.ts | 4 +- .../test/browser/foldingDecorations.test.ts | 253 ++++++++++++++++++ 2 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/vs/editor/contrib/folding/test/browser/foldingDecorations.test.ts diff --git a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts index d39e8fb515baf..2ca820827aa8b 100644 --- a/src/vs/editor/contrib/folding/browser/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/browser/foldingDecorations.ts @@ -13,7 +13,7 @@ import { editorSelectionBackground, iconForeground, registerColor, transparent } import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; -const foldBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hcDark: null, hcLight: null }, localize('foldBackgroundBackground', "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations."), true); +export const foldBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hcDark: null, hcLight: null }, localize('foldBackgroundBackground', "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations."), true); registerColor('editorGutter.foldingControlForeground', { dark: iconForeground, light: iconForeground, hcDark: iconForeground, hcLight: iconForeground }, localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.')); export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown, localize('foldingExpandedIcon', 'Icon for expanded ranges in the editor glyph margin.')); @@ -21,7 +21,7 @@ export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.ch export const foldingManualCollapsedIcon = registerIcon('folding-manual-collapsed', foldingCollapsedIcon, localize('foldingManualCollapedIcon', 'Icon for manually collapsed ranges in the editor glyph margin.')); export const foldingManualExpandedIcon = registerIcon('folding-manual-expanded', foldingExpandedIcon, localize('foldingManualExpandedIcon', 'Icon for manually expanded ranges in the editor glyph margin.')); -const foldedBackgroundMinimap = { color: themeColorFromId(foldBackground), position: MinimapPosition.Inline }; +export const foldedBackgroundMinimap = { color: themeColorFromId(foldBackground), position: MinimapPosition.Inline }; export class FoldingDecorationProvider implements IDecorationProvider { diff --git a/src/vs/editor/contrib/folding/test/browser/foldingDecorations.test.ts b/src/vs/editor/contrib/folding/test/browser/foldingDecorations.test.ts new file mode 100644 index 0000000000000..a95019c68494a --- /dev/null +++ b/src/vs/editor/contrib/folding/test/browser/foldingDecorations.test.ts @@ -0,0 +1,253 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; +import { foldedBackgroundMinimap, foldingCollapsedIcon, FoldingDecorationProvider, foldingExpandedIcon, foldingManualCollapsedIcon, foldingManualExpandedIcon } from 'vs/editor/contrib/folding/browser/foldingDecorations'; +import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; + +suite('Folding Decoration Provider', () => { + const ELLIPSES = '\u22EF'; /* ellipses unicode character */ + + const INJECTED_COLLAPSED_TEXT_OPTIONS: InjectedTextOptions = { + content: ELLIPSES, + inlineClassName: 'collapsed-text', + inlineClassNameAffectsLetterSpacing: true, + cursorStops: InjectedTextCursorStops.None + }; + + const COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION: IModelDecorationOptions = { + description: 'folding-collapsed-highlighted-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + className: 'folded-background', + minimap: foldedBackgroundMinimap, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon), + hideContent: true + }; + + const MANUALLY_COLLAPSED_VISUAL_DECORATION: IModelDecorationOptions = { + description: 'folding-manually-collapsed-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon), + hideContent: true + }; + + const MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION: IModelDecorationOptions = { + description: 'folding-manually-collapsed-highlighted-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + className: 'folded-background', + minimap: foldedBackgroundMinimap, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualCollapsedIcon), + hideContent: true + }; + + const NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION: IModelDecorationOptions = { + description: 'folding-no-controls-range-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + className: 'folded-background', + minimap: foldedBackgroundMinimap, + isWholeLine: true, + hideContent: true + }; + + const NO_CONTROLS_COLLAPSED_RANGE_DECORATION: IModelDecorationOptions = { + description: 'folding-no-controls-range-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + hideContent: true + }; + + const COLLAPSED_VISUAL_DECORATION: IModelDecorationOptions = { + description: 'folding-collapsed-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingCollapsedIcon), + hideContent: true, + }; + + const MANUALLY_EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-manually-expanded-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingManualExpandedIcon), + before: { content: '' } + }); + + const MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-manually-expanded-auto-hide-visual-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingManualExpandedIcon), + before: { content: '' } + }); + + const EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-expanded-visual-decoration', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: 'alwaysShowFoldIcons ' + ThemeIcon.asClassName(foldingExpandedIcon), + before: { content: '' } + }); + + const EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({ + description: 'folding-expanded-auto-hide-visual-decoration', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + isWholeLine: true, + firstLineDecorationClassName: ThemeIcon.asClassName(foldingExpandedIcon), + before: { content: '' } + }); + + const NO_CONTROLS_EXPANDED_RANGE_DECORATION = ModelDecorationOptions.register({ + description: 'folding-no-controls-range-decoration', + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + isWholeLine: true, + before: { content: '' } + }); + + const HIDDEN_RANGE_DECORATION = ModelDecorationOptions.register({ + description: 'folding-hidden-range-decoration', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + before: { content: '' } + }); + + let model: TextModel; + let editor: ICodeEditor; + + setup(() => { + model = createTextModel(''); + editor = createTestCodeEditor(model); + }); + + teardown(() => { + model.dispose(); + editor.dispose(); + }); + + test('getDecorationOption, hidden', () => { + const provider = new FoldingDecorationProvider(editor); + const options = provider.getDecorationOption(false, true, false); + assert.deepStrictEqual(options, HIDDEN_RANGE_DECORATION); + }); + + test('getDecorationOption, no controls', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingControls = 'never'; + const options = provider.getDecorationOption(false, false, false); + assert.deepStrictEqual(options, NO_CONTROLS_EXPANDED_RANGE_DECORATION); + }); + + test('getDecorationOption, auto hide controls', () => { + const provider = new FoldingDecorationProvider(editor); + const options = provider.getDecorationOption(false, false, false); + assert.deepStrictEqual(options, EXPANDED_AUTO_HIDE_VISUAL_DECORATION); + }); + + test('getDecorationOption, always show controls', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingControls = 'always'; + const options = provider.getDecorationOption(false, false, false); + assert.deepStrictEqual(options, EXPANDED_VISUAL_DECORATION); + }); + + test('getDecorationOption, auto hide controls, manually expanded', () => { + const provider = new FoldingDecorationProvider(editor); + const options = provider.getDecorationOption(false, false, true); + assert.deepStrictEqual(options, MANUALLY_EXPANDED_AUTO_HIDE_VISUAL_DECORATION); + }); + + test('getDecorationOption, always show controls, manually expanded', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingControls = 'always'; + const options = provider.getDecorationOption(false, false, true); + assert.deepStrictEqual(options, MANUALLY_EXPANDED_VISUAL_DECORATION); + }); + + test('getDecorationOption, no controls, collapsed', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingHighlights = false; + provider.showFoldingControls = 'never'; + const options = provider.getDecorationOption(true, false, false); + assert.deepStrictEqual(options, applyCollapsedText(NO_CONTROLS_COLLAPSED_RANGE_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, auto hide controls, collapsed', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingHighlights = false; + const options = provider.getDecorationOption(true, false, false); + assert.deepStrictEqual(options, applyCollapsedText(COLLAPSED_VISUAL_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, always show controls, collapsed', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingHighlights = false; + provider.showFoldingControls = 'always'; + const options = provider.getDecorationOption(true, false, false); + assert.deepStrictEqual(options, applyCollapsedText(COLLAPSED_VISUAL_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, no controls, collapsed, highlighted', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingControls = 'never'; + const options = provider.getDecorationOption(true, false, false); + assert.deepStrictEqual(options, applyCollapsedText(NO_CONTROLS_COLLAPSED_HIGHLIGHTED_RANGE_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, auto hide controls, collapsed, highlighted', () => { + const provider = new FoldingDecorationProvider(editor); + const options = provider.getDecorationOption(true, false, false); + assert.deepStrictEqual(options, applyCollapsedText(COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, always show controls, collapsed, highlighted', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingControls = 'always'; + const options = provider.getDecorationOption(true, false, false); + assert.deepStrictEqual(options, applyCollapsedText(COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, manually collapsed', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingHighlights = false; + const options = provider.getDecorationOption(true, false, true); + assert.deepStrictEqual(options, applyCollapsedText(MANUALLY_COLLAPSED_VISUAL_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, manually collapsed, highlighted', () => { + const provider = new FoldingDecorationProvider(editor); + const options = provider.getDecorationOption(true, false, true); + assert.deepStrictEqual(options, applyCollapsedText(MANUALLY_COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION, ELLIPSES)); + }); + + test('getDecorationOption, collapsed with custom text', () => { + const provider = new FoldingDecorationProvider(editor); + provider.showFoldingHighlights = false; + const options = provider.getDecorationOption(true, false, false, 'custom text'); + assert.deepStrictEqual(options, applyCollapsedText(COLLAPSED_VISUAL_DECORATION, 'custom text')); + }); + + test('getDecorationOption, collapsed with custom text, highlighted', () => { + const provider = new FoldingDecorationProvider(editor); + const options = provider.getDecorationOption(true, false, false, 'custom text'); + assert.deepStrictEqual(options, applyCollapsedText(COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION, 'custom text')); + }); + + function applyCollapsedText(decorationOptions: IModelDecorationOptions, collapsedText: string) { + const before: InjectedTextOptions = { ...INJECTED_COLLAPSED_TEXT_OPTIONS, content: replaceVisibleWhiteSpace(collapsedText) }; + return ModelDecorationOptions.register({ ...decorationOptions, before }); + } + + function replaceVisibleWhiteSpace(str: string) { + const noBreakWhitespace = '\xa0'; + return str.replace(/[ \t]/g, noBreakWhitespace); + } +}); + + From e353b188c2cab19dd8980bb4765dfa9e0b2e26f5 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Thu, 22 Dec 2022 23:12:16 -0500 Subject: [PATCH 14/19] support having an empty collapsedText --- src/vs/editor/common/textModelEvents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 31684a4aa4dbc..da43c42fcc8b6 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -155,7 +155,7 @@ export class LineInjectedText { public static fromDecorations(decorations: IModelDecoration[]): LineInjectedText[] { const result: LineInjectedText[] = []; for (const decoration of decorations) { - if (decoration.options.before && decoration.options.before.content.length > 0) { + if (decoration.options.before) { result.push(new LineInjectedText( decoration.ownerId, decoration.range.startLineNumber, @@ -164,7 +164,7 @@ export class LineInjectedText { decoration.options.hideContent ? 2 : 0, //collapsedText should always render last )); } - if (decoration.options.after && decoration.options.after.content.length > 0) { + if (decoration.options.after) { result.push(new LineInjectedText( decoration.ownerId, decoration.range.endLineNumber, From 8bec5d82f8ae973fb7c263549205252624a0f438 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Thu, 22 Dec 2022 23:12:56 -0500 Subject: [PATCH 15/19] update modelLineProjection tests to include inline fold cases --- .../viewModel/modelLineProjection.test.ts | 738 +++++++++++++++++- 1 file changed, 737 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index ecc3ac4178ec3..b3c71122846e7 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { EndOfLinePreference } from 'vs/editor/common/model'; +import { EndOfLinePreference, IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import * as languages from 'vs/editor/common/languages'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; @@ -939,6 +939,742 @@ suite('SplitLinesCollection', () => { }); }); + test('getViewLinesData - with inline folding', () => { + const partiallyFoldLineDecoration: IModelDeltaDecoration = { + range: new Range(1, 9, 1, 13), + options: { + before: { + content: '', + inlineClassName: 'myClassName' + }, + description: 'partial-fold', + hideContent: true + } + }; + //Invalid decoration, can't fold a full line with empty collapsedText. + //Should render the whole line. + const fullLineFoldDecoration: IModelDeltaDecoration = { + range: new Range(6, 1, 6, 62), + options: { + before: { + content: '', + inlineClassName: 'myClassName' + }, + description: 'full-fold', + hideContent: true + } + }; + + model.deltaDecorations([], [partiallyFoldLineDecoration, fullLineFoldDecoration]); + + withSplitLinesCollection(model, 'off', 0, (splitLinesCollection) => { + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); + + const _expected: ITestMinimapLineRenderingData[] = [ + { + content: 'class Ni', + minColumn: 1, + maxColumn: 9, + tokens: [ + { endIndex: 5, value: 1 }, + { endIndex: 6, value: 2 }, + { endIndex: 8, value: 3 }, + ] + }, + { + content: ' function hi() {', + minColumn: 1, + maxColumn: 17, + tokens: [ + { endIndex: 1, value: 5 }, + { endIndex: 9, value: 6 }, + { endIndex: 10, value: 7 }, + { endIndex: 12, value: 8 }, + { endIndex: 16, value: 9 }, + ] + }, + { + content: ' console.log("Hello world");', + minColumn: 1, + maxColumn: 30, + tokens: [ + { endIndex: 2, value: 10 }, + { endIndex: 9, value: 11 }, + { endIndex: 10, value: 12 }, + { endIndex: 13, value: 13 }, + { endIndex: 14, value: 14 }, + { endIndex: 27, value: 15 }, + { endIndex: 29, value: 16 }, + ] + }, + { + content: ' }', + minColumn: 1, + maxColumn: 3, + tokens: [ + { endIndex: 2, value: 17 }, + ] + }, + { + content: ' function hello() {', + minColumn: 1, + maxColumn: 20, + tokens: [ + { endIndex: 1, value: 18 }, + { endIndex: 9, value: 19 }, + { endIndex: 10, value: 20 }, + { endIndex: 15, value: 21 }, + { endIndex: 19, value: 22 }, + ] + }, + { + content: ' console.log("Hello world, this is a somewhat longer line");', + minColumn: 1, + maxColumn: 62, + tokens: [ + { endIndex: 2, value: 23 }, + { endIndex: 9, value: 24 }, + { endIndex: 10, value: 25 }, + { endIndex: 13, value: 26 }, + { endIndex: 14, value: 27 }, + { endIndex: 59, value: 28 }, + { endIndex: 61, value: 29 }, + ] + }, + { + minColumn: 1, + maxColumn: 3, + content: ' }', + tokens: [ + { endIndex: 2, value: 30 }, + ] + }, + { + minColumn: 1, + maxColumn: 2, + content: '}', + tokens: [ + { endIndex: 1, value: 31 }, + ] + } + ]; + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[1], + _expected[2], + _expected[3], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + ]); + + splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 5); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + ]); + }); + }); + + test('getViewLinesData - with inline folding and collapsedText', () => { + const partiallyFoldLineDecoration: IModelDeltaDecoration = { + range: new Range(1, 9, 1, 13), + options: { + before: { + content: 'some text', + inlineClassName: 'myClassName' + }, + description: 'partial-fold', + hideContent: true + } + }; + + const fullLineFoldDecoration: IModelDeltaDecoration = { + range: new Range(6, 1, 6, 62), + options: { + before: { + content: 'some another text', + inlineClassName: 'myClassName' + }, + description: 'full-fold', + hideContent: true + } + }; + + model.deltaDecorations([], [partiallyFoldLineDecoration, fullLineFoldDecoration]); + + withSplitLinesCollection(model, 'off', 0, (splitLinesCollection) => { + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); + + const _expected: ITestMinimapLineRenderingData[] = [ + { + content: 'class Nisome text', + minColumn: 1, + maxColumn: 18, + tokens: [ + { endIndex: 5, value: 1 }, + { endIndex: 6, value: 2 }, + { endIndex: 8, value: 3 }, + { endIndex: 17, value: 1 }, + ] + }, + { + content: ' function hi() {', + minColumn: 1, + maxColumn: 17, + tokens: [ + { endIndex: 1, value: 5 }, + { endIndex: 9, value: 6 }, + { endIndex: 10, value: 7 }, + { endIndex: 12, value: 8 }, + { endIndex: 16, value: 9 }, + ] + }, + { + content: ' console.log("Hello world");', + minColumn: 1, + maxColumn: 30, + tokens: [ + { endIndex: 2, value: 10 }, + { endIndex: 9, value: 11 }, + { endIndex: 10, value: 12 }, + { endIndex: 13, value: 13 }, + { endIndex: 14, value: 14 }, + { endIndex: 27, value: 15 }, + { endIndex: 29, value: 16 }, + ] + }, + { + content: ' }', + minColumn: 1, + maxColumn: 3, + tokens: [ + { endIndex: 2, value: 17 }, + ] + }, + { + content: ' function hello() {', + minColumn: 1, + maxColumn: 20, + tokens: [ + { endIndex: 1, value: 18 }, + { endIndex: 9, value: 19 }, + { endIndex: 10, value: 20 }, + { endIndex: 15, value: 21 }, + { endIndex: 19, value: 22 }, + ] + }, + { + content: 'some another text', + minColumn: 1, + maxColumn: 18, + tokens: [ + { endIndex: 17, value: 1 }, + ] + }, + { + minColumn: 1, + maxColumn: 3, + content: ' }', + tokens: [ + { endIndex: 2, value: 30 }, + ] + }, + { + minColumn: 1, + maxColumn: 2, + content: '}', + tokens: [ + { endIndex: 1, value: 31 }, + ] + } + ]; + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[1], + _expected[2], + _expected[3], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + ]); + + splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 5); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + ]); + }); + }); + + test('getViewLinesData - with inline fold and wrapping', () => { + const beforeWrappingFoldDecoration: IModelDeltaDecoration = { + range: new Range(3, 13, 3, 30), + options: { + before: { + content: 'yup gone', + inlineClassName: 'myClassName' + }, + description: 'before-wrapping-fold', + hideContent: true + } + }; + + const afterWrappingFoldDecoration: IModelDeltaDecoration = { + range: new Range(6, 59, 6, 62), + options: { + before: { + content: 'yup also gone', + inlineClassName: 'myClassName' + }, + description: 'after-wrapping-fold', + hideContent: true + } + }; + + model.deltaDecorations([], [beforeWrappingFoldDecoration, afterWrappingFoldDecoration]); + + + withSplitLinesCollection(model, 'wordWrapColumn', 30, (splitLinesCollection) => { + assert.strictEqual(splitLinesCollection.getViewLineCount(), 11); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); + + const _expected: ITestMinimapLineRenderingData[] = [ + { + content: 'class Nice {', + minColumn: 1, + maxColumn: 13, + tokens: [ + { endIndex: 5, value: 1 }, + { endIndex: 6, value: 2 }, + { endIndex: 10, value: 3 }, + { endIndex: 12, value: 4 }, + ] + }, + { + content: ' function hi() {', + minColumn: 1, + maxColumn: 17, + tokens: [ + { endIndex: 1, value: 5 }, + { endIndex: 9, value: 6 }, + { endIndex: 10, value: 7 }, + { endIndex: 12, value: 8 }, + { endIndex: 16, value: 9 }, + ] + }, + { + content: ' console.loyup gone', + minColumn: 1, + maxColumn: 21, + tokens: [ + { endIndex: 2, value: 10 }, + { endIndex: 9, value: 11 }, + { endIndex: 10, value: 12 }, + { endIndex: 12, value: 13 }, + { endIndex: 20, value: 1 }, + ] + }, + { + content: ' }', + minColumn: 1, + maxColumn: 3, + tokens: [ + { endIndex: 2, value: 17 }, + ] + }, + { + content: ' function hello() {', + minColumn: 1, + maxColumn: 20, + tokens: [ + { endIndex: 1, value: 18 }, + { endIndex: 9, value: 19 }, + { endIndex: 10, value: 20 }, + { endIndex: 15, value: 21 }, + { endIndex: 19, value: 22 }, + ] + }, + { + content: ' console.log("Hello ', + minColumn: 1, + maxColumn: 22, + tokens: [ + { endIndex: 2, value: 23 }, + { endIndex: 9, value: 24 }, + { endIndex: 10, value: 25 }, + { endIndex: 13, value: 26 }, + { endIndex: 14, value: 27 }, + { endIndex: 21, value: 28 }, + ] + }, + { + content: ' world, this is a ', + minColumn: 13, + maxColumn: 30, + tokens: [ + { endIndex: 29, value: 28 }, + ] + }, + { + content: ' somewhat longer ', + minColumn: 13, + maxColumn: 29, + tokens: [ + { endIndex: 28, value: 28 }, + ] + }, + { + content: ' lineyup also gone', + minColumn: 13, + maxColumn: 30, + tokens: [ + { endIndex: 16, value: 28 }, + { endIndex: 29, value: 1 }, + ] + }, + { + content: ' }', + minColumn: 1, + maxColumn: 3, + tokens: [ + { endIndex: 2, value: 30 }, + ] + }, + { + content: '}', + minColumn: 1, + maxColumn: 2, + tokens: [ + { endIndex: 1, value: 31 }, + ] + } + ]; + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[1], + _expected[2], + _expected[3], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + _expected[8], + _expected[9], + _expected[10], + ]); + + splitLinesCollection.setHiddenAreas([new Range(2, 1, 4, 1)]); + assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(3, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(4, 1), false); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(5, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(6, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(7, 1), true); + assert.strictEqual(splitLinesCollection.modelPositionIsVisible(8, 1), true); + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + _expected[8], + _expected[9], + ]); + }); + }); + + + test('getViewLinesData - with inline fold, wrapping and injected text', () => { + const beforeInjectedTextDecoration: IModelDeltaDecoration = { + range: new Range(1, 4, 1, 13), + options: { + description: 'example', + before: { + content: 'nothing to see here', + inlineClassName: 'myClassName' + }, + hideContent: true + } + }; + const longInjectedTextDecoration: IModelDeltaDecoration = { + range: new Range(1, 9, 1, 10), + options: { + description: 'example', + after: { + content: 'very very long injected text that causes a line break', + inlineClassName: 'myClassName' + }, + } + }; + const anotherInjectedTextDecoration: IModelDeltaDecoration = { + range: new Range(5, 9, 5, 10), + options: { + description: 'example', + after: { + content: 'some injected text', + inlineClassName: 'myClassName' + } + } + }; + const afterInjectedTextDecoration: IModelDeltaDecoration = { + range: new Range(5, 16, 5, 20), + options: { + description: 'example', + before: { + content: 'should show after', + inlineClassName: 'myClassName' + }, + hideContent: true + } + }; + + model.deltaDecorations([], [beforeInjectedTextDecoration, longInjectedTextDecoration, anotherInjectedTextDecoration, afterInjectedTextDecoration]); + + withSplitLinesCollection(model, 'wordWrapColumn', 30, (splitLinesCollection) => { + assert.strictEqual(splitLinesCollection.getViewLineCount(), 14); + + const _expected: ITestMinimapLineRenderingData[] = [ + { + content: 'clanothing to see here', + minColumn: 1, + maxColumn: 23, + tokens: [ + { endIndex: 3, value: 1 }, + { endIndex: 22, value: 1 }, + ] + }, + { + content: ' function hi() {', + minColumn: 1, + maxColumn: 17, + tokens: [ + { endIndex: 1, value: 5 }, + { endIndex: 9, value: 6 }, + { endIndex: 10, value: 7 }, + { endIndex: 12, value: 8 }, + { endIndex: 16, value: 9 }, + ] + }, + { + content: ' console.log("Hello ', + minColumn: 1, + maxColumn: 22, + tokens: [ + { endIndex: 2, value: 10 }, + { endIndex: 9, value: 11 }, + { endIndex: 10, value: 12 }, + { endIndex: 13, value: 13 }, + { endIndex: 14, value: 14 }, + { endIndex: 21, value: 15 }, + ] + }, + { + content: ' world");', + minColumn: 13, + maxColumn: 21, + tokens: [ + { endIndex: 18, value: 15 }, + { endIndex: 20, value: 16 }, + ] + }, + { + content: ' }', + minColumn: 1, + maxColumn: 3, + tokens: [ + { endIndex: 2, value: 17 }, + ] + }, + { + content: ' functionsome injected ', + minColumn: 1, + maxColumn: 24, + tokens: [ + { endIndex: 1, value: 18 }, + { endIndex: 9, value: 19 }, + { endIndex: 23, value: 1 }, + ] + }, + { + content: ' text helloshould show ', + minColumn: 9, + maxColumn: 31, + tokens: [ + { endIndex: 12, value: 1 }, + { endIndex: 13, value: 20 }, + { endIndex: 18, value: 21 }, + { endIndex: 30, value: 1 }, + ] + }, + { + content: ' after', + minColumn: 9, + maxColumn: 14, + tokens: [ + { endIndex: 13, value: 1 }, + ] + }, + { + content: ' console.log("Hello ', + minColumn: 1, + maxColumn: 22, + tokens: [ + { endIndex: 2, value: 23 }, + { endIndex: 9, value: 24 }, + { endIndex: 10, value: 25 }, + { endIndex: 13, value: 26 }, + { endIndex: 14, value: 27 }, + { endIndex: 21, value: 28 }, + ] + }, + { + content: ' world, this is a ', + minColumn: 13, + maxColumn: 30, + tokens: [ + { endIndex: 29, value: 28 }, + ] + }, + { + content: ' somewhat longer ', + minColumn: 13, + maxColumn: 29, + tokens: [ + { endIndex: 28, value: 28 }, + ] + }, + { + content: ' line");', + minColumn: 13, + maxColumn: 20, + tokens: [ + { endIndex: 17, value: 28 }, + { endIndex: 19, value: 29 }, + ] + }, + { + content: ' }', + minColumn: 1, + maxColumn: 3, + tokens: [ + { endIndex: 2, value: 30 }, + ] + }, + { + content: '}', + minColumn: 1, + maxColumn: 2, + tokens: [ + { endIndex: 1, value: 31 }, + ] + } + ]; + + assertAllMinimapLinesRenderingData(splitLinesCollection, [ + _expected[0], + _expected[1], + _expected[2], + _expected[3], + _expected[4], + _expected[5], + _expected[6], + _expected[7], + _expected[8], + _expected[9], + _expected[10], + _expected[11], + _expected[12], + _expected[13], + ]); + + const data = splitLinesCollection.getViewLinesData(1, 14, new Array(14).fill(true)); + assert.deepStrictEqual( + data.map((d) => ({ + inlineDecorations: d.inlineDecorations?.map((d) => ({ + startOffset: d.startOffset, + endOffset: d.endOffset, + })), + })), + [ + { inlineDecorations: [{ startOffset: 3, endOffset: 22 }] }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: [{ startOffset: 9, endOffset: 23 }] }, + { inlineDecorations: [{ startOffset: 8, endOffset: 12 }, { startOffset: 18, endOffset: 35 }] }, + { inlineDecorations: [{ startOffset: 8, endOffset: 13 }] }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + { inlineDecorations: undefined }, + ] + ); + }); + }); + + + function withSplitLinesCollection(model: TextModel, wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded', wordWrapColumn: number, callback: (splitLinesCollection: ViewModelLinesFromProjectedModel) => void): void { const configuration = new TestConfiguration({ wordWrap: wordWrap, From e98387cf44d3ec94435bd1b845ab33ca21ac0cfb Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 2 Jan 2023 15:46:13 -0500 Subject: [PATCH 16/19] move collapsedText and startColumn properties to vscode.proposed.collapsedText --- .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostTypeConverters.ts | 2 +- .../common/extensionsApiProposals.ts | 1 + src/vscode-dts/vscode.d.ts | 12 +--- .../vscode.proposed.collapsedText.d.ts | 55 +++++++++++++++++++ 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.collapsedText.d.ts diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 48e67749ad885..fc2fe2093724a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1273,6 +1273,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FileType: files.FileType, FilePermission: files.FilePermission, FoldingRange: extHostTypes.FoldingRange, + FoldingRange2: extHostTypes.FoldingRange, FoldingRangeKind: extHostTypes.FoldingRangeKind, FunctionBreakpoint: extHostTypes.FunctionBreakpoint, InlineCompletionItem: extHostTypes.InlineSuggestion, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d15f1f6455e09..57df2e07fd970 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1375,7 +1375,7 @@ export namespace ProgressLocation { } export namespace FoldingRange { - export function from(r: vscode.FoldingRange): languages.FoldingRange { + export function from(r: vscode.FoldingRange2): languages.FoldingRange { const range: languages.FoldingRange = { start: r.start + 1, end: r.end + 1, diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index d8a6dd45156b2..fed5b4c86e773 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -8,6 +8,7 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', + collapsedText: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.collapsedText.d.ts', commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index b49946bb5af41..600560edf1a99 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -5175,11 +5175,6 @@ declare module 'vscode' { */ start: number; - /** - * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. - */ - startColumn?: number; - /** * The zero-based end line of the range to fold. The folded area ends with the line's last character. * To be valid, the end must be zero or larger and smaller than the number of lines in the document. @@ -5195,11 +5190,6 @@ declare module 'vscode' { */ kind?: FoldingRangeKind; - /** - * The text to be shown instead of the folded area. - */ - collapsedText?: string; - /** * Creates a new folding range. * @@ -5207,7 +5197,7 @@ declare module 'vscode' { * @param end The end line of the folded range. * @param kind The kind of the folding range. */ - constructor(start: number, end: number, kind?: FoldingRangeKind, collapsedText?: string); + constructor(start: number, end: number, kind?: FoldingRangeKind); } /** diff --git a/src/vscode-dts/vscode.proposed.collapsedText.d.ts b/src/vscode-dts/vscode.proposed.collapsedText.d.ts new file mode 100644 index 0000000000000..76941ba6c6bf4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.collapsedText.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * A line based folding range. To be valid, start and end line must be bigger than zero and smaller than the number of lines in the document. + * Invalid ranges will be ignored. + */ + export class FoldingRange2 { + + /** + * The zero-based start line of the range to fold. + * The folded area starts at the start column if set, otherwise, after the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + start: number; + + /** + * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + */ + startColumn?: number; + + /** + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + end: number; + + /** + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands + * like 'Fold all comments'. See + * {@link FoldingRangeKind} for an enumeration of all kinds. + * If not set, the range is originated from a syntax element. + */ + kind?: FoldingRangeKind; + + /** + * The text to be shown instead of the folded area. + */ + collapsedText?: string; + + /** + * Creates a new folding range. + * + * @param start The start line of the folded range. + * @param end The end line of the folded range. + * @param kind The kind of the folding range. + */ + constructor(start: number, end: number, kind?: FoldingRangeKind, collapsedText?: string, startColumn?: number); + } +} From b6420013259a00624a29ba9af514f4a9a58473fe Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 2 Jan 2023 16:59:52 -0500 Subject: [PATCH 17/19] make sanitizeAndMerge consider ranges duplicate if one has startColumn == undefined and other has startColumn == lineMaxColumn --- src/vs/editor/contrib/folding/browser/folding.ts | 2 +- .../editor/contrib/folding/browser/foldingModel.ts | 4 ++-- .../editor/contrib/folding/browser/foldingRanges.ts | 13 +++++++++---- .../folding/test/browser/foldingRanges.test.ts | 10 +++++----- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 0127e2af7c06f..f5036f68fdf7c 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -1130,7 +1130,7 @@ class FoldRangeFromSelectionAction extends FoldingAction { collapseRanges.sort((a, b) => { return a.startLineNumber - b.startLineNumber; }); - const newRanges = FoldingRegions.sanitizeAndMerge(foldingModel.regions, collapseRanges, editor.getModel()?.getLineCount()); + const newRanges = FoldingRegions.sanitizeAndMerge(foldingModel.regions, collapseRanges, editor.getModel()); foldingModel.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } } diff --git a/src/vs/editor/contrib/folding/browser/foldingModel.ts b/src/vs/editor/contrib/folding/browser/foldingModel.ts index 7238b75c6e454..18ce0096b2f34 100644 --- a/src/vs/editor/contrib/folding/browser/foldingModel.ts +++ b/src/vs/editor/contrib/folding/browser/foldingModel.ts @@ -112,7 +112,7 @@ export class FoldingModel { public update(newRegions: FoldingRegions, blockedLineNumers: number[] = []): void { const foldedOrManualRanges = this._currentFoldedOrManualRanges(blockedLineNumers); - const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, foldedOrManualRanges, this._textModel.getLineCount()); + const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, foldedOrManualRanges, this._textModel); this.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } @@ -234,7 +234,7 @@ export class FoldingModel { } } - const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, rangesToRestore, maxLineNumber); + const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, rangesToRestore, this._textModel); this.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } diff --git a/src/vs/editor/contrib/folding/browser/foldingRanges.ts b/src/vs/editor/contrib/folding/browser/foldingRanges.ts index 86f9214a66fcf..6d49ab2e57f2a 100644 --- a/src/vs/editor/contrib/folding/browser/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/browser/foldingRanges.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ITextModel } from 'vs/editor/common/model'; + export interface ILineRange { startLineNumber: number; endLineNumber: number; @@ -322,8 +324,8 @@ export class FoldingRegions { public static sanitizeAndMerge( rangesA: FoldingRegions | FoldRange[], rangesB: FoldingRegions | FoldRange[], - maxLineNumber: number | undefined): FoldRange[] { - maxLineNumber = maxLineNumber ?? Number.MAX_VALUE; + textModel: ITextModel | null): FoldRange[] { + const maxLineNumber = textModel?.getLineCount() ?? Number.MAX_VALUE; const getIndexedFunction = (r: FoldingRegions | FoldRange[], limit: number) => { return Array.isArray(r) @@ -344,8 +346,11 @@ export class FoldingRegions { const resultRanges: FoldRange[] = []; while (nextA || nextB) { - const aStartColumn = nextA?.startColumn ?? MAX_COLUMN_NUMBER; - const bStartColumn = nextB?.startColumn ?? MAX_COLUMN_NUMBER; + const aMaxColumn = (textModel && nextA) ? textModel.getLineMaxColumn(nextA.startLineNumber) : MAX_COLUMN_NUMBER; + const aStartColumn = nextA?.startColumn ?? aMaxColumn; + + const bMaxColumn = (textModel && nextB) ? textModel.getLineMaxColumn(nextB.startLineNumber) : MAX_COLUMN_NUMBER; + const bStartColumn = nextB?.startColumn ?? bMaxColumn; const aStartsAtB = nextA && nextB && nextA.startLineNumber === nextB.startLineNumber && aStartColumn === bStartColumn; 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 b7bd6855552d6..6cdb2100d7caf 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -139,7 +139,7 @@ suite('FoldingRanges', () => { foldRange(21, 81, true, FoldSource.provider, 'Z', 'Z ct'), // invalid, overlapping foldRange(22, 80, true, FoldSource.provider, 'D2', 'D2 ct'), // should merge with D1 ]; - const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, null); console.log(JSON.stringify(result, null, '\t')); assert.strictEqual(result.length, 3, 'result length1'); assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'A', 'A ct'), 'A1'); @@ -163,7 +163,7 @@ suite('FoldingRanges', () => { foldRange(80, 90, true, FoldSource.userDefined, 'b4', 'b4 ct'), // overlaps a6 foldRange(92, 100, true, FoldSource.userDefined, 'b5', 'b5 ct'), // valid ]; - const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, null); assert.strictEqual(result.length, 9, 'result length1'); assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), 'P1'); assertEqualRanges(result[1], foldRange(2, 100, false, FoldSource.provider, 'a2', 'a2 ct'), 'P2'); @@ -187,7 +187,7 @@ suite('FoldingRanges', () => { foldRange(20, 28, true, FoldSource.provider, 'b2', 'b2 ct'), // should remain foldRange(30, 39, true, FoldSource.recovered, 'b3', 'b3 ct'), // should remain ]; - const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, null); assert.strictEqual(result.length, 5, 'result length3'); assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), 'R1'); assertEqualRanges(result[1], foldRange(10, 29, true, FoldSource.provider, 'a2', 'a2 ct'), 'R2'); @@ -204,7 +204,7 @@ suite('FoldingRanges', () => { foldRange(20, 28, true, FoldSource.provider, 'b1', 'b1 ct'), // hidden foldRange(30, 38, true, FoldSource.provider, 'b2', 'b2 ct'), // hidden ]; - const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, null); assert.strictEqual(result.length, 3, 'result length4'); assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'a1', 'a1 ct'), 'R1'); assertEqualRanges(result[1], foldRange(20, 28, true, FoldSource.recovered, 'b1', 'b1 ct'), 'R2'); @@ -226,7 +226,7 @@ suite('FoldingRanges', () => { foldRange(2, 80, true, undefined, undefined, undefined, 2), // invalid, out of order foldRange(3, 81, true, FoldSource.provider, 'Z3', 'Z3 ct'), // invalid, overlapping ]; - const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, 100); + const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, null); assert.strictEqual(result.length, 4, 'result length1'); assertEqualRanges(result[0], foldRange(2, 100, false, FoldSource.provider, 'A', 'A ct', 2), 'A'); assertEqualRanges(result[1], foldRange(2, 100, true, FoldSource.provider, 'B', 'B ct', 3), 'B'); From 84ba9e51e28c684dcb51fb5e659739bd9b7b1332 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Mon, 2 Jan 2023 17:02:26 -0500 Subject: [PATCH 18/19] clean up --- src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts | 1 - 1 file changed, 1 deletion(-) 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 6cdb2100d7caf..2d2ed120e5b30 100644 --- a/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts +++ b/src/vs/editor/contrib/folding/test/browser/foldingRanges.test.ts @@ -140,7 +140,6 @@ suite('FoldingRanges', () => { foldRange(22, 80, true, FoldSource.provider, 'D2', 'D2 ct'), // should merge with D1 ]; const result = FoldingRegions.sanitizeAndMerge(regionSet1, regionSet2, null); - console.log(JSON.stringify(result, null, '\t')); assert.strictEqual(result.length, 3, 'result length1'); assertEqualRanges(result[0], foldRange(1, 100, false, FoldSource.provider, 'A', 'A ct'), 'A1'); assertEqualRanges(result[1], foldRange(20, 80, true, FoldSource.provider, 'C1', 'C1 ct'), 'C1'); From 5ed4abc7fe540d2c97c83d951200ef6bbe237708 Mon Sep 17 00:00:00 2001 From: Mohammad Baqer Date: Thu, 5 Jan 2023 10:09:52 -0500 Subject: [PATCH 19/19] enforce enabledApiProposals checking for collapsedText --- .../api/common/extHostLanguageFeatures.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 5b2b58a9e402b..d5e26ad62a679 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -33,7 +33,7 @@ import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; import { isCancellationError, NotImplementedError } from 'vs/base/common/errors'; import { raceCancellationError } from 'vs/base/common/async'; -import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; // --- adapter @@ -2263,7 +2263,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext, token: CancellationToken): Promise { - return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context, token), undefined, token); + const provideFoldingRangesWrapper = (adapter: FoldingProviderAdapter) => adapter.provideFoldingRanges(URI.revive(resource), context, token).then(result => { + const data = this._adapter.get(handle); + if (data && result) { + for (const range of result) { + if (range.collapsedText || range.startColumn !== undefined) { + checkProposedApiEnabled(data.extension, 'collapsedText'); + } + } + } + return result; + }); + return this._withAdapter(handle, FoldingProviderAdapter, provideFoldingRangesWrapper, undefined, token); } // --- smart select