All files / languages/triplate/providers triplate-diagnostic-provider.ts

80.55% Statements 29/36
77.77% Branches 14/18
55.55% Functions 5/9
82.85% Lines 29/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                            6x           6x     6x   6x             2x 1x                 6x 4x           4x 1x 1x     3x   3x 1x   2x 2x   2x   1x           1x 1x   1x 1x   1x         1x 1x 1x   1x 1x 1x                          
import * as vscode from 'vscode';
import { compile, isTemplate, TriplateError } from 'triplate';
import { TRIPLATE_LANGUAGE_IDS } from '@src/services/document/document-languages';
import { KeyedDebouncer } from '@src/utilities/debounce';
 
/**
 * Publishes diagnostics for triplate template syntax errors by running the triplate
 * compiler over template documents. The language server strips triplate frontmatter
 * before validating the underlying SPARQL/Turtle, so triplate's own parser errors
 * would otherwise go unreported.
 */
export class TriplateDiagnosticProvider implements vscode.Disposable {
	private readonly _collection: vscode.DiagnosticCollection;
 
	private readonly _subscriptions: vscode.Disposable[] = [];
 
	/**
	 * Debounces per-edit validation: `compile()` runs a full template parse and
	 * would otherwise execute on every keystroke in every open document.
	 */
	private readonly _changeDebouncer = new KeyedDebouncer<string>(300);
 
	constructor() {
		this._collection = vscode.languages.createDiagnosticCollection('triplate');
 
		this._subscriptions.push(
			this._collection,
			this._changeDebouncer,
			vscode.workspace.onDidOpenTextDocument(doc => this._validate(doc)),
			vscode.workspace.onDidChangeTextDocument(e => {
				// Every document change is reported here, including those of output channels
				// and large files in other languages; only template languages are validated.
				if (TRIPLATE_LANGUAGE_IDS.has(e.document.languageId)) {
					this._changeDebouncer.schedule(e.document.uri.toString(), () => this._validate(e.document));
				}
			}),
			vscode.workspace.onDidCloseTextDocument(doc => {
				this._changeDebouncer.cancel(doc.uri.toString());
				this._collection.delete(doc.uri);
			}),
		);
 
		for (const doc of vscode.workspace.textDocuments) {
			this._validate(doc);
		}
	}
 
	private _validate(document: vscode.TextDocument): void {
		// Check the language before reading the text, which copies the whole document.
		if (!TRIPLATE_LANGUAGE_IDS.has(document.languageId)) {
			this._collection.delete(document.uri);
			return;
		}
 
		const text = document.getText();
 
		if (!isTemplate(text)) {
			this._collection.delete(document.uri);
		} else {
			try {
				compile(text);
 
				this._collection.delete(document.uri);
			} catch (error) {
				this._collection.set(document.uri, [this._toDiagnostic(document, error)]);
			}
		}
	}
 
	private _toDiagnostic(document: vscode.TextDocument, error: unknown): vscode.Diagnostic {
		const message = error instanceof Error ? error.message : 'Failed to compile triplate template.';
		const range = this._toRange(document, error);
 
		const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error);
		diagnostic.source = 'triplate';
 
		return diagnostic;
	}
 
	private _toRange(document: vscode.TextDocument, error: unknown): vscode.Range {
		// TriplateError carries 1-based line/column for the offending location.
		Eif (error instanceof TriplateError && error.line !== undefined) {
			const line = error.line - 1;
			const character = (error.column ?? 1) - 1;
 
			Eif (line >= 0 && line < document.lineCount) {
				const endCharacter = Math.max(character + 1, document.lineAt(line).range.end.character);
				return new vscode.Range(line, character, line, endCharacter);
			}
		}
 
		return new vscode.Range(0, 0, 0, 0);
	}
 
	dispose(): void {
		for (const sub of this._subscriptions) {
			sub.dispose();
		}
	}
}