All files / services/core workspace-indexer-service.ts

84.74% Statements 50/59
80% Branches 20/25
80% Functions 8/10
84.48% Lines 49/58

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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183                              11x   11x         11x     11x 11x 11x   11x             2x               8x         8x   8x       8x     8x   8x 5x   5x 6x 6x   6x 1x     5x 5x   5x 1x 1x     4x 1x   3x     4x     5x   5x     8x   8x   8x   8x                   3x   3x       3x         3x                           1x   1x 1x       1x   1x       1x   1x                         20x               1x 1x                      
import * as vscode from 'vscode';
import { IWorkspaceFileService } from './workspace-file-service.interface';
import { IWorkspaceIndexerService } from './workspace-indexer.interface';
import { IDocumentFactory } from '../document/document-factory.interface';
import { DocumentContextService } from '../document/document-context-service';
import { getConfig } from '@src/utilities/vscode/config';
 
/**
 * Service for indexing RDF documents in the current workspace.
 * Uses WorkspaceFileService for file discovery to avoid duplicate workspace scans.
 */
export class WorkspaceIndexerService implements IWorkspaceIndexerService {
	/**
	 * Indicates if all workspace files have been indexed.
	 */
	private _indexed = false;
 
	private readonly _onDidFinishIndexing = new vscode.EventEmitter<boolean>();
 
	/**
	 * An event that is fired when all workspace files have been indexed.
	 */
	readonly onDidFinishIndexing = this._onDidFinishIndexing.event;
 
	constructor(
		private readonly documentFactory: IDocumentFactory,
		private readonly contextService: DocumentContextService,
		private readonly workspaceFileService: IWorkspaceFileService
	) {
		vscode.commands.executeCommand('setContext', 'mentor.workspace.isIndexing', false);
	}
 
	/**
	 * Indicates if all workspace files have been indexed.
	 */
	get indexed(): boolean {
		return this._indexed;
	}
 
	/**
	 * Builds an index of all RDF resources in the current workspace.
	 * Uses files discovered by WorkspaceFileService instead of scanning again.
	 */
	async indexWorkspace(force: boolean = false): Promise<void> {
		return vscode.window.withProgress({
			location: vscode.ProgressLocation.Window,
			title: "Indexing workspace",
			cancellable: false
		}, async (progress) => {
			vscode.commands.executeCommand('setContext', 'mentor.workspace.isIndexing', true);
 
			this._reportProgress(progress, 0);
 
			// The default value is set to Number.MAX_SAFE_INTEGER to disable the 
			// file size limit and make issues with the configuration more visible.
			const maxSize = getConfig().get<number>('index.maxFileSize', Number.MAX_SAFE_INTEGER);
 
			// Use the files already discovered by WorkspaceFileService
			const uris = this.workspaceFileService.files;
 
			if (uris.length > 0) {
				const startTime = performance.now();
 
				for (let i = 0; i < uris.length; i++) {
					const uri = uris[i];
					const u = uri.toString();
 
					if (this.contextService.contexts[u] && !force) {
						continue;
					}
 
					const stat = await vscode.workspace.fs.stat(uri);
					const size = stat.size;
 
					if (size > maxSize && !force) {
						console.debug(`Mentor: Skipping large file ${uri.toString()} (${size} bytes)`);
						continue;
					}
 
					if (this.documentFactory.isSupportedNotebookFile(uri)) {
						this._indexNotebookDocument(uri, force);
					} else {
						this._indexTextDocument(uri, force);
					}
 
					this._reportProgress(progress, Math.round(((i + 1) / uris.length) * 100));
				}
 
				const endTime = performance.now();
 
				console.debug(`Mentor: Indexing took ${endTime - startTime} ms`);
			}
 
			this._indexed = true;
 
			this._reportProgress(progress, 100);
 
			vscode.commands.executeCommand('setContext', 'mentor.workspace.isIndexing', false);
 
			this._onDidFinishIndexing.fire(true);
		});
	}
 
	/**
	 * Index a regular text document.
	 * @param uri The URI of the document to index.
	 * @param force Whether to force re-indexing of the document.
	 */
	private async _indexTextDocument(uri: vscode.Uri, force: boolean): Promise<void> {
		try {
			// Open the document to trigger the language server to analyze it.
			const document = await vscode.workspace.openTextDocument(uri);
 
			// Re-check after the async open: handleActiveEditorChanged may have registered
			// this context while we were awaiting openTextDocument (TOCTOU guard).
			Iif (this.contextService.contexts[document.uri.toString()] && !force) {
				return;
			}
 
			// Try to load the document so that its graph is created and can be used for showing definitions, descriptions etc..
			await this.contextService.loadDocument(document);
		} catch (error) {
			// VS Code may refuse to open files it considers binary (e.g., files containing
			// ASCII control characters like W3C test files). Skip these gracefully.
			console.warn(`Mentor: Skipping file ${uri.toString()} (cannot be opened as text)`);
		}
	}
 
	/**
	 * Index RDF cells within a notebook document.
	 * @param notebookUri The URI of the notebook file.
	 * @param force Whether to force re-indexing of already indexed cells.
	 */
	private async _indexNotebookDocument(notebookUri: vscode.Uri, force: boolean): Promise<void> {
		const notebook = await vscode.workspace.openNotebookDocument(notebookUri);
 
		for (const cell of notebook.getCells()) {
			Iif (!this.documentFactory.isTripleSourceLanguage(cell.document.languageId)) {
				continue;
			}
 
			const cellUri = cell.document.uri.toString();
 
			Iif (this.contextService.contexts[cellUri] && !force) {
				continue;
			}
 
			try {
				// Load the cell document to create its context
				await this.contextService.loadDocument(cell.document);
			} catch (error) {
				console.error(`Mentor: Failed to index notebook cell ${cellUri}:`, error);
			}
		}
	}
 
	/**
	 * Reports progress to the user.
	 * @param progress The progress to report.
	 * @param increment The increment to report.
	 */
	private _reportProgress(progress: vscode.Progress<{ message?: string, increment?: number }>, increment: number): void {
		progress.report({ message: increment + "%" });
	}
 
	/**
	 * Wait for all workspace files to be indexed.
	 * @returns A promise that resolves when all workspace files were indexed.
	 */
	async waitForIndexed(): Promise<void> {
		Eif (this._indexed) {
			return;
		}
 
		return new Promise((resolve) => {
			const listener = this._onDidFinishIndexing.event(() => {
				listener.dispose();
				resolve();
			});
		});
	}
}