236 lines
8.7 KiB
TypeScript
236 lines
8.7 KiB
TypeScript
/**
|
|
* contexts/folding_range.ts
|
|
*
|
|
* Copyright (C) 2020,2021 CismonX <admin@cismon.net>
|
|
*
|
|
* This file is part of vscode-texinfo.
|
|
*
|
|
* vscode-texinfo is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation, either version 3 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* vscode-texinfo is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* vscode-texinfo. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import * as vscode from 'vscode';
|
|
import { lineNumToRange } from '../utils/misc';
|
|
import { FoldingRange, Range, NamedLine } from '../utils/types';
|
|
import DocumentContext from './document';
|
|
|
|
/**
|
|
* Stores information about folding ranges for a document.
|
|
*
|
|
* Actually, more than folding ranges (e.g. code lens) is handled within this context, so I believe
|
|
* we should use another name...
|
|
*/
|
|
export default class FoldingRangeContext {
|
|
|
|
/**
|
|
* Get VSCode folding ranges from the context.
|
|
*/
|
|
get foldingRanges() {
|
|
return this._foldingRanges ?? this.calculateFoldingRanges();
|
|
}
|
|
|
|
/**
|
|
* Get node values of document as VSCode code lenses.
|
|
*/
|
|
get nodeValues() {
|
|
this._foldingRanges ?? this.calculateFoldingRanges();
|
|
return this.nodes;
|
|
}
|
|
|
|
/**
|
|
* Update folding range context based on document change event.
|
|
*
|
|
* @param events Events describing the changes in the document.
|
|
*/
|
|
update(events: readonly vscode.TextDocumentContentChangeEvent[]) {
|
|
this.contentMayChange = true;
|
|
if (this._foldingRanges === undefined) return false;
|
|
const eol = this.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n';
|
|
for (const event of events) {
|
|
// Clear cached folding range when line count changes.
|
|
if (event.text.split(eol).length !== 1 || event.range.start.line !== event.range.end.line) {
|
|
this._foldingRanges = undefined;
|
|
this.nodes = [];
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
clear() {
|
|
if (this.contentMayChange) {
|
|
this._foldingRanges = undefined;
|
|
}
|
|
}
|
|
|
|
constructor(private readonly documentContext: DocumentContext) {}
|
|
|
|
private readonly document = this.documentContext.document;
|
|
|
|
/**
|
|
* Regex for matching subsection/section/chapter (-like) commands.
|
|
*/
|
|
private static readonly nodeFormat = RegExp('^@(?:(node)|(subsection|unnumberedsubsec|appendixsubsec|subheading)|' +
|
|
'(section|unnumberedsec|appendixsec|heading)|(chapter|unnumbered|appendix|majorheading|chapheading)) (.*)$');
|
|
|
|
private _foldingRanges?: FoldingRange[];
|
|
|
|
private nodes = <vscode.CodeLens[]>[];
|
|
|
|
private commentRange?: Range;
|
|
private headerStart?: number;
|
|
private closingChapter?: number;
|
|
private closingSection?: number;
|
|
private closingSubsection?: number;
|
|
|
|
private contentMayChange = true;
|
|
|
|
private addRange(start: number, end: number, extraArgs: {
|
|
name?: string,
|
|
detail?: string,
|
|
kind?: vscode.FoldingRangeKind
|
|
}) {
|
|
(this._foldingRanges ??= [])
|
|
.push({ name: extraArgs.name ?? '', detail: extraArgs.detail ?? '', start, end, kind: extraArgs.kind });
|
|
}
|
|
|
|
/**
|
|
* Calculate and update folding ranges for the document.
|
|
*
|
|
* @param start Starting line number.
|
|
* @param end Ending line number.
|
|
*/
|
|
private calculateFoldingRanges() {
|
|
this.contentMayChange = false;
|
|
this._foldingRanges = [];
|
|
this.clearTemporaries();
|
|
let closingBlocks = <NamedLine[]>[];
|
|
let lastLine = this.document.lineCount - 1;
|
|
let verbatim = false;
|
|
for (let idx = lastLine; idx >= 0; --idx) {
|
|
const line = this.document.lineAt(idx).text.trimLeft();
|
|
if (!line.startsWith('@')) continue;
|
|
if (!verbatim) {
|
|
if (line === '@bye') {
|
|
lastLine = idx;
|
|
// Abort anything after `@bye`.
|
|
this._foldingRanges = [];
|
|
closingBlocks = [];
|
|
this.clearTemporaries();
|
|
continue;
|
|
}
|
|
if (this.processComment(line, idx)) continue;
|
|
}
|
|
// Process block.
|
|
if (line.startsWith('@end ')) {
|
|
if (verbatim) continue;
|
|
const name = line.substring(5).trimRight();
|
|
if (name === 'verbatim') {
|
|
verbatim = true;
|
|
}
|
|
closingBlocks.push({ name: name, line: idx });
|
|
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);
|
|
}
|
|
}
|
|
if (this.commentRange !== undefined) {
|
|
this.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment });
|
|
}
|
|
return this._foldingRanges;
|
|
}
|
|
|
|
private clearTemporaries() {
|
|
this.commentRange = undefined;
|
|
this.headerStart = undefined;
|
|
this.nodes = [];
|
|
this.closingSubsection = this.closingSection = this.closingChapter = undefined;
|
|
}
|
|
|
|
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 processComment(lineText: string, lineNum: number) {
|
|
if (!lineText.startsWith('@c')) return false;
|
|
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.addRange(lineNum, this.headerStart, { kind: vscode.FoldingRangeKind.Region });
|
|
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.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment });
|
|
this.commentRange = undefined;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private processNode(lineText: string, lineNum: number, lastLineNum: number) {
|
|
const result = lineText.match(FoldingRangeContext.nodeFormat);
|
|
if (result === null) return false;
|
|
// Node identifier.
|
|
if (result[1] !== undefined) {
|
|
this.nodes.push(new vscode.CodeLens(lineNumToRange(lineNum), {
|
|
title: '$(go-to-file) Goto node in preview',
|
|
command: 'texinfo.preview.goto',
|
|
arguments: [this.document, result[5]],
|
|
}));
|
|
return true;
|
|
}
|
|
// Subsection level node.
|
|
if (result[2] !== undefined) {
|
|
this.addRange(lineNum, this.closingSubsection ?? lastLineNum, { name: result[2], detail: result[5] });
|
|
this.closingSubsection = this.getLastTextLine(lineNum - 1);
|
|
return true;
|
|
}
|
|
// Section level node.
|
|
if (result[3] !== undefined) {
|
|
this.addRange(lineNum, this.closingSection ?? lastLineNum, { name: result[3], detail: result[5] });
|
|
this.closingSubsection = this.closingSection = this.getLastTextLine(lineNum - 1);
|
|
return true;
|
|
}
|
|
// Chapter level node.
|
|
if (result[4] !== undefined) {
|
|
this.addRange(lineNum, this.closingChapter ?? lastLineNum, { name: result[4], detail: result[5] });
|
|
this.closingSubsection = this.closingSection = this.closingChapter = this.getLastTextLine(lineNum - 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|