Use native VS Code APIs: openTextDocument for file resolution, workspace.fs.stat, Uri.joinPath

- Replace manual fs.readFile + LRU cache with vscode.workspace.openTextDocument
  (reuses VS Code's document buffers, picks up unsaved changes)
- Replace fs.stat with vscode.workspace.fs.stat
- Replace vscode.Uri.file(path.join(...)) with vscode.Uri.joinPath
- Store wsRootUri alongside wsRoot for native Uri operations
- Remove fs/promises import (only createReadStream remains for streaming parser)
This commit is contained in:
Karol Krzosa
2026-02-16 15:04:43 +01:00
parent dd1e0aed90
commit 6a8679649d

View File

@@ -1,6 +1,5 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs/promises";
import { createReadStream } from "fs";
import * as readline from "readline";
@@ -239,19 +238,15 @@ function substringSearch(db: TagDatabase, query: string, limit: number): CtagsEn
// ---- Lazy line number resolution ----
/** Cache of file contents for lazy resolution, keyed by absolute path */
const fileContentCache = new Map<string, string[] | null>();
async function getFileLines(absPath: string): Promise<string[] | null> {
const cached = fileContentCache.get(absPath);
if (cached !== undefined) { return cached; }
/**
* Get file contents via VS Code's document model.
* Uses already-loaded buffers when available, picks up unsaved changes,
* and lets VS Code manage its own caching — no duplicate memory usage.
*/
async function getDocument(uri: vscode.Uri): Promise<vscode.TextDocument | null> {
try {
const content = await fs.readFile(absPath, "utf-8");
const lines = content.split("\n");
fileContentCache.set(absPath, lines);
return lines;
return await vscode.workspace.openTextDocument(uri);
} catch {
fileContentCache.set(absPath, null);
return null;
}
}
@@ -260,7 +255,7 @@ async function getFileLines(absPath: string): Promise<string[] | null> {
* Resolve the line number of an entry lazily. If already resolved, returns immediately.
* Otherwise reads the source file (with caching) and finds the pattern.
*/
async function resolveLineNumber(entry: CtagsEntry, root: string): Promise<number> {
async function resolveLineNumber(entry: CtagsEntry, rootUri: vscode.Uri): Promise<number> {
if (entry.lineNumber >= 0) { return entry.lineNumber; }
const searchText = extractSearchText(entry.pattern);
@@ -269,15 +264,14 @@ async function resolveLineNumber(entry: CtagsEntry, root: string): Promise<numbe
return 0;
}
const absPath = path.join(root, entry.file);
const lines = await getFileLines(absPath);
if (!lines) {
const doc = await getDocument(vscode.Uri.joinPath(rootUri, entry.file));
if (!doc) {
entry.lineNumber = 0;
return 0;
}
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(searchText)) {
for (let i = 0; i < doc.lineCount; i++) {
if (doc.lineAt(i).text.includes(searchText)) {
entry.lineNumber = i;
return i;
}
@@ -288,34 +282,18 @@ async function resolveLineNumber(entry: CtagsEntry, root: string): Promise<numbe
/**
* Resolve line numbers for a batch of entries.
* Groups by file to avoid redundant reads.
*/
async function resolveEntries(entries: CtagsEntry[], root: string): Promise<void> {
// Collect unique files that need resolution
const filesToResolve = new Set<string>();
async function resolveEntries(entries: CtagsEntry[], rootUri: vscode.Uri): Promise<void> {
for (const e of entries) {
if (e.lineNumber === -1) {
filesToResolve.add(e.file);
}
}
// Pre-load files in parallel (up to 20 at a time)
const files = Array.from(filesToResolve);
const batchSize = 20;
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
await Promise.all(batch.map(f => getFileLines(path.join(root, f))));
}
// Now resolve all entries (file contents are cached)
for (const e of entries) {
if (e.lineNumber === -1) {
await resolveLineNumber(e, root);
await resolveLineNumber(e, rootUri);
}
}
}
function entryToLocation(entry: CtagsEntry, root: string): vscode.Location {
function entryToLocation(entry: CtagsEntry, rootUri: vscode.Uri): vscode.Location {
const ln = entry.lineNumber >= 0 ? entry.lineNumber : 0;
const uri = vscode.Uri.file(path.join(root, entry.file));
const uri = vscode.Uri.joinPath(rootUri, entry.file);
const pos = new vscode.Position(ln, 0);
return new vscode.Location(uri, pos);
}
@@ -371,6 +349,7 @@ function formatDuration(ms: number): string {
let db: TagDatabase = { entries: [], nameIndex: new Map(), fileIndex: new Map(), sorted: [] };
let wsRoot = "";
let wsRootUri: vscode.Uri;
let statusBarItem: vscode.StatusBarItem;
let isLoading = false;
@@ -404,13 +383,13 @@ async function loadTags(progress?: vscode.Progress<{ message?: string; increment
try {
// Stage 1: Stream-parse the file
logInfo("Stage 1/3: Stream-parsing tags file...");
logInfo("Stage 1/2: Stream-parsing tags file...");
const t1 = performance.now();
let fileSizeKB = "?";
let fileSizeBytes = 0;
try {
const stat = await fs.stat(tagsPath);
const stat = await vscode.workspace.fs.stat(vscode.Uri.file(tagsPath));
fileSizeBytes = stat.size;
fileSizeKB = (fileSizeBytes / 1024).toFixed(1);
logInfo(` File size: ${fileSizeKB} KB`);
@@ -428,7 +407,7 @@ async function loadTags(progress?: vscode.Progress<{ message?: string; increment
logInfo(` ${withLineNum} with line numbers, ${needsResolve} need lazy resolution`);
// Stage 2: Build indexes (name, file, sorted)
logInfo("Stage 2/3: Building indexes...");
logInfo("Stage 2/2: Building indexes...");
if (progress) { progress.report({ message: `Building index for ${entries.length} tags...` }); }
statusBarItem.text = `$(sync~spin) vsctags: indexing...`;
const t2 = performance.now();
@@ -439,10 +418,6 @@ async function loadTags(progress?: vscode.Progress<{ message?: string; increment
logInfo(` Sorted array: ${newDb.sorted.length} entries`);
logInfo(` Index build took ${formatDuration(indexTime)}`);
// Stage 3: Clear file content cache (stale data from previous load)
logInfo("Stage 3/3: Clearing resolution cache...");
fileContentCache.clear();
// Commit
db = newDb;
@@ -496,9 +471,9 @@ class CtagsDefinitionProvider implements vscode.DefinitionProvider {
const entries = db.nameIndex.get(word);
if (!entries || entries.length === 0) { return undefined; }
// Lazy-resolve line numbers before navigating
await resolveEntries(entries, wsRoot);
await resolveEntries(entries, wsRootUri);
logInfo(`Definition lookup: "${word}" -> ${entries.length} result(s)`);
return entries.map((e) => entryToLocation(e, wsRoot));
return entries.map((e) => entryToLocation(e, wsRootUri));
}
}
@@ -511,7 +486,7 @@ class CtagsHoverProvider implements vscode.HoverProvider {
if (!word) { return undefined; }
const entries = db.nameIndex.get(word);
if (!entries || entries.length === 0) { return undefined; }
await resolveEntries(entries, wsRoot);
await resolveEntries(entries, wsRootUri);
const lines: string[] = [];
for (const entry of entries) {
@@ -558,7 +533,7 @@ class CtagsWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
entry.name,
entryToSymbolKind(entry.kind),
entry.scope,
entryToLocation(entry, wsRoot),
entryToLocation(entry, wsRootUri),
),
);
@@ -586,7 +561,7 @@ class CtagsDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
entry.name,
entryToSymbolKind(entry.kind),
entry.scope,
entryToLocation(entry, wsRoot),
entryToLocation(entry, wsRootUri),
),
);
@@ -604,9 +579,9 @@ class CtagsReferenceProvider implements vscode.ReferenceProvider {
if (!word) { return undefined; }
const entries = db.nameIndex.get(word);
if (!entries || entries.length === 0) { return undefined; }
await resolveEntries(entries, wsRoot);
await resolveEntries(entries, wsRootUri);
logInfo(`References lookup: "${word}" -> ${entries.length} result(s)`);
return entries.map((e) => entryToLocation(e, wsRoot));
return entries.map((e) => entryToLocation(e, wsRootUri));
}
}
@@ -623,7 +598,8 @@ export function activate(context: vscode.ExtensionContext) {
logInfo("No workspace folder open. Extension idle.");
return;
}
wsRoot = folders[0].uri.fsPath;
wsRootUri = folders[0].uri;
wsRoot = wsRootUri.fsPath;
logInfo(`Workspace root: ${wsRoot}`);
// Status bar
@@ -679,7 +655,6 @@ export function activate(context: vscode.ExtensionContext) {
if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = undefined; }
logInfo("Tags file deleted.");
db = { entries: [], nameIndex: new Map(), fileIndex: new Map(), sorted: [] };
fileContentCache.clear();
updateStatusBar();
vscode.window.showInformationMessage("[vsctags] Tags file removed.");
});
@@ -715,5 +690,4 @@ export function activate(context: vscode.ExtensionContext) {
export function deactivate() {
logInfo("Extension deactivated.");
fileContentCache.clear();
}