Major refactor.
This commit is contained in:
parent
2c5aec48ed
commit
3b34510feb
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* context/document.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import DocumentSymbolContext from './document_symbol';
|
||||||
|
import FoldingRangeContext from './folding_range';
|
||||||
|
import PreviewContext from './preview';
|
||||||
|
|
||||||
|
export default class DocumentContext {
|
||||||
|
|
||||||
|
readonly foldingRange = new FoldingRangeContext(this.document);
|
||||||
|
|
||||||
|
readonly documentSymbol = new DocumentSymbolContext(this);
|
||||||
|
|
||||||
|
private preview?: PreviewContext;
|
||||||
|
|
||||||
|
initPreview() {
|
||||||
|
return this.preview ??= new PreviewContext(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreview() {
|
||||||
|
return this.preview;
|
||||||
|
}
|
||||||
|
|
||||||
|
closePreview() {
|
||||||
|
this.preview = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(readonly document: vscode.TextDocument) {}
|
||||||
|
}
|
|
@ -1,29 +1,19 @@
|
||||||
/**
|
/**
|
||||||
* symbol.ts
|
* context/document_symbol.ts
|
||||||
*
|
*
|
||||||
* @author CismonX <admin@cismon.net>
|
* @author CismonX <admin@cismon.net>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import Document from './document';
|
import DocumentContext from './document';
|
||||||
import { FoldingRange } from './folding';
|
import { lineNumToRange } from '../utils/misc';
|
||||||
import { lineNumToRange, Optional } from './utils';
|
import { FoldingRange, Optional } from '../utils/types';
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide document symbol information for Texinfo documents.
|
|
||||||
*/
|
|
||||||
export class DocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
|
||||||
|
|
||||||
provideDocumentSymbols(document: vscode.TextDocument) {
|
|
||||||
return Document.of(document).symbol.values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context for symbols in a Texinfo document.
|
* Context for symbols in a Texinfo document.
|
||||||
*/
|
*/
|
||||||
export class DocumentSymbolContext {
|
export default class DocumentSymbolContext {
|
||||||
|
|
||||||
private document = this.documentContext.document;
|
private document = this.documentContext.document;
|
||||||
|
|
||||||
|
@ -40,6 +30,8 @@ export class DocumentSymbolContext {
|
||||||
this.symbols = undefined;
|
this.symbols = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly documentContext: DocumentContext) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate document symbols based on folding ranges.
|
* Calculate document symbols based on folding ranges.
|
||||||
*/
|
*/
|
||||||
|
@ -49,8 +41,6 @@ export class DocumentSymbolContext {
|
||||||
.forEach(range => range.kind ?? (ranges[range.start] = range));
|
.forEach(range => range.kind ?? (ranges[range.start] = range));
|
||||||
return this.symbols = foldingRangeToSymbols(ranges, 0, ranges.length);
|
return this.symbols = foldingRangeToSymbols(ranges, 0, ranges.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly documentContext: Document) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RangeNode = Optional<FoldingRange>;
|
type RangeNode = Optional<FoldingRange>;
|
|
@ -1,28 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* folding.ts
|
* context/folding_range.ts
|
||||||
*
|
*
|
||||||
* @author CismonX <admin@cismon.net>
|
* @author CismonX <admin@cismon.net>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import Document from './document';
|
import { FoldingRange, Range } from '../utils/types';
|
||||||
import { Range } from './utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide folding range info for Texinfo documents.
|
|
||||||
*/
|
|
||||||
export class FoldingRangeProvider implements vscode.FoldingRangeProvider {
|
|
||||||
|
|
||||||
provideFoldingRanges(document: vscode.TextDocument) {
|
|
||||||
return Document.of(document).foldingRange.values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores information about folding ranges for a document.
|
* Stores information about folding ranges for a document.
|
||||||
*/
|
*/
|
||||||
export class FoldingRangeContext {
|
export default class FoldingRangeContext {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get VSCode folding ranges from the context.
|
* Get VSCode folding ranges from the context.
|
||||||
|
@ -105,13 +94,16 @@ export class FoldingRangeContext {
|
||||||
} else {
|
} else {
|
||||||
closingBlocks.push(closingBlock);
|
closingBlocks.push(closingBlock);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (this.commentRange !== undefined) {
|
||||||
|
this.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment });
|
||||||
|
this.commentRange = undefined;
|
||||||
}
|
}
|
||||||
return this.foldingRanges;
|
return this.foldingRanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private processComment(lineText: string, lineNum: number) {
|
private processComment(lineText: string, lineNum: number) {
|
||||||
if (lineText.startsWith('@c')) {
|
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)) {
|
||||||
|
@ -127,15 +119,15 @@ export class FoldingRangeContext {
|
||||||
this.commentRange = { start: lineNum, end: lineNum };
|
this.commentRange = { start: lineNum, end: lineNum };
|
||||||
} else if (this.commentRange.start - 1 === lineNum) {
|
} else if (this.commentRange.start - 1 === lineNum) {
|
||||||
this.commentRange.start = lineNum;
|
this.commentRange.start = lineNum;
|
||||||
}
|
} else {
|
||||||
return true;
|
|
||||||
} else 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 });
|
||||||
this.commentRange = undefined;
|
this.commentRange = undefined;
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly document: vscode.TextDocument) {}
|
||||||
|
|
||||||
private processNode(lineText: string, lineNum: number, lastLineNum: number) {
|
private processNode(lineText: string, lineNum: number, lastLineNum: number) {
|
||||||
if (lineText.startsWith('@subsection ')) {
|
if (lineText.startsWith('@subsection ')) {
|
||||||
const detail = lineText.substring(12);
|
const detail = lineText.substring(12);
|
||||||
|
@ -173,22 +165,4 @@ export class FoldingRangeContext {
|
||||||
(this.foldingRanges ??= [])
|
(this.foldingRanges ??= [])
|
||||||
.push(new FoldingRange(extraArgs.name ?? '', extraArgs.detail ?? '', start, end, extraArgs.kind));
|
.push(new FoldingRange(extraArgs.name ?? '', extraArgs.detail ?? '', start, end, extraArgs.kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly document: vscode.TextDocument) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VSCode folding range with name and description.
|
|
||||||
*/
|
|
||||||
export class FoldingRange extends vscode.FoldingRange {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly name: string,
|
|
||||||
readonly detail: string,
|
|
||||||
start: number,
|
|
||||||
end: number,
|
|
||||||
kind?: vscode.FoldingRangeKind,
|
|
||||||
) {
|
|
||||||
super(start, end, kind);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* preview.ts
|
* context/preview.ts
|
||||||
*
|
*
|
||||||
* @author CismonX <admin@cismon.net>
|
* @author CismonX <admin@cismon.net>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
|
@ -7,34 +7,34 @@
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import Converter from './converter';
|
import DocumentContext from './document';
|
||||||
import Diagnosis from './diagnosis';
|
import ContextMapping from '../context_mapping';
|
||||||
import Document from './document';
|
import Diagnosis from '../diagnosis';
|
||||||
import Logger from './logger';
|
import Logger from '../logger';
|
||||||
import Options from './options';
|
import Options from '../options';
|
||||||
import { prompt, transformHtmlImageUri } from './utils';
|
import Converter from '../utils/converter';
|
||||||
|
import { prompt } from '../utils/misc';
|
||||||
|
import { Operator, Optional } from '../utils/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Texinfo document preview.
|
* Stores information of a Texinfo document preview.
|
||||||
*/
|
*/
|
||||||
export default class Preview {
|
export default class PreviewContext {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create (if not yet created) and show preview for a Texinfo document.
|
* Create (if not yet created) and show preview for a Texinfo document.
|
||||||
*
|
*
|
||||||
* @param editor The editor where the document is being held.
|
* @param editor The editor where the document is being held.
|
||||||
*/
|
*/
|
||||||
static async show(editor: vscode.TextEditor) {
|
static async showPreview(editor: vscode.TextEditor) {
|
||||||
const document = editor.document;
|
const document = editor.document;
|
||||||
const documentContext = Document.get(document);
|
|
||||||
if (documentContext === undefined) return;
|
|
||||||
// Only show preview for saved files, as we're not gonna send document content to `makeinfo` via STDIN.
|
// 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.
|
// Instead, the file will be loaded from disk.
|
||||||
if (document.isUntitled) {
|
if (document.isUntitled) {
|
||||||
if (!await prompt('Save this document to display preview.', 'Save')) return;
|
if (!await prompt('Save this document to display preview.', 'Save')) return;
|
||||||
if (!await document.save()) return;
|
if (!await document.save()) return;
|
||||||
}
|
}
|
||||||
documentContext.initPreview().panel.reveal();
|
ContextMapping.getDocumentContext(document).initPreview().panel.reveal();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly document = this.documentContext.document;
|
private readonly document = this.documentContext.document;
|
||||||
|
@ -53,26 +53,12 @@ export default class Preview {
|
||||||
*/
|
*/
|
||||||
private pendingUpdate = false;
|
private pendingUpdate = false;
|
||||||
|
|
||||||
constructor(private readonly documentContext: Document) {
|
|
||||||
this.panel = vscode.window.createWebviewPanel('texinfo.preview', '', vscode.ViewColumn.Beside,
|
|
||||||
{ enableFindWidget: true, retainContextWhenHidden: true });
|
|
||||||
this.disposables.push(this.panel.onDidDispose(() => this.close()));
|
|
||||||
this.updateTitle();
|
|
||||||
this.updateWebview();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateTitle() {
|
|
||||||
const updating = this.updating ? '(Updating) ' : '';
|
|
||||||
const fileName = path.basename(this.document.fileName);
|
|
||||||
this.panel.title = `${updating}Preview ${fileName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.instance.delete(this.document);
|
Diagnosis.delete(this.document);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWebview() {
|
async updateWebview() {
|
||||||
|
@ -84,21 +70,16 @@ export default class Preview {
|
||||||
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 Converter.convertToHtml(this.document.fileName);
|
const { data, error } = await new Converter(this.document.fileName, this.imageTransformer).convert();
|
||||||
if (error) {
|
if (error) {
|
||||||
Logger.instance.log(error);
|
Logger.log(error);
|
||||||
Diagnosis.instance.update(this.document, error);
|
Diagnosis.update(this.document, error);
|
||||||
|
} else {
|
||||||
|
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.instance.show());
|
.then(result => result && Logger.show());
|
||||||
} else if (Options.displayImage) {
|
|
||||||
const pathName = path.dirname(this.document.fileName);
|
|
||||||
// To display images in webviews, image URIs in HTML should be converted to VSCode-recognizable ones.
|
|
||||||
this.panel.webview.html = transformHtmlImageUri(data, src => {
|
|
||||||
const srcUri = vscode.Uri.file(pathName + '/' + src);
|
|
||||||
return this.panel.webview.asWebviewUri(srcUri).toString();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.panel.webview.html = data;
|
this.panel.webview.html = data;
|
||||||
}
|
}
|
||||||
|
@ -106,4 +87,30 @@ export default class Preview {
|
||||||
this.updateTitle();
|
this.updateTitle();
|
||||||
this.pendingUpdate && this.updateWebview();
|
this.pendingUpdate && this.updateWebview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(private readonly documentContext: DocumentContext) {
|
||||||
|
this.panel = vscode.window.createWebviewPanel('texinfo.preview', '', vscode.ViewColumn.Beside,
|
||||||
|
{ enableFindWidget: true, retainContextWhenHidden: true, enableScripts: true });
|
||||||
|
this.disposables.push(this.panel.onDidDispose(() => this.close()));
|
||||||
|
this.updateTitle();
|
||||||
|
this.updateWebview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private get imageTransformer(): Optional<Operator<string>> {
|
||||||
|
if (!Options.displayImage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const pathName = path.dirname(this.document.fileName);
|
||||||
|
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 updateTitle() {
|
||||||
|
const updating = this.updating ? '(Updating) ' : '';
|
||||||
|
const fileName = path.basename(this.document.fileName);
|
||||||
|
this.panel.title = `${updating}Preview ${fileName}`;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* document.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import DocumentContext from './context/document';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage mappings between Texinfo documents and corresponding contexts.
|
||||||
|
*/
|
||||||
|
export default class ContextMapping implements vscode.Disposable {
|
||||||
|
|
||||||
|
private static singleton?: ContextMapping;
|
||||||
|
|
||||||
|
static get instance() {
|
||||||
|
return ContextMapping.singleton ??= new ContextMapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDocumentContext(document: vscode.TextDocument) {
|
||||||
|
let documentContext = ContextMapping.instance.value.get(document);
|
||||||
|
if (documentContext === undefined) {
|
||||||
|
ContextMapping.instance.value.set(document, documentContext = new DocumentContext(document));
|
||||||
|
}
|
||||||
|
return documentContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
static onDocumentUpdate(event: vscode.TextDocumentChangeEvent) {
|
||||||
|
const documentContext = ContextMapping.getDocumentContextIfExist(event.document);
|
||||||
|
if (documentContext?.foldingRange.update(event.contentChanges)) {
|
||||||
|
documentContext.documentSymbol.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static onDocumentSave(document: vscode.TextDocument) {
|
||||||
|
const documentContext = ContextMapping.getDocumentContextIfExist(document);
|
||||||
|
documentContext?.getPreview()?.updateWebview();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* converter.ts
|
|
||||||
*
|
|
||||||
* @author CismonX <admin@cismon.net>
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Options from './options';
|
|
||||||
import { exec } from './utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Texinfo to HTML converter.
|
|
||||||
*/
|
|
||||||
export default class Converter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Texinfo document to HTML.
|
|
||||||
*
|
|
||||||
* @param path Path to the Texinfo document.
|
|
||||||
*/
|
|
||||||
static async convertToHtml(path: string) {
|
|
||||||
return await new Converter().convert(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The options to be passed to the `makeinfo` command.
|
|
||||||
*/
|
|
||||||
private readonly options = ['-o', '-', '--no-split', '--html'];
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
Options.noHeaders && this.options.push('--no-headers');
|
|
||||||
Options.force && this.options.push('--force');
|
|
||||||
Options.noValidation && this.options.push('--no-validate');
|
|
||||||
Options.noWarnings && this.options.push('--no-warn');
|
|
||||||
this.options.push(`--error-limit=${Options.errorLimit}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async convert(path: string) {
|
|
||||||
const maxBuffer = Options.maxSize * 1024 * 1024;
|
|
||||||
return await exec(Options.makeinfo, this.options.concat(path), maxBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,7 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { isDefined, lineNumToRange } from './utils';
|
import { lineNumToRange } from './utils/misc';
|
||||||
|
import { isDefined } from './utils/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage diagnostic information of Texinfo documents.
|
* Manage diagnostic information of Texinfo documents.
|
||||||
|
@ -19,27 +20,28 @@ export default class Diagnosis implements vscode.Disposable {
|
||||||
return Diagnosis.singleton ??= new Diagnosis();
|
return Diagnosis.singleton ??= new Diagnosis();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly diagnostics = vscode.languages.createDiagnosticCollection('texinfo');
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.diagnostics.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate diagnostic information based on error log from `makeinfo`.
|
* Generate diagnostic information based on error log from `makeinfo`.
|
||||||
*
|
*
|
||||||
* @param document
|
* @param document
|
||||||
* @param logText
|
* @param logText
|
||||||
*/
|
*/
|
||||||
update(document: vscode.TextDocument, logText: string) {
|
static 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').filter(line => line.startsWith(fileName))
|
||||||
.map(line => logLineToDiagnostic(line.substring(fileName.length + 1))).filter(isDefined);
|
.map(line => logLineToDiagnostic(line.substring(fileName.length + 1))).filter(isDefined);
|
||||||
this.diagnostics.set(document.uri, diagnostics);
|
Diagnosis.instance.diagnostics.set(document.uri, diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(document: vscode.TextDocument) {
|
static delete(document: vscode.TextDocument) {
|
||||||
this.diagnostics.delete(document.uri);
|
Diagnosis.instance.diagnostics.delete(document.uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly diagnostics = vscode.languages.createDiagnosticCollection('texinfo');
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.diagnostics.dispose();
|
||||||
|
Diagnosis.singleton = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
/**
|
|
||||||
* document.ts
|
|
||||||
*
|
|
||||||
* @author CismonX <admin@cismon.net>
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { FoldingRangeContext } from './folding';
|
|
||||||
import Preview from './preview';
|
|
||||||
import { DocumentSymbolContext } from './symbol';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages context and events for a document.
|
|
||||||
*/
|
|
||||||
export default class Document {
|
|
||||||
|
|
||||||
private static readonly map = new Map<vscode.TextDocument, Document>();
|
|
||||||
|
|
||||||
static of(document: vscode.TextDocument) {
|
|
||||||
let documentContext = Document.map.get(document);
|
|
||||||
if (documentContext === undefined) {
|
|
||||||
Document.map.set(document, documentContext = new Document(document));
|
|
||||||
}
|
|
||||||
return documentContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get(document: vscode.TextDocument) {
|
|
||||||
return document.languageId === 'texinfo' ? Document.of(document) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
static update(event: vscode.TextDocumentChangeEvent) {
|
|
||||||
const documentContext = Document.get(event.document);
|
|
||||||
if (documentContext?.foldingRange.update(event.contentChanges)) {
|
|
||||||
documentContext.symbol.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static save(document: vscode.TextDocument) {
|
|
||||||
const documentContext = Document.get(document);
|
|
||||||
documentContext?.preview?.updateWebview();
|
|
||||||
}
|
|
||||||
|
|
||||||
static close(document: vscode.TextDocument) {
|
|
||||||
Document.map.get(document)?.preview?.close();
|
|
||||||
Document.map.delete(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
static clear() {
|
|
||||||
Document.map.forEach(document => document.preview?.close());
|
|
||||||
Document.map.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly foldingRange = new FoldingRangeContext(this.document);
|
|
||||||
|
|
||||||
readonly symbol = new DocumentSymbolContext(this);
|
|
||||||
|
|
||||||
private preview?: Preview;
|
|
||||||
|
|
||||||
initPreview() {
|
|
||||||
return this.preview ??= new Preview(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
closePreview() {
|
|
||||||
this.preview = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(readonly document: vscode.TextDocument) {}
|
|
||||||
}
|
|
|
@ -1,37 +1,30 @@
|
||||||
/**
|
/**
|
||||||
* extension.ts - extension entry
|
* extension.ts
|
||||||
*
|
*
|
||||||
* @author CismonX <admin@cismon.net>
|
* @author CismonX <admin@cismon.net>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import ContextMapping from './context_mapping';
|
||||||
import Diagnosis from './diagnosis';
|
import Diagnosis from './diagnosis';
|
||||||
import Document from './document';
|
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import Options from './options';
|
import Options from './options';
|
||||||
import Preview from './preview';
|
import PreviewContext from './context/preview';
|
||||||
import { CompletionItemProvider } from './completion';
|
import CompletionItemProvider from './providers/completion_item';
|
||||||
import { FoldingRangeProvider } from './folding';
|
import DocumentSymbolProvider from './providers/document_symbol';
|
||||||
import { DocumentSymbolProvider } from './symbol';
|
import FoldingRangeProvider from './providers/folding_range';
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.workspace.onDidOpenTextDocument(Document.of),
|
ContextMapping.instance, Diagnosis.instance, Logger.instance, Options.instance,
|
||||||
vscode.workspace.onDidChangeTextDocument(Document.update),
|
vscode.workspace.onDidChangeTextDocument(ContextMapping.onDocumentUpdate),
|
||||||
vscode.workspace.onDidSaveTextDocument(Document.save),
|
vscode.workspace.onDidSaveTextDocument(ContextMapping.onDocumentSave),
|
||||||
vscode.workspace.onDidCloseTextDocument(Document.close),
|
vscode.workspace.onDidCloseTextDocument(ContextMapping.onDocumentClose),
|
||||||
vscode.workspace.onDidChangeConfiguration(Options.clear),
|
vscode.workspace.onDidChangeConfiguration(Options.clear),
|
||||||
vscode.commands.registerTextEditorCommand('texinfo.showPreview', Preview.show),
|
vscode.commands.registerTextEditorCommand('texinfo.showPreview', PreviewContext.showPreview),
|
||||||
vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'),
|
vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'),
|
||||||
vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()),
|
|
||||||
vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()),
|
vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()),
|
||||||
Diagnosis.instance,
|
vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {
|
|
||||||
Document.clear();
|
|
||||||
Logger.destroy();
|
|
||||||
Options.clear();
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as vscode from 'vscode';
|
||||||
/**
|
/**
|
||||||
* Logger which prints message to VSCode output channel.
|
* Logger which prints message to VSCode output channel.
|
||||||
*/
|
*/
|
||||||
export default class Logger {
|
export default class Logger implements vscode.Disposable {
|
||||||
|
|
||||||
private static singleton?: Logger;
|
private static singleton?: Logger;
|
||||||
|
|
||||||
|
@ -18,23 +18,23 @@ export default class Logger {
|
||||||
return Logger.singleton ??= new Logger();
|
return Logger.singleton ??= new Logger();
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroy() {
|
static log(message: string) {
|
||||||
Logger.instance.outputChannel.dispose();
|
const dateTime = new Date().toLocaleString(undefined, { hour12: false });
|
||||||
Logger.singleton = undefined;
|
Logger.instance.outputChannel.appendLine(`[ ${dateTime} ]\n${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static show() {
|
||||||
|
Logger.instance.outputChannel.show(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private outputChannel: vscode.OutputChannel;
|
private outputChannel: vscode.OutputChannel;
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
Logger.instance.outputChannel.dispose();
|
||||||
|
Logger.singleton = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.outputChannel = vscode.window.createOutputChannel('Texinfo');
|
this.outputChannel = vscode.window.createOutputChannel('Texinfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
log(message: string) {
|
|
||||||
const dateTime = new Date().toLocaleString(undefined, { hour12: false });
|
|
||||||
this.outputChannel.appendLine(`[ ${dateTime} ]\n${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
|
||||||
this.outputChannel.show(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,18 +12,14 @@ import * as vscode from 'vscode';
|
||||||
*
|
*
|
||||||
* See `contributes.configuration` of package.json for details.
|
* See `contributes.configuration` of package.json for details.
|
||||||
*/
|
*/
|
||||||
export default class Options {
|
export default class Options implements vscode.Disposable {
|
||||||
|
|
||||||
private static singleton?: Options;
|
private static singleton?: Options;
|
||||||
|
|
||||||
private static get instance() {
|
static get instance() {
|
||||||
return Options.singleton ??= new Options('texinfo');
|
return Options.singleton ??= new Options('texinfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
static clear() {
|
|
||||||
Options.singleton = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get makeinfo() {
|
static get makeinfo() {
|
||||||
return Options.instance.getString('makeinfo');
|
return Options.instance.getString('makeinfo');
|
||||||
}
|
}
|
||||||
|
@ -33,7 +29,7 @@ export default class Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
static get maxSize() {
|
static get maxSize() {
|
||||||
return Options.instance.getNumber('preview.maxSize');
|
return Options.instance.getNumber('preview.maxSize') * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get errorLimit() {
|
static get errorLimit() {
|
||||||
|
@ -56,6 +52,10 @@ export default class Options {
|
||||||
return Options.instance.getBoolean('preview.displayImage');
|
return Options.instance.getBoolean('preview.displayImage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static clear() {
|
||||||
|
Options.singleton = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly configuration: vscode.WorkspaceConfiguration;
|
private readonly configuration: vscode.WorkspaceConfiguration;
|
||||||
|
|
||||||
private constructor(section: string) {
|
private constructor(section: string) {
|
||||||
|
@ -73,4 +73,8 @@ export default class Options {
|
||||||
private getNumber(section: string) {
|
private getNumber(section: string) {
|
||||||
return this.configuration.get(section, 0);
|
return this.configuration.get(section, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
Options.singleton = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* completion.ts
|
* providers/completion_item.ts
|
||||||
*
|
*
|
||||||
* @author CismonX <admin@cismon.net>
|
* @author CismonX <admin@cismon.net>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
|
@ -10,7 +10,7 @@ import * as vscode from 'vscode';
|
||||||
/**
|
/**
|
||||||
* Provide code completion info for Texinfo documents.
|
* Provide code completion info for Texinfo documents.
|
||||||
*/
|
*/
|
||||||
export class CompletionItemProvider implements vscode.CompletionItemProvider {
|
export default class CompletionItemProvider implements vscode.CompletionItemProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full list of completion items.
|
* Full list of completion items.
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* providers/document_symbol.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import ContextMapping from '../context_mapping';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide document symbol information for Texinfo documents.
|
||||||
|
*/
|
||||||
|
export default class DocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||||
|
|
||||||
|
provideDocumentSymbols(document: vscode.TextDocument) {
|
||||||
|
return ContextMapping.getDocumentContext(document).documentSymbol.values;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* providers/folding_range.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import ContextMapping from '../context_mapping';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide folding range info for Texinfo documents.
|
||||||
|
*/
|
||||||
|
export default class FoldingRangeProvider implements vscode.FoldingRangeProvider {
|
||||||
|
|
||||||
|
provideFoldingRanges(document: vscode.TextDocument) {
|
||||||
|
return ContextMapping.getDocumentContext(document).foldingRange.values;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* converter.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Options from '../options';
|
||||||
|
import DOM from './dom';
|
||||||
|
import { exec } from './misc';
|
||||||
|
import { Operator } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Texinfo to HTML converter.
|
||||||
|
*/
|
||||||
|
export default class Converter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options to be passed to the `makeinfo` command.
|
||||||
|
*/
|
||||||
|
private readonly options = ['-o', '-', '--no-split', '--html'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly path: string,
|
||||||
|
private readonly imgTransformer?: Operator<string>,
|
||||||
|
private readonly insertScript?: string,
|
||||||
|
) {
|
||||||
|
Options.noHeaders && this.options.push('--no-headers');
|
||||||
|
Options.force && this.options.push('--force');
|
||||||
|
Options.noValidation && this.options.push('--no-validate');
|
||||||
|
Options.noWarnings && this.options.push('--no-warn');
|
||||||
|
this.options.push(`--error-limit=${Options.errorLimit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert() {
|
||||||
|
const result = await exec(Options.makeinfo, this.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);
|
||||||
|
this.imgTransformer && dom.transformImageUri(this.imgTransformer);
|
||||||
|
this.insertScript && dom.insertScript(this.insertScript);
|
||||||
|
result.data = dom.outerHTML;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* utils/dom.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as htmlparser from 'node-html-parser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM manipulation utilities.
|
||||||
|
*/
|
||||||
|
export default class DOM {
|
||||||
|
|
||||||
|
private dom?: htmlparser.HTMLElement;
|
||||||
|
|
||||||
|
private changed = false;
|
||||||
|
|
||||||
|
private get value() {
|
||||||
|
return this.dom ??= htmlparser.parse(this.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
get outerHTML() {
|
||||||
|
if (this.changed) {
|
||||||
|
this.html = this.value.outerHTML;
|
||||||
|
this.changed = false;
|
||||||
|
}
|
||||||
|
return this.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform and replace the `src` attribute value of all `img` elements from HTML using given function.
|
||||||
|
*
|
||||||
|
* @param transformer
|
||||||
|
*/
|
||||||
|
transformImageUri(transformer: (src: string) => string) {
|
||||||
|
const elements = this.value.querySelectorAll('img');
|
||||||
|
if (elements.length === 0) return;
|
||||||
|
elements.forEach(element => {
|
||||||
|
const src = element.getAttribute('src');
|
||||||
|
src && element.setAttribute('src', transformer(src));
|
||||||
|
});
|
||||||
|
this.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertScript(script: string) {
|
||||||
|
this.value.querySelector('head').insertAdjacentHTML('beforeend', `<script>${script}</script>`);
|
||||||
|
this.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private html: string) {}
|
||||||
|
}
|
|
@ -1,26 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* utils.ts
|
* utils/misc.ts
|
||||||
*
|
*
|
||||||
* @author CismonX <admin@cismon.net>
|
* @author CismonX <admin@cismon.net>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import * as htmlparser from 'node-html-parser';
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { ExecResult } from './types';
|
||||||
/**
|
|
||||||
* Open a prompt with two buttons, "Confirm" and "Cancel", and wait for user action.
|
|
||||||
*
|
|
||||||
* @param message The message to be displayed on the prompt.
|
|
||||||
* @param confirm Text to be displayed on the "Confirm" button.
|
|
||||||
* @param error Whether the prompt is shown as an error message. Default false.
|
|
||||||
* @returns Whether the user clicked the "Confirm" button.
|
|
||||||
*/
|
|
||||||
export async function prompt(message: string, confirm: string, error = false) {
|
|
||||||
const func = error ? vscode.window.showErrorMessage : vscode.window.showInformationMessage;
|
|
||||||
return confirm === await func(message, confirm, 'Cancel');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute command and fetch output.
|
* Execute command and fetch output.
|
||||||
|
@ -43,21 +30,16 @@ export function exec(path: string, args: string[], maxBuffer: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform and replace the `src` attribute value of all `img` elements from given HTML code using given function.
|
* Open a prompt with two buttons, "Confirm" and "Cancel", and wait for user action.
|
||||||
*
|
*
|
||||||
* @param htmlCode
|
* @param message The message to be displayed on the prompt.
|
||||||
* @param transformer
|
* @param confirm Text to be displayed on the "Confirm" button.
|
||||||
* @returns The HTML code after transformation.
|
* @param error Whether the prompt is shown as an error message. Default false.
|
||||||
|
* @returns Whether the user clicked the "Confirm" button.
|
||||||
*/
|
*/
|
||||||
export function transformHtmlImageUri(htmlCode: string, transformer: (src: string) => string) {
|
export async function prompt(message: string, confirm: string, error = false) {
|
||||||
const dom = htmlparser.parse(htmlCode);
|
const func = error ? vscode.window.showErrorMessage : vscode.window.showInformationMessage;
|
||||||
const elements = dom.querySelectorAll('img');
|
return confirm === await func(message, confirm, 'Cancel');
|
||||||
elements.forEach(element => {
|
|
||||||
const src = element.getAttribute('src');
|
|
||||||
src && element.setAttribute('src', transformer(src));
|
|
||||||
});
|
|
||||||
// If nothing is transformed, return the original HTML code, for better performance.
|
|
||||||
return elements.length === 0 ? htmlCode : dom.outerHTML;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,13 +53,3 @@ export function lineNumToRange(startLine: number, endLine = startLine) {
|
||||||
const endPosition = new vscode.Position(endLine, Number.MAX_SAFE_INTEGER);
|
const endPosition = new vscode.Position(endLine, Number.MAX_SAFE_INTEGER);
|
||||||
return new vscode.Range(startPosition, endPosition);
|
return new vscode.Range(startPosition, endPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDefined<T>(value: T | undefined): value is T {
|
|
||||||
return value !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Optional<T> = T | undefined;
|
|
||||||
|
|
||||||
export type ExecResult = { data?: string, error: string };
|
|
||||||
|
|
||||||
export type Range = { start: number, end: number };
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* utils/types.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export type Optional<T> = T | undefined;
|
||||||
|
|
||||||
|
export type Operator<T> = (arg: T) => T;
|
||||||
|
|
||||||
|
export type Range = { start: number, end: number };
|
||||||
|
|
||||||
|
export type ExecResult = { data?: string, error: string };
|
||||||
|
|
||||||
|
export function isDefined<T>(value: Optional<T>): value is T {
|
||||||
|
return value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VSCode folding range with name and description.
|
||||||
|
*/
|
||||||
|
export class FoldingRange extends vscode.FoldingRange {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly name: string,
|
||||||
|
readonly detail: string,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
kind?: vscode.FoldingRangeKind,
|
||||||
|
) {
|
||||||
|
super(start, end, kind);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
"lib": [
|
"lib": [
|
||||||
"ES2019"
|
"ES2019"
|
||||||
],
|
],
|
||||||
|
"strictNullChecks": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
Loading…
Reference in New Issue