diff --git a/src/diagnosis.ts b/src/diagnosis.ts new file mode 100644 index 0000000..0da80e2 --- /dev/null +++ b/src/diagnosis.ts @@ -0,0 +1,53 @@ +/** + * diagnosis.ts + * + * @author CismonX + * @license MIT + */ + +import * as vscode from 'vscode'; +import { isDefined, lineNumToRange } from './utils'; + +/** + * Manage diagnostic information of Texinfo documents. + */ +export default class Diagnosis implements vscode.Disposable { + + private static singleton?: Diagnosis; + + static get instance() { + 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) { + 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); + } + + delete(document: vscode.TextDocument) { + this.diagnostics.delete(document.uri); + } +} + +function logLineToDiagnostic(lineText: string) { + const lineNum = Number.parseInt(lineText) - 1; + // Ignore error that does not correspond a line. + if (Number.isNaN(lineNum)) return undefined; + const message = lineText.substring(lineNum.toString().length + 2); + const severity = message.startsWith('warning:') ? vscode.DiagnosticSeverity.Warning : undefined; + return new vscode.Diagnostic(lineNumToRange(lineNum), message, severity); +} diff --git a/src/extension.ts b/src/extension.ts index e390343..bbe8d8b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ */ import * as vscode from 'vscode'; +import Diagnosis from './diagnosis'; import Document from './document'; import Logger from './logger'; import Options from './options'; @@ -25,6 +26,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'), vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()), vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()), + Diagnosis.instance, ); } diff --git a/src/preview.ts b/src/preview.ts index 82f38ed..695d8c0 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -8,6 +8,7 @@ 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'; @@ -70,6 +71,8 @@ export default class Preview { 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); } async updateWebview() { @@ -81,20 +84,23 @@ 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); - let htmlCode = await Converter.convertToHtml(this.document.fileName); - if (htmlCode === undefined) { + const { data, error } = await Converter.convertToHtml(this.document.fileName); + if (error) { + Logger.instance.log(error); + Diagnosis.instance.update(this.document, error); + } + if (data === undefined) { prompt(`Failed to show preview for ${this.document.fileName}.`, 'Show log', true) - .then(result => result && Logger.show()); + .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(); + }); } 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. - htmlCode = transformHtmlImageUri(htmlCode, src => { - const srcUri = vscode.Uri.file(pathName + '/' + src); - return this.panel.webview.asWebviewUri(srcUri).toString(); - }); - } - this.panel.webview.html = htmlCode; + this.panel.webview.html = data; } this.updating = false; this.updateTitle(); diff --git a/src/symbol.ts b/src/symbol.ts index aa6e124..7bcada4 100644 --- a/src/symbol.ts +++ b/src/symbol.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import Document from './document'; import { FoldingRange } from './folding'; -import { Optional } from './utils'; +import { lineNumToRange, Optional } from './utils'; /** * Provide document symbol information for Texinfo documents. @@ -60,11 +60,8 @@ function foldingRangeToSymbols(ranges: readonly RangeNode[], start: number, end: for (let idx = start; idx < end; ++idx) { const node = ranges[idx]; if (node === undefined) continue; - const startPosition = new vscode.Position(idx, 0); - const endFirstLine = new vscode.Position(idx, Number.MAX_SAFE_INTEGER); - const endLastLine = new vscode.Position(node.end, Number.MAX_SAFE_INTEGER); - const range = new vscode.Range(startPosition, endLastLine); - const selectionRange = new vscode.Range(startPosition, endFirstLine); + const range = lineNumToRange(idx, node.end); + const selectionRange = lineNumToRange(idx); const symbol = new vscode.DocumentSymbol('@' + node.name, node.detail, vscode.SymbolKind.String, range, selectionRange); symbol.children = foldingRangeToSymbols(ranges, idx + 1, node.end); diff --git a/src/utils.ts b/src/utils.ts index 60d0c1d..647898e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -60,6 +60,22 @@ export function transformHtmlImageUri(htmlCode: string, transformer: (src: strin return elements.length === 0 ? htmlCode : dom.outerHTML; } +/** + * Convert line numbers to VSCode range. + * + * @param startLine + * @param endLine Default to `startLine`. + */ +export function lineNumToRange(startLine: number, endLine = startLine) { + const startPosition = new vscode.Position(startLine, 0); + 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 };