From bb95e9a8f1458728e3f100191b58cd6f5365d933 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 28 Jan 2026 14:06:29 -0500 Subject: [PATCH 01/14] fix #1638 --- .vscode/launch.json | 16 ---------------- CONTRIBUTING.md | 2 -- src/utils/documentIndex.ts | 30 ++++++++++++++++++------------ 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6c041caf..e41fac94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,22 +2,6 @@ { "version": "0.2.0", "configurations": [ - { - "name": "Launch Extension Alone", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--disable-extensions", - "--extensionDevelopmentPath=${workspaceRoot}" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": [ - "${workspaceRoot}/dist/**/*.js" - ], - "preLaunchTask": "npm: webpack" - }, { "name": "Launch Extension", "type": "extensionHost", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fa18e40..fa2e8cc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,8 +44,6 @@ Then, open the debug panel by clicking the `Run and Debug` icon on the Activity option from the top menu, and click start. A new window will launch with the title `[Extension Development Host]`. Do your testing here. -If you want to disable all other extensions when testing in the Extension Development Host, choose the `Launch Extension Alone` option instead. - ### Pull requests Work should be done on a unique branch -- not the master branch. Pull requests require the approval of two PMC members, as described in the [Governance document](GOVERNANCE.md). PMC review is often high level, so in addition to that, you should request a review by someone familiar with the technical details of your particular pull request. diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index c610cb27..20955443 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -310,19 +310,25 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr .get("syncLocalChanges"); const sync: boolean = api.active && (syncLocalChanges == "all" || (syncLocalChanges == "vscodeOnly" && touchedByVSCode.has(uriString))); - touchedByVSCode.delete(uriString); - if (isClassOrRtn(uri)) { - // Remove the class/routine in the file from the index, - // then delete it on the server if required - const change = removeDocumentFromIndex(uri, documents, uris); - if (sync && change.removed) { - debouncedDelete(change.removed); + for (const [subUri, _] of uris.entries()) { + if (!subUri.startsWith(uriString)) { + continue; + } + const uri = vscode.Uri.parse(subUri); + touchedByVSCode.delete(uri.toString()); + if (isClassOrRtn(uri)) { + // Remove the class/routine in the file from the index, + // then delete it on the server if required + const change = removeDocumentFromIndex(uri, documents, uris); + if (sync && change.removed) { + debouncedDelete(change.removed); + } + } else if (sync && isImportableLocalFile(uri)) { + // Delete this web application file or Studio abstract document on the server + const docName = getServerDocName(uri); + if (!docName) return; + debouncedDelete(docName); } - } else if (sync && isImportableLocalFile(uri)) { - // Delete this web application file or Studio abstract document on the server - const docName = getServerDocName(uri); - if (!docName) return; - debouncedDelete(docName); } }); wsFolderIndex.set(wsFolder.uri.toString(), { watcher, documents, uris }); From 587a799bc98a13765cccf6db6eec43e1684f77c5 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 28 Jan 2026 14:37:11 -0500 Subject: [PATCH 02/14] The original string is right there! --- src/utils/documentIndex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 20955443..b568598c 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -314,8 +314,8 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (!subUri.startsWith(uriString)) { continue; } + touchedByVSCode.delete(subUri); const uri = vscode.Uri.parse(subUri); - touchedByVSCode.delete(uri.toString()); if (isClassOrRtn(uri)) { // Remove the class/routine in the file from the index, // then delete it on the server if required From 72cbc7adb56161135e4685099f5d13655b007662 Mon Sep 17 00:00:00 2001 From: klu Date: Fri, 30 Jan 2026 16:34:44 -0500 Subject: [PATCH 03/14] uriIsParentOf --- src/utils/documentIndex.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index b568598c..cc9d2e93 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -14,6 +14,9 @@ import { displayableUri, isCompilable, } from "."; +import { + uriIsParentOf +} from "../utils"; import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; import { compile, importFile } from "../commands/compile"; @@ -310,22 +313,22 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr .get("syncLocalChanges"); const sync: boolean = api.active && (syncLocalChanges == "all" || (syncLocalChanges == "vscodeOnly" && touchedByVSCode.has(uriString))); - for (const [subUri, _] of uris.entries()) { - if (!subUri.startsWith(uriString)) { + for (const subUriString of uris.keys()) { + touchedByVSCode.delete(subUriString); + const subUri = vscode.Uri.parse(subUriString); + if (!uriIsParentOf(uri, subUri)) { continue; } - touchedByVSCode.delete(subUri); - const uri = vscode.Uri.parse(subUri); - if (isClassOrRtn(uri)) { + if (isClassOrRtn(subUri)) { // Remove the class/routine in the file from the index, // then delete it on the server if required - const change = removeDocumentFromIndex(uri, documents, uris); + const change = removeDocumentFromIndex(subUri, documents, uris); if (sync && change.removed) { debouncedDelete(change.removed); } - } else if (sync && isImportableLocalFile(uri)) { + } else if (sync && isImportableLocalFile(subUri)) { // Delete this web application file or Studio abstract document on the server - const docName = getServerDocName(uri); + const docName = getServerDocName(subUri); if (!docName) return; debouncedDelete(docName); } From ba66ff95cacc39a877686b3721c639008880af5f Mon Sep 17 00:00:00 2001 From: klu Date: Mon, 2 Feb 2026 14:33:51 -0500 Subject: [PATCH 04/14] fix style --- CONTRIBUTING.md | 1 + src/utils/documentIndex.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa2e8cc1..3cb7efc8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ The extensions in the [official extension pack](https://docs.intersystems.com/co 1. [Node.js](https://nodejs.org/) 22 1. Windows, macOS, or Linux 1. [Visual Studio Code](https://code.visualstudio.com/) +1. [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) (Optional) ### Setup diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index cc9d2e93..9d52aba6 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -14,9 +14,7 @@ import { displayableUri, isCompilable, } from "."; -import { - uriIsParentOf -} from "../utils"; +import { uriIsParentOf } from "../utils"; import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; import { compile, importFile } from "../commands/compile"; From dc83898929725af5b0a41bb42bfd4fc2a4f15f07 Mon Sep 17 00:00:00 2001 From: klu Date: Tue, 3 Feb 2026 17:45:26 -0500 Subject: [PATCH 05/14] include importable files in the index --- src/commands/compile.ts | 9 +- src/commands/export.ts | 7 +- src/extension.ts | 13 +-- src/utils/documentIndex.ts | 172 ++++++++++++++++--------------------- src/utils/index.ts | 10 +-- 5 files changed, 96 insertions(+), 115 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 17f9cb4f..3b81ae35 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -26,6 +26,7 @@ import { handleError, isClassDeployed, isClassOrRtn, + isImportableLocalFile, isCompilable, lastUsedLocalUri, notIsfs, @@ -36,7 +37,7 @@ import { } from "../utils"; import { StudioActions } from "./studio"; import { NodeBase, PackageNode, RootNode } from "../explorer/nodes"; -import { getUrisForDocument, updateIndexForDocument } from "../utils/documentIndex"; +import { getUrisForDocument, updateIndex } from "../utils/documentIndex"; async function compileFlags(): Promise { const defaultFlags = config().compileFlags; @@ -245,9 +246,9 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] // Re-throw the error throw e; }); - if (isClassOrRtn(file.uri)) { + if (isClassOrRtn(file.uri.path) || isImportableLocalFile(file.uri)) { // Update the document index - updateIndexForDocument(file.uri, undefined, undefined, content); + updateIndex(file.uri, content); } } else if (filesystemSchemas.includes(file.uri.scheme)) { fileSystemProvider.fireFileChanged(file.uri); @@ -659,7 +660,7 @@ export async function importLocalFilesToServerSideFolder(wsFolderUri: vscode.Uri return; } // Filter out non-ISC files - uris = uris.filter(isClassOrRtn); + uris = uris.filter((uri) => isClassOrRtn(uri.path)); if (uris.length == 0) { vscode.window.showErrorMessage("No classes or routines were selected.", "Dismiss"); return; diff --git a/src/commands/export.ts b/src/commands/export.ts index 5d28252c..48d09095 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -8,6 +8,7 @@ import { getWsFolder, handleError, isClassOrRtn, + isImportableLocalFile, lastUsedLocalUri, notNull, outputChannel, @@ -20,7 +21,7 @@ import { } from "../utils"; import { pickDocuments } from "../utils/documentPicker"; import { NodeBase } from "../explorer/nodes"; -import { updateIndexForDocument } from "../utils/documentIndex"; +import { updateIndex } from "../utils/documentIndex"; export function getCategory(fileName: string, addCategory: any | boolean): string { const fileExt = fileName.split(".").pop().toLowerCase(); @@ -112,9 +113,9 @@ async function exportFile(wsFolderUri: vscode.Uri, namespace: string, name: stri // Re-throw the error throw e; }); - if (isClassOrRtn(fileUri)) { + if (isClassOrRtn(fileUri.path) || isImportableLocalFile(fileUri)) { // Update the document index - updateIndexForDocument(fileUri, undefined, undefined, content); + updateIndex(fileUri, content); } const ws = workspaceFolderOfUri(fileUri); const mtime = Number(new Date(data.result.ts + "Z")); diff --git a/src/extension.ts b/src/extension.ts index fd0198fb..26e39c81 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -95,6 +95,7 @@ import { otherDocExts, getWsServerConnection, isClassOrRtn, + isImportableLocalFile, addWsServerRootFolderData, getWsFolder, exportedUris, @@ -143,7 +144,7 @@ import { indexWorkspaceFolder, removeIndexOfWorkspaceFolder, storeTouchedByVSCode, - updateIndexForDocument, + updateIndex, } from "./utils/documentIndex"; import { WorkspaceNode, NodeBase } from "./explorer/nodes"; import { showPlanWebview } from "./commands/showPlanPanel"; @@ -1090,11 +1091,13 @@ export async function activate(context: vscode.ExtensionContext): Promise { checkChangedOnServer(currentFile(event.document)); } if ( - [clsLangId, macLangId, intLangId, incLangId].includes(event.document.languageId) && - notIsfs(event.document.uri) + notIsfs(event.document.uri) && + ([clsLangId, macLangId, intLangId, incLangId].includes(event.document.languageId) || + isClassOrRtn(event.document.uri.path) || + isImportableLocalFile(event.document.uri)) ) { // Update the local workspace folder index to incorporate this change - updateIndexForDocument(event.document.uri); + updateIndex(event.document.uri); } }), vscode.window.onDidChangeActiveTextEditor(async (editor) => { @@ -1426,7 +1429,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { e.files // Attempt to fill in stub content for classes and routines that // are not server-side files and were not created due to an export - .filter((f) => notIsfs(f) && isClassOrRtn(f) && !exportedUris.has(f.toString())) + .filter((f) => notIsfs(f) && isClassOrRtn(f.path) && !exportedUris.has(f.toString())) .forEach(async (uri) => { // Need to wait in case file was created using "Save As..." // because in that case the file gets created without diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 9d52aba6..4224c23b 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -5,7 +5,6 @@ import { RateLimiter, currentFileFromContent, exportedUris, - getServerDocName, isClassOrRtn, isImportableLocalFile, notIsfs, @@ -23,9 +22,9 @@ import { sendClientSideSyncTelemetryEvent } from "../extension"; interface WSFolderIndex { /** The `FileSystemWatcher` for this workspace folder */ watcher: vscode.FileSystemWatcher; - /** Map of InterSystems classes and routines in this workspace to their `Uri`s */ + /** Map of document names (i.e., server-side names) to VSCode URIs */ documents: Map; - /** Map of stringified `Uri`s to their InterSystems class/routine name */ + /** Map of VSCode URIs to document names */ uris: Map; } @@ -180,8 +179,8 @@ function outputDelete(docName: string, status: string, ts: string): number { /** Create index of `wsFolder` and set up a `FileSystemWatcher` to keep the index up to date */ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Promise { if (!notIsfs(wsFolder.uri)) return; - const documents: Map = new Map(); - const uris: Map = new Map(); + const documents: WSFolderIndex["documents"] = new Map(); + const uris: WSFolderIndex["uris"] = 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() @@ -190,26 +189,32 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // A cache of the last time each file was last changed const lastChangeMtimes: Map = new Map(); // Index classes and routines that currently exist - vscode.workspace - .findFiles(new vscode.RelativePattern(wsFolder, "{**/*.cls,**/*.mac,**/*.int,**/*.inc}")) - .then((files) => files.forEach((file) => fsRateLimiter.call(() => updateIndexForDocument(file, documents, uris)))); + vscode.workspace.findFiles(new vscode.RelativePattern(wsFolder, "{**/*}")).then((files) => + files.forEach((file) => + fsRateLimiter.call(() => { + if (isClassOrRtn(file.path) || isImportableLocalFile(file)) { + return updateIndexInternal(file, documents, uris, true); + } + }) + ) + ); // Watch for all file changes const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(wsFolder, "**/*")); const debouncedCompile = generateCompileFn(); const debouncedDelete = generateDeleteFn(wsFolder.uri); - const updateIndexAndSyncChanges = async (uri: vscode.Uri, created = false): Promise => { - if (uri.scheme != wsFolder.uri.scheme) { - // We don't care about virtual files that might be - // part of the workspace folder, like "git" files - return; - } - if (vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() != wsFolder.uri.toString()) { - // This file is not in this workspace folder. This can occur if there - // are two workspace folders open where one is a subfolder of the other - // and the file being changed is in the subfolder. This event will fire - // for both watchers, but VS Code will correctly report that the file - // is in the subfolder workspace folder, so the parent watcher can - // safely ignore the event. + const notToSync = (uri: vscode.Uri) => + // We don't care about virtual files that might be + // part of the workspace folder, like "git" files + uri.scheme != wsFolder.uri.scheme || + // This file is not in this workspace folder. This can occur if there + // are two workspace folders open where one is a subfolder of the other + // and the file being changed is in the subfolder. This event will fire + // for both watchers, but VS Code will correctly report that the file + // is in the subfolder workspace folder, so the parent watcher can + // safely ignore the event. + vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() != wsFolder.uri.toString(); + async function syncChange(uri: vscode.Uri, created = false): Promise { + if (notToSync(uri)) { return; } if (!uri.path.split("/").pop().includes(".")) { @@ -253,15 +258,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr const vscodeChange = touchedByVSCode.has(uriString); const sync = api.active && (syncLocalChanges == "all" || (syncLocalChanges == "vscodeOnly" && vscodeChange)); touchedByVSCode.delete(uriString); - let change: WSFolderIndexChange = {}; - if (isClassOrRtn(uri)) { - change = await updateIndexForDocument(uri, documents, uris); - } else if (sync && isImportableLocalFile(uri)) { - change.addedOrChanged = await getCurrentFile(uri); - if (change.addedOrChanged?.fileName) { - sendClientSideSyncTelemetryEvent(change.addedOrChanged.fileName.split(".").pop().toLowerCase()); - } - } + const change = await updateIndexInternal(uri, documents, uris, sync); if (!sync || (!change.addedOrChanged && !change.removed)) return; if (change.addedOrChanged) { // Create or update the document on the server @@ -286,22 +283,9 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // Delete document on the server debouncedDelete(change.removed); } - }; - watcher.onDidChange((uri) => restRateLimiter.call(() => updateIndexAndSyncChanges(uri))); - watcher.onDidCreate((uri) => restRateLimiter.call(() => updateIndexAndSyncChanges(uri, true))); - watcher.onDidDelete((uri) => { - if (uri.scheme != wsFolder.uri.scheme) { - // We don't care about virtual files that might be - // part of the workspace folder, like "git" files - return; - } - if (vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() != wsFolder.uri.toString()) { - // This file is not in this workspace folder. This can occur if there - // are two workspace folders open where one is a subfolder of the other - // and the file being changed is in the subfolder. This event will fire - // for both watchers, but VS Code will correctly report that the file - // is in the subfolder workspace folder, so the parent watcher can - // safely ignore the event. + } + function syncDelete(uri: vscode.Uri): void { + if (notToSync(uri)) { return; } const uriString = uri.toString(); @@ -317,21 +301,19 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (!uriIsParentOf(uri, subUri)) { continue; } - if (isClassOrRtn(subUri)) { - // Remove the class/routine in the file from the index, + if (sync && (isClassOrRtn(subUri.path) || isImportableLocalFile(subUri))) { + // Remove the class/routine, web application file, or Studio abstract document from the index, // then delete it on the server if required const change = removeDocumentFromIndex(subUri, documents, uris); - if (sync && change.removed) { + if (change.removed) { debouncedDelete(change.removed); } - } else if (sync && isImportableLocalFile(subUri)) { - // Delete this web application file or Studio abstract document on the server - const docName = getServerDocName(subUri); - if (!docName) return; - debouncedDelete(docName); } } - }); + } + watcher.onDidChange((uri) => restRateLimiter.call(() => syncChange(uri))); + watcher.onDidCreate((uri) => restRateLimiter.call(() => syncChange(uri, true))); + watcher.onDidDelete(syncDelete); wsFolderIndex.set(wsFolder.uri.toString(), { watcher, documents, uris }); } @@ -348,46 +330,50 @@ export function removeIndexOfWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): * 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( +export async function updateIndex(uri: vscode.Uri, content?: string[] | Buffer): Promise { + const wsFolder = vscode.workspace.getWorkspaceFolder(uri); + if (!wsFolder) return {}; + const index = wsFolderIndex.get(wsFolder.uri.toString()); + if (!index) return {}; + return updateIndexInternal(uri, index.documents, index.uris, true, content); +} + +async function updateIndexInternal( uri: vscode.Uri, - documents?: Map, - uris?: Map, + documents: WSFolderIndex["documents"], + uris: WSFolderIndex["uris"], + sync: boolean, content?: string[] | Buffer ): Promise { const result: WSFolderIndexChange = {}; const uriString = uri.toString(); - if (!documents) { - const wsFolder = vscode.workspace.getWorkspaceFolder(uri); - if (!wsFolder) return result; - const index = wsFolderIndex.get(wsFolder.uri.toString()); - if (!index) return result; - documents = index.documents; - uris = index.uris; - } - const documentName = uris.get(uriString); 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 - 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); - if (documentName) { - // Remove the outdated reference - const oldDocumentUris = documents.get(documentName); - if (!oldDocumentUris) return result; - const idx = oldDocumentUris.findIndex((f) => f.toString() == uriString); - if (idx == -1) return result; - if (documentUris.length > 1) { - documentUris.splice(idx, 1); - documents.set(documentName, documentUris); - } else { - documents.delete(documentName); - result.removed = documentName; - } + if (isImportableLocalFile(uri) && sync) { + sendClientSideSyncTelemetryEvent(file.fileName.split(".").pop().toLowerCase()); + } + const documentUris = documents.get(file.name) ?? []; + if (documentUris.some((u) => u.toString() == uriString)) { + // No need to update the index since this document is already present + return result; + } + documentUris.push(uri); + documents.set(file.name, documentUris); + const oldDocumentName = uris.get(uriString); + uris.set(uriString, file.name); + if (oldDocumentName) { + // Remove the outdated reference + const oldDocumentUris = documents.get(oldDocumentName); + if (!oldDocumentUris) return result; + const idx = oldDocumentUris.findIndex((f) => f.toString() == uriString); + if (idx == -1) return result; + if (documentUris.length > 1) { + documentUris.splice(idx, 1); + documents.set(oldDocumentName, documentUris); + } else { + documents.delete(oldDocumentName); + result.removed = oldDocumentName; } } return result; @@ -396,19 +382,11 @@ export async function updateIndexForDocument( /** Remove the entries in the index for `uri` */ function removeDocumentFromIndex( uri: vscode.Uri, - documents?: Map, - uris?: Map + documents: Map, + uris: Map ): WSFolderIndexChange { const result: WSFolderIndexChange = {}; const uriString = uri.toString(); - if (!documents) { - const wsFolder = vscode.workspace.getWorkspaceFolder(uri); - if (!wsFolder) return result; - const index = wsFolderIndex.get(wsFolder.uri.toString()); - if (!index) return result; - documents = index.documents; - uris = index.uris; - } const documentName = uris.get(uriString); if (!documentName) return result; // Remove it from the index diff --git a/src/utils/index.ts b/src/utils/index.ts index 6a2eafb6..4311b50e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -229,7 +229,7 @@ export function currentFileFromContent(uri: vscode.Uri, content: string | Buffer const fileExt = fileName.split(".").pop().toLowerCase(); if ( notIsfs(uri) && - !isClassOrRtn(uri) && + !isClassOrRtn(uri.path) && // This is a non-class or routine local file, so check if we can import it !isImportableLocalFile(uri) ) { @@ -292,7 +292,7 @@ export function currentFile(document?: vscode.TextDocument): CurrentTextFile { const fileExt = fileName.split(".").pop().toLowerCase(); if ( notIsfs(document.uri) && - !isClassOrRtn(document.uri) && + !isClassOrRtn(document.uri.path) && // This is a non-class or routine local file, so check if we can import it !isImportableLocalFile(document.uri) ) { @@ -887,10 +887,8 @@ export function base64EncodeContent(content: Buffer): string[] { } /** 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() - ); +export function isClassOrRtn(uriOrName: string): boolean { + return ["cls", "mac", "int", "inc"].includes(uriOrName.split(".").pop().toLowerCase()); } interface ConnQPItem extends vscode.QuickPickItem { From 21334621107106ea63f8790a549ada8109fa0026 Mon Sep 17 00:00:00 2001 From: klu Date: Tue, 3 Feb 2026 18:20:37 -0500 Subject: [PATCH 06/14] reduce diffs --- src/utils/documentIndex.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 4224c23b..8908757d 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -360,20 +360,20 @@ async function updateIndexInternal( } documentUris.push(uri); documents.set(file.name, documentUris); - const oldDocumentName = uris.get(uriString); + const documentName = uris.get(uriString); uris.set(uriString, file.name); - if (oldDocumentName) { + if (documentName) { // Remove the outdated reference - const oldDocumentUris = documents.get(oldDocumentName); + const oldDocumentUris = documents.get(documentName); if (!oldDocumentUris) return result; const idx = oldDocumentUris.findIndex((f) => f.toString() == uriString); if (idx == -1) return result; if (documentUris.length > 1) { documentUris.splice(idx, 1); - documents.set(oldDocumentName, documentUris); + documents.set(documentName, documentUris); } else { - documents.delete(oldDocumentName); - result.removed = oldDocumentName; + documents.delete(documentName); + result.removed = documentName; } } return result; From 0471def53522f9c54408babc6dff4ee6fa29aaf5 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 09:44:40 -0500 Subject: [PATCH 07/14] resolve comments --- src/commands/compile.ts | 2 +- src/utils/documentIndex.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 3b81ae35..ef95ca52 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -246,7 +246,7 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] // Re-throw the error throw e; }); - if (isClassOrRtn(file.uri.path) || isImportableLocalFile(file.uri)) { + if (isClassOrRtn(file.uri.path)) { // Update the document index updateIndex(file.uri, content); } diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 8908757d..77bdf168 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -213,7 +213,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr // is in the subfolder workspace folder, so the parent watcher can // safely ignore the event. vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() != wsFolder.uri.toString(); - async function syncChange(uri: vscode.Uri, created = false): Promise { + async function updateIndexAndSyncChange(uri: vscode.Uri, created = false): Promise { if (notToSync(uri)) { return; } @@ -284,7 +284,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr debouncedDelete(change.removed); } } - function syncDelete(uri: vscode.Uri): void { + function updateIndexAndSyncDelete(uri: vscode.Uri): void { if (notToSync(uri)) { return; } @@ -301,7 +301,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (!uriIsParentOf(uri, subUri)) { continue; } - if (sync && (isClassOrRtn(subUri.path) || isImportableLocalFile(subUri))) { + if (sync) { // Remove the class/routine, web application file, or Studio abstract document from the index, // then delete it on the server if required const change = removeDocumentFromIndex(subUri, documents, uris); @@ -311,9 +311,9 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr } } } - watcher.onDidChange((uri) => restRateLimiter.call(() => syncChange(uri))); - watcher.onDidCreate((uri) => restRateLimiter.call(() => syncChange(uri, true))); - watcher.onDidDelete(syncDelete); + watcher.onDidChange((uri) => restRateLimiter.call(() => updateIndexAndSyncChange(uri))); + watcher.onDidCreate((uri) => restRateLimiter.call(() => updateIndexAndSyncChange(uri, true))); + watcher.onDidDelete(updateIndexAndSyncDelete); wsFolderIndex.set(wsFolder.uri.toString(), { watcher, documents, uris }); } From 7067e8c6d93a32f649fc987e7e658399f6d75b3e Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 09:47:23 -0500 Subject: [PATCH 08/14] quick fix --- src/commands/compile.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index ef95ca52..1a4281d7 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -26,7 +26,6 @@ import { handleError, isClassDeployed, isClassOrRtn, - isImportableLocalFile, isCompilable, lastUsedLocalUri, notIsfs, From 34c51120237c6bb2c5bcbb6628b8f4c6aa7bf751 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 14:55:31 -0500 Subject: [PATCH 09/14] fix an uri check bug and rename an util --- src/commands/unitTest.ts | 11 +++++------ src/utils/documentIndex.ts | 4 ++-- src/utils/index.ts | 9 +++++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index bebd162b..63545666 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -8,7 +8,8 @@ import { notIsfs, displayableUri, stripClassMemberNameQuotes, - uriIsParentOf, + uriIsAncestorOf, + uriContains, } from "../utils"; import { fileSpecFromURI, isfsConfig } from "../utils/FileProviderUtil"; import { AtelierAPI } from "../api"; @@ -62,9 +63,8 @@ const textDecoder = new TextDecoder(); /** Find the root `TestItem` for `uri` */ function rootItemForItem(testController: vscode.TestController, uri: vscode.Uri): vscode.TestItem | undefined { let rootItem: vscode.TestItem; - const uriString = uri.toString(); for (const [, i] of testController.items) { - if (uriIsParentOf(i.uri, uri) || uriString == i.uri.toString()) { + if (uriContains(i.uri, uri)) { rootItem = i; break; } @@ -422,10 +422,9 @@ async function runHandler( // Add the initial items to the queue to process const queue: vscode.TestItem[] = []; - const rootUriString = root.uri.toString(); if (request.include?.length) { request.include.forEach((i) => { - if (uriIsParentOf(root.uri, i.uri) || i.uri.toString() == rootUriString) { + if (uriContains(root.uri, i.uri)) { queue.push(i); } }); @@ -1082,7 +1081,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Update root items if needed e.removed.forEach((wf) => { testController.items.forEach((i) => { - if (uriIsParentOf(wf.uri, i.uri)) { + if (uriIsAncestorOf(wf.uri, i.uri)) { // Remove this TestItem classesForRoot.delete(i); testController.items.delete(i.id); diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 77bdf168..81a273be 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -12,8 +12,8 @@ import { outputChannel, displayableUri, isCompilable, + uriContains, } from "."; -import { uriIsParentOf } from "../utils"; import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; import { compile, importFile } from "../commands/compile"; @@ -298,7 +298,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr for (const subUriString of uris.keys()) { touchedByVSCode.delete(subUriString); const subUri = vscode.Uri.parse(subUriString); - if (!uriIsParentOf(uri, subUri)) { + if (!uriContains(uri, subUri)) { continue; } if (sync) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 4311b50e..4fdbe69a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -801,8 +801,8 @@ export function parseClassMemberDefinition( }; } -/** Returns `true` if `uri1` is a parent of `uri2`. */ -export function uriIsParentOf(uri1: vscode.Uri, uri2: vscode.Uri): boolean { +/** Returns `true` if `uri1` is an ancestor of `uri2`. */ +export function uriIsAncestorOf(uri1: vscode.Uri, uri2: vscode.Uri): boolean { uri1 = uri1.with({ path: !uri1.path.endsWith("/") ? `${uri1.path}/` : uri1.path }); return ( uri2 @@ -814,6 +814,11 @@ export function uriIsParentOf(uri1: vscode.Uri, uri2: vscode.Uri): boolean { ); } +/** Returns `true` if `uri1` is exactly `uri2` or an ancestor of it: */ +export function uriContains(uri1: vscode.Uri, uri2: vscode.Uri): boolean { + return uri1.toString() == uri2.toString() || uriIsAncestorOf(uri1, uri2); +} + /** Get the text of file `uri`. Works for all file systems and the `objectscript` `DocumentContentProvider`. */ export async function getFileText(uri: vscode.Uri): Promise { if (uri.scheme == OBJECTSCRIPT_FILE_SCHEMA) { From 24b7edf1e52f194c5a0ff835ab786df86bcb8f33 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 15:28:09 -0500 Subject: [PATCH 10/14] uriContains --- src/commands/unitTest.ts | 3 +-- src/utils/index.ts | 21 +++++++-------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index 63545666..1c2fad82 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -8,7 +8,6 @@ import { notIsfs, displayableUri, stripClassMemberNameQuotes, - uriIsAncestorOf, uriContains, } from "../utils"; import { fileSpecFromURI, isfsConfig } from "../utils/FileProviderUtil"; @@ -1081,7 +1080,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Update root items if needed e.removed.forEach((wf) => { testController.items.forEach((i) => { - if (uriIsAncestorOf(wf.uri, i.uri)) { + if (uriContains(wf.uri, i.uri)) { // Remove this TestItem classesForRoot.delete(i); testController.items.delete(i.id); diff --git a/src/utils/index.ts b/src/utils/index.ts index 4fdbe69a..09c7d787 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -801,24 +801,17 @@ export function parseClassMemberDefinition( }; } -/** Returns `true` if `uri1` is an ancestor of `uri2`. */ -export function uriIsAncestorOf(uri1: vscode.Uri, uri2: vscode.Uri): boolean { - uri1 = uri1.with({ path: !uri1.path.endsWith("/") ? `${uri1.path}/` : uri1.path }); +/** Returns `true` if `uri1` is equal to or an ancestor of `uri2`. */ +export function uriContains(uri1: vscode.Uri, uri2: vscode.Uri): boolean { return ( - uri2 - .with({ query: "", fragment: "" }) - .toString() - .startsWith(uri1.with({ query: "", fragment: "" }).toString()) && - uri1.query == uri2.query && - uri1.fragment == uri2.fragment + // All non-path components are identical. + uri1.with({ path: "" }).toString == uri2.with({ path: "" }).toString && + // uri2.path "properly" starts with uri1.path. + uri2.path.startsWith(uri1.path) && + (uri1.path.endsWith("/") || ["", "/"].includes(uri2.path.slice(uri1.path.length, uri1.path.length + 1))) ); } -/** Returns `true` if `uri1` is exactly `uri2` or an ancestor of it: */ -export function uriContains(uri1: vscode.Uri, uri2: vscode.Uri): boolean { - return uri1.toString() == uri2.toString() || uriIsAncestorOf(uri1, uri2); -} - /** Get the text of file `uri`. Works for all file systems and the `objectscript` `DocumentContentProvider`. */ export async function getFileText(uri: vscode.Uri): Promise { if (uri.scheme == OBJECTSCRIPT_FILE_SCHEMA) { From b2f06e8a6a4ff3c7f89e36ae850f3dcee9fdd52b Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 15:49:50 -0500 Subject: [PATCH 11/14] comment --- src/utils/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index 09c7d787..255ffc36 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -801,10 +801,11 @@ export function parseClassMemberDefinition( }; } -/** Returns `true` if `uri1` is equal to or an ancestor of `uri2`. */ +/** Returns `true` if `uri1` is equal to or an ancestor of `uri2`. + * Non-path components (e.g., scheme, fragment, and query) must be identical. + */ export function uriContains(uri1: vscode.Uri, uri2: vscode.Uri): boolean { return ( - // All non-path components are identical. uri1.with({ path: "" }).toString == uri2.with({ path: "" }).toString && // uri2.path "properly" starts with uri1.path. uri2.path.startsWith(uri1.path) && From 240e3d583c6d189d457234999573f0e6d9d65b22 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 16:54:29 -0500 Subject: [PATCH 12/14] rename to uriStartsWith --- src/commands/unitTest.ts | 8 ++++---- src/utils/documentIndex.ts | 4 ++-- src/utils/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index 1c2fad82..e3c8f14f 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -8,7 +8,7 @@ import { notIsfs, displayableUri, stripClassMemberNameQuotes, - uriContains, + uriStartsWith, } from "../utils"; import { fileSpecFromURI, isfsConfig } from "../utils/FileProviderUtil"; import { AtelierAPI } from "../api"; @@ -63,7 +63,7 @@ const textDecoder = new TextDecoder(); function rootItemForItem(testController: vscode.TestController, uri: vscode.Uri): vscode.TestItem | undefined { let rootItem: vscode.TestItem; for (const [, i] of testController.items) { - if (uriContains(i.uri, uri)) { + if (uriStartsWith(i.uri, uri)) { rootItem = i; break; } @@ -423,7 +423,7 @@ async function runHandler( const queue: vscode.TestItem[] = []; if (request.include?.length) { request.include.forEach((i) => { - if (uriContains(root.uri, i.uri)) { + if (uriStartsWith(root.uri, i.uri)) { queue.push(i); } }); @@ -1080,7 +1080,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Update root items if needed e.removed.forEach((wf) => { testController.items.forEach((i) => { - if (uriContains(wf.uri, i.uri)) { + if (uriStartsWith(wf.uri, i.uri)) { // Remove this TestItem classesForRoot.delete(i); testController.items.delete(i.id); diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 81a273be..03841d1f 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -12,7 +12,7 @@ import { outputChannel, displayableUri, isCompilable, - uriContains, + uriStartsWith, } from "."; import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; @@ -298,7 +298,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr for (const subUriString of uris.keys()) { touchedByVSCode.delete(subUriString); const subUri = vscode.Uri.parse(subUriString); - if (!uriContains(uri, subUri)) { + if (!uriStartsWith(uri, subUri)) { continue; } if (sync) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 255ffc36..5755d47c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -804,7 +804,7 @@ export function parseClassMemberDefinition( /** Returns `true` if `uri1` is equal to or an ancestor of `uri2`. * Non-path components (e.g., scheme, fragment, and query) must be identical. */ -export function uriContains(uri1: vscode.Uri, uri2: vscode.Uri): boolean { +export function uriStartsWith(uri1: vscode.Uri, uri2: vscode.Uri): boolean { return ( uri1.with({ path: "" }).toString == uri2.with({ path: "" }).toString && // uri2.path "properly" starts with uri1.path. From e2c0ddd4165ef72ba9fda4eb6e0c9633165db8cc Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 16:58:09 -0500 Subject: [PATCH 13/14] .at is shorter than .slice --- src/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index 5755d47c..303f7f36 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -809,7 +809,7 @@ export function uriStartsWith(uri1: vscode.Uri, uri2: vscode.Uri): boolean { uri1.with({ path: "" }).toString == uri2.with({ path: "" }).toString && // uri2.path "properly" starts with uri1.path. uri2.path.startsWith(uri1.path) && - (uri1.path.endsWith("/") || ["", "/"].includes(uri2.path.slice(uri1.path.length, uri1.path.length + 1))) + (uri1.path.endsWith("/") || [undefined, "/"].includes(uri2.path.at(uri1.path.length))) ); } From 07b3fc40383fbf010faa4db3d4e14e0180386f02 Mon Sep 17 00:00:00 2001 From: klu Date: Wed, 4 Feb 2026 17:19:43 -0500 Subject: [PATCH 14/14] rename to uriIsAncestorOf --- src/commands/unitTest.ts | 8 ++++---- src/utils/documentIndex.ts | 4 ++-- src/utils/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index e3c8f14f..078be2c0 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -8,7 +8,7 @@ import { notIsfs, displayableUri, stripClassMemberNameQuotes, - uriStartsWith, + uriIsAncestorOf, } from "../utils"; import { fileSpecFromURI, isfsConfig } from "../utils/FileProviderUtil"; import { AtelierAPI } from "../api"; @@ -63,7 +63,7 @@ const textDecoder = new TextDecoder(); function rootItemForItem(testController: vscode.TestController, uri: vscode.Uri): vscode.TestItem | undefined { let rootItem: vscode.TestItem; for (const [, i] of testController.items) { - if (uriStartsWith(i.uri, uri)) { + if (uriIsAncestorOf(i.uri, uri)) { rootItem = i; break; } @@ -423,7 +423,7 @@ async function runHandler( const queue: vscode.TestItem[] = []; if (request.include?.length) { request.include.forEach((i) => { - if (uriStartsWith(root.uri, i.uri)) { + if (uriIsAncestorOf(root.uri, i.uri)) { queue.push(i); } }); @@ -1080,7 +1080,7 @@ export function setUpTestController(context: vscode.ExtensionContext): vscode.Di // Update root items if needed e.removed.forEach((wf) => { testController.items.forEach((i) => { - if (uriStartsWith(wf.uri, i.uri)) { + if (uriIsAncestorOf(wf.uri, i.uri)) { // Remove this TestItem classesForRoot.delete(i); testController.items.delete(i.id); diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 03841d1f..19614fa3 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -12,7 +12,7 @@ import { outputChannel, displayableUri, isCompilable, - uriStartsWith, + uriIsAncestorOf, } from "."; import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; @@ -298,7 +298,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr for (const subUriString of uris.keys()) { touchedByVSCode.delete(subUriString); const subUri = vscode.Uri.parse(subUriString); - if (!uriStartsWith(uri, subUri)) { + if (!uriIsAncestorOf(uri, subUri)) { continue; } if (sync) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 303f7f36..58d4828a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -804,7 +804,7 @@ export function parseClassMemberDefinition( /** Returns `true` if `uri1` is equal to or an ancestor of `uri2`. * Non-path components (e.g., scheme, fragment, and query) must be identical. */ -export function uriStartsWith(uri1: vscode.Uri, uri2: vscode.Uri): boolean { +export function uriIsAncestorOf(uri1: vscode.Uri, uri2: vscode.Uri): boolean { return ( uri1.with({ path: "" }).toString == uri2.with({ path: "" }).toString && // uri2.path "properly" starts with uri1.path.