195 lines
6.4 KiB
TypeScript
195 lines
6.4 KiB
TypeScript
/**
|
|
* folding.ts
|
|
*
|
|
* @author CismonX <admin@cismon.net>
|
|
* @license MIT
|
|
*/
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
/**
|
|
* Provide folding range info for Texinfo documents.
|
|
*/
|
|
export class FoldingRangeProvider implements vscode.FoldingRangeProvider {
|
|
|
|
provideFoldingRanges(document: vscode.TextDocument) {
|
|
return FoldingRangeContext.get(document).values;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stores information about folding ranges for a document.
|
|
*/
|
|
export class FoldingRangeContext {
|
|
|
|
private static readonly map = new Map<vscode.TextDocument, FoldingRangeContext>();
|
|
|
|
/**
|
|
* Initialize folding range context for a document.
|
|
*
|
|
* @param document
|
|
*/
|
|
static open(document: vscode.TextDocument) {
|
|
document.languageId === 'texinfo' && FoldingRangeContext.get(document);
|
|
}
|
|
|
|
/**
|
|
* Get existing folding range context of a document, or create one if not exist.
|
|
*
|
|
* @param document
|
|
*/
|
|
static get(document: vscode.TextDocument) {
|
|
return FoldingRangeContext.map.get(document) ?? new FoldingRangeContext(document);
|
|
}
|
|
|
|
/**
|
|
* Update the folding range context of a document based on its change event.
|
|
*
|
|
* @param event Change event of a document.
|
|
*/
|
|
static update(event: vscode.TextDocumentChangeEvent) {
|
|
if (event.document.languageId === 'texinfo') {
|
|
FoldingRangeContext.get(event.document).update(event.contentChanges);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy the folding range context of a document.
|
|
*
|
|
* @param document
|
|
*/
|
|
static close(document: vscode.TextDocument) {
|
|
FoldingRangeContext.map.delete(document);
|
|
}
|
|
|
|
/**
|
|
* Destroy all existing folding range contexts.
|
|
*/
|
|
static clear() {
|
|
FoldingRangeContext.map.clear();
|
|
}
|
|
|
|
/**
|
|
* Get VSCode folding ranges from the context.
|
|
*/
|
|
get values() {
|
|
return this.foldingRanges ?? (this.foldingRanges = this.calculateFoldingRanges());
|
|
}
|
|
|
|
private foldingRanges?: vscode.FoldingRange[];
|
|
|
|
private commentRange?: { start: number, end: number };
|
|
|
|
private headerStart?: number;
|
|
|
|
private constructor(private readonly document: vscode.TextDocument) {
|
|
FoldingRangeContext.map.set(document, this);
|
|
}
|
|
|
|
/**
|
|
* Calculate and update folding ranges for the document.
|
|
*
|
|
* @param start Starting line number.
|
|
* @param end Ending line number.
|
|
*/
|
|
private calculateFoldingRanges() {
|
|
this.headerStart = undefined;
|
|
const closingBlocks = <{ name: string, line: number }[]>[];
|
|
let verbatim = false;
|
|
for (let idx = this.document.lineCount - 1; idx >= 0; --idx) {
|
|
const line = this.document.lineAt(idx).text;
|
|
if (!line.startsWith('@')) {
|
|
continue;
|
|
}
|
|
if (!verbatim) {
|
|
if (line === '@bye') {
|
|
// Abort anything after `@bye`.
|
|
this.foldingRanges = undefined;
|
|
this.commentRange = undefined;
|
|
this.headerStart = undefined;
|
|
continue;
|
|
}
|
|
if (this.processComment(line, idx)) {
|
|
continue;
|
|
}
|
|
}
|
|
// Process block.
|
|
if (line.startsWith('@end ')) {
|
|
if (verbatim) {
|
|
continue;
|
|
}
|
|
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.insertRange(idx, closingBlock.line);
|
|
// If `verbatim == true` goes here, this line must be the `@verbatim` line.
|
|
verbatim = false;
|
|
} else {
|
|
closingBlocks.push(closingBlock);
|
|
}
|
|
}
|
|
}
|
|
if (this.commentRange !== undefined) {
|
|
this.insertRange(this.commentRange.start, this.commentRange.end, vscode.FoldingRangeKind.Comment);
|
|
this.commentRange = undefined;
|
|
}
|
|
return this.foldingRanges;
|
|
}
|
|
|
|
private processComment(lineText: string, lineNum: number) {
|
|
if (lineText.startsWith('@c')) {
|
|
if (!lineText.startsWith(' ', 2) && !lineText.startsWith('omment ', 2)) {
|
|
return false;
|
|
}
|
|
// Check for opening/closing header.
|
|
if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) {
|
|
if (this.headerStart === undefined) {
|
|
this.headerStart = lineNum;
|
|
} else {
|
|
this.insertRange(lineNum, this.headerStart);
|
|
this.headerStart = undefined;
|
|
}
|
|
return true;
|
|
}
|
|
if (this.commentRange === undefined) {
|
|
this.commentRange = { start: lineNum, end: lineNum };
|
|
} else if (this.commentRange.start - 1 === lineNum) {
|
|
this.commentRange.start = lineNum;
|
|
} else {
|
|
this.insertRange(this.commentRange.start, this.commentRange.end, vscode.FoldingRangeKind.Comment);
|
|
this.commentRange = { start: lineNum, end: lineNum };
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private insertRange(start: number, end: number, kind?: vscode.FoldingRangeKind) {
|
|
(this.foldingRanges ?? (this.foldingRanges = [])).push(new vscode.FoldingRange(start, end, kind));
|
|
}
|
|
|
|
/**
|
|
* Update folding range context based on document change event.
|
|
*
|
|
* @param events Events describing the changes in the document.
|
|
*/
|
|
private update(events: readonly vscode.TextDocumentContentChangeEvent[]) {
|
|
if (this.foldingRanges === undefined) {
|
|
return;
|
|
}
|
|
for (const event of events) {
|
|
const updatedLines = event.text.split(this.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n').length;
|
|
// Clear folding range buffer when line count changes.
|
|
if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) {
|
|
this.foldingRanges = undefined;
|
|
}
|
|
}
|
|
}
|
|
}
|