diff --git a/src/completion.ts b/src/completion.ts index 2a40417..0ab3c99 100644 --- a/src/completion.ts +++ b/src/completion.ts @@ -94,21 +94,17 @@ export class CompletionItemProvider implements vscode.CompletionItemProvider { token: vscode.CancellationToken, context: vscode.CompletionContext, ) { + // Triggered in the middle of a word. if (context.triggerKind === vscode.CompletionTriggerKind.Invoke) { const wordRange = document.getWordRangeAtPosition(position); - if (wordRange === undefined) { - return undefined; - } + if (wordRange === undefined) return undefined; + // Normalize position so that it can be treated as triggered by '@' character. position = wordRange.start; - if (document.getText(new vscode.Range(position.translate(0, -1), position)) !== '@') { - return undefined; - } - } - if (position.character === 1) { - return this.completionItems; + if (document.getText(new vscode.Range(position.translate(0, -1), position)) !== '@') return undefined; } + if (position.character === 1) return this.completionItems; + // Check whether the '@' character is escaped. if (document.getText(new vscode.Range(position.translate(0, -2), position.translate(0, -1))) === '@') { - // The '@' character is escaped. return undefined; } else { return this.completionItems; diff --git a/src/document.ts b/src/document.ts new file mode 100644 index 0000000..af36f3a --- /dev/null +++ b/src/document.ts @@ -0,0 +1,64 @@ +/** + * document.ts + * + * @author CismonX + * @license MIT + */ + +import * as vscode from 'vscode'; +import { FoldingRangeContext } from './folding'; +import Preview from './preview'; + +/** + * 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); + documentContext?.foldingRange.update(event.contentChanges); + } + + 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); + + 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 f9cc017..c44e2b5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,20 +6,18 @@ */ import * as vscode from 'vscode'; +import Document from './document'; import Options from './options'; import Preview from './preview'; import { CompletionItemProvider } from './completion'; -import { FoldingRangeProvider, FoldingRangeContext } from './folding'; +import { FoldingRangeProvider } from './folding'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument(Preview.update), - vscode.workspace.onDidCloseTextDocument((document) => { - Preview.close(document); - FoldingRangeContext.close(document); - }), - vscode.workspace.onDidOpenTextDocument(FoldingRangeContext.open), - vscode.workspace.onDidChangeTextDocument(FoldingRangeContext.update), + vscode.workspace.onDidOpenTextDocument(Document.of), + vscode.workspace.onDidChangeTextDocument(Document.update), + vscode.workspace.onDidSaveTextDocument(Document.save), + vscode.workspace.onDidCloseTextDocument(Document.close), vscode.commands.registerTextEditorCommand('texinfo.showPreview', Preview.show), vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'), vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()), @@ -27,7 +25,6 @@ export function activate(context: vscode.ExtensionContext) { } export function deactivate() { - Preview.clear(); + Document.clear(); Options.clear(); - FoldingRangeContext.clear(); } diff --git a/src/folding.ts b/src/folding.ts index e01eac7..2cf6d6e 100644 --- a/src/folding.ts +++ b/src/folding.ts @@ -6,6 +6,7 @@ */ import * as vscode from 'vscode'; +import Document from './document'; /** * Provide folding range info for Texinfo documents. @@ -13,7 +14,7 @@ import * as vscode from 'vscode'; export class FoldingRangeProvider implements vscode.FoldingRangeProvider { provideFoldingRanges(document: vscode.TextDocument) { - return FoldingRangeContext.get(document).values; + return Document.of(document).foldingRange.values; } } @@ -22,53 +23,6 @@ export class FoldingRangeProvider implements vscode.FoldingRangeProvider { */ export class FoldingRangeContext { - private static readonly map = new Map(); - - /** - * Initialize folding range context for a document. - * - * @param document - */ - static open(document: vscode.TextDocument) { - document.languageId === 'texinfo' && FoldingRangeContext.get(document); - } - - /** - * Get existing folding range context of a document, or create one if not exist. - * - * @param document - */ - static get(document: vscode.TextDocument) { - return FoldingRangeContext.map.get(document) ?? new FoldingRangeContext(document); - } - - /** - * Update the folding range context of a document based on its change event. - * - * @param event Change event of a document. - */ - static update(event: vscode.TextDocumentChangeEvent) { - if (event.document.languageId === 'texinfo') { - FoldingRangeContext.get(event.document).update(event.contentChanges); - } - } - - /** - * Destroy the folding range context of a document. - * - * @param document - */ - static close(document: vscode.TextDocument) { - FoldingRangeContext.map.delete(document); - } - - /** - * Destroy all existing folding range contexts. - */ - static clear() { - FoldingRangeContext.map.clear(); - } - /** * Get VSCode folding ranges from the context. */ @@ -76,14 +30,28 @@ export class FoldingRangeContext { return this.foldingRanges ?? (this.foldingRanges = this.calculateFoldingRanges()); } - private foldingRanges?: vscode.FoldingRange[]; + private foldingRanges?: FoldingRange[]; private commentRange?: { start: number, end: number }; private headerStart?: number; - private constructor(private readonly document: vscode.TextDocument) { - FoldingRangeContext.map.set(document, this); + /** + * Update folding range context based on document change event. + * + * @param events Events describing the changes in the document. + */ + update(events: readonly vscode.TextDocumentContentChangeEvent[]) { + if (this.foldingRanges === undefined) return false; + for (const event of events) { + const updatedLines = event.text.split(this.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n').length; + // Clear cached folding range when line count changes. + if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) { + this.foldingRanges = undefined; + return true; + } + } + return false; } /** @@ -93,41 +61,34 @@ export class FoldingRangeContext { * @param end Ending line number. */ private calculateFoldingRanges() { + this.foldingRanges = []; this.headerStart = undefined; const closingBlocks = <{ name: string, line: number }[]>[]; let verbatim = false; for (let idx = this.document.lineCount - 1; idx >= 0; --idx) { const line = this.document.lineAt(idx).text; - if (!line.startsWith('@')) { - continue; - } + if (!line.startsWith('@')) continue; if (!verbatim) { if (line === '@bye') { // Abort anything after `@bye`. - this.foldingRanges = undefined; + this.foldingRanges = []; this.commentRange = undefined; this.headerStart = undefined; continue; } - if (this.processComment(line, idx)) { - continue; - } + if (this.processComment(line, idx)) continue; } // Process block. if (line.startsWith('@end ')) { - if (verbatim) { - continue; - } + if (verbatim) continue; const name = line.substring(5); name === 'verbatim' && (verbatim = true); closingBlocks.push({ name: name, line: idx }); } else { const closingBlock = closingBlocks.pop(); - if (closingBlock === undefined) { - continue; - } + if (closingBlock === undefined) continue; if (line.substring(1, closingBlock.name.length + 2).trim() === closingBlock.name) { - this.insertRange(idx, closingBlock.line); + this.addRange(idx, closingBlock.line, { name: closingBlock.name }); // If `verbatim == true` goes here, this line must be the `@verbatim` line. verbatim = false; } else { @@ -135,24 +96,18 @@ export class FoldingRangeContext { } } } - if (this.commentRange !== undefined) { - this.insertRange(this.commentRange.start, this.commentRange.end, 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; - } + 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.insertRange(lineNum, this.headerStart); + this.addRange(lineNum, this.headerStart, { kind: vscode.FoldingRangeKind.Region }); this.headerStart = undefined; } return true; @@ -161,34 +116,28 @@ export class FoldingRangeContext { this.commentRange = { start: lineNum, end: lineNum }; } else if (this.commentRange.start - 1 === lineNum) { this.commentRange.start = lineNum; - } else { - this.insertRange(this.commentRange.start, this.commentRange.end, vscode.FoldingRangeKind.Comment); - this.commentRange = { start: lineNum, end: lineNum }; } return true; + } else if (this.commentRange !== undefined) { + this.addRange(this.commentRange.start, this.commentRange.end, { kind: vscode.FoldingRangeKind.Comment }); + this.commentRange = undefined; } return false; } - private insertRange(start: number, end: number, kind?: vscode.FoldingRangeKind) { - (this.foldingRanges ?? (this.foldingRanges = [])).push(new vscode.FoldingRange(start, end, kind)); + private addRange(start: number, end: number, extraArgs: { name?: string, kind?: vscode.FoldingRangeKind }) { + (this.foldingRanges ??= []).push(new FoldingRange(extraArgs.name ?? '', start, end, extraArgs.kind)); } - /** - * Update folding range context based on document change event. - * - * @param events Events describing the changes in the document. - */ - private update(events: readonly vscode.TextDocumentContentChangeEvent[]) { - if (this.foldingRanges === undefined) { - return; - } - for (const event of events) { - const updatedLines = event.text.split(this.document.eol === vscode.EndOfLine.LF ? '\n' : '\r\n').length; - // Clear folding range buffer when line count changes. - if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) { - this.foldingRanges = undefined; - } - } + constructor(private readonly document: vscode.TextDocument) {} +} + +/** + * VSCode folding range with name. + */ +export class FoldingRange extends vscode.FoldingRange { + + constructor(readonly name: string, start: number, end: number, kind?: vscode.FoldingRangeKind) { + super(start, end, kind); } } diff --git a/src/options.ts b/src/options.ts index ba3770d..b59aa96 100644 --- a/src/options.ts +++ b/src/options.ts @@ -17,7 +17,7 @@ export default class Options { private static singleton?: Options; private static get instance() { - return Options.singleton ?? (Options.singleton = new Options('texinfo')); + return Options.singleton ??= new Options('texinfo'); } static clear() { diff --git a/src/preview.ts b/src/preview.ts index 1759abc..5dd24d4 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 Document from './document'; import Options from './options'; import * as utils from './utils'; @@ -16,8 +17,6 @@ import * as utils from './utils'; */ export default class Preview { - private static readonly map = new Map(); - /** * Create (if not yet created) and show preview for a Texinfo document. * @@ -25,47 +24,18 @@ export default class Preview { */ static async show(editor: vscode.TextEditor) { const document = editor.document; - if (document.isUntitled) { - if (!await utils.prompt('Save this document to display preview.', 'Save')) { - return; - } - if (!await document.save()) { - return; - } + const documentContext = Document.get(document); + if (documentContext === undefined) { + return; } - (Preview.map.get(document) ?? new Preview(document)).panel.reveal(); + if (document.isUntitled) { + if (!await utils.prompt('Save this document to display preview.', 'Save')) return; + if (!await document.save()) return; + } + documentContext.initPreview().panel.reveal(); } - /** - * If the document has a corresponding Texinfo preview, update the preview. - * - * @param document - */ - static update(document: vscode.TextDocument) { - Preview.getByDocument(document)?.updateWebview(); - } - - /** - * If the document has a corresponding Texinfo preview, close the preview. - * - * @param document - */ - static close(document: vscode.TextDocument) { - Preview.getByDocument(document)?.destroy(); - } - - static clear() { - Preview.map.forEach((preview) => preview.destroy()); - } - - /** - * Get associated preview instance of the given document. - * - * @param document - */ - private static getByDocument(document: vscode.TextDocument) { - return document.languageId !== 'texinfo' ? undefined : Preview.map.get(document); - } + private readonly document = this.documentContext.document; private readonly panel: vscode.WebviewPanel; @@ -81,10 +51,9 @@ export default class Preview { */ private pendingUpdate = false; - private constructor(private readonly document: vscode.TextDocument) { + constructor(private readonly documentContext: Document) { this.panel = vscode.window.createWebviewPanel('texinfo.preview', '', vscode.ViewColumn.Beside); - this.disposables.push(this.panel.onDidDispose(() => this.destroy())); - Preview.map.set(document, this); + this.disposables.push(this.panel.onDidDispose(() => this.close())); this.updateWebview(); } @@ -94,13 +63,13 @@ export default class Preview { this.panel.title = `${updating}Preview ${fileName}`; } - private destroy() { - this.disposables.forEach((event) => event.dispose()); + close() { + this.disposables.forEach(event => event.dispose()); this.panel.dispose(); - Preview.map.delete(this.document); + this.documentContext.closePreview(); } - private async updateWebview() { + async updateWebview() { if (this.updating) { this.pendingUpdate = true; return; @@ -116,7 +85,7 @@ export default class Preview { 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 = utils.transformHtmlImageUri(htmlCode, (src) => { + htmlCode = utils.transformHtmlImageUri(htmlCode, src => { const srcUri = vscode.Uri.file(pathName + '/' + src); return this.panel.webview.asWebviewUri(srcUri).toString(); }); diff --git a/src/utils.ts b/src/utils.ts index 3ab834c..d68fb3b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -29,7 +29,7 @@ export async function prompt(message: string, confirm: string) { * @returns The output data, or `undefined` if execution fails. */ export function exec(path: string, args: string[], maxBuffer: number) { - return new Promise((resolve) => { + return new Promise(resolve => { child_process.execFile(path, args, { maxBuffer: maxBuffer }, (error, stdout, stderr) => { if (error) { console.error(stderr ? stderr : error); @@ -52,7 +52,7 @@ export function exec(path: string, args: string[], maxBuffer: number) { export function transformHtmlImageUri(htmlCode: string, transformer: (src: string) => string) { const dom = htmlparser.parse(htmlCode); const elements = dom.querySelectorAll('img'); - elements.forEach((element) => { + elements.forEach(element => { const src = element.getAttribute('src'); src && element.setAttribute('src', transformer(src)); });