vscode-texinfo/src/contexts/folding_range.ts

284 lines
9.5 KiB
TypeScript
Raw Normal View History

2020-10-13 20:15:27 +00:00
/**
2020-10-25 18:14:13 +00:00
* contexts/folding_range.ts
2021-03-16 12:01:13 +00:00
*
* 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/>.
2020-10-13 20:15:27 +00:00
*/
import * as vscode from 'vscode';
2020-11-11 12:29:30 +00:00
import { lineNumToRange } from '../utils/misc';
2020-10-26 17:28:11 +00:00
import { FoldingRange, Range, NamedLine } from '../utils/types';
2021-04-19 12:43:20 +00:00
import DocumentContext from './document';
2020-10-13 20:15:27 +00:00
2020-10-14 22:08:10 +00:00
/**
* Stores information about folding ranges for a document.
2020-11-11 12:29:30 +00:00
*
* Actually, more than folding ranges (e.g. code lens) is handled within
* this context, so perhaps we should use another name...
2020-10-14 22:08:10 +00:00
*/
2021-08-11 17:23:04 +00:00
export default class FoldingRangeContext
{
2020-10-14 22:08:10 +00:00
/**
* Get VSCode folding ranges from the context.
*/
2021-04-19 12:43:20 +00:00
get foldingRanges() {
2021-05-25 04:16:56 +00:00
return this._foldingRanges ?? this._calculateFoldingRanges();
2020-10-14 19:22:32 +00:00
}
2020-11-11 12:29:30 +00:00
/**
* Get node values of document as VSCode code lenses.
*/
get nodeValues() {
2021-05-25 04:16:56 +00:00
this._foldingRanges ?? this._calculateFoldingRanges();
return this._nodes;
2020-11-11 12:29:30 +00:00
}
2020-10-20 20:07:44 +00:00
/**
* Update folding range context based on document change event.
*
* @param events Events describing the changes in the document.
*/
update(events: readonly vscode.TextDocumentContentChangeEvent[]) {
2021-05-25 04:16:56 +00:00
this._contentMayChange = true;
2021-10-22 18:36:51 +00:00
if (this._foldingRanges === undefined) {
return false;
}
2021-05-25 04:16:56 +00:00
const eol = this._document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n';
2020-10-20 20:07:44 +00:00
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
) {
2021-04-19 12:43:20 +00:00
this._foldingRanges = undefined;
2021-05-25 04:16:56 +00:00
this._nodes = [];
2020-10-20 20:07:44 +00:00
return true;
}
}
return false;
2020-10-13 20:15:27 +00:00
}
clear() {
2021-05-25 04:16:56 +00:00
if (this._contentMayChange) {
2021-04-22 09:07:13 +00:00
this._foldingRanges = undefined;
}
}
2021-05-25 04:16:56 +00:00
constructor(private readonly _documentContext: DocumentContext) {}
2021-04-19 12:43:20 +00:00
2021-05-25 04:16:56 +00:00
private readonly _document = this._documentContext.document;
2021-04-19 12:43:20 +00:00
/**
* 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)) (.*)$');
2021-04-19 12:43:20 +00:00
private _foldingRanges?: FoldingRange[];
2021-05-25 04:16:56 +00:00
private _nodes = <vscode.CodeLens[]>[];
2021-04-19 12:43:20 +00:00
2021-05-25 04:16:56 +00:00
private _commentRange?: Range;
private _headerStart?: number;
private _closingChapter?: number;
private _closingSection?: number;
private _closingSubsection?: number;
2021-04-19 12:43:20 +00:00
2021-05-25 04:16:56 +00:00
private _contentMayChange = true;
2021-04-19 12:43:20 +00:00
2021-05-25 04:16:56 +00:00
private _addRange(start: number, end: number, extraArgs: {
2021-04-19 12:43:20 +00:00
name?: string,
detail?: string,
kind?: vscode.FoldingRangeKind
}) {
const items = {
name: extraArgs.name ?? '',
detail: extraArgs.detail ?? '',
start,
end,
kind: extraArgs.kind,
};
(this._foldingRanges ??= []).push(items);
2021-04-19 12:43:20 +00:00
}
2021-03-15 12:43:38 +00:00
2020-10-14 22:08:10 +00:00
/**
* Calculate and update folding ranges for the document.
*
* @param start Starting line number.
* @param end Ending line number.
*/
2021-05-25 04:16:56 +00:00
private _calculateFoldingRanges() {
this._contentMayChange = false;
2021-04-19 12:43:20 +00:00
this._foldingRanges = [];
2021-05-25 04:16:56 +00:00
this._clearTemporaries();
2020-10-26 17:28:11 +00:00
let closingBlocks = <NamedLine[]>[];
2021-05-25 04:16:56 +00:00
let lastLine = this._document.lineCount - 1;
let verbatim = false;
for (let idx = lastLine; idx >= 0; --idx) {
2021-05-25 04:16:56 +00:00
const line = this._document.lineAt(idx).text.trimLeft();
2021-08-11 17:23:04 +00:00
if (!line.startsWith('@')) {
continue;
}
if (!verbatim) {
if (line === '@bye') {
lastLine = idx;
// Abort anything after `@bye`.
2021-04-19 12:43:20 +00:00
this._foldingRanges = [];
2020-10-25 17:52:13 +00:00
closingBlocks = [];
2021-05-25 04:16:56 +00:00
this._clearTemporaries();
continue;
}
2021-08-11 17:23:04 +00:00
if (this._processComment(line, idx)) {
continue;
}
2020-10-13 20:15:27 +00:00
}
2020-10-15 18:43:07 +00:00
// Process block.
if (line.startsWith('@end ')) {
2021-10-22 18:36:51 +00:00
if (verbatim) {
continue;
}
2020-11-13 11:02:12 +00:00
const name = line.substring(5).trimRight();
2021-04-19 12:43:20 +00:00
if (name === 'verbatim') {
verbatim = true;
}
closingBlocks.push({ name: name, line: idx });
continue;
}
2021-08-11 17:23:04 +00:00
if (!verbatim && this._processNode(line, idx, lastLine)) {
continue;
}
const closingBlock = closingBlocks.pop();
2021-08-11 17:23:04 +00:00
if (closingBlock === undefined) {
continue;
}
const name = closingBlock.name;
if (line.substring(1, name.length + 2).trim() === name) {
this._addRange(idx, closingBlock.line, { name: name });
// If `verbatim == true` goes here,
// this line must be the `@verbatim` line.
verbatim = false;
2020-10-15 18:43:07 +00:00
} else {
closingBlocks.push(closingBlock);
2020-10-15 18:43:07 +00:00
}
2020-10-24 21:45:32 +00:00
}
2021-05-25 04:16:56 +00:00
if (this._commentRange !== undefined) {
2021-08-11 17:23:04 +00:00
this._addRange(
this._commentRange.start,
this._commentRange.end,
{ kind: vscode.FoldingRangeKind.Comment },
);
2020-10-13 20:15:27 +00:00
}
2021-04-19 12:43:20 +00:00
return this._foldingRanges;
}
2021-05-25 04:16:56 +00:00
private _clearTemporaries() {
this._commentRange = undefined;
this._headerStart = undefined;
this._nodes = [];
this._closingSubsection = this._closingSection = this._closingChapter
= undefined;
2021-04-19 12:43:20 +00:00
}
2021-05-25 04:16:56 +00:00
private _getLastTextLine(lineNum: number, limit = 3) {
2021-04-19 12:43:20 +00:00
for (let idx = lineNum; idx > lineNum - limit; --idx) {
2021-05-25 04:16:56 +00:00
const line = this._document.lineAt(idx).text;
2021-10-22 18:36:51 +00:00
if (line.startsWith('@node ')) {
return idx - 1;
}
if (line === '') {
return idx;
}
2021-04-19 12:43:20 +00:00
}
return lineNum;
2020-10-13 20:15:27 +00:00
}
2021-05-25 04:16:56 +00:00
private _processComment(lineText: string, lineNum: number) {
2021-10-22 18:36:51 +00:00
if (!lineText.startsWith('@c')) {
return false;
}
if (lineText.charAt(2) != ' ' && !lineText.startsWith('omment ', 2)) {
2021-04-19 12:43:20 +00:00
return false;
}
2020-10-24 21:45:32 +00:00
// Check for opening/closing header.
if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) {
2021-05-25 04:16:56 +00:00
if (this._headerStart === undefined) {
this._headerStart = lineNum;
2020-10-24 21:45:32 +00:00
} else {
this._addRange(lineNum, this._headerStart,
{ kind: vscode.FoldingRangeKind.Region });
2021-05-25 04:16:56 +00:00
this._headerStart = undefined;
2020-10-13 20:15:27 +00:00
}
return true;
2020-10-24 21:45:32 +00:00
}
2021-05-25 04:16:56 +00:00
if (this._commentRange === undefined) {
this._commentRange = { start: lineNum, end: lineNum };
} else if (this._commentRange.start - 1 === lineNum) {
this._commentRange.start = lineNum;
2020-10-24 21:45:32 +00:00
} else {
this._addRange(this._commentRange.start, this._commentRange.end,
{ kind: vscode.FoldingRangeKind.Comment });
2021-05-25 04:16:56 +00:00
this._commentRange = undefined;
2020-10-13 20:15:27 +00:00
}
2020-10-24 21:45:32 +00:00
return true;
2020-10-13 20:15:27 +00:00
}
private _processNode(
lineText: string,
lineNum: number,
lastLineNum: number,
) {
2021-05-25 04:16:56 +00:00
const result = lineText.match(FoldingRangeContext._nodeFormat);
2021-10-22 18:36:51 +00:00
if (result === null) {
return false;
}
2020-11-11 12:29:30 +00:00
// Node identifier.
2020-10-25 17:52:13 +00:00
if (result[1] !== undefined) {
2021-05-25 04:16:56 +00:00
this._nodes.push(new vscode.CodeLens(lineNumToRange(lineNum), {
2020-12-31 06:33:15 +00:00
title: '$(go-to-file) Goto node in preview',
2020-11-11 12:29:30 +00:00
command: 'texinfo.preview.goto',
2021-05-25 04:16:56 +00:00
arguments: [this._document, result[5]],
2020-11-11 12:29:30 +00:00
}));
return true;
}
// Subsection level node.
if (result[2] !== undefined) {
this._addRange(lineNum, this._closingSubsection ?? lastLineNum,
{ name: result[2], detail: result[5] });
2021-05-25 04:16:56 +00:00
this._closingSubsection = this._getLastTextLine(lineNum - 1);
return true;
2020-10-25 17:52:13 +00:00
}
// Section level node.
2020-11-11 12:29:30 +00:00
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;
2020-10-25 17:52:13 +00:00
}
// Chapter level node.
2020-11-11 12:29:30 +00:00
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;
}
2020-10-14 22:08:10 +00:00
}