From 5a2dc9f886c9a16f415d53e950fdb0059564434f Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Fri, 31 Jan 2025 08:35:47 -0500 Subject: [PATCH 1/3] Client-side improvements --- src/commands/compile.ts | 20 +++- src/commands/export.ts | 44 +++---- src/extension.ts | 3 +- src/providers/DocumentContentProvider.ts | 4 +- .../FileSystemProvider/TextSearchProvider.ts | 13 +- src/utils/documentIndex.ts | 100 ++++++++-------- src/utils/index.ts | 111 ++++++++++-------- 7 files changed, 161 insertions(+), 134 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index bbc8e47a..f6d75310 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -23,14 +23,16 @@ import { exportedUris, handleError, isClassDeployed, + isClassOrRtn, notIsfs, notNull, outputChannel, + RateLimiter, routineNameTypeRegex, - throttleRequests, } from "../utils"; import { StudioActions } from "./studio"; import { NodeBase, PackageNode, RootNode } from "../explorer/nodes"; +import { updateIndexForDocument } from "../utils/documentIndex"; async function compileFlags(): Promise { const defaultFlags = config().compileFlags; @@ -226,6 +228,10 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] file.uri, Buffer.isBuffer(content) ? content : new TextEncoder().encode(content.join("\n")) ); + if (isClassOrRtn(file.uri)) { + // Update the document index + updateIndexForDocument(file.uri, undefined, undefined, content); + } exportedUris.push(file.uri.toString()); } else if (filesystemSchemas.includes(file.uri.scheme)) { fileSystemProvider.fireFileChanged(file.uri); @@ -414,9 +420,10 @@ export async function namespaceCompile(askFlags = false): Promise { async function importFiles(files: vscode.Uri[], noCompile = false) { const toCompile: CurrentFile[] = []; + const rateLimiter = new RateLimiter(50); await Promise.allSettled( - files.map( - throttleRequests((uri: vscode.Uri) => { + files.map((uri) => + rateLimiter.call(async () => { return vscode.workspace.fs .readFile(uri) .then((contentBytes) => { @@ -661,7 +668,7 @@ export async function importLocalFilesToServerSideFolder(wsFolderUri: vscode.Uri return; } // Filter out non-ISC files - uris = uris.filter((uri) => ["cls", "mac", "int", "inc"].includes(uri.path.split(".").pop().toLowerCase())); + uris = uris.filter(isClassOrRtn); if (uris.length == 0) { vscode.window.showErrorMessage("No classes or routines were selected.", "Dismiss"); return; @@ -711,9 +718,10 @@ export async function importLocalFilesToServerSideFolder(wsFolderUri: vscode.Uri docs.map((e) => e.name) ); // Import the files + const rateLimiter = new RateLimiter(50); return Promise.allSettled( - docs.map( - throttleRequests((doc: { name: string; content: string; uri: vscode.Uri }) => { + docs.map((doc) => + rateLimiter.call(async () => { // Allow importing over deployed classes since the XML import // command and SMP, terminal, and Studio imports allow it return importFileFromContent(doc.name, doc.content, api, false, true).then(() => { diff --git a/src/commands/export.ts b/src/commands/export.ts index 5b44dd69..2d94e3a6 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -4,17 +4,19 @@ import { AtelierAPI } from "../api"; import { config, explorerProvider, OBJECTSCRIPT_FILE_SCHEMA, schemas, workspaceState } from "../extension"; import { currentFile, - currentFileFromContent, exportedUris, handleError, + isClassOrRtn, notNull, outputChannel, + RateLimiter, stringifyError, - throttleRequests, uriOfWorkspaceFolder, + workspaceFolderOfUri, } from "../utils"; import { pickDocuments } from "../utils/documentPicker"; import { NodeBase } from "../explorer/nodes"; +import { updateIndexForDocument } from "../utils/documentIndex"; export function getCategory(fileName: string, addCategory: any | boolean): string { const fileExt = fileName.split(".").pop().toLowerCase(); @@ -99,28 +101,19 @@ async function exportFile(wsFolderUri: vscode.Uri, namespace: string, name: stri throw new Error("Received malformed JSON object from server fetching document"); } const content = data.result.content; - - // Local function to update local record of mtime - const recordMtime = async () => { - const contentString = Buffer.isBuffer(content) ? "" : content.join("\n"); - const file = currentFileFromContent(fileUri, contentString); - const serverTime = Number(new Date(data.result.ts + "Z")); - await workspaceState.update(`${file.uniqueId}:mtime`, serverTime); - }; - if (Buffer.isBuffer(content)) { - // This is a binary file - await vscode.workspace.fs.writeFile(fileUri, content); - exportedUris.push(fileUri.toString()); - await recordMtime(); - log("Success"); - } else { - // This is a text file - const joinedContent = content.join("\n"); - await vscode.workspace.fs.writeFile(fileUri, new TextEncoder().encode(joinedContent)); - exportedUris.push(fileUri.toString()); - await recordMtime(); - log("Success"); + await vscode.workspace.fs.writeFile( + fileUri, + Buffer.isBuffer(content) ? content : new TextEncoder().encode(content.join("\n")) + ); + if (isClassOrRtn(fileUri)) { + // Update the document index + updateIndexForDocument(fileUri, undefined, undefined, content); } + exportedUris.push(fileUri.toString()); + const ws = workspaceFolderOfUri(fileUri); + const mtime = Number(new Date(data.result.ts + "Z")); + if (ws) await workspaceState.update(`${ws}:${name}:mtime`, mtime > 0 ? mtime : undefined); + log("Success"); } catch (error) { const errorStr = stringifyError(error); log(errorStr == "" ? "ERROR" : errorStr); @@ -146,6 +139,7 @@ export async function exportList(files: string[], workspaceFolder: string, names const { atelier, folder, addCategory, map } = config("export", workspaceFolder); const root = wsFolderUri.fsPath + (folder.length ? path.sep + folder : ""); outputChannel.show(true); + const rateLimiter = new RateLimiter(50); return vscode.window.withProgress( { title: `Exporting ${files.length == 1 ? files[0] : files.length + " documents"}`, @@ -154,8 +148,8 @@ export async function exportList(files: string[], workspaceFolder: string, names }, () => Promise.allSettled( - files.map( - throttleRequests((file: string) => + files.map((file) => + rateLimiter.call(() => exportFile(wsFolderUri, namespace, file, getFileName(root, file, atelier, addCategory, map)) ) ) diff --git a/src/extension.ts b/src/extension.ts index d226ced9..4475ccb0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -101,6 +101,7 @@ import { cspApps, otherDocExts, getWsServerConnection, + isClassOrRtn, } from "./utils"; import { ObjectScriptDiagnosticProvider } from "./providers/ObjectScriptDiagnosticProvider"; import { DocumentLinkProvider } from "./providers/DocumentLinkProvider"; @@ -1162,7 +1163,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { return Promise.all( e.files .filter(notIsfs) - .filter((uri) => ["cls", "inc", "int", "mac"].includes(uri.path.split(".").pop().toLowerCase())) + .filter(isClassOrRtn) .map(async (uri) => { // Determine the file name const workspace = workspaceFolderOfUri(uri); diff --git a/src/providers/DocumentContentProvider.ts b/src/providers/DocumentContentProvider.ts index 3a0c5519..09036650 100644 --- a/src/providers/DocumentContentProvider.ts +++ b/src/providers/DocumentContentProvider.ts @@ -5,7 +5,7 @@ import { AtelierAPI } from "../api"; import { getFileName } from "../commands/export"; import { config, FILESYSTEM_SCHEMA, FILESYSTEM_READONLY_SCHEMA, OBJECTSCRIPT_FILE_SCHEMA } from "../extension"; -import { currentWorkspaceFolder, notIsfs, uriOfWorkspaceFolder } from "../utils"; +import { currentWorkspaceFolder, isClassOrRtn, notIsfs, uriOfWorkspaceFolder } from "../utils"; import { getUrisForDocument } from "../utils/documentIndex"; export function compareConns( @@ -52,7 +52,7 @@ export class DocumentContentProvider implements vscode.TextDocumentContentProvid if (!notIsfs(wsFolder.uri)) return; const conf = vscode.workspace.getConfiguration("objectscript.export", wsFolder); const confFolder = conf.get("folder", ""); - if (["cls", "mac", "int", "inc"].includes(name.split(".").pop().toLowerCase())) { + if (isClassOrRtn(name)) { // Use the document index to find the local URI const uris = getUrisForDocument(name, wsFolder); switch (uris.length) { diff --git a/src/providers/FileSystemProvider/TextSearchProvider.ts b/src/providers/FileSystemProvider/TextSearchProvider.ts index 342a99c3..f7e6fdec 100644 --- a/src/providers/FileSystemProvider/TextSearchProvider.ts +++ b/src/providers/FileSystemProvider/TextSearchProvider.ts @@ -3,7 +3,7 @@ import { makeRe } from "minimatch"; import { AsyncSearchRequest, SearchResult, SearchMatch } from "../../api/atelier"; import { AtelierAPI } from "../../api"; import { DocumentContentProvider } from "../DocumentContentProvider"; -import { handleError, notNull, outputChannel, throttleRequests } from "../../utils"; +import { handleError, notNull, outputChannel, RateLimiter } from "../../utils"; import { config } from "../../extension"; import { fileSpecFromURI } from "../../utils/FileProviderUtil"; @@ -280,6 +280,7 @@ export class TextSearchProvider implements vscode.TextSearchProvider { const api = new AtelierAPI(options.folder); const params = new URLSearchParams(options.folder.query); const decoder = new TextDecoder(); + const rateLimiter = new RateLimiter(50); let counter = 0; if (!api.enabled) { return { @@ -470,7 +471,7 @@ export class TextSearchProvider implements vscode.TextSearchProvider { return api.verifiedCancel(id, false); } // Process matches - filePromises.push(...pollResp.result.map(throttleRequests(reportMatchesForFile))); + filePromises.push(...pollResp.result.map((file) => rateLimiter.call(() => reportMatchesForFile(file)))); if (pollResp.retryafter) { await new Promise((resolve) => { setTimeout(resolve, 50); @@ -524,8 +525,8 @@ export class TextSearchProvider implements vscode.TextSearchProvider { requestGroups.push(group); } searchPromise = Promise.allSettled( - requestGroups.map( - throttleRequests((group: string[]) => + requestGroups.map((group) => + rateLimiter.call(() => api .actionSearch({ query: pattern, @@ -628,8 +629,8 @@ export class TextSearchProvider implements vscode.TextSearchProvider { return; } const resultsPromise = Promise.allSettled( - files.map( - throttleRequests(async (file: SearchResult): Promise => { + files.map((file) => + rateLimiter.call(() => { if (token.isCancellationRequested) { return; } diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 67b2382c..362468ea 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -2,10 +2,11 @@ import * as vscode from "vscode"; import { CurrentBinaryFile, CurrentTextFile, - currentFile, + RateLimiter, currentFileFromContent, exportedUris, getServerDocName, + isClassOrRtn, isImportableLocalFile, notIsfs, openCustomEditors, @@ -34,49 +35,42 @@ interface WSFolderIndexChange { /** Map of stringified workspace folder `Uri`s to collection of InterSystems classes and routines contained therein */ const wsFolderIndex: Map = new Map(); -/** Glob pattern that matches classes and routines */ -const filePattern = "{**/*.cls,**/*.mac,**/*.int,**/*.inc}"; - /** We want decoding errors to be thrown */ const textDecoder = new TextDecoder("utf-8", { fatal: true }); /** The number of milliseconds that we should wait before sending a compile or delete request */ const debounceDelay = 500; -/** Returns `true` if `uri` has a class or routine file extension */ -function isClassOrRtn(uri: vscode.Uri): boolean { - return ["cls", "mac", "int", "inc"].includes(uri.path.split(".").pop().toLowerCase()); -} - /** - * Create an object describing the file in `uri`. Will use the version - * of the file in VS Code if it's loaded and supports binary files. + * Create an object describing the file in `uri`. + * Supports binary files and will use `content` if it's defined. */ async function getCurrentFile( uri: vscode.Uri, - forceText = false + forceText = false, + content?: string[] | Buffer ): Promise { - const uriString = uri.toString(); - const textDocument = vscode.workspace.textDocuments.find((d) => d.uri.toString() == uriString); - if (textDocument) { - return currentFile(textDocument); - } else { - try { - const contentBytes = await vscode.workspace.fs.readFile(uri); - const contentBuffer = Buffer.from(contentBytes); - return currentFileFromContent( - uri, - forceText || isText(uri.path.split("/").pop(), contentBuffer) ? textDecoder.decode(contentBytes) : contentBuffer - ); - } catch (error) { - // Either a vscode.FileSystemError from readFile() - // or a TypeError from decode(). Don't log TypeError - // since the file may be a non-text file that has - // an extension that we interpret as text (like cls or mac). - // We should ignore such files rather than alerting the user. - if (error instanceof vscode.FileSystemError) { - outputChannel.appendLine(`Failed to read contents of '${uri.toString(true)}': ${error.toString()}`); - } + if (content) { + // forceText is always true when content is passed + return currentFileFromContent(uri, Buffer.isBuffer(content) ? textDecoder.decode(content) : content.join("\n")); + } + try { + const contentBytes = await vscode.workspace.fs.readFile(uri); + const contentBuffer = Buffer.from(contentBytes); + return currentFileFromContent( + uri, + forceText || isText(uri.path.split("/").pop(), contentBuffer) ? textDecoder.decode(contentBytes) : contentBuffer + ); + } catch (error) { + // Either a vscode.FileSystemError from readFile() + // or a TypeError from decode(). Don't log TypeError + // since the file may be a non-text file that has + // an extension that we interpret as text (like cls or mac). + // Also don't log "FileNotFound" errors, which are probably + // caused by concurrency issues. We should ignore such files + // rather than alerting the user. + if (error instanceof vscode.FileSystemError && error.code != "FileNotFound") { + outputChannel.appendLine(`Failed to read contents of '${uri.toString(true)}': ${error.toString()}`); } } } @@ -150,9 +144,15 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (!notIsfs(wsFolder.uri)) return; const documents: Map = new Map(); const uris: Map = new Map(); + // Limit the initial indexing to 250 files at once to avoid EMFILE errors + const fsRateLimiter = new RateLimiter(250); + // Limit FileSystemWatcher events that may produce a putDoc() + // request to 50 concurrent calls to avoid hammering the server + const restRateLimiter = new RateLimiter(50); // Index classes and routines that currently exist - const files = await vscode.workspace.findFiles(new vscode.RelativePattern(wsFolder, filePattern)); - for (const file of files) updateIndexForDocument(file, documents, uris); + vscode.workspace + .findFiles(new vscode.RelativePattern(wsFolder, "{**/*.cls,**/*.mac,**/*.int,**/*.inc}")) + .then((files) => files.forEach((file) => fsRateLimiter.call(() => updateIndexForDocument(file, documents, uris)))); // Watch for all file changes const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(wsFolder, "**/*")); const debouncedCompile = generateCompileFn(); @@ -164,6 +164,14 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // and any updates to the class will be handled by that editor return; } + const exportedIdx = exportedUris.findIndex((e) => e == uriString); + if (exportedIdx != -1) { + // This creation/change event was fired due to a server + // export, so don't re-sync the file with the server. + // The index has already been updated. + exportedUris.splice(exportedIdx, 1); + return; + } const conf = vscode.workspace.getConfiguration("objectscript", uri); const sync: boolean = conf.get("syncLocalChanges"); let change: WSFolderIndexChange = {}; @@ -173,13 +181,6 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr change.addedOrChanged = await getCurrentFile(uri); } if (!sync || (!change.addedOrChanged && !change.removed)) return; - const exportedIdx = exportedUris.findIndex((e) => e == uriString); - if (exportedIdx != -1) { - // This creation/change event was fired due to a server - // export, so don't re-sync the file with the server - exportedUris.splice(exportedIdx, 1); - return; - } const api = new AtelierAPI(uri); if (!api.active) return; if (change.addedOrChanged) { @@ -188,6 +189,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr .then(() => { if (conf.get("compileOnSave")) debouncedCompile(change.addedOrChanged); }) + // importFile handles any server errors .catch(() => {}); } if (change.removed) { @@ -195,8 +197,8 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr debouncedDelete(change.removed); } }; - watcher.onDidChange((uri) => updateIndexAndSyncChanges(uri)); - watcher.onDidCreate((uri) => updateIndexAndSyncChanges(uri)); + watcher.onDidChange((uri) => restRateLimiter.call(() => updateIndexAndSyncChanges(uri))); + watcher.onDidCreate((uri) => restRateLimiter.call(() => updateIndexAndSyncChanges(uri))); watcher.onDidDelete((uri) => { const sync: boolean = vscode.workspace.getConfiguration("objectscript", uri).get("syncLocalChanges"); const api = new AtelierAPI(uri); @@ -226,11 +228,15 @@ export function removeIndexOfWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): wsFolderIndex.delete(key); } -/** Update the entries in the index for `uri` */ +/** + * Update the entries in the index for `uri`. `content` will only be passed if this + * function is called for a document that was just exported from the server. + */ export async function updateIndexForDocument( uri: vscode.Uri, documents?: Map, - uris?: Map + uris?: Map, + content?: string[] | Buffer ): Promise { const result: WSFolderIndexChange = {}; const uriString = uri.toString(); @@ -243,7 +249,7 @@ export async function updateIndexForDocument( uris = index.uris; } const documentName = uris.get(uriString); - const file = await getCurrentFile(uri, true); + const file = await getCurrentFile(uri, true, content); if (!file) return result; result.addedOrChanged = file; // This file contains an InterSystems document, so add it to the index diff --git a/src/utils/index.ts b/src/utils/index.ts index f8c6f70c..f89d2db9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -222,7 +222,7 @@ export function currentFileFromContent(uri: vscode.Uri, content: string | Buffer const fileExt = fileName.split(".").pop().toLowerCase(); if ( notIsfs(uri) && - !["cls", "mac", "int", "inc"].includes(fileExt) && + !isClassOrRtn(uri) && // This is a non-class or routine local file, so check if we can import it !isImportableLocalFile(uri) ) { @@ -296,7 +296,7 @@ export function currentFile(document?: vscode.TextDocument): CurrentTextFile { const fileExt = fileName.split(".").pop().toLowerCase(); if ( notIsfs(document.uri) && - !["cls", "mac", "int", "inc"].includes(fileExt) && + !isClassOrRtn(document.uri) && // This is a non-class or routine local file, so check if we can import it !isImportableLocalFile(document.uri) ) { @@ -769,6 +769,13 @@ export function base64EncodeContent(content: Buffer): string[] { return result; } +/** Returns `true` if `uri` has a class or routine file extension */ +export function isClassOrRtn(uriOrName: vscode.Uri | string): boolean { + return ["cls", "mac", "int", "inc"].includes( + (uriOrName instanceof vscode.Uri ? uriOrName.path : uriOrName).split(".").pop().toLowerCase() + ); +} + interface ConnQPItem extends vscode.QuickPickItem { uri: vscode.Uri; ns: string; @@ -816,52 +823,62 @@ export async function getWsServerConnection(minVersion?: string): Promise c?.uri ?? null); } -// --------------------------------------------------------------------- -// Source: https://github.com/amsterdamharu/lib/blob/master/src/index.js - -const promiseLike = (x) => x !== undefined && typeof x.then === "function"; -const ifPromise = (fn) => (x) => (promiseLike(x) ? x.then(fn) : fn(x)); - -/* - causes a promise returning function not to be called - until less than max are active - usage example: - max2 = throttle(2); - urls = [url1,url2,url3...url100] - Promise.all(//even though a 100 promises are created, only 2 are active - urls.map(max2(fetch)) - ) -*/ -const throttle = (max: number): ((fn: any) => (arg: any) => Promise) => { - let que = []; - let queIndex = -1; - let running = 0; - const wait = (resolve, fn, arg) => () => resolve(ifPromise(fn)(arg)) || true; //should always return true - const nextInQue = () => { - ++queIndex; - if (typeof que[queIndex] === "function") { - return que[queIndex](); +class Semaphore { + /** Queue of tasks waiting to acquire the semaphore */ + private _tasks: (() => void)[] = []; + /** Current available slots in the semaphore */ + private _counter: number; + + constructor(maxConcurrent: number) { + // Initialize the counter with the maximum number of concurrent tasks + this._counter = maxConcurrent; + } + + /** Acquire a slot in the semaphore */ + async acquire(): Promise { + return new Promise((resolve) => { + if (this._counter > 0) { + // If a slot is available, decrease the counter and resolve immediately + this._counter--; + resolve(); + } else { + // If no slots are available, add the task to the queue + this._tasks.push(resolve); + } + }); + } + + /** Release a slot in the semaphore */ + release(): void { + if (this._tasks.length > 0) { + // If there are tasks waiting, take the next task from the queue and run it + const nextTask = this._tasks.shift(); + if (nextTask) nextTask(); } else { - que = []; - queIndex = -1; - running = 0; - return "Does not matter, not used"; - } - }; - const queItem = (fn, arg) => new Promise((resolve, reject) => que.push(wait(resolve, fn, arg))); - return (fn) => (arg) => { - const p = queItem(fn, arg).then((x) => nextInQue() && x); - running++; - if (running <= max) { - nextInQue(); + // If no tasks are waiting, increase the counter + this._counter++; } - return p; - }; -}; + } +} -// --------------------------------------------------------------------- +export class RateLimiter { + private _semaphore: Semaphore; -/** - * Wrap around each promise in array to avoid overloading the server. - */ -export const throttleRequests = throttle(50); + constructor(maxConcurrent: number) { + // Initialize the semaphore with the maximum number of concurrent tasks + this._semaphore = new Semaphore(maxConcurrent); + } + + /** Execute a function with rate limiting */ + async call(fn: () => Promise): Promise { + // Acquire a slot in the semaphore. Will not reject. + await this._semaphore.acquire(); + try { + // Execute the provided function + return await fn(); + } finally { + // Always release the slot in the semaphore after the function completes + this._semaphore.release(); + } + } +} From 9d60e10e8facf5ba7664cc9d0a6c7105de1e6af5 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Tue, 4 Feb 2025 07:48:41 -0500 Subject: [PATCH 2/3] Update documentIndex.ts --- src/utils/documentIndex.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 362468ea..0750194d 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -87,7 +87,10 @@ function generateCompileFn(): (doc: CurrentTextFile | CurrentBinaryFile) => void clearTimeout(timeout); // Compile right away if this document is in the active text editor - if (vscode.window.activeTextEditor?.document.uri.toString() == doc.uri.toString()) { + // and there are no other documents in the queue. This is needed + // to avoid noticeable latency when a user is editing a client-side + // file, saves it, and the auto-compile kicks in. + if (docs.length == 1 && vscode.window.activeTextEditor?.document.uri.toString() == doc.uri.toString()) { compile([...docs]); docs.length = 0; return; From f5cf5f0ddeeaa061852a22f09c608bb0e09ea737 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 6 Feb 2025 14:41:58 -0500 Subject: [PATCH 3/3] Prevent duplicate Uris in the index --- src/utils/documentIndex.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 0750194d..cf10ef44 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -258,6 +258,7 @@ export async function updateIndexForDocument( // This file contains an InterSystems document, so add it to the index if (!documentName || (documentName && documentName != file.name)) { const documentUris = documents.get(file.name) ?? []; + if (documentUris.some((u) => u.toString() == uriString)) return result; documentUris.push(uri); documents.set(file.name, documentUris); uris.set(uriString, file.name);