All files / providers resource-tooltip-provider.ts

65.71% Statements 23/35
44.44% Branches 8/18
75% Functions 3/4
65.71% Lines 23/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                    8x   8x     8x 8x       8x   8x 6x   6x 2x     4x   4x 2x     2x       2x   2x 2x                                     8x               2x 2x     2x             2x     8x             2x      
import * as vscode from 'vscode';
import { container } from 'tsyringe';
import { Store, NamedNode } from '@faubulous/mentor-rdf';
import { ServiceToken } from '@src/services/tokens';
import { IDocumentContextService } from '@src/services/document';
 
/**
 * Provides hover information for tokens in RDF documents and for HTTP/HTTPS URIs in any file type.
 */
export class ResourceTooltipProvider implements vscode.HoverProvider {	
	private readonly _contextService = container.resolve<IDocumentContextService>(ServiceToken.DocumentContextService);
 
	private readonly _store = container.resolve<Store>(ServiceToken.Store);
 
	constructor() {
		const context = container.resolve<vscode.ExtensionContext>(ServiceToken.ExtensionContext);
		context.subscriptions.push(vscode.languages.registerHoverProvider('*', this));
	}
 
	provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult<vscode.Hover> {
		const context = this._contextService.contexts[document.uri.toString()];
 
		if (context) {
			const iri = context.getIriAtPosition(position);
 
			if (iri) {
				return new vscode.Hover(context.getResourceTooltip(iri));
			}
 
			const literalValue = context.getLiteralAtPosition(position);
 
			if (literalValue) {
				return new vscode.Hover(literalValue);
			}
 
			return null;
		}
 
		// Generic fallback: detect HTTP/HTTPS URIs in any file type.
		const iri = this._getUriAtTextPosition(document, position);
 
		Eif (!iri) {
			return null;
		}
 
		// Only surface known resources (those with triples in the workspace store).
		let isKnown = false;
 
		for (const _ of this._store.matchAll(undefined, new NamedNode(iri), null, null, false)) {
			isKnown = true;
			break;
		}
 
		if (!isKnown) {
			return null;
		}
 
		// Any loaded context shares the same workspace store for label/description lookups.
		const workspaceContext = this._contextService.activeContext
			?? Object.values(this._contextService.contexts).find(c => c.isLoaded);
 
		Iif (!workspaceContext) {
			return null;
		}
 
		return new vscode.Hover(workspaceContext.getResourceTooltip(iri));
	}
 
	private _getUriAtTextPosition(document: vscode.TextDocument, position: vscode.Position): string | null {
		const line = document.lineAt(position.line).text;
		const cursor = position.character;
 
		// Patterns in order of specificity; captured group 1 is the clean URI without delimiters.
		const patterns: [RegExp, number][] = [
			[/<(https?:\/\/[^>\s]+)>/g, 1],
			[/"(https?:\/\/[^"]+)"/g, 1],
			[/'(https?:\/\/[^']+)'/g, 1],
			[/(https?:\/\/[^\s"'<>`(){}\[\],;]+)/g, 1],
		];
 
		for (const [pattern, group] of patterns) {
			let match: RegExpExecArray | null;
 
			while ((match = pattern.exec(line)) !== null) {
				if (cursor >= match.index && cursor <= match.index + match[0].length) {
					return match[group];
				}
			}
		}
 
		return null;
	}
}