From a4e16a40c2da891f05b97f740834eeb44bafa6bd Mon Sep 17 00:00:00 2001 From: Marie Flores Date: Fri, 11 Mar 2022 11:19:13 +0100 Subject: [PATCH] Add code folding based on heading level --- .vscode/launch.json | 1 + src/asciidocEngine.ts | 6 +- src/asciidocParser.ts | 6 +- src/extension.ts | 3 +- src/features/foldingProvider.ts | 37 +++++++++++ src/tableOfContentsProvider.ts | 3 +- src/test/documentSymbolProvider.test.ts | 2 +- src/test/foldingProvider.test.ts | 87 +++++++++++++++++++++++++ 8 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 src/features/foldingProvider.ts create mode 100644 src/test/foldingProvider.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 2abeb2df..ea5e358b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,6 +24,7 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ + "--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/dist/src/test/suite" ], diff --git a/src/asciidocEngine.ts b/src/asciidocEngine.ts index 730602ca..148eccd3 100644 --- a/src/asciidocEngine.ts +++ b/src/asciidocEngine.ts @@ -7,6 +7,7 @@ import { AsciidocContributions } from './asciidocExtensions' import { Slugifier } from './slugify' import { AsciidocParser } from './asciidocParser' import { Asciidoctor } from '@asciidoctor/core' +import { SkinnyTextDocument } from './util/document' const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/ @@ -64,9 +65,8 @@ export class AsciidocEngine { return { output, document } } - public async load (documentUri: vscode.Uri, source: string): Promise { - const textDocument = await vscode.workspace.openTextDocument(documentUri) - const { document } = await this.getEngine().parseText(source, textDocument) + public async load (textDocument: SkinnyTextDocument): Promise { + const { document } = await this.getEngine().parseText(textDocument.getText(), textDocument) return document } } diff --git a/src/asciidocParser.ts b/src/asciidocParser.ts index 673ba28b..c212cd2d 100644 --- a/src/asciidocParser.ts +++ b/src/asciidocParser.ts @@ -53,7 +53,7 @@ export class AsciidocParser { public convertUsingJavascript ( text: string, - doc: vscode.TextDocument, + doc: SkinnyTextDocument, forHTMLSave: boolean, backend: string, context?: vscode.ExtensionContext, @@ -176,7 +176,7 @@ export class AsciidocParser { public async parseText ( text: string, - doc: vscode.TextDocument, + doc: SkinnyTextDocument, forHTMLSave: boolean = false, backend: string = 'webview-html5', context?: vscode.ExtensionContext, @@ -185,7 +185,7 @@ export class AsciidocParser { return this.convertUsingJavascript(text, doc, forHTMLSave, backend, context, editor) } - private reportErrors (memoryLogger: Asciidoctor.MemoryLogger, textDocument: vscode.TextDocument) { + private reportErrors (memoryLogger: Asciidoctor.MemoryLogger, textDocument: SkinnyTextDocument) { const diagnostics = [] memoryLogger.getMessages().forEach((error) => { //console.log(error); //Error from asciidoctor.js diff --git a/src/extension.ts b/src/extension.ts index 4a965ed9..cb80e826 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { githubSlugifier } from './slugify' import { AsciidocFileIncludeAutoCompletionMonitor } from './util/includeAutoCompletion' import { AttributeReferenceProvider } from './features/attributeReferenceProvider' import { BuiltinDocumentAttributeProvider } from './features/builtinDocumentAttributeProvider' +import AsciidocFoldingRangeProvider from './features/foldingProvider' export function activate (context: vscode.ExtensionContext) { const contributions = getAsciidocExtensionContributions(context) @@ -47,8 +48,8 @@ export function activate (context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerWorkspaceSymbolProvider(new AsciidocWorkspaceSymbolProvider(symbolProvider))) context.subscriptions.push(vscode.languages.registerCompletionItemProvider(selector, new AttributeReferenceProvider(contributions.extensionUri), '{')) context.subscriptions.push(vscode.languages.registerCompletionItemProvider(selector, new BuiltinDocumentAttributeProvider(contributions.extensionUri), ':')) + context.subscriptions.push(vscode.languages.registerFoldingRangeProvider(selector, new AsciidocFoldingRangeProvider(engine))) const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager) - const commandManager = new CommandManager() context.subscriptions.push(commandManager) commandManager.register(new commands.ShowPreviewCommand(previewManager)) diff --git a/src/features/foldingProvider.ts b/src/features/foldingProvider.ts new file mode 100644 index 00000000..6d89c9bc --- /dev/null +++ b/src/features/foldingProvider.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode' + +import { AsciidocEngine } from '../asciidocEngine' +import { TableOfContentsProvider } from '../tableOfContentsProvider' + +export default class AsciidocFoldingRangeProvider implements vscode.FoldingRangeProvider { + constructor ( + private readonly engine: AsciidocEngine + ) { + } + + public async provideFoldingRanges ( + document: vscode.TextDocument, + _token: vscode.CancellationToken + ): Promise { + const tableOfContentsProvider = new TableOfContentsProvider(this.engine, document) + const tableOfContents = await tableOfContentsProvider.getToc() + + return tableOfContents.map((entry, startIndex) => { + const start = entry.line + let end: number | undefined + for (let i = startIndex + 1; i < tableOfContents.length; ++i) { + if (tableOfContents[i].level <= entry.level) { + end = tableOfContents[i].line - 1 + break + } + } + return new vscode.FoldingRange( + start, + typeof end === 'number' ? end : document.lineCount - 1) + }) + } +} diff --git a/src/tableOfContentsProvider.ts b/src/tableOfContentsProvider.ts index 8a950571..4fa11eee 100644 --- a/src/tableOfContentsProvider.ts +++ b/src/tableOfContentsProvider.ts @@ -28,6 +28,7 @@ export class TableOfContentsProvider { try { this.toc = await this.buildToc(this.document) } catch (e) { + console.log(e) this.toc = [] } } @@ -42,7 +43,7 @@ export class TableOfContentsProvider { private async buildToc (document: SkinnyTextDocument): Promise { const toc: TocEntry[] = [] - const adoc = await this.engine.load(document.uri, document.getText()) + const adoc = await this.engine.load(document) adoc.findBy({ context: 'section' }, function (section) { toc.push({ diff --git a/src/test/documentSymbolProvider.test.ts b/src/test/documentSymbolProvider.test.ts index b6cc0552..df0598e4 100644 --- a/src/test/documentSymbolProvider.test.ts +++ b/src/test/documentSymbolProvider.test.ts @@ -9,7 +9,7 @@ import SymbolProvider from '../features/documentSymbolProvider' import { InMemoryDocument } from './inMemoryDocument' import { createNewAsciidocEngine } from './engine' -const testFileName = vscode.Uri.file('test.md') +const testFileName = vscode.Uri.file('test.adoc') function getSymbolsForFile (fileContents: string) { const doc = new InMemoryDocument(testFileName, fileContents) diff --git a/src/test/foldingProvider.test.ts b/src/test/foldingProvider.test.ts new file mode 100644 index 00000000..54da0b67 --- /dev/null +++ b/src/test/foldingProvider.test.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'mocha' +import * as vscode from 'vscode' +import AsciidocFoldingProvider from '../features/foldingProvider' +import { createNewAsciidocEngine } from './engine' +import { InMemoryDocument } from './inMemoryDocument' + +const testFileName = vscode.Uri.file('test.adoc') + +suite('asciidoc.FoldingProvider', () => { + test('Should not return anything for empty document', async () => { + const folds = await getFoldsForDocument('') + assert.strictEqual(folds.length, 0) + }) + + test('Should not return anything for document without headers', async () => { + const folds = await getFoldsForDocument(`a +*b* afas +a=b +a`) + assert.strictEqual(folds.length, 0) + }) + + test('Should fold from header to end of document', async () => { + const folds = await getFoldsForDocument(`= a + +== b + +c +d`) + assert.strictEqual(folds.length, 2) + const firstFold = folds[0] + assert.strictEqual(firstFold.start, 0) + assert.strictEqual(firstFold.end, 5) + }) + + test('Should leave single newline before next header', async () => { + const folds = await getFoldsForDocument(` +== a +x + +== b +y`) + assert.strictEqual(folds.length, 2) + const firstFold = folds[0] + assert.strictEqual(firstFold.start, 1) + assert.strictEqual(firstFold.end, 3) + }) + + test('Should collapse multiple newlines to single newline before next header', async () => { + const folds = await getFoldsForDocument(` += a +x + + + += b +y`) + assert.strictEqual(folds.length, 2) + const firstFold = folds[0] + assert.strictEqual(firstFold.start, 1) + assert.strictEqual(firstFold.end, 5) + }) + + test('Should not collapse if there is no newline before next header', async () => { + const folds = await getFoldsForDocument(` += a +x +== b +y`) + assert.strictEqual(folds.length, 1) + const firstFold = folds[0] + assert.strictEqual(firstFold.start, 1) + assert.strictEqual(firstFold.end, 4) + }) +}) + +async function getFoldsForDocument (contents: string) { + const doc = new InMemoryDocument(testFileName, contents) + const provider = new AsciidocFoldingProvider(createNewAsciidocEngine()) + return await provider.provideFoldingRanges(doc, new vscode.CancellationTokenSource().token) +}