From 4cbbcdfd4d614ea38e4d4017dd421b166616a75a Mon Sep 17 00:00:00 2001 From: CismonX Date: Fri, 23 Oct 2020 06:42:02 +0800 Subject: [PATCH] Support folding & document symbol on nodes. --- README.md | 3 +- src/folding.ts | 85 ++++++++++++++++++++++++++++++++++++++++---------- src/symbol.ts | 14 ++++----- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ee11496..7e2a512 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ Texinfo language support for Visual Studio Code. * Completion for most @\-commands. * Code snippets for block and brace commands. * **Folding** - * Fold on blocks, headers and multiline comments. + * Fold on nodes, block commands, headers and multiline comments. + * Breadcrumb navigation of folding blocks. * **Preview** * Display HTML preview in a webview. * Texinfo to HTML conversion is provided by [GNU Texinfo](https://www.gnu.org/software/texinfo). diff --git a/src/folding.ts b/src/folding.ts index 2cf6d6e..0baa202 100644 --- a/src/folding.ts +++ b/src/folding.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import Document from './document'; +import { Range } from './utils'; /** * Provide folding range info for Texinfo documents. @@ -27,15 +28,21 @@ export class FoldingRangeContext { * Get VSCode folding ranges from the context. */ get values() { - return this.foldingRanges ?? (this.foldingRanges = this.calculateFoldingRanges()); + return this.foldingRanges ??= this.calculateFoldingRanges(); } private foldingRanges?: FoldingRange[]; - private commentRange?: { start: number, end: number }; + private commentRange?: Range; private headerStart?: number; + private closingChapter?: number; + + private closingSection?: number; + + private closingSubsection?: number; + /** * Update folding range context based on document change event. * @@ -64,12 +71,14 @@ export class FoldingRangeContext { this.foldingRanges = []; this.headerStart = undefined; const closingBlocks = <{ name: string, line: number }[]>[]; + let lastLine = this.document.lineCount - 1; let verbatim = false; - for (let idx = this.document.lineCount - 1; idx >= 0; --idx) { + for (let idx = lastLine; idx >= 0; --idx) { const line = this.document.lineAt(idx).text; if (!line.startsWith('@')) continue; if (!verbatim) { if (line === '@bye') { + lastLine = idx; // Abort anything after `@bye`. this.foldingRanges = []; this.commentRange = undefined; @@ -84,17 +93,19 @@ export class FoldingRangeContext { const name = line.substring(5); name === 'verbatim' && (verbatim = true); closingBlocks.push({ name: name, line: idx }); - } else { - const closingBlock = closingBlocks.pop(); - if (closingBlock === undefined) continue; - if (line.substring(1, closingBlock.name.length + 2).trim() === closingBlock.name) { - this.addRange(idx, closingBlock.line, { name: closingBlock.name }); - // If `verbatim == true` goes here, this line must be the `@verbatim` line. - verbatim = false; - } else { - closingBlocks.push(closingBlock); - } + continue; } + if (!verbatim && this.processNode(line, idx, lastLine)) continue; + const closingBlock = closingBlocks.pop(); + if (closingBlock === undefined) continue; + if (line.substring(1, closingBlock.name.length + 2).trim() === closingBlock.name) { + this.addRange(idx, closingBlock.line, { name: closingBlock.name }); + // If `verbatim == true` goes here, this line must be the `@verbatim` line. + verbatim = false; + } else { + closingBlocks.push(closingBlock); + } + } return this.foldingRanges; } @@ -125,19 +136,59 @@ export class FoldingRangeContext { return false; } - private addRange(start: number, end: number, extraArgs: { name?: string, kind?: vscode.FoldingRangeKind }) { - (this.foldingRanges ??= []).push(new FoldingRange(extraArgs.name ?? '', start, end, extraArgs.kind)); + private processNode(lineText: string, lineNum: number, lastLineNum: number) { + if (lineText.startsWith('@subsection ')) { + const detail = lineText.substring(12); + this.addRange(lineNum, this.closingSubsection ?? lastLineNum, { name: 'subsection', detail: detail }); + this.closingSubsection = this.getLastTextLine(lineNum - 1); + return true; + } else if (lineText.startsWith('@section ')) { + const detail = lineText.substring(9); + this.addRange(lineNum, this.closingSection ?? lastLineNum, { name: 'section', detail: detail }); + this.closingSubsection = this.closingSection = this.getLastTextLine(lineNum - 1); + return true; + } else if (lineText.startsWith('@chapter ')) { + const detail = lineText.substring(9); + this.addRange(lineNum, this.closingChapter ?? lastLineNum, { name: 'chapter', detail: detail }); + this.closingSubsection = this.closingSection = this.closingChapter = this.getLastTextLine(lineNum - 1); + return true; + } + return false; + } + + private getLastTextLine(lineNum: number, limit = 3) { + for (let idx = lineNum; idx > lineNum - limit; --idx) { + const line = this.document.lineAt(idx).text; + if (line.startsWith('@node ')) return idx - 1; + if (line === '') return idx; + } + return lineNum; + } + + private addRange(start: number, end: number, extraArgs: { + name?: string, + detail?: string, + kind?: vscode.FoldingRangeKind + }) { + (this.foldingRanges ??= []) + .push(new FoldingRange(extraArgs.name ?? '', extraArgs.detail ?? '', start, end, extraArgs.kind)); } constructor(private readonly document: vscode.TextDocument) {} } /** - * VSCode folding range with name. + * VSCode folding range with name and description. */ export class FoldingRange extends vscode.FoldingRange { - constructor(readonly name: string, start: number, end: number, kind?: vscode.FoldingRangeKind) { + constructor( + readonly name: string, + readonly detail: string, + start: number, + end: number, + kind?: vscode.FoldingRangeKind, + ) { super(start, end, kind); } } diff --git a/src/symbol.ts b/src/symbol.ts index a8198b1..d6e2624 100644 --- a/src/symbol.ts +++ b/src/symbol.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import Document from './document'; import { FoldingRange } from './folding'; +import { Optional } from './utils'; /** * Provide document symbol information for Texinfo documents. @@ -50,7 +51,7 @@ export class DocumentSymbolContext { const ranges = Array(this.document.lineCount); this.foldingRanges.forEach(range => { if (range.kind !== undefined) return; - ranges[range.start] = { name: range.name, end: range.end }; + ranges[range.start] = range; }); return this.symbols = this.rangeToSymbols(ranges, 0, ranges.length); } @@ -59,15 +60,14 @@ export class DocumentSymbolContext { const symbols = []; for (let idx = start; idx < end; ++idx) { const node = ranges[idx]; - if (node === undefined) { - continue; - } + if (node === undefined) continue; const startPosition = new vscode.Position(idx, 0); const endFirstLine = new vscode.Position(idx, Number.MAX_SAFE_INTEGER); const endLastLine = new vscode.Position(node.end, Number.MAX_SAFE_INTEGER); const range = new vscode.Range(startPosition, endLastLine); - const selection = new vscode.Range(startPosition, endFirstLine); - const symbol = new vscode.DocumentSymbol('@' + node.name, '', vscode.SymbolKind.String, range, selection); + const selectionRange = new vscode.Range(startPosition, endFirstLine); + const symbol = new vscode.DocumentSymbol('@' + node.name, node.detail, + vscode.SymbolKind.String, range, selectionRange); symbol.children = this.rangeToSymbols(ranges, idx + 1, node.end); symbols.push(symbol); idx = node.end; @@ -78,4 +78,4 @@ export class DocumentSymbolContext { constructor(private readonly documentContext: Document) {} } -type RangeNode = { name: string, end: number } | undefined; +type RangeNode = Optional;