Major refactor.

This commit is contained in:
CismonX 2020-10-25 05:45:32 +08:00
parent 2c5aec48ed
commit 3b34510feb
Signed by: cismonx
GPG Key ID: 3094873E29A482FB
19 changed files with 403 additions and 308 deletions

34
src/context/document.ts Normal file
View File

@ -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) {}
}

View File

@ -1,29 +1,19 @@
/**
* symbol.ts
* context/document_symbol.ts
*
* @author CismonX <admin@cismon.net>
* @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<FoldingRange>;

View File

@ -1,28 +1,17 @@
/**
* folding.ts
* context/folding_range.ts
*
* @author CismonX <admin@cismon.net>
* @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);
}
}

View File

@ -1,5 +1,5 @@
/**
* preview.ts
* context/preview.ts
*
* @author CismonX <admin@cismon.net>
* @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<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}`;
}
}

57
src/context_mapping.ts Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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) {}
}

View File

@ -1,37 +1,30 @@
/**
* extension.ts - extension entry
* extension.ts
*
* @author CismonX <admin@cismon.net>
* @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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -1,5 +1,5 @@
/**
* completion.ts
* providers/completion_item.ts
*
* @author CismonX <admin@cismon.net>
* @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.

View File

@ -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;
}
}

View File

@ -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;
}
}

46
src/utils/converter.ts Normal file
View File

@ -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;
}
}

52
src/utils/dom.ts Normal file
View File

@ -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) {}
}

View File

@ -1,26 +1,13 @@
/**
* utils.ts
* utils/misc.ts
*
* @author CismonX <admin@cismon.net>
* @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<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 };

36
src/utils/types.ts Normal file
View File

@ -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);
}
}

View File

@ -6,6 +6,7 @@
"lib": [
"ES2019"
],
"strictNullChecks": true,
"sourceMap": true,
"rootDir": "src",
"strict": true,