diff --git a/extensions/markdown-language-features/src/features/foldingProvider.ts b/extensions/markdown-language-features/src/features/foldingProvider.ts index 236908a1f1f..5f0d6480ff3 100644 --- a/extensions/markdown-language-features/src/features/foldingProvider.ts +++ b/extensions/markdown-language-features/src/features/foldingProvider.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContentsProvider } from '../tableOfContentsProvider'; +import { Token } from 'markdown-it'; const rangeLimit = 5000; @@ -16,15 +17,44 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi private readonly engine: MarkdownEngine ) { } + private async getRegions(document: vscode.TextDocument): Promise { + + const isStartRegion = (t: string) => /^\s*/.test(t); + const isEndRegion = (t: string) => /^\s*/.test(t); + + const isRegionMarker = (token: Token) => token.type === 'html_block' && + (isStartRegion(token.content) || isEndRegion(token.content)); + + + const tokens = await this.engine.parse(document.uri, document.getText()); + const regionMarkers = tokens.filter(isRegionMarker) + .map(token => ({ line: token.map[0], isStart: isStartRegion(token.content) })); + + const nestingStack: { line: number, isStart: boolean }[] = []; + return regionMarkers + .map(marker => { + if (marker.isStart) { + nestingStack.push(marker); + } else if (nestingStack.length && nestingStack[nestingStack.length - 1].isStart) { + return new vscode.FoldingRange(nestingStack.pop()!.line, marker.line, vscode.FoldingRangeKind.Region); + } else { + // noop: invalid nesting (i.e. [end, start] or [start, end, end]) + } + return null; + }) + .filter((region: vscode.FoldingRange | null): region is vscode.FoldingRange => !!region); + } + public async provideFoldingRanges( document: vscode.TextDocument, _: vscode.FoldingContext, _token: vscode.CancellationToken ): Promise { const tocProvider = new TableOfContentsProvider(this.engine, document); - let toc = await tocProvider.getToc(); - if (toc.length > rangeLimit) { - toc = toc.slice(0, rangeLimit); + let [regions, toc] = await Promise.all([this.getRegions(document), tocProvider.getToc()]); + + if (toc.length > rangeLimit - regions.length) { + toc = toc.slice(0, rangeLimit - regions.length); } const foldingRanges = toc.map((entry, startIndex) => { @@ -44,7 +74,6 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi typeof end === 'number' ? end : document.lineCount - 1); }); - - return foldingRanges; + return [...regions, ...foldingRanges]; } } \ No newline at end of file diff --git a/extensions/markdown-language-features/src/test/foldingProvider.test.ts b/extensions/markdown-language-features/src/test/foldingProvider.test.ts index 44c570d64d4..cd3b82d599e 100644 --- a/extensions/markdown-language-features/src/test/foldingProvider.test.ts +++ b/extensions/markdown-language-features/src/test/foldingProvider.test.ts @@ -78,6 +78,31 @@ y`); assert.strictEqual(firstFold.end, 2); }); + test('Should fold nested markers', async () => { + const folds = await getFoldsForDocument(`a + +b + +b.a + +b + +b.b + +b + +a`); + assert.strictEqual(folds.length, 3); + const [outer, first, second] = folds.sort((a, b) => a.start - b.start); + + assert.strictEqual(outer.start, 1); + assert.strictEqual(outer.end, 11); + assert.strictEqual(first.start, 3); + assert.strictEqual(first.end, 5); + assert.strictEqual(second.start, 7); + assert.strictEqual(second.end, 9); + }); + });