Refactor code.
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
f86534985b
commit
9c82892bae
|
@ -21,54 +21,85 @@
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import DocumentContext from './contexts/document';
|
import DocumentContext from './contexts/document';
|
||||||
|
import GlobalContext from './global_context';
|
||||||
|
import { prompt } from './utils/misc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage mappings between Texinfo documents and corresponding contexts.
|
* Manage mappings between Texinfo documents and corresponding document-specific contexts.
|
||||||
*/
|
*/
|
||||||
export default class ContextMapping implements vscode.Disposable {
|
export default class ContextMapping implements vscode.Disposable {
|
||||||
|
|
||||||
private static singleton?: ContextMapping;
|
getDocumentContext(document: vscode.TextDocument) {
|
||||||
|
let documentContext = this.map.get(document);
|
||||||
static get instance() {
|
|
||||||
return ContextMapping.singleton ??= new ContextMapping();
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDocumentContext(document: vscode.TextDocument) {
|
|
||||||
let documentContext = ContextMapping.instance.value.get(document);
|
|
||||||
if (documentContext === undefined) {
|
if (documentContext === undefined) {
|
||||||
ContextMapping.instance.value.set(document, documentContext = new DocumentContext(document));
|
documentContext = new DocumentContext(this.globalContext, document);
|
||||||
|
this.map.set(document, documentContext);
|
||||||
}
|
}
|
||||||
return documentContext;
|
return documentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
static onDocumentUpdate(event: vscode.TextDocumentChangeEvent) {
|
dispose() {
|
||||||
const documentContext = ContextMapping.getDocumentContextIfExist(event.document);
|
this.map.forEach(documentContext => documentContext.getPreview()?.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly globalContext: GlobalContext) {
|
||||||
|
globalContext.subscribe(
|
||||||
|
vscode.commands.registerTextEditorCommand('texinfo.preview.show', this.showPreview.bind(this)),
|
||||||
|
vscode.commands.registerCommand('texinfo.preview.goto', this.gotoPreview.bind(this)),
|
||||||
|
vscode.workspace.onDidChangeTextDocument(this.onDocumentUpdate.bind(this)),
|
||||||
|
vscode.workspace.onDidCloseTextDocument(this.onDocumentClose.bind(this)),
|
||||||
|
vscode.workspace.onDidSaveTextDocument(this.onDocumentSave.bind(this)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly map = new Map<vscode.TextDocument, DocumentContext>();
|
||||||
|
|
||||||
|
private getDocumentContextIfExist(document: vscode.TextDocument) {
|
||||||
|
return document.languageId === 'texinfo' ? this.getDocumentContext(document) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jump to the corresponding section of document preview by node name.
|
||||||
|
*
|
||||||
|
* @param document
|
||||||
|
* @param nodeName
|
||||||
|
*/
|
||||||
|
private gotoPreview(document: vscode.TextDocument, nodeName: string) {
|
||||||
|
this.getDocumentContext(document).initPreview().goto(nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDocumentClose(document: vscode.TextDocument) {
|
||||||
|
this.map.get(document)?.getPreview()?.close();
|
||||||
|
this.map.delete(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDocumentSave(document: vscode.TextDocument) {
|
||||||
|
const documentContext = this.getDocumentContextIfExist(document);
|
||||||
|
if (documentContext === undefined) return;
|
||||||
|
documentContext.foldingRange.clear();
|
||||||
|
documentContext.getPreview()?.updateWebview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDocumentUpdate(event: vscode.TextDocumentChangeEvent) {
|
||||||
|
const documentContext = this.getDocumentContextIfExist(event.document);
|
||||||
if (documentContext?.foldingRange.update(event.contentChanges)) {
|
if (documentContext?.foldingRange.update(event.contentChanges)) {
|
||||||
documentContext.documentSymbol.clear();
|
documentContext.documentSymbol.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static onDocumentSave(document: vscode.TextDocument) {
|
/**
|
||||||
const documentContext = ContextMapping.getDocumentContextIfExist(document);
|
* Create (if not yet created) and show preview for a Texinfo document.
|
||||||
if (documentContext !== undefined) {
|
*
|
||||||
documentContext.foldingRange.clear();
|
* @param editor The editor where the document is being held.
|
||||||
documentContext.getPreview()?.updateWebview();
|
*/
|
||||||
|
private async showPreview(editor: vscode.TextEditor) {
|
||||||
|
const document = editor.document;
|
||||||
|
// Only show preview for saved files, as we're not gonna send document content to `makeinfo` via STDIN.
|
||||||
|
// Instead, the file will be loaded from disk.
|
||||||
|
if (document.isUntitled) {
|
||||||
|
if (!await prompt('Save this document to display preview.', 'Save')) return;
|
||||||
|
if (!await document.save()) return;
|
||||||
}
|
}
|
||||||
}
|
this.getDocumentContext(document).initPreview().show();
|
||||||
|
|
||||||
static onDocumentClose(document: vscode.TextDocument) {
|
|
||||||
ContextMapping.instance.value.get(document)?.getPreview()?.close();
|
|
||||||
ContextMapping.instance.value.delete(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getDocumentContextIfExist(document: vscode.TextDocument) {
|
|
||||||
return document.languageId === 'texinfo' ? ContextMapping.getDocumentContext(document) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly value = new Map<vscode.TextDocument, DocumentContext>();
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.value.forEach(documentContext => documentContext.getPreview()?.close());
|
|
||||||
ContextMapping.singleton = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import GlobalContext from '../global_context';
|
||||||
import DocumentSymbolContext from './document_symbol';
|
import DocumentSymbolContext from './document_symbol';
|
||||||
import FoldingRangeContext from './folding_range';
|
import FoldingRangeContext from './folding_range';
|
||||||
import PreviewContext from './preview';
|
import PreviewContext from './preview';
|
||||||
|
@ -29,12 +30,10 @@ import PreviewContext from './preview';
|
||||||
*/
|
*/
|
||||||
export default class DocumentContext {
|
export default class DocumentContext {
|
||||||
|
|
||||||
readonly foldingRange = new FoldingRangeContext(this.document);
|
readonly foldingRange = new FoldingRangeContext(this);
|
||||||
|
|
||||||
readonly documentSymbol = new DocumentSymbolContext(this);
|
readonly documentSymbol = new DocumentSymbolContext(this);
|
||||||
|
|
||||||
private preview?: PreviewContext;
|
|
||||||
|
|
||||||
initPreview() {
|
initPreview() {
|
||||||
return this.preview ??= new PreviewContext(this);
|
return this.preview ??= new PreviewContext(this);
|
||||||
}
|
}
|
||||||
|
@ -47,5 +46,7 @@ export default class DocumentContext {
|
||||||
this.preview = undefined;
|
this.preview = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(readonly document: vscode.TextDocument) {}
|
constructor(readonly globalContext: GlobalContext, readonly document: vscode.TextDocument) {}
|
||||||
|
|
||||||
|
private preview?: PreviewContext;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,26 +29,26 @@ import { FoldingRange, Optional } from '../utils/types';
|
||||||
*/
|
*/
|
||||||
export default class DocumentSymbolContext {
|
export default class DocumentSymbolContext {
|
||||||
|
|
||||||
private document = this.documentContext.document;
|
get documentSymbols() {
|
||||||
|
return this._documentSymbols ??= this.calculcateDocumentSymbols();
|
||||||
private documentSymbols?: vscode.DocumentSymbol[];
|
|
||||||
|
|
||||||
get values() {
|
|
||||||
return this.documentSymbols ??= this.calculcateDocumentSymbols();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.documentSymbols = undefined;
|
this._documentSymbols = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly documentContext: DocumentContext) {}
|
constructor(private readonly documentContext: DocumentContext) {}
|
||||||
|
|
||||||
|
private _documentSymbols?: vscode.DocumentSymbol[];
|
||||||
|
|
||||||
|
private readonly document = this.documentContext.document;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate document symbols based on folding ranges.
|
* Calculate document symbols based on folding ranges.
|
||||||
*/
|
*/
|
||||||
private calculcateDocumentSymbols() {
|
private calculcateDocumentSymbols() {
|
||||||
const ranges = Array<Optional<FoldingRange>>(this.document.lineCount);
|
const ranges = Array<Optional<FoldingRange>>(this.document.lineCount);
|
||||||
this.documentContext.foldingRange.values.forEach(range => range.kind ?? (ranges[range.start] = range));
|
this.documentContext.foldingRange.foldingRanges.forEach(range => range.kind ?? (ranges[range.start] = range));
|
||||||
return foldingRangeToSymbols(ranges, 0, ranges.length);
|
return foldingRangeToSymbols(ranges, 0, ranges.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,8 @@ function foldingRangeToSymbols(ranges: readonly Optional<FoldingRange>[], start:
|
||||||
if (node === undefined) continue;
|
if (node === undefined) continue;
|
||||||
const range = lineNumToRange(idx, node.end);
|
const range = lineNumToRange(idx, node.end);
|
||||||
const selectionRange = lineNumToRange(idx);
|
const selectionRange = lineNumToRange(idx);
|
||||||
const symbol = new vscode.DocumentSymbol('@' + node.name, node.detail,
|
const symbol = new vscode.DocumentSymbol('@' + node.name, node.detail, vscode.SymbolKind.String,
|
||||||
vscode.SymbolKind.String, range, selectionRange);
|
range, selectionRange);
|
||||||
symbol.children = foldingRangeToSymbols(ranges, idx + 1, node.end);
|
symbol.children = foldingRangeToSymbols(ranges, idx + 1, node.end);
|
||||||
symbols.push(symbol);
|
symbols.push(symbol);
|
||||||
idx = node.end;
|
idx = node.end;
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { lineNumToRange } from '../utils/misc';
|
import { lineNumToRange } from '../utils/misc';
|
||||||
import { FoldingRange, Range, NamedLine } from '../utils/types';
|
import { FoldingRange, Range, NamedLine } from '../utils/types';
|
||||||
|
import DocumentContext from './document';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores information about folding ranges for a document.
|
* Stores information about folding ranges for a document.
|
||||||
|
@ -31,40 +32,18 @@ import { FoldingRange, Range, NamedLine } from '../utils/types';
|
||||||
*/
|
*/
|
||||||
export default class FoldingRangeContext {
|
export default class FoldingRangeContext {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get VSCode folding ranges from the context.
|
* Get VSCode folding ranges from the context.
|
||||||
*/
|
*/
|
||||||
get values() {
|
get foldingRanges() {
|
||||||
return this.foldingRanges ?? this.calculateFoldingRanges();
|
return this._foldingRanges ?? this.calculateFoldingRanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get node values of document as VSCode code lenses.
|
* Get node values of document as VSCode code lenses.
|
||||||
*/
|
*/
|
||||||
get nodeValues() {
|
get nodeValues() {
|
||||||
this.foldingRanges ?? this.calculateFoldingRanges();
|
this._foldingRanges ?? this.calculateFoldingRanges();
|
||||||
return this.nodes;
|
return this.nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,12 +54,12 @@ export default class FoldingRangeContext {
|
||||||
*/
|
*/
|
||||||
update(events: readonly vscode.TextDocumentContentChangeEvent[]) {
|
update(events: readonly vscode.TextDocumentContentChangeEvent[]) {
|
||||||
this.contentMayChange = true;
|
this.contentMayChange = true;
|
||||||
if (this.foldingRanges === undefined) return false;
|
if (this._foldingRanges === undefined) return false;
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const updatedLines = event.text.split(this.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n').length;
|
const updatedLines = event.text.split(this.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n').length;
|
||||||
// Clear cached folding range when line count changes.
|
// Clear cached folding range when line count changes.
|
||||||
if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) {
|
if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) {
|
||||||
this.foldingRanges = undefined;
|
this._foldingRanges = undefined;
|
||||||
this.nodes = [];
|
this.nodes = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -90,10 +69,39 @@ export default class FoldingRangeContext {
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
if (!this.contentMayChange) return;
|
if (!this.contentMayChange) return;
|
||||||
this.foldingRanges = undefined;
|
this._foldingRanges = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly document: vscode.TextDocument) {}
|
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.
|
* Calculate and update folding ranges for the document.
|
||||||
|
@ -103,7 +111,7 @@ export default class FoldingRangeContext {
|
||||||
*/
|
*/
|
||||||
private calculateFoldingRanges() {
|
private calculateFoldingRanges() {
|
||||||
this.contentMayChange = false;
|
this.contentMayChange = false;
|
||||||
this.foldingRanges = [];
|
this._foldingRanges = [];
|
||||||
this.clearTemporaries();
|
this.clearTemporaries();
|
||||||
let closingBlocks = <NamedLine[]>[];
|
let closingBlocks = <NamedLine[]>[];
|
||||||
let lastLine = this.document.lineCount - 1;
|
let lastLine = this.document.lineCount - 1;
|
||||||
|
@ -115,7 +123,7 @@ export default class FoldingRangeContext {
|
||||||
if (line === '@bye') {
|
if (line === '@bye') {
|
||||||
lastLine = idx;
|
lastLine = idx;
|
||||||
// Abort anything after `@bye`.
|
// Abort anything after `@bye`.
|
||||||
this.foldingRanges = [];
|
this._foldingRanges = [];
|
||||||
closingBlocks = [];
|
closingBlocks = [];
|
||||||
this.clearTemporaries();
|
this.clearTemporaries();
|
||||||
continue;
|
continue;
|
||||||
|
@ -126,7 +134,9 @@ export default class FoldingRangeContext {
|
||||||
if (line.startsWith('@end ')) {
|
if (line.startsWith('@end ')) {
|
||||||
if (verbatim) continue;
|
if (verbatim) continue;
|
||||||
const name = line.substring(5).trimRight();
|
const name = line.substring(5).trimRight();
|
||||||
name === 'verbatim' && (verbatim = true);
|
if (name === 'verbatim') {
|
||||||
|
verbatim = true;
|
||||||
|
}
|
||||||
closingBlocks.push({ name: name, line: idx });
|
closingBlocks.push({ name: name, line: idx });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -144,12 +154,30 @@ export default class FoldingRangeContext {
|
||||||
if (this.commentRange !== undefined) {
|
if (this.commentRange !== undefined) {
|
||||||
this.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment });
|
this.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment });
|
||||||
}
|
}
|
||||||
return this.foldingRanges;
|
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) {
|
private processComment(lineText: string, lineNum: number) {
|
||||||
if (!lineText.startsWith('@c')) return false;
|
if (!lineText.startsWith('@c')) return false;
|
||||||
if (!lineText.startsWith(' ', 2) && !lineText.startsWith('omment ', 2)) return false;
|
if (!lineText.startsWith(' ', 2) && !lineText.startsWith('omment ', 2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Check for opening/closing header.
|
// Check for opening/closing header.
|
||||||
if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) {
|
if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) {
|
||||||
if (this.headerStart === undefined) {
|
if (this.headerStart === undefined) {
|
||||||
|
@ -203,29 +231,4 @@ export default class FoldingRangeContext {
|
||||||
}
|
}
|
||||||
return false;
|
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({ name: extraArgs.name ?? '', detail: extraArgs.detail ?? '', start, end, kind: extraArgs.kind });
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearTemporaries() {
|
|
||||||
this.commentRange = undefined;
|
|
||||||
this.headerStart = undefined;
|
|
||||||
this.nodes = [];
|
|
||||||
this.closingSubsection = this.closingSection = this.closingChapter = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,6 @@
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import DocumentContext from './document';
|
import DocumentContext from './document';
|
||||||
import ContextMapping from '../context_mapping';
|
|
||||||
import Diagnosis from '../diagnosis';
|
|
||||||
import Logger from '../logger';
|
|
||||||
import Options from '../options';
|
|
||||||
import Converter from '../utils/converter';
|
import Converter from '../utils/converter';
|
||||||
import { getNodeHtmlRef, prompt } from '../utils/misc';
|
import { getNodeHtmlRef, prompt } from '../utils/misc';
|
||||||
import { Operator, Optional } from '../utils/types';
|
import { Operator, Optional } from '../utils/types';
|
||||||
|
@ -35,81 +31,20 @@ import { Operator, Optional } from '../utils/types';
|
||||||
*/
|
*/
|
||||||
export default class PreviewContext {
|
export default class PreviewContext {
|
||||||
|
|
||||||
/**
|
|
||||||
* Create (if not yet created) and show preview for a Texinfo document.
|
|
||||||
*
|
|
||||||
* @param editor The editor where the document is being held.
|
|
||||||
*/
|
|
||||||
static async showPreview(editor: vscode.TextEditor) {
|
|
||||||
const document = editor.document;
|
|
||||||
// Only show preview for saved files, as we're not gonna send document content to `makeinfo` via STDIN.
|
|
||||||
// Instead, the file will be loaded from disk.
|
|
||||||
if (document.isUntitled) {
|
|
||||||
if (!await prompt('Save this document to display preview.', 'Save')) return;
|
|
||||||
if (!await document.save()) return;
|
|
||||||
}
|
|
||||||
ContextMapping.getDocumentContext(document).initPreview().panel.reveal();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jump to the corresponding section of document preview by node name.
|
|
||||||
*
|
|
||||||
* @param document
|
|
||||||
* @param nodeName
|
|
||||||
*/
|
|
||||||
static gotoPreview(document: vscode.TextDocument, nodeName: string) {
|
|
||||||
ContextMapping.getDocumentContext(document).initPreview().panel.webview
|
|
||||||
.postMessage({ command: 'goto', value: getNodeHtmlRef(nodeName) });
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly document = this.documentContext.document;
|
|
||||||
|
|
||||||
private readonly panel: vscode.WebviewPanel;
|
|
||||||
|
|
||||||
private readonly disposables = <vscode.Disposable[]>[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the preview is updating.
|
|
||||||
*/
|
|
||||||
private updating = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a preview update request is pending.
|
|
||||||
*/
|
|
||||||
private pendingUpdate = false;
|
|
||||||
|
|
||||||
private get imageTransformer(): Optional<Operator<string>> {
|
|
||||||
if (!Options.localImage) return undefined;
|
|
||||||
const pathName = path.dirname(this.document.fileName);
|
|
||||||
return src => {
|
|
||||||
// Do not transform URIs of online images.
|
|
||||||
if (src.startsWith('https://') || src.startsWith('http://')) return src;
|
|
||||||
const srcUri = vscode.Uri.file(pathName + '/' + src);
|
|
||||||
// To display images in webviews, image URIs in HTML should be converted to VSCode-recognizable ones.
|
|
||||||
return this.panel.webview.asWebviewUri(srcUri).toString();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private get script() {
|
|
||||||
if (!Options.enableCodeLens) return undefined;
|
|
||||||
return "window.addEventListener('message', event => {" +
|
|
||||||
"const message = event.data;" +
|
|
||||||
"switch (message.command) {" +
|
|
||||||
"case 'goto':" +
|
|
||||||
"window.location.hash = message.value;" +
|
|
||||||
// We may want to scroll to the same node again.
|
|
||||||
"history.pushState('', '', window.location.pathname);" +
|
|
||||||
"break;" +
|
|
||||||
"}" +
|
|
||||||
"})";
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.disposables.forEach(event => event.dispose());
|
this.disposables.forEach(event => event.dispose());
|
||||||
this.panel.dispose();
|
this.panel.dispose();
|
||||||
this.documentContext.closePreview();
|
this.documentContext.closePreview();
|
||||||
// Only show diagnostic information when the preview is active.
|
// Only show diagnostic information when the preview is active.
|
||||||
Diagnosis.delete(this.document);
|
this.diagnosis.delete(this.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
goto(nodeName: string) {
|
||||||
|
this.panel.webview.postMessage({ command: 'goto', value: getNodeHtmlRef(nodeName) });
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.panel.reveal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWebview() {
|
async updateWebview() {
|
||||||
|
@ -121,17 +56,17 @@ export default class PreviewContext {
|
||||||
this.pendingUpdate = false;
|
this.pendingUpdate = false;
|
||||||
// Inform the user that the preview is updating if `makeinfo` takes too long.
|
// Inform the user that the preview is updating if `makeinfo` takes too long.
|
||||||
setTimeout(() => this.updating && this.updateTitle(), 500);
|
setTimeout(() => this.updating && this.updateTitle(), 500);
|
||||||
const { data, error } = await new Converter(this.document.fileName)
|
const { data, error } = await new Converter(this.document.fileName, this.globalContext.options, this.logger)
|
||||||
.convertToHtml(this.imageTransformer, this.script);
|
.convertToHtml(this.imageTransformer, this.script);
|
||||||
if (error) {
|
if (error) {
|
||||||
Logger.log(error);
|
this.logger.log(error);
|
||||||
Diagnosis.update(this.document, error);
|
this.diagnosis.update(this.document, error);
|
||||||
} else {
|
} else {
|
||||||
Diagnosis.delete(this.document);
|
this.diagnosis.delete(this.document);
|
||||||
}
|
}
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
prompt(`Failed to show preview for ${this.document.fileName}.`, 'Show log', true)
|
prompt(`Failed to show preview for ${this.document.fileName}.`, 'Show log', true)
|
||||||
.then(result => result && Logger.show());
|
.then(result => result && this.logger.show());
|
||||||
} else {
|
} else {
|
||||||
this.panel.webview.html = data;
|
this.panel.webview.html = data;
|
||||||
}
|
}
|
||||||
|
@ -148,6 +83,51 @@ export default class PreviewContext {
|
||||||
this.updateWebview();
|
this.updateWebview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly document = this.documentContext.document;
|
||||||
|
private readonly globalContext = this.documentContext.globalContext;
|
||||||
|
private readonly diagnosis = this.globalContext.diagnosis;
|
||||||
|
private readonly logger = this.globalContext.logger;
|
||||||
|
|
||||||
|
private readonly disposables = <vscode.Disposable[]>[];
|
||||||
|
|
||||||
|
private readonly panel: vscode.WebviewPanel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a preview update request is pending.
|
||||||
|
*/
|
||||||
|
private pendingUpdate = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the preview is updating.
|
||||||
|
*/
|
||||||
|
private updating = false;
|
||||||
|
|
||||||
|
private get imageTransformer(): Optional<Operator<string>> {
|
||||||
|
if (!this.globalContext.options.localImage) return undefined;
|
||||||
|
const pathName = path.dirname(this.document.fileName);
|
||||||
|
return src => {
|
||||||
|
// Do not transform URIs of online images.
|
||||||
|
if (src.startsWith('https://') || src.startsWith('http://')) return src;
|
||||||
|
const srcUri = vscode.Uri.file(pathName + '/' + src);
|
||||||
|
// To display images in webviews, image URIs in HTML should be converted to VSCode-recognizable ones.
|
||||||
|
return this.panel.webview.asWebviewUri(srcUri).toString();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private get script() {
|
||||||
|
if (!this.globalContext.options.enableCodeLens) return undefined;
|
||||||
|
return "window.addEventListener('message', event => {" +
|
||||||
|
"const message = event.data;" +
|
||||||
|
"switch (message.command) {" +
|
||||||
|
"case 'goto':" +
|
||||||
|
"window.location.hash = message.value;" +
|
||||||
|
// We may want to scroll to the same node again.
|
||||||
|
"history.pushState('', '', window.location.pathname);" +
|
||||||
|
"break;" +
|
||||||
|
"}" +
|
||||||
|
"})";
|
||||||
|
}
|
||||||
|
|
||||||
private updateTitle() {
|
private updateTitle() {
|
||||||
const updating = this.updating ? '(Updating) ' : '';
|
const updating = this.updating ? '(Updating) ' : '';
|
||||||
const fileName = path.basename(this.document.fileName);
|
const fileName = path.basename(this.document.fileName);
|
||||||
|
|
|
@ -28,10 +28,8 @@ import { isDefined } from './utils/types';
|
||||||
*/
|
*/
|
||||||
export default class Diagnosis implements vscode.Disposable {
|
export default class Diagnosis implements vscode.Disposable {
|
||||||
|
|
||||||
private static singleton?: Diagnosis;
|
delete(document: vscode.TextDocument) {
|
||||||
|
this.diagnostics.delete(document.uri);
|
||||||
static get instance() {
|
|
||||||
return Diagnosis.singleton ??= new Diagnosis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,29 +38,26 @@ export default class Diagnosis implements vscode.Disposable {
|
||||||
* @param document
|
* @param document
|
||||||
* @param logText
|
* @param logText
|
||||||
*/
|
*/
|
||||||
static update(document: vscode.TextDocument, logText: string) {
|
update(document: vscode.TextDocument, logText: string) {
|
||||||
const fileName = document.uri.path;
|
const fileName = document.uri.path;
|
||||||
const diagnostics = logText.split('\n').filter(line => line.startsWith(fileName))
|
const diagnostics = logText.split('\n')
|
||||||
.map(line => logLineToDiagnostic(line.substring(fileName.length + 1))).filter(isDefined);
|
.filter(line => line.startsWith(fileName))
|
||||||
Diagnosis.instance.diagnostics.set(document.uri, diagnostics);
|
.map(line => logLineToDiagnostic(line.substring(fileName.length + 1)))
|
||||||
|
.filter(isDefined);
|
||||||
|
this.diagnostics.set(document.uri, diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
static delete(document: vscode.TextDocument) {
|
|
||||||
Diagnosis.instance.diagnostics.delete(document.uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly diagnostics = vscode.languages.createDiagnosticCollection('texinfo');
|
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.diagnostics.dispose();
|
this.diagnostics.dispose();
|
||||||
Diagnosis.singleton = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly diagnostics = vscode.languages.createDiagnosticCollection('texinfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
function logLineToDiagnostic(lineText: string) {
|
function logLineToDiagnostic(lineText: string) {
|
||||||
const lineNum = Number.parseInt(lineText) - 1;
|
const lineNum = parseInt(lineText) - 1;
|
||||||
// Ignore error that does not correspond a line.
|
// Ignore error that does not correspond a line.
|
||||||
if (Number.isNaN(lineNum)) return undefined;
|
if (isNaN(lineNum)) return undefined;
|
||||||
const message = lineText.substring(lineNum.toString().length + 2);
|
const message = lineText.substring(lineNum.toString().length + 2);
|
||||||
const severity = message.startsWith('warning:') ? vscode.DiagnosticSeverity.Warning : undefined;
|
const severity = message.startsWith('warning:') ? vscode.DiagnosticSeverity.Warning : undefined;
|
||||||
return new vscode.Diagnostic(lineNumToRange(lineNum), message, severity);
|
return new vscode.Diagnostic(lineNumToRange(lineNum), message, severity);
|
||||||
|
|
|
@ -20,31 +20,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import ContextMapping from './context_mapping';
|
import GlobalContext from './global_context';
|
||||||
import Diagnosis from './diagnosis';
|
|
||||||
import Indicator from './indicator';
|
|
||||||
import Logger from './logger';
|
|
||||||
import Options from './options';
|
|
||||||
import PreviewContext from './contexts/preview';
|
|
||||||
import CodeLensProvider from './providers/code_lens';
|
|
||||||
import CompletionItemProvider from './providers/completion_item';
|
|
||||||
import DocumentSymbolProvider from './providers/document_symbol';
|
|
||||||
import FoldingRangeProvider from './providers/folding_range';
|
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
context.subscriptions.push(
|
new GlobalContext(context);
|
||||||
ContextMapping.instance, Diagnosis.instance, Indicator.instance, Logger.instance, Options.instance,
|
|
||||||
vscode.window.onDidChangeActiveTextEditor(Indicator.onTextEditorChange),
|
|
||||||
vscode.workspace.onDidChangeTextDocument(ContextMapping.onDocumentUpdate),
|
|
||||||
vscode.workspace.onDidSaveTextDocument(ContextMapping.onDocumentSave),
|
|
||||||
vscode.workspace.onDidCloseTextDocument(ContextMapping.onDocumentClose),
|
|
||||||
vscode.workspace.onDidChangeConfiguration(Options.clear),
|
|
||||||
vscode.commands.registerTextEditorCommand('texinfo.preview.show', PreviewContext.showPreview),
|
|
||||||
vscode.commands.registerCommand('texinfo.preview.goto', PreviewContext.gotoPreview),
|
|
||||||
vscode.commands.registerCommand('texinfo.indicator.click', Indicator.click),
|
|
||||||
vscode.languages.registerCodeLensProvider('texinfo', new CodeLensProvider()),
|
|
||||||
vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'),
|
|
||||||
vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()),
|
|
||||||
vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* global_context.ts
|
||||||
|
*
|
||||||
|
* Copyright (C) 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 ContextMapping from './context_mapping';
|
||||||
|
import Diagnosis from './diagnosis';
|
||||||
|
import Indicator from './indicator';
|
||||||
|
import Logger from './logger';
|
||||||
|
import Options from './options';
|
||||||
|
import CodeLensProvider from './providers/code_lens';
|
||||||
|
import CompletionItemProvider from './providers/completion_item';
|
||||||
|
import DocumentSymbolProvider from './providers/document_symbol';
|
||||||
|
import FoldingRangeProvider from './providers/folding_range';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage extension-level global-scope contexts.
|
||||||
|
*/
|
||||||
|
export default class GlobalContext {
|
||||||
|
|
||||||
|
readonly contextMapping = new ContextMapping(this);
|
||||||
|
|
||||||
|
readonly diagnosis = new Diagnosis;
|
||||||
|
|
||||||
|
readonly indicator = new Indicator(this);
|
||||||
|
|
||||||
|
readonly logger = new Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: `Options`' no singleton. Do not wire directly, always use `globalContext.options` instead.
|
||||||
|
*/
|
||||||
|
get options() {
|
||||||
|
return this._options ??= new Options;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(...items: vscode.Disposable[]) {
|
||||||
|
this.context.subscriptions.push(...items);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly context: vscode.ExtensionContext) {
|
||||||
|
this.subscribe(this.contextMapping, this.diagnosis, this.indicator, this.logger,
|
||||||
|
vscode.languages.registerCodeLensProvider('texinfo', new CodeLensProvider(this)),
|
||||||
|
vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(this), '@'),
|
||||||
|
vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider(this)),
|
||||||
|
vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider(this)),
|
||||||
|
vscode.workspace.onDidChangeConfiguration(this.refreshOptions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _options?: Options;
|
||||||
|
|
||||||
|
private refreshOptions() {
|
||||||
|
this._options = undefined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import Options from './options';
|
import GlobalContext from './global_context';
|
||||||
import { exec } from './utils/misc';
|
import { exec } from './utils/misc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,66 +28,61 @@ import { exec } from './utils/misc';
|
||||||
*/
|
*/
|
||||||
export default class Indicator implements vscode.Disposable {
|
export default class Indicator implements vscode.Disposable {
|
||||||
|
|
||||||
private static singleton?: Indicator;
|
|
||||||
|
|
||||||
static async click() {
|
|
||||||
await Indicator.instance.updateStatus();
|
|
||||||
Indicator.instance.refresh(vscode.window.activeTextEditor);
|
|
||||||
}
|
|
||||||
|
|
||||||
static onTextEditorChange(editor?: vscode.TextEditor) {
|
|
||||||
Indicator.instance.refresh(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get instance() {
|
|
||||||
return this.singleton ??= new Indicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
private statusBarItem: vscode.StatusBarItem;
|
|
||||||
|
|
||||||
private gnuTexinfoAvailable = false;
|
|
||||||
|
|
||||||
get canDisplayPreview() {
|
get canDisplayPreview() {
|
||||||
return this.gnuTexinfoAvailable;
|
return this._canDisplayPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.statusBarItem.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly globalContext: GlobalContext) {
|
||||||
|
globalContext.subscribe(
|
||||||
|
vscode.commands.registerCommand('texinfo.indicator.click', this.click.bind(this)),
|
||||||
|
vscode.window.onDidChangeActiveTextEditor(this.refresh.bind(this)),
|
||||||
|
);
|
||||||
|
this.updateStatus().then(() => this.refresh(vscode.window.activeTextEditor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _canDisplayPreview = false;
|
||||||
|
|
||||||
|
private statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||||
|
|
||||||
|
private async click() {
|
||||||
|
await this.updateStatus();
|
||||||
|
this.refresh(vscode.window.activeTextEditor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private refresh(editor?: vscode.TextEditor) {
|
private refresh(editor?: vscode.TextEditor) {
|
||||||
if (editor === undefined || editor.document.languageId != 'texinfo') {
|
if (editor?.document.languageId === 'texinfo') {
|
||||||
this.statusBarItem.hide();
|
|
||||||
} else {
|
|
||||||
this.statusBarItem.show();
|
this.statusBarItem.show();
|
||||||
|
} else {
|
||||||
|
this.statusBarItem.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateStatus() {
|
private async updateStatus() {
|
||||||
const output = await exec(Options.makeinfo, ['--version'], Options.maxSize);
|
const options = this.globalContext.options;
|
||||||
|
const output = await exec(options.makeinfo, ['--version'], options.maxSize);
|
||||||
const result = output.data?.match(/\(GNU texinfo\) (.*)\n/);
|
const result = output.data?.match(/\(GNU texinfo\) (.*)\n/);
|
||||||
let tooltip = '', icon: string, version = '';
|
let tooltip = '', icon: string, version = '';
|
||||||
if (result && result[1]) {
|
if (result && result[1]) {
|
||||||
version = result[1];
|
version = result[1];
|
||||||
if (!isNaN(+version) && +version < 6.7) {
|
if (!isNaN(+version) && +version < 6.7) {
|
||||||
icon = '$(warning)';
|
icon = '$(warning)';
|
||||||
tooltip = `GNU Texinfo (${Options.makeinfo}) is outdated (${version} < 6.7).`;
|
tooltip = `GNU Texinfo (${options.makeinfo}) is outdated (${version} < 6.7).`;
|
||||||
} else {
|
} else {
|
||||||
|
// Unrecognizable version. Assume it is okay.
|
||||||
icon = '$(check)';
|
icon = '$(check)';
|
||||||
}
|
}
|
||||||
this.gnuTexinfoAvailable = true;
|
this._canDisplayPreview = true;
|
||||||
} else {
|
} else {
|
||||||
icon = '$(close)';
|
icon = '$(close)';
|
||||||
tooltip = `GNU Texinfo (${Options.makeinfo}) is not correctly installed or configured.`;
|
tooltip = `GNU Texinfo (${options.makeinfo}) is not correctly installed or configured.`;
|
||||||
this.gnuTexinfoAvailable = false;
|
this._canDisplayPreview = false;
|
||||||
}
|
}
|
||||||
|
this.statusBarItem.command = 'texinfo.indicator.click';
|
||||||
this.statusBarItem.text = `${icon} GNU Texinfo ${version}`;
|
this.statusBarItem.text = `${icon} GNU Texinfo ${version}`;
|
||||||
this.statusBarItem.tooltip = tooltip;
|
this.statusBarItem.tooltip = tooltip;
|
||||||
this.statusBarItem.command = 'texinfo.indicator.click';
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
|
||||||
this.updateStatus().then(() => this.refresh(vscode.window.activeTextEditor));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.statusBarItem.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,29 +26,18 @@ import * as vscode from 'vscode';
|
||||||
*/
|
*/
|
||||||
export default class Logger implements vscode.Disposable {
|
export default class Logger implements vscode.Disposable {
|
||||||
|
|
||||||
private static singleton?: Logger;
|
log(message: string) {
|
||||||
|
|
||||||
static get instance() {
|
|
||||||
return Logger.singleton ??= new Logger();
|
|
||||||
}
|
|
||||||
|
|
||||||
static log(message: string) {
|
|
||||||
const dateTime = new Date().toLocaleString(undefined, { hour12: false });
|
const dateTime = new Date().toLocaleString(undefined, { hour12: false });
|
||||||
Logger.instance.outputChannel.appendLine(`[ ${dateTime} ]\n${message}`);
|
this.outputChannel.appendLine(`[ ${dateTime} ]\n${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static show() {
|
show() {
|
||||||
Logger.instance.outputChannel.show(true);
|
this.outputChannel.show(true);
|
||||||
}
|
|
||||||
|
|
||||||
private outputChannel: vscode.OutputChannel;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
this.outputChannel = vscode.window.createOutputChannel('Texinfo');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
Logger.instance.outputChannel.dispose();
|
this.outputChannel.dispose();
|
||||||
Logger.singleton = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private outputChannel = vscode.window.createOutputChannel('Texinfo');
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,69 +24,55 @@ import * as vscode from 'vscode';
|
||||||
/**
|
/**
|
||||||
* Fetch extension option values.
|
* Fetch extension option values.
|
||||||
*
|
*
|
||||||
* See `contributes.configuration` of package.json for details.
|
* See the `contributes.configuration` entry in package.json for details.
|
||||||
*/
|
*/
|
||||||
export default class Options implements vscode.Disposable {
|
export default class Options {
|
||||||
|
|
||||||
private static singleton?: Options;
|
get enableSnippets() {
|
||||||
|
return this.getBoolean('completion.enableSnippets');
|
||||||
static get instance() {
|
|
||||||
return Options.singleton ??= new Options('texinfo');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get makeinfo() {
|
get hideSnippetCommands() {
|
||||||
return Options.instance.getString('makeinfo');
|
return this.getBoolean('completion.hideSnippetCommands');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get enableCodeLens() {
|
get enableCodeLens() {
|
||||||
return Options.instance.getBoolean('enableCodeLens');
|
return this.getBoolean('enableCodeLens');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get enableSnippets() {
|
get makeinfo() {
|
||||||
return Options.instance.getBoolean('completion.enableSnippets');
|
return this.getString('makeinfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get hideSnippetCommands() {
|
get customCSS() {
|
||||||
return Options.instance.getBoolean('completion.hideSnippetCommands');
|
return this.getString('preview.customCSS');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get noHeaders() {
|
get errorLimit() {
|
||||||
return Options.instance.getBoolean('preview.noHeaders');
|
return this.getNumber('preview.errorLimit');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get maxSize() {
|
get localImage() {
|
||||||
return Options.instance.getNumber('preview.maxSize') * 1024 * 1024;
|
return this.getBoolean('preview.localImage');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get errorLimit() {
|
get maxSize() {
|
||||||
return Options.instance.getNumber('preview.errorLimit');
|
return this.getNumber('preview.maxSize') * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get noValidation() {
|
get noHeaders() {
|
||||||
return Options.instance.getBoolean('preview.noValidation');
|
return this.getBoolean('preview.noHeaders');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get noWarnings() {
|
get noValidation() {
|
||||||
return Options.instance.getBoolean('preview.noWarnings');
|
return this.getBoolean('preview.noValidation');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get localImage() {
|
get noWarnings() {
|
||||||
return Options.instance.getBoolean('preview.localImage');
|
return this.getBoolean('preview.noWarnings');
|
||||||
}
|
}
|
||||||
|
|
||||||
static get customCSS() {
|
private readonly configuration = vscode.workspace.getConfiguration('texinfo');
|
||||||
return Options.instance.getString('preview.customCSS');
|
|
||||||
}
|
|
||||||
|
|
||||||
static clear() {
|
|
||||||
Options.singleton = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly configuration: vscode.WorkspaceConfiguration;
|
|
||||||
|
|
||||||
private getString(section: string) {
|
|
||||||
return this.configuration.get(section, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
private getBoolean(section: string) {
|
private getBoolean(section: string) {
|
||||||
return this.configuration.get(section, false);
|
return this.configuration.get(section, false);
|
||||||
|
@ -96,11 +82,7 @@ export default class Options implements vscode.Disposable {
|
||||||
return this.configuration.get(section, 0);
|
return this.configuration.get(section, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(section: string) {
|
private getString(section: string) {
|
||||||
this.configuration = vscode.workspace.getConfiguration(section);
|
return this.configuration.get(section, '');
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
Options.singleton = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import ContextMapping from '../context_mapping';
|
import GlobalContext from '../global_context';
|
||||||
import Indicator from '../indicator';
|
|
||||||
import Options from '../options';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide code lenses for Texinfo document.
|
* Provide code lenses for Texinfo document.
|
||||||
|
@ -30,8 +28,10 @@ import Options from '../options';
|
||||||
export default class CodeLensProvider implements vscode.CodeLensProvider {
|
export default class CodeLensProvider implements vscode.CodeLensProvider {
|
||||||
|
|
||||||
provideCodeLenses(document: vscode.TextDocument) {
|
provideCodeLenses(document: vscode.TextDocument) {
|
||||||
if (!Options.enableCodeLens) return undefined;
|
if (!this.globalContext.options.enableCodeLens) return undefined;
|
||||||
if (!Indicator.instance.canDisplayPreview) return undefined;
|
if (!this.globalContext.indicator.canDisplayPreview) return undefined;
|
||||||
return ContextMapping.getDocumentContext(document).foldingRange.nodeValues;
|
return this.globalContext.contextMapping.getDocumentContext(document).foldingRange.nodeValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly globalContext: GlobalContext) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import Options from '../options';
|
import GlobalContext from '../global_context';
|
||||||
import { CompletionItem } from '../utils/types';
|
import { CompletionItem } from '../utils/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,8 +28,6 @@ import { CompletionItem } from '../utils/types';
|
||||||
*/
|
*/
|
||||||
export default class CompletionItemProvider implements vscode.CompletionItemProvider {
|
export default class CompletionItemProvider implements vscode.CompletionItemProvider {
|
||||||
|
|
||||||
private completionItems?: CompletionItem[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full list of completion items.
|
* Full list of completion items.
|
||||||
*
|
*
|
||||||
|
@ -41,10 +39,10 @@ export default class CompletionItemProvider implements vscode.CompletionItemProv
|
||||||
* which means that GFDL applies to lines 48-398 of this file, while the remainder
|
* which means that GFDL applies to lines 48-398 of this file, while the remainder
|
||||||
* is under GPL like other source code files of the project.
|
* is under GPL like other source code files of the project.
|
||||||
*/
|
*/
|
||||||
private get values() {
|
private get completionItems() {
|
||||||
const enableSnippets = Options.enableSnippets;
|
const enableSnippets = this.oldOptions.enableSnippets;
|
||||||
const hideSnippetCommands = Options.hideSnippetCommands;
|
const hideSnippetCommands = this.oldOptions.hideSnippetCommands;
|
||||||
return this.completionItems ??= [
|
return this._completionItems ??= [
|
||||||
command('ampchar', 'Insert an ampersand, "&"', { hasEmptyBrace: true }),
|
command('ampchar', 'Insert an ampersand, "&"', { hasEmptyBrace: true }),
|
||||||
command('atchar', 'Insert an at sign, "@"', { hasEmptyBrace: true }),
|
command('atchar', 'Insert an at sign, "@"', { hasEmptyBrace: true }),
|
||||||
command('backslashchar', 'Insert a blackslash, "\\"', { hasEmptyBrace: true }),
|
command('backslashchar', 'Insert a blackslash, "\\"', { hasEmptyBrace: true }),
|
||||||
|
@ -402,8 +400,6 @@ export default class CompletionItemProvider implements vscode.CompletionItemProv
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private oldOptions?: Options;
|
|
||||||
|
|
||||||
provideCompletionItems(
|
provideCompletionItems(
|
||||||
document: vscode.TextDocument,
|
document: vscode.TextDocument,
|
||||||
position: vscode.Position,
|
position: vscode.Position,
|
||||||
|
@ -422,18 +418,25 @@ export default class CompletionItemProvider implements vscode.CompletionItemProv
|
||||||
if (document.getText(new vscode.Range(position.translate(0, -1), position)) !== '@') return undefined;
|
if (document.getText(new vscode.Range(position.translate(0, -1), position)) !== '@') return undefined;
|
||||||
}
|
}
|
||||||
// Check whether options has changed.
|
// Check whether options has changed.
|
||||||
if (this.oldOptions !== Options.instance) {
|
const newOptions = this.globalContext.options;
|
||||||
this.oldOptions = Options.instance;
|
if (this.oldOptions !== newOptions) {
|
||||||
this.completionItems = undefined;
|
this.oldOptions = newOptions;
|
||||||
|
this._completionItems = undefined;
|
||||||
}
|
}
|
||||||
if (position.character === 1) return this.values;
|
if (position.character === 1) return this.completionItems;
|
||||||
// Check whether the '@' character is escaped.
|
// Check whether the '@' character is escaped.
|
||||||
if (document.getText(new vscode.Range(position.translate(0, -2), position.translate(0, -1))) === '@') {
|
if (document.getText(new vscode.Range(position.translate(0, -2), position.translate(0, -1))) === '@') {
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
return this.values;
|
return this.completionItems;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly globalContext: GlobalContext) {}
|
||||||
|
|
||||||
|
private _completionItems?: CompletionItem[];
|
||||||
|
|
||||||
|
private oldOptions = this.globalContext.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import ContextMapping from '../context_mapping';
|
import GlobalContext from '../global_context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide document symbol information for Texinfo documents.
|
* Provide document symbol information for Texinfo documents.
|
||||||
|
@ -28,6 +28,8 @@ import ContextMapping from '../context_mapping';
|
||||||
export default class DocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
export default class DocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||||
|
|
||||||
provideDocumentSymbols(document: vscode.TextDocument) {
|
provideDocumentSymbols(document: vscode.TextDocument) {
|
||||||
return ContextMapping.getDocumentContext(document).documentSymbol.values;
|
return this.globalContext.contextMapping.getDocumentContext(document).documentSymbol.documentSymbols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly globalContext: GlobalContext) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import ContextMapping from '../context_mapping';
|
import GlobalContext from '../global_context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide folding range info for Texinfo documents.
|
* Provide folding range info for Texinfo documents.
|
||||||
|
@ -28,6 +28,8 @@ import ContextMapping from '../context_mapping';
|
||||||
export default class FoldingRangeProvider implements vscode.FoldingRangeProvider {
|
export default class FoldingRangeProvider implements vscode.FoldingRangeProvider {
|
||||||
|
|
||||||
provideFoldingRanges(document: vscode.TextDocument) {
|
provideFoldingRanges(document: vscode.TextDocument) {
|
||||||
return ContextMapping.getDocumentContext(document).foldingRange.values;
|
return this.globalContext.contextMapping.getDocumentContext(document).foldingRange.foldingRanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly globalContext: GlobalContext) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,25 @@ import { Operator } from './types';
|
||||||
*/
|
*/
|
||||||
export default class Converter {
|
export default class Converter {
|
||||||
|
|
||||||
|
async convertToHtml(imgTransformer?: Operator<string>, insertScript?: string) {
|
||||||
|
const options = ['-o-', '--no-split', '--html', `--error-limit=${this.options.errorLimit}`];
|
||||||
|
this.options.noHeaders && options.push('--no-headers');
|
||||||
|
this.options.noValidation && options.push('--no-validate');
|
||||||
|
this.options.noWarnings && options.push('--no-warn');
|
||||||
|
this.options.customCSS && this.includeCustomCSS(this.options.customCSS, options);
|
||||||
|
const result = await exec(this.options.makeinfo, options.concat(this.path), this.options.maxSize);
|
||||||
|
if (result.data !== undefined) {
|
||||||
|
// No worry about performance here, as the DOM is lazily initialized.
|
||||||
|
const dom = new DOM(result.data);
|
||||||
|
imgTransformer && dom.transformImageUri(imgTransformer);
|
||||||
|
insertScript && dom.insertScript(insertScript);
|
||||||
|
result.data = dom.outerHTML;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private readonly path: string, private readonly options: Options, private readonly logger: Logger) {}
|
||||||
|
|
||||||
private includeCustomCSS(cssFileURI: string, options: string[]) {
|
private includeCustomCSS(cssFileURI: string, options: string[]) {
|
||||||
try {
|
try {
|
||||||
const uri = vscode.Uri.parse(cssFileURI, true);
|
const uri = vscode.Uri.parse(cssFileURI, true);
|
||||||
|
@ -46,26 +65,7 @@ export default class Converter {
|
||||||
throw URIError;
|
throw URIError;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.log(`Cannot load custom CSS. Invalid URI: '${cssFileURI}'`);
|
this.logger.log(`Cannot load custom CSS. Invalid URI: '${cssFileURI}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly path: string) {}
|
|
||||||
|
|
||||||
async convertToHtml(imgTransformer?: Operator<string>, insertScript?: string) {
|
|
||||||
const options = ['-o', '-', '--no-split', '--html', `--error-limit=${Options.errorLimit}`];
|
|
||||||
Options.noHeaders && options.push('--no-headers');
|
|
||||||
Options.noValidation && options.push('--no-validate');
|
|
||||||
Options.noWarnings && options.push('--no-warn');
|
|
||||||
Options.customCSS && this.includeCustomCSS(Options.customCSS, options);
|
|
||||||
const result = await exec(Options.makeinfo, options.concat(this.path), Options.maxSize);
|
|
||||||
if (result.data !== undefined) {
|
|
||||||
// No worry about performance here, as the DOM is lazily initialized.
|
|
||||||
const dom = new DOM(result.data);
|
|
||||||
imgTransformer && dom.transformImageUri(imgTransformer);
|
|
||||||
insertScript && dom.insertScript(insertScript);
|
|
||||||
result.data = dom.outerHTML;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,18 +23,10 @@ import * as htmlparser from 'node-html-parser';
|
||||||
import { Operator } from './types';
|
import { Operator } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DOM manipulation utilities.
|
* Parse HTML into DOM and transform elements.
|
||||||
*/
|
*/
|
||||||
export default class DOM {
|
export default class DOM {
|
||||||
|
|
||||||
private dom?: htmlparser.HTMLElement;
|
|
||||||
|
|
||||||
private changed = false;
|
|
||||||
|
|
||||||
private get value() {
|
|
||||||
return this.dom ??= htmlparser.parse(this.html);
|
|
||||||
}
|
|
||||||
|
|
||||||
get outerHTML() {
|
get outerHTML() {
|
||||||
if (this.changed) {
|
if (this.changed) {
|
||||||
this.html = this.value.outerHTML;
|
this.html = this.value.outerHTML;
|
||||||
|
@ -43,6 +35,11 @@ export default class DOM {
|
||||||
return this.html;
|
return this.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertScript(script: string) {
|
||||||
|
this.value.querySelector('head').insertAdjacentHTML('beforeend', `<script>${script}</script>`);
|
||||||
|
this.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform and replace the `src` attribute value of all `img` elements from HTML using given function.
|
* Transform and replace the `src` attribute value of all `img` elements from HTML using given function.
|
||||||
*
|
*
|
||||||
|
@ -58,10 +55,13 @@ export default class DOM {
|
||||||
this.changed = true;
|
this.changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
insertScript(script: string) {
|
|
||||||
this.value.querySelector('head').insertAdjacentHTML('beforeend', `<script>${script}</script>`);
|
|
||||||
this.changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private html: string) {}
|
constructor(private html: string) {}
|
||||||
|
|
||||||
|
private _value?: htmlparser.HTMLElement;
|
||||||
|
|
||||||
|
private changed = false;
|
||||||
|
|
||||||
|
private get value() {
|
||||||
|
return this._value ??= htmlparser.parse(this.html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,15 +32,9 @@ import { ExecResult } from './types';
|
||||||
* @returns The output data, or `undefined` if execution fails.
|
* @returns The output data, or `undefined` if execution fails.
|
||||||
*/
|
*/
|
||||||
export function exec(path: string, args: string[], maxBuffer: number) {
|
export function exec(path: string, args: string[], maxBuffer: number) {
|
||||||
return new Promise<ExecResult>(resolve => {
|
return new Promise<ExecResult>(resolve => child_process.execFile(path, args, { maxBuffer: maxBuffer },
|
||||||
child_process.execFile(path, args, { maxBuffer: maxBuffer }, (error, stdout, stderr) => {
|
(error, stdout, stderr) => resolve(
|
||||||
if (error) {
|
error ? { error: stderr ? stderr : error.message } : { data: stdout, error: stderr })));
|
||||||
resolve({ error: stderr ? stderr : error.message });
|
|
||||||
} else {
|
|
||||||
resolve({ data: stdout, error: stderr });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,19 +21,19 @@
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
export type Optional<T> = T | undefined;
|
export type CompletionItem = vscode.CompletionItem & { snippet?: boolean };
|
||||||
|
|
||||||
export type Operator<T> = (arg: T) => T;
|
|
||||||
|
|
||||||
export type Range = { start: number, end: number };
|
|
||||||
|
|
||||||
export type NamedLine = { name: string, line: number };
|
|
||||||
|
|
||||||
export type ExecResult = { data?: string, error: string };
|
export type ExecResult = { data?: string, error: string };
|
||||||
|
|
||||||
export type FoldingRange = vscode.FoldingRange & { name: string, detail: string };
|
export type FoldingRange = vscode.FoldingRange & { name: string, detail: string };
|
||||||
|
|
||||||
export type CompletionItem = vscode.CompletionItem & { snippet?: boolean };
|
export type NamedLine = { name: string, line: number };
|
||||||
|
|
||||||
|
export type Operator<T> = (arg: T) => T;
|
||||||
|
|
||||||
|
export type Optional<T> = T | undefined;
|
||||||
|
|
||||||
|
export type Range = { start: number, end: number };
|
||||||
|
|
||||||
export function isDefined<T>(value: Optional<T>): value is T {
|
export function isDefined<T>(value: Optional<T>): value is T {
|
||||||
return value !== undefined;
|
return value !== undefined;
|
||||||
|
|
Loading…
Reference in New Issue