From 3b34510feb650c59899af294d4a467644cbdaa78 Mon Sep 17 00:00:00 2001 From: CismonX Date: Sun, 25 Oct 2020 05:45:32 +0800 Subject: [PATCH] Major refactor. --- src/context/document.ts | 34 ++++++++ src/{symbol.ts => context/document_symbol.ts} | 24 ++---- src/{folding.ts => context/folding_range.ts} | 76 ++++++----------- src/{ => context}/preview.ts | 85 ++++++++++--------- src/context_mapping.ts | 57 +++++++++++++ src/converter.ts | 42 --------- src/diagnosis.ts | 24 +++--- src/document.ts | 69 --------------- src/extension.ts | 31 +++---- src/logger.ts | 26 +++--- src/options.ts | 18 ++-- .../completion_item.ts} | 4 +- src/providers/document_symbol.ts | 19 +++++ src/providers/folding_range.ts | 19 +++++ src/utils/converter.ts | 46 ++++++++++ src/utils/dom.ts | 52 ++++++++++++ src/{utils.ts => utils/misc.ts} | 48 +++-------- src/utils/types.ts | 36 ++++++++ tsconfig.json | 1 + 19 files changed, 403 insertions(+), 308 deletions(-) create mode 100644 src/context/document.ts rename src/{symbol.ts => context/document_symbol.ts} (76%) rename src/{folding.ts => context/folding_range.ts} (76%) rename src/{ => context}/preview.ts (65%) create mode 100644 src/context_mapping.ts delete mode 100644 src/converter.ts delete mode 100644 src/document.ts rename src/{completion.ts => providers/completion_item.ts} (99%) create mode 100644 src/providers/document_symbol.ts create mode 100644 src/providers/folding_range.ts create mode 100644 src/utils/converter.ts create mode 100644 src/utils/dom.ts rename src/{utils.ts => utils/misc.ts} (63%) create mode 100644 src/utils/types.ts diff --git a/src/context/document.ts b/src/context/document.ts new file mode 100644 index 0000000..6da727d --- /dev/null +++ b/src/context/document.ts @@ -0,0 +1,34 @@ +/** + * context/document.ts + * + * @author CismonX + * @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) {} +} diff --git a/src/symbol.ts b/src/context/document_symbol.ts similarity index 76% rename from src/symbol.ts rename to src/context/document_symbol.ts index 7bcada4..f0b3333 100644 --- a/src/symbol.ts +++ b/src/context/document_symbol.ts @@ -1,29 +1,19 @@ /** - * symbol.ts + * context/document_symbol.ts * * @author CismonX * @license MIT */ import * as vscode from 'vscode'; -import Document from './document'; -import { FoldingRange } from './folding'; -import { lineNumToRange, Optional } from './utils'; - -/** - * Provide document symbol information for Texinfo documents. - */ -export class DocumentSymbolProvider implements vscode.DocumentSymbolProvider { - - provideDocumentSymbols(document: vscode.TextDocument) { - return Document.of(document).symbol.values; - } -} +import DocumentContext from './document'; +import { lineNumToRange } from '../utils/misc'; +import { FoldingRange, Optional } from '../utils/types'; /** * Context for symbols in a Texinfo document. */ -export class DocumentSymbolContext { +export default class DocumentSymbolContext { private document = this.documentContext.document; @@ -40,6 +30,8 @@ export class DocumentSymbolContext { this.symbols = undefined; } + constructor(private readonly documentContext: DocumentContext) {} + /** * Calculate document symbols based on folding ranges. */ @@ -49,8 +41,6 @@ export class DocumentSymbolContext { .forEach(range => range.kind ?? (ranges[range.start] = range)); return this.symbols = foldingRangeToSymbols(ranges, 0, ranges.length); } - - constructor(private readonly documentContext: Document) {} } type RangeNode = Optional; diff --git a/src/folding.ts b/src/context/folding_range.ts similarity index 76% rename from src/folding.ts rename to src/context/folding_range.ts index 0baa202..3b7ef5f 100644 --- a/src/folding.ts +++ b/src/context/folding_range.ts @@ -1,28 +1,17 @@ /** - * folding.ts + * context/folding_range.ts * * @author CismonX * @license MIT */ import * as vscode from 'vscode'; -import Document from './document'; -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; - } -} +import { FoldingRange, Range } from '../utils/types'; /** * Stores information about folding ranges for a document. */ -export class FoldingRangeContext { +export default class FoldingRangeContext { /** * Get VSCode folding ranges from the context. @@ -105,37 +94,40 @@ export class FoldingRangeContext { } else { 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; } private processComment(lineText: string, lineNum: number) { - if (lineText.startsWith('@c')) { - if (!lineText.startsWith(' ', 2) && !lineText.startsWith('omment ', 2)) return false; - // Check for opening/closing header. - if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) { - if (this.headerStart === undefined) { - this.headerStart = lineNum; - } else { - this.addRange(lineNum, this.headerStart, { kind: vscode.FoldingRangeKind.Region }); - this.headerStart = undefined; - } - return true; - } - if (this.commentRange === undefined) { - this.commentRange = { start: lineNum, end: lineNum }; - } else if (this.commentRange.start - 1 === lineNum) { - this.commentRange.start = lineNum; + if (!lineText.startsWith('@c')) return false; + if (!lineText.startsWith(' ', 2) && !lineText.startsWith('omment ', 2)) return false; + // Check for opening/closing header. + if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) { + if (this.headerStart === undefined) { + this.headerStart = lineNum; + } else { + this.addRange(lineNum, this.headerStart, { kind: vscode.FoldingRangeKind.Region }); + this.headerStart = undefined; } return true; - } else if (this.commentRange !== undefined) { + } + if (this.commentRange === undefined) { + this.commentRange = { start: lineNum, end: lineNum }; + } else if (this.commentRange.start - 1 === lineNum) { + this.commentRange.start = lineNum; + } else { this.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment }); this.commentRange = undefined; } - return false; + return true; } + constructor(private readonly document: vscode.TextDocument) {} + private processNode(lineText: string, lineNum: number, lastLineNum: number) { if (lineText.startsWith('@subsection ')) { const detail = lineText.substring(12); @@ -173,22 +165,4 @@ export class FoldingRangeContext { (this.foldingRanges ??= []) .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); - } } diff --git a/src/preview.ts b/src/context/preview.ts similarity index 65% rename from src/preview.ts rename to src/context/preview.ts index 695d8c0..2d0fd75 100644 --- a/src/preview.ts +++ b/src/context/preview.ts @@ -1,5 +1,5 @@ /** - * preview.ts + * context/preview.ts * * @author CismonX * @license MIT @@ -7,34 +7,34 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import Converter from './converter'; -import Diagnosis from './diagnosis'; -import Document from './document'; -import Logger from './logger'; -import Options from './options'; -import { prompt, transformHtmlImageUri } from './utils'; +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 { 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. * * @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 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. // 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; } - documentContext.initPreview().panel.reveal(); + ContextMapping.getDocumentContext(document).initPreview().panel.reveal(); } private readonly document = this.documentContext.document; @@ -53,26 +53,12 @@ export default class Preview { */ 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() { this.disposables.forEach(event => event.dispose()); this.panel.dispose(); this.documentContext.closePreview(); // Only show diagnostic information when the preview is active. - Diagnosis.instance.delete(this.document); + Diagnosis.delete(this.document); } async updateWebview() { @@ -84,21 +70,16 @@ export default class Preview { this.pendingUpdate = false; // Inform the user that the preview is updating if `makeinfo` takes too long. 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) { - Logger.instance.log(error); - Diagnosis.instance.update(this.document, error); + Logger.log(error); + Diagnosis.update(this.document, error); + } else { + Diagnosis.delete(this.document); } if (data === undefined) { prompt(`Failed to show preview for ${this.document.fileName}.`, 'Show log', true) - .then(result => result && Logger.instance.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(); - }); + .then(result => result && Logger.show()); } else { this.panel.webview.html = data; } @@ -106,4 +87,30 @@ export default class Preview { this.updateTitle(); 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> { + 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}`; + } } diff --git a/src/context_mapping.ts b/src/context_mapping.ts new file mode 100644 index 0000000..f12c865 --- /dev/null +++ b/src/context_mapping.ts @@ -0,0 +1,57 @@ +/** + * document.ts + * + * @author CismonX + * @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(); + + dispose() { + this.value.forEach(documentContext => documentContext.getPreview()?.close()); + ContextMapping.singleton = undefined; + } +} diff --git a/src/converter.ts b/src/converter.ts deleted file mode 100644 index e35a161..0000000 --- a/src/converter.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * converter.ts - * - * @author CismonX - * @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); - } -} diff --git a/src/diagnosis.ts b/src/diagnosis.ts index 0da80e2..a4522d2 100644 --- a/src/diagnosis.ts +++ b/src/diagnosis.ts @@ -6,7 +6,8 @@ */ 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. @@ -19,27 +20,28 @@ export default class Diagnosis implements vscode.Disposable { 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`. * * @param document * @param logText */ - update(document: vscode.TextDocument, logText: string) { + static update(document: vscode.TextDocument, logText: string) { const fileName = document.uri.path; const diagnostics = logText.split('\n').filter(line => line.startsWith(fileName)) .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) { - this.diagnostics.delete(document.uri); + static delete(document: vscode.TextDocument) { + Diagnosis.instance.diagnostics.delete(document.uri); + } + + private readonly diagnostics = vscode.languages.createDiagnosticCollection('texinfo'); + + dispose() { + this.diagnostics.dispose(); + Diagnosis.singleton = undefined; } } diff --git a/src/document.ts b/src/document.ts deleted file mode 100644 index 786ed76..0000000 --- a/src/document.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * document.ts - * - * @author CismonX - * @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(); - - 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) {} -} diff --git a/src/extension.ts b/src/extension.ts index bbe8d8b..fe1b9c1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,37 +1,30 @@ /** - * extension.ts - extension entry + * extension.ts * * @author CismonX * @license MIT */ import * as vscode from 'vscode'; +import ContextMapping from './context_mapping'; import Diagnosis from './diagnosis'; -import Document from './document'; import Logger from './logger'; import Options from './options'; -import Preview from './preview'; -import { CompletionItemProvider } from './completion'; -import { FoldingRangeProvider } from './folding'; -import { DocumentSymbolProvider } from './symbol'; +import PreviewContext from './context/preview'; +import CompletionItemProvider from './providers/completion_item'; +import DocumentSymbolProvider from './providers/document_symbol'; +import FoldingRangeProvider from './providers/folding_range'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(Document.of), - vscode.workspace.onDidChangeTextDocument(Document.update), - vscode.workspace.onDidSaveTextDocument(Document.save), - vscode.workspace.onDidCloseTextDocument(Document.close), + ContextMapping.instance, Diagnosis.instance, Logger.instance, Options.instance, + vscode.workspace.onDidChangeTextDocument(ContextMapping.onDocumentUpdate), + vscode.workspace.onDidSaveTextDocument(ContextMapping.onDocumentSave), + vscode.workspace.onDidCloseTextDocument(ContextMapping.onDocumentClose), 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.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()), vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()), - Diagnosis.instance, + vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()), ); } - -export function deactivate() { - Document.clear(); - Logger.destroy(); - Options.clear(); -} diff --git a/src/logger.ts b/src/logger.ts index 0364d43..18eaf64 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -10,7 +10,7 @@ import * as vscode from 'vscode'; /** * Logger which prints message to VSCode output channel. */ -export default class Logger { +export default class Logger implements vscode.Disposable { private static singleton?: Logger; @@ -18,23 +18,23 @@ export default class Logger { return Logger.singleton ??= new Logger(); } - static destroy() { - Logger.instance.outputChannel.dispose(); - Logger.singleton = undefined; + static log(message: string) { + const dateTime = new Date().toLocaleString(undefined, { hour12: false }); + Logger.instance.outputChannel.appendLine(`[ ${dateTime} ]\n${message}`); + } + + static show() { + Logger.instance.outputChannel.show(true); } private outputChannel: vscode.OutputChannel; + dispose() { + Logger.instance.outputChannel.dispose(); + Logger.singleton = undefined; + } + private constructor() { 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); - } } diff --git a/src/options.ts b/src/options.ts index 8c7e243..f5b6013 100644 --- a/src/options.ts +++ b/src/options.ts @@ -12,18 +12,14 @@ import * as vscode from 'vscode'; * * 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 get instance() { + static get instance() { return Options.singleton ??= new Options('texinfo'); } - static clear() { - Options.singleton = undefined; - } - static get makeinfo() { return Options.instance.getString('makeinfo'); } @@ -33,7 +29,7 @@ export default class Options { } static get maxSize() { - return Options.instance.getNumber('preview.maxSize'); + return Options.instance.getNumber('preview.maxSize') * 1024 * 1024; } static get errorLimit() { @@ -56,6 +52,10 @@ export default class Options { return Options.instance.getBoolean('preview.displayImage'); } + static clear() { + Options.singleton = undefined; + } + private readonly configuration: vscode.WorkspaceConfiguration; private constructor(section: string) { @@ -73,4 +73,8 @@ export default class Options { private getNumber(section: string) { return this.configuration.get(section, 0); } + + dispose() { + Options.singleton = undefined; + } } diff --git a/src/completion.ts b/src/providers/completion_item.ts similarity index 99% rename from src/completion.ts rename to src/providers/completion_item.ts index 0ab3c99..e9ed810 100644 --- a/src/completion.ts +++ b/src/providers/completion_item.ts @@ -1,5 +1,5 @@ /** - * completion.ts + * providers/completion_item.ts * * @author CismonX * @license MIT @@ -10,7 +10,7 @@ import * as vscode from 'vscode'; /** * 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. diff --git a/src/providers/document_symbol.ts b/src/providers/document_symbol.ts new file mode 100644 index 0000000..b758f99 --- /dev/null +++ b/src/providers/document_symbol.ts @@ -0,0 +1,19 @@ +/** + * providers/document_symbol.ts + * + * @author CismonX + * @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; + } +} diff --git a/src/providers/folding_range.ts b/src/providers/folding_range.ts new file mode 100644 index 0000000..c2b5640 --- /dev/null +++ b/src/providers/folding_range.ts @@ -0,0 +1,19 @@ +/** + * providers/folding_range.ts + * + * @author CismonX + * @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; + } +} diff --git a/src/utils/converter.ts b/src/utils/converter.ts new file mode 100644 index 0000000..207c8fa --- /dev/null +++ b/src/utils/converter.ts @@ -0,0 +1,46 @@ +/** + * converter.ts + * + * @author CismonX + * @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, + 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; + } +} diff --git a/src/utils/dom.ts b/src/utils/dom.ts new file mode 100644 index 0000000..060de7d --- /dev/null +++ b/src/utils/dom.ts @@ -0,0 +1,52 @@ +/** + * utils/dom.ts + * + * @author CismonX + * @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', ``); + this.changed = true; + } + + constructor(private html: string) {} +} diff --git a/src/utils.ts b/src/utils/misc.ts similarity index 63% rename from src/utils.ts rename to src/utils/misc.ts index 647898e..0927723 100644 --- a/src/utils.ts +++ b/src/utils/misc.ts @@ -1,26 +1,13 @@ /** - * utils.ts + * utils/misc.ts * * @author CismonX * @license MIT */ import * as child_process from 'child_process'; -import * as htmlparser from 'node-html-parser'; import * as vscode from 'vscode'; - -/** - * 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'); -} +import { ExecResult } from './types'; /** * 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 transformer - * @returns The HTML code after transformation. + * @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 function transformHtmlImageUri(htmlCode: string, transformer: (src: string) => string) { - const dom = htmlparser.parse(htmlCode); - const elements = dom.querySelectorAll('img'); - 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; +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'); } /** @@ -71,13 +53,3 @@ export function lineNumToRange(startLine: number, endLine = startLine) { const endPosition = new vscode.Position(endLine, Number.MAX_SAFE_INTEGER); return new vscode.Range(startPosition, endPosition); } - -export function isDefined(value: T | undefined): value is T { - return value !== undefined; -} - -export type Optional = T | undefined; - -export type ExecResult = { data?: string, error: string }; - -export type Range = { start: number, end: number }; diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..f89404e --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,36 @@ +/** + * utils/types.ts + * + * @author CismonX + * @license MIT + */ + +import * as vscode from 'vscode'; + +export type Optional = T | undefined; + +export type Operator = (arg: T) => T; + +export type Range = { start: number, end: number }; + +export type ExecResult = { data?: string, error: string }; + +export function isDefined(value: Optional): 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); + } +} diff --git a/tsconfig.json b/tsconfig.json index d4effb6..b0a7a88 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "lib": [ "ES2019" ], + "strictNullChecks": true, "sourceMap": true, "rootDir": "src", "strict": true,