All files / languages/turtle/providers turtle-auto-define-prefix-provider.ts

94.23% Statements 49/52
89.47% Branches 34/38
100% Functions 9/9
93.47% Lines 43/46

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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154                              38x       38x 38x   61x   38x   2x   1x           2x 1x     2x               3x   3x   1x                                     4x   4x 1x         3x 3x   3x 2x       3x                   2x   2x   1x   1x 1x                     11x   11x 2x     9x 9x   9x 7x   7x           3x       6x   6x 5x     5x 1x     4x   4x 3x          
import * as vscode from 'vscode';
import { RdfToken } from '@faubulous/mentor-rdf-parsers';
import { IDocumentContextService } from '@src/services/document';
import { TurtlePrefixDefinitionService } from '@src/languages/turtle/services/turtle-prefix-definition-service';
import { getConfig } from '@src/utilities/vscode/config';
import { getContentStartOffset, getTokenIndexAtPosition } from '@src/utilities';
import { isTemplate } from 'triplate';
 
/**
 * A provider that automatically defines namespace prefixes when a colon is typed
 * in a prefixed name. Detection runs synchronously on the keystroke: the document
 * is tokenized locally (no language-server round-trip) and the prefix declaration
 * is inserted immediately.
 */
export class TurtleAutoDefinePrefixProvider implements vscode.Disposable {
	private readonly _disposables: vscode.Disposable[] = [];
 
	constructor(
		languages: string[],
		private readonly _prefixService: TurtlePrefixDefinitionService,
		private readonly _contextService: IDocumentContextService
	) {
		const filter = languages.map(language => ({ language }));
 
		this._disposables.push(
			vscode.workspace.onDidChangeTextDocument(e => {
				if (!filter.some(f => f.language === e.document.languageId)) return;
 
				this._onDidChangeTextDocument(e);
			})
		);
	}
 
	dispose(): void {
		for (const d of this._disposables) {
			d.dispose();
		}
 
		this._disposables.length = 0;
	}
 
	/**
	 * Auto-defines a prefix when a colon is typed in a prefixed name. Runs synchronously
	 * by tokenizing the document locally.
	 */
	private _onDidChangeTextDocument(e: vscode.TextDocumentChangeEvent): void {
		const change = e.contentChanges[0];
 
		if (!change?.text.endsWith(':')) return;
 
		Eif (!getConfig().get('prefixes.autoDefinePrefixes')) return;
 
		// Prefixed names typed in triplate frontmatter (example values) are not tokenized
		// by the RDF lexer, so handle them directly rather than via the token-based path.
		if (this._tryAutoDefineFrontmatterPrefix(e.document, change.range.start)) {
			return;
		}
 
		void this._tryAutoDefineBodyPrefix(e.document, change.range.start);
	}
 
	/**
	 * Attempts to auto-define a prefix for a prefixed name typed as an example value in
	 * the triplate frontmatter, e.g. `type: schema:Person`. Returns `true` when the colon
	 * was typed inside the frontmatter (so the token-based path should be skipped).
	 * @param document The text document.
	 * @param position The position at which the colon was typed.
	 */
	private _tryAutoDefineFrontmatterPrefix(document: vscode.TextDocument, position: vscode.Position): boolean {
		const text = document.getText();
 
		if (!isTemplate(text) || document.offsetAt(position) >= getContentStartOffset(text)) {
			return false;
		}
 
		// A pname colon appears in a value position, i.e. after a `name:` binding on the
		// same line. The captured group is the prefix immediately before the typed colon.
		const beforeColon = document.lineAt(position.line).text.substring(0, position.character);
		const match = /^\s*[A-Za-z_][\w-]*\s*:\s*([A-Za-z_][\w.-]*)$/.exec(beforeColon);
 
		if (match) {
			void this._defineFrontmatterPrefix(document, match[1]);
		}
 
		// The colon was typed in the frontmatter; the token-based path does not apply.
		return true;
	}
 
	/**
	 * Declares the given prefix in the document body (below the frontmatter) unless it is
	 * already defined.
	 * @param document The text document.
	 * @param prefix The prefix to declare.
	 */
	private async _defineFrontmatterPrefix(document: vscode.TextDocument, prefix: string): Promise<void> {
		const context = this._contextService.getContextFromUri(document.uri.toString());
 
		if (!context || context.namespaces[prefix]) return;
 
		const edit = await this._prefixService.implementPrefixes(document, [{ prefix, namespaceIri: undefined }]);
 
		Eif (edit.size > 0) {
			vscode.workspace.applyEdit(edit);
		}
	}
 
	/**
	 * Auto-defines a prefix for a prefixed name typed in the document body by tokenizing
	 * the current text locally and inspecting the token at the typed colon.
	 * @param document The text document.
	 * @param position The position at which the colon was typed.
	 */
	private async _tryAutoDefineBodyPrefix(document: vscode.TextDocument, position: vscode.Position): Promise<void> {
		const context = this._contextService.getContextFromUri(document.uri.toString());
 
		if (!context || !context.isTokenized()) {
			return;
		}
 
		const tokens = context.tokenizeDocument(document);
		const n = getTokenIndexAtPosition(tokens, position);
 
		if (n > 0) {
			const previousToken = tokens[n - 1];
 
			switch (previousToken.tokenType.name) {
				// Do not auto-implement prefixes when manually typing a prefix definition.
				case RdfToken.PREFIX.name:
				case RdfToken.TTL_PREFIX.name:
				// Do not implement prefixes for URI schemes, starting with <.
				case RdfToken.IRIREF.name:
					return;
			}
		}
 
		const currentToken = tokens[n];
 
		if (currentToken && currentToken.image && currentToken.tokenType.name === RdfToken.PNAME_NS.name) {
			const prefix = currentToken.image.substring(0, currentToken.image.length - 1);
 
			// Do not implement prefixes that are already defined.
			if (context.namespaces[prefix]) {
				return;
			}
 
			const edit = await this._prefixService.implementPrefixes(document, [{ prefix, namespaceIri: undefined }]);
 
			if (edit.size > 0) {
				vscode.workspace.applyEdit(edit);
			}
		}
	}
}