From 8fdd45ca0b684578ecdbc186fe356e0ecae15687 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 4 Mar 2026 15:46:18 -0500 Subject: [PATCH 01/14] draft --- src/api/index.ts | 7 ++++++- src/commands/compile.ts | 29 ++++++++++++++++++++++++++++- src/utils/index.ts | 5 +++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index c8ee4eaa..9fb6d19c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -626,7 +626,7 @@ export class AtelierAPI { } // api v1+ - public getDoc(name: string, scope: vscode.Uri | string, mtime?: number): Promise> { + public getDoc(name: string, scope: vscode.Uri | string, mtime?: number, storageOnly: boolean = false): Promise> { let params, headers; name = this.transformNameIfCsp(name); if ( @@ -642,6 +642,11 @@ export class AtelierAPI { .get("multilineMethodArgs") ) { params = { format: "udl-multiline" }; + } else { + params = {} + } + if (storageOnly) { + params["storageOnly"] = "1" } if (mtime && mtime > 0) { headers = { "IF-NONE-MATCH": new Date(mtime).toISOString().replace(/T|Z/g, " ").trim() }; diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 10cf6a95..a6ac5c09 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -24,6 +24,7 @@ import { exportedUris, getWsFolder, handleError, + isClass, isClassDeployed, isClassOrRtn, isCompilable, @@ -37,6 +38,7 @@ import { import { StudioActions } from "./studio"; import { NodeBase, PackageNode, RootNode } from "../explorer/nodes"; import { getUrisForDocument, updateIndex } from "../utils/documentIndex"; +import { Document } from "../api/atelier"; /** * For files being locally edited, get and return its mtime timestamp from workspace-state cache if present there, @@ -218,7 +220,32 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] const mtime = Number(new Date(doc.ts + "Z")); workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined); if (notIsfs(file.uri)) { - const content = await api.getDoc(file.name, file.uri).then((data) => data.result.content); + let content: Document["content"]; + if (isClass(file.uri.path)) { + // Insert/update the storage part of class definition. + content = new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); + let storageBegin = -1; + let storageEnd = -1; + let classEnd; + for (let i = 0; i < content.length; i ++) { + if (content[i].startsWith("")) { + storageEnd = i; + } else if (content[i].startsWith("}")) { + classEnd = i; + } + } + let storage = (await api.getDoc(file.name, file.uri, undefined, true)).result.content; + storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/g) : storage; + if ((0 <= storageBegin) && (storageBegin < storageEnd)) { + content.splice(storageBegin, storageEnd - storageEnd, ...storage) + } else { + content.splice(classEnd, 0, ...storage) + } + } else { + content = (await api.getDoc(file.name, file.uri)).result.content; + } exportedUris.add(file.uri.toString()); // Set optimistically await vscode.workspace.fs .writeFile( diff --git a/src/utils/index.ts b/src/utils/index.ts index e95d90fd..e9141e51 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -885,6 +885,11 @@ export function base64EncodeContent(content: Buffer): string[] { return result; } +/** Returns `true` if `uri` has a class file extension */ +export function isClass(uriOrName: string): boolean { + return "cls" == uriOrName.split(".").pop().toLowerCase(); +} + /** Returns `true` if `uri` has a class or routine file extension */ export function isClassOrRtn(uriOrName: string): boolean { return ["cls", "mac", "int", "inc"].includes(uriOrName.split(".").pop().toLowerCase()); From 453936c4afe1f3e3b19c209ea38898ba740799fb Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 4 Mar 2026 16:17:48 -0500 Subject: [PATCH 02/14] final clean --- src/commands/compile.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index a6ac5c09..3ceeda4e 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -224,13 +224,13 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] if (isClass(file.uri.path)) { // Insert/update the storage part of class definition. content = new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); - let storageBegin = -1; - let storageEnd = -1; - let classEnd; + let storageBegin: number; // the last "Storage ..." line + let storageEnd: number; // the first "}" after storageBegin + let classEnd: number; // the last "}" for (let i = 0; i < content.length; i ++) { - if (content[i].startsWith("")) { + } else if ((storageBegin !== undefined) && (storageEnd === undefined) && content[i].startsWith("}")) { storageEnd = i; } else if (content[i].startsWith("}")) { classEnd = i; @@ -238,8 +238,12 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] } let storage = (await api.getDoc(file.name, file.uri, undefined, true)).result.content; storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/g) : storage; - if ((0 <= storageBegin) && (storageBegin < storageEnd)) { - content.splice(storageBegin, storageEnd - storageEnd, ...storage) + if ((storageBegin && storageEnd) !== undefined) { + // when replacing an existing storage definition, we don't need extra empty lines (if any). + while (storage[storage.length-1] == "") { + storage.pop() + } + content.splice(storageBegin, 1 + storageEnd - storageBegin, ...storage) } else { content.splice(classEnd, 0, ...storage) } From 2488e6b2a74f1b965b58f02ab316a91822dbd6d7 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 4 Mar 2026 16:28:35 -0500 Subject: [PATCH 03/14] boring case first --- src/commands/compile.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 3ceeda4e..3ce2b41e 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -221,7 +221,9 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined); if (notIsfs(file.uri)) { let content: Document["content"]; - if (isClass(file.uri.path)) { + if (!isClass(file.uri.path)) { + content = (await api.getDoc(file.name, file.uri)).result.content; + } else { // Insert/update the storage part of class definition. content = new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); let storageBegin: number; // the last "Storage ..." line @@ -247,8 +249,6 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] } else { content.splice(classEnd, 0, ...storage) } - } else { - content = (await api.getDoc(file.name, file.uri)).result.content; } exportedUris.add(file.uri.toString()); // Set optimistically await vscode.workspace.fs From a68c192c773bf51a18dc5a13453e1a022228c828 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 4 Mar 2026 16:31:14 -0500 Subject: [PATCH 04/14] lint --- src/api/index.ts | 11 ++++++++--- src/commands/compile.ts | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 9fb6d19c..6935d5b1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -626,7 +626,12 @@ export class AtelierAPI { } // api v1+ - public getDoc(name: string, scope: vscode.Uri | string, mtime?: number, storageOnly: boolean = false): Promise> { + public getDoc( + name: string, + scope: vscode.Uri | string, + mtime?: number, + storageOnly: boolean = false + ): Promise> { let params, headers; name = this.transformNameIfCsp(name); if ( @@ -643,10 +648,10 @@ export class AtelierAPI { ) { params = { format: "udl-multiline" }; } else { - params = {} + params = {}; } if (storageOnly) { - params["storageOnly"] = "1" + params["storageOnly"] = "1"; } if (mtime && mtime > 0) { headers = { "IF-NONE-MATCH": new Date(mtime).toISOString().replace(/T|Z/g, " ").trim() }; diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 3ce2b41e..06ec726c 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -225,14 +225,14 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] content = (await api.getDoc(file.name, file.uri)).result.content; } else { // Insert/update the storage part of class definition. - content = new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); - let storageBegin: number; // the last "Storage ..." line - let storageEnd: number; // the first "}" after storageBegin - let classEnd: number; // the last "}" - for (let i = 0; i < content.length; i ++) { + content = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); + let storageBegin: number; // the last "Storage ..." line + let storageEnd: number; // the first "}" after storageBegin + let classEnd: number; // the last "}" + for (let i = 0; i < content.length; i++) { if (content[i].startsWith("Storage ")) { storageBegin = i; - } else if ((storageBegin !== undefined) && (storageEnd === undefined) && content[i].startsWith("}")) { + } else if (storageBegin !== undefined && storageEnd === undefined && content[i].startsWith("}")) { storageEnd = i; } else if (content[i].startsWith("}")) { classEnd = i; @@ -242,12 +242,12 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/g) : storage; if ((storageBegin && storageEnd) !== undefined) { // when replacing an existing storage definition, we don't need extra empty lines (if any). - while (storage[storage.length-1] == "") { - storage.pop() + while (storage[storage.length - 1] == "") { + storage.pop(); } - content.splice(storageBegin, 1 + storageEnd - storageBegin, ...storage) + content.splice(storageBegin, 1 + storageEnd - storageBegin, ...storage); } else { - content.splice(classEnd, 0, ...storage) + content.splice(classEnd, 0, ...storage); } } exportedUris.add(file.uri.toString()); // Set optimistically From efda009bfdf5e0fb604dff034cdf73f90041c476 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Thu, 5 Mar 2026 09:55:08 -0500 Subject: [PATCH 05/14] Brett's comments --- package.json | 7 ++++++- src/commands/compile.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3d19e625..71a6029d 100644 --- a/package.json +++ b/package.json @@ -1416,6 +1416,11 @@ "type": "boolean", "default": true }, + "objectscript.refreshClassesOnSync": { + "description": "Automatically refresh classes after any save or change by compilation", + "type": "boolean", + "default": false + }, "objectscript.multilineMethodArgs": { "markdownDescription": "List method arguments on multiple lines, if the server supports it. **NOTE:** Only supported on IRIS 2019.1.2, 2020.1.1+, 2021.1.0+ and subsequent versions! On all other versions, this setting will have no effect.", "type": "boolean", @@ -1756,4 +1761,4 @@ "extensionDependencies": [ "intersystems-community.servermanager" ] -} +} \ No newline at end of file diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 06ec726c..22c266c5 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -221,7 +221,12 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined); if (notIsfs(file.uri)) { let content: Document["content"]; - if (!isClass(file.uri.path)) { + if ( + !( + isClass(file.uri.path) && + !vscode.workspace.getConfiguration("objectscript", file.uri).get("refreshClassesOnSync") + ) + ) { content = (await api.getDoc(file.name, file.uri)).result.content; } else { // Insert/update the storage part of class definition. From 590b13831bc9b5b672e39d27b6e7c7acd671d6e9 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Thu, 5 Mar 2026 15:06:59 -0500 Subject: [PATCH 06/14] server hook --- src/api/index.ts | 14 ++- src/commands/compile.ts | 197 +++++++++++++++++++++---------------- src/utils/documentIndex.ts | 28 +++--- 3 files changed, 141 insertions(+), 98 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 6935d5b1..6fa94799 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -674,7 +674,19 @@ export class AtelierAPI { name: string, data: { enc: boolean; content: string[]; mtime: number }, ignoreConflict?: boolean - ): Promise { + ): Promise< + Atelier.Response<{ + name: string; + db: string; + ts: string; + cat: "CLS" | "CSP" | "RTN" | "OTH"; + status: string; + enc: boolean; + flag?: "1"; + content: string[]; + ext: any; + }> + > { const params = { ignoreConflict }; name = this.transformNameIfCsp(name); const headers = {}; diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 22c266c5..b7e0fa42 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -82,6 +82,7 @@ export async function checkChangedOnServer(file: CurrentTextFile | CurrentBinary return mtime; } +// Synchronize the client version and the server version of the same file export async function importFile( file: CurrentTextFile | CurrentBinaryFile, ignoreConflict?: boolean, @@ -116,8 +117,8 @@ export async function importFile( mtime < 0 || (notIsfs(file.uri) && vscode.workspace.getConfiguration("objectscript", file.uri).get("overwriteServerChanges")); - return api - .putDoc( + try { + const data = await api.putDoc( file.name, { content, @@ -125,69 +126,74 @@ export async function importFile( mtime, }, ignoreConflict - ) - .then((data) => { - // Update cache entry - workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); - - // In case another extension has used an 'objectscript://' uri to load a document read-only from the server, - // make it reload with what we just imported to the server. - const serverUri = DocumentContentProvider.getUri( - file.name, - file.workspaceFolder, - undefined, - false, - undefined, - true + ); + workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); + if (data.result.flag === "1") { + // putDoc returns new Storage definitions and the file must be a CLS + let content: string[]; + (content = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g)), + (content = updateStorage(content, data.result.content)); + await vscode.workspace.fs.writeFile( + file.uri, + new TextEncoder().encode( + content.join(((file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n") + ) ); - documentContentProvider.update(serverUri.with({ scheme: OBJECTSCRIPT_FILE_SCHEMA })); - }) - .catch((error) => { - if (error?.statusCode == 409) { - const choices: string[] = []; - if (!enc) { - choices.push("Compare"); - } - choices.push("Overwrite on Server", "Pull Server Changes", "Cancel"); - return vscode.window - .showErrorMessage( - `Failed to import '${file.name}': The version of the file on the server is newer. + } + // In case another extension has used an 'objectscript://' uri to load a document read-only from the server, + // make it reload with what we just imported to the server. + const serverUri = DocumentContentProvider.getUri( + file.name, + file.workspaceFolder, + undefined, + false, + undefined, + true + ); + documentContentProvider.update(serverUri.with({ scheme: OBJECTSCRIPT_FILE_SCHEMA })); + return; + } catch (error) { + if (error?.statusCode == 409) { + const choices: string[] = []; + if (!enc) { + choices.push("Compare"); + } + choices.push("Overwrite on Server", "Pull Server Changes", "Cancel"); + const action = await vscode.window.showErrorMessage( + `Failed to import '${file.name}': The version of the file on the server is newer. What do you want to do?`, - ...choices - ) - .then((action) => { - switch (action) { - case "Compare": - return vscode.commands - .executeCommand( - "vscode.diff", - vscode.Uri.file(file.name).with({ - scheme: OBJECTSCRIPT_FILE_SCHEMA, - authority: file.workspaceFolder, - query: file.name.includes("/") ? "csp" : "", - }), - file.uri, - `Server • ${file.name} ↔ Local • ${file.fileName}` - ) - .then(() => Promise.reject()); - case "Overwrite on Server": - // Clear cache entry - workspaceState.update(`${file.uniqueId}:mtime`, undefined); - // Overwrite - return importFile(file, true, true); - case "Pull Server Changes": - loadChanges([file]); - return Promise.reject(); - case "Cancel": - return Promise.reject(); - } - return Promise.reject(); - }); - } else { - handleError(error, `Failed to save file '${file.name}' on the server.`); - return Promise.reject(); + ...choices + ); + switch (action) { + case "Compare": + await vscode.commands.executeCommand( + "vscode.diff", + vscode.Uri.file(file.name).with({ + scheme: OBJECTSCRIPT_FILE_SCHEMA, + authority: file.workspaceFolder, + query: file.name.includes("/") ? "csp" : "", + }), + file.uri, + `Server • ${file.name} ↔ Local • ${file.fileName}` + ); + return Promise.reject(); + case "Overwrite on Server": + // Clear cache entry + workspaceState.update(`${file.uniqueId}:mtime`, undefined); + // Overwrite + return importFile(file, true, true); + case "Pull Server Changes": + loadChanges([file]); + return Promise.reject(); + case "Cancel": + return Promise.reject(); } - }); + return Promise.reject(); + } else { + handleError(error, `Failed to save file '${file.name}' on the server.`); + return Promise.reject(); + } + } } function updateOthers(others: string[], baseUri: vscode.Uri) { @@ -231,29 +237,9 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] } else { // Insert/update the storage part of class definition. content = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); - let storageBegin: number; // the last "Storage ..." line - let storageEnd: number; // the first "}" after storageBegin - let classEnd: number; // the last "}" - for (let i = 0; i < content.length; i++) { - if (content[i].startsWith("Storage ")) { - storageBegin = i; - } else if (storageBegin !== undefined && storageEnd === undefined && content[i].startsWith("}")) { - storageEnd = i; - } else if (content[i].startsWith("}")) { - classEnd = i; - } - } let storage = (await api.getDoc(file.name, file.uri, undefined, true)).result.content; storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/g) : storage; - if ((storageBegin && storageEnd) !== undefined) { - // when replacing an existing storage definition, we don't need extra empty lines (if any). - while (storage[storage.length - 1] == "") { - storage.pop(); - } - content.splice(storageBegin, 1 + storageEnd - storageBegin, ...storage); - } else { - content.splice(classEnd, 0, ...storage); - } + content = updateStorage(content, storage); } exportedUris.add(file.uri.toString()); // Set optimistically await vscode.workspace.fs @@ -286,6 +272,51 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] ); } +function updateStorage(content: string[], storage: string[]): string[] { + const storageMap = storageToMap(storage); + let contentString = content.join("\n"); + contentString = contentString + // update existing Storages + .replaceAll(/\n(\s*storage\s+(\w+)\s*{\s*)([^}]*?)(\s*})/gim, (_match, beforeXML, name, _oldXML, afterXML) => { + const newXML = storageMap.get(name); + if (newXML === undefined) { + return ""; + } + storageMap.delete(name); + return "\n" + beforeXML + newXML + afterXML; + }); + contentString = contentString + // insert remaining Storages + .replace(/}\s*$/m, (m) => { + for (const [name, content] of storageMap.entries()) { + m = `Storage ${name}\n{\n${content}\n}\n\n${m}`; + } + return m; + }); + return contentString.split("\n"); +} + +function storageToMap(storage: string[]): Map { + const map: Map = new Map(); + let k: string; + let v = []; + for (const line of storage) { + if (line.startsWith("Storage ")) { + k = line.slice("Storage ".length, line.length); + v = []; + } else if (line === "{") { + continue; + } else if (line === "}") { + map.set(k, v.join("\n")); + } else if (line === "") { + continue; + } else { + v.push(line); + } + } + return map; +} + export async function compile(docs: (CurrentTextFile | CurrentBinaryFile)[], askFlags = false): Promise { docs = docs.filter(notNull); if (!docs.length) return; diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 82c0d840..7ba01f85 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -262,22 +262,22 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (!sync || (!change.addedOrChanged && !change.removed)) return; if (change.addedOrChanged) { // Create or update the document on the server - importFile(change.addedOrChanged) - .then(() => { - outputImport(change.addedOrChanged.name, uri); - if (conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name)) { - // Compile right away if this document is in the active text editor. - // 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 (vscodeChange && vscode.window.activeTextEditor?.document.uri.toString() == uriString) { - compile([change.addedOrChanged]); - } else { - debouncedCompile(change.addedOrChanged); - } + try { + await importFile(change.addedOrChanged); + outputImport(change.addedOrChanged.name, uri); + if (conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name)) { + // Compile right away if this document is in the active text editor. + // 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 (vscodeChange && vscode.window.activeTextEditor?.document.uri.toString() == uriString) { + compile([change.addedOrChanged]); + } else { + debouncedCompile(change.addedOrChanged); } - }) + } + } catch (_) { // importFile handles any server errors - .catch(() => {}); + } } if (change.removed) { // Delete document on the server From 52cbb02a9d8369e33bc0efd253d19bd34212d212 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Thu, 5 Mar 2026 17:01:34 -0500 Subject: [PATCH 07/14] sync on save should not depend on compile on save --- src/api/index.ts | 2 +- src/commands/compile.ts | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 6fa94799..dc93d0a6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -682,7 +682,7 @@ export class AtelierAPI { cat: "CLS" | "CSP" | "RTN" | "OTH"; status: string; enc: boolean; - flag?: "1"; + flags?: 0 | 1; content: string[]; ext: any; }> diff --git a/src/commands/compile.ts b/src/commands/compile.ts index b7e0fa42..50512ce7 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -128,17 +128,16 @@ export async function importFile( ignoreConflict ); workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); - if (data.result.flag === "1") { - // putDoc returns new Storage definitions and the file must be a CLS - let content: string[]; - (content = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g)), - (content = updateStorage(content, data.result.content)); - await vscode.workspace.fs.writeFile( - file.uri, - new TextEncoder().encode( - content.join(((file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n") - ) - ); + if (data.result.flags === 1) { + // If flags === 1, putDoc returns new Storage definitions and the file must be a CLS + const oldContent = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)); + const oldContentArray = oldContent.split(/\r?\n/g); + const newContentArray = updateStorage(oldContentArray, data.result.content); + if (oldContentArray.some((oldLine, index) => oldLine !== newContentArray[index])) { + const EOL = ((file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n"; + const newContent = newContentArray.join(EOL); + await vscode.workspace.fs.writeFile(file.uri, new TextEncoder().encode(newContent)); + } } // In case another extension has used an 'objectscript://' uri to load a document read-only from the server, // make it reload with what we just imported to the server. From 16b8c741998678430327f4bd04a6ecea0a919445 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Thu, 5 Mar 2026 17:41:41 -0500 Subject: [PATCH 08/14] done --- src/commands/compile.ts | 55 +++++++++++++++++++------------------- src/utils/documentIndex.ts | 5 ++-- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 50512ce7..575eaaa6 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -85,6 +85,7 @@ export async function checkChangedOnServer(file: CurrentTextFile | CurrentBinary // Synchronize the client version and the server version of the same file export async function importFile( file: CurrentTextFile | CurrentBinaryFile, + willCompile: boolean, ignoreConflict?: boolean, skipDeplCheck = false ): Promise { @@ -128,7 +129,7 @@ export async function importFile( ignoreConflict ); workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); - if (data.result.flags === 1) { + if (data.result.flags === 1 && !willCompile) { // If flags === 1, putDoc returns new Storage definitions and the file must be a CLS const oldContent = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)); const oldContentArray = oldContent.split(/\r?\n/g); @@ -180,7 +181,7 @@ What do you want to do?`, // Clear cache entry workspaceState.update(`${file.uniqueId}:mtime`, undefined); // Overwrite - return importFile(file, true, true); + return importFile(file, willCompile, true, true); case "Pull Server Changes": loadChanges([file]); return Promise.reject(); @@ -384,14 +385,16 @@ export async function importAndCompile(document?: vscode.TextDocument, askFlags return; } - return ( - importFile(file) - .then(() => { - if (isCompilable(file.name)) compile([file], askFlags); - }) - // importFile handles any server errors - .catch(() => {}) - ); + try { + if (isCompilable(file.name)) { + await importFile(file, true); + compile([file], askFlags); + } else { + await importFile(file, false); + } + } catch (_) { + // importFile handles any server errors + } } export async function compileOnly(document?: vscode.TextDocument, askFlags = false): Promise { @@ -461,28 +464,26 @@ async function importFiles(files: vscode.Uri[], noCompile = false) { await Promise.allSettled( files.map((uri) => rateLimiter.call(async () => { - return vscode.workspace.fs - .readFile(uri) - .then((contentBytes) => - currentFileFromContent( - uri, - isText(uri.path.split("/").pop(), Buffer.from(contentBytes)) - ? new TextDecoder().decode(contentBytes) - : Buffer.from(contentBytes) - ) - ) - .then((curFile) => { - if (curFile) { - if (typeof curFile.content == "string" && isCompilable(curFile.name)) toCompile.push(curFile); - return importFile(curFile).then(() => outputChannel.appendLine("Imported file: " + curFile.fileName)); - } - }); + const contentBytes = await vscode.workspace.fs.readFile(uri); + const curFile = currentFileFromContent( + uri, + isText(uri.path.split("/").pop(), Buffer.from(contentBytes)) + ? new TextDecoder().decode(contentBytes) + : Buffer.from(contentBytes) + ); + if (curFile) { + if (typeof curFile.content == "string" && isCompilable(curFile.name)) { + toCompile.push(curFile); + } + await importFile(curFile, !noCompile); + outputChannel.appendLine("Imported file: " + curFile.fileName); + } }) ) ); if (!noCompile && toCompile.length > 0) { - return compile(toCompile); + await compile(toCompile); } return; } diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 7ba01f85..ebbc9d54 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -263,13 +263,14 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (change.addedOrChanged) { // Create or update the document on the server try { - await importFile(change.addedOrChanged); + const willCompile = vscodeChange && vscode.window.activeTextEditor?.document.uri.toString() == uriString; + await importFile(change.addedOrChanged, willCompile); outputImport(change.addedOrChanged.name, uri); if (conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name)) { // Compile right away if this document is in the active text editor. // 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 (vscodeChange && vscode.window.activeTextEditor?.document.uri.toString() == uriString) { + if (willCompile) { compile([change.addedOrChanged]); } else { debouncedCompile(change.addedOrChanged); From 7b4952c8fe385aedea2ee358efebe89439a1964f Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Fri, 6 Mar 2026 15:27:17 +0000 Subject: [PATCH 09/14] Brett's comments --- src/api/atelier.d.ts | 2 +- src/api/index.ts | 14 +------------- src/commands/compile.ts | 24 +++++++++++++----------- src/utils/documentIndex.ts | 2 +- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/api/atelier.d.ts b/src/api/atelier.d.ts index f79e92b0..b261b552 100644 --- a/src/api/atelier.d.ts +++ b/src/api/atelier.d.ts @@ -41,7 +41,7 @@ export interface Document { cat: "RTN" | "CLS" | "CSP" | "OTH"; status: string; enc: boolean; - flags: number; + flags?: 0 | 1; content: string[] | Buffer; ext?: UserAction | UserAction[]; } diff --git a/src/api/index.ts b/src/api/index.ts index dc93d0a6..d15bf669 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -674,19 +674,7 @@ export class AtelierAPI { name: string, data: { enc: boolean; content: string[]; mtime: number }, ignoreConflict?: boolean - ): Promise< - Atelier.Response<{ - name: string; - db: string; - ts: string; - cat: "CLS" | "CSP" | "RTN" | "OTH"; - status: string; - enc: boolean; - flags?: 0 | 1; - content: string[]; - ext: any; - }> - > { + ): Promise> { const params = { ignoreConflict }; name = this.transformNameIfCsp(name); const headers = {}; diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 575eaaa6..519f1155 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -132,8 +132,9 @@ export async function importFile( if (data.result.flags === 1 && !willCompile) { // If flags === 1, putDoc returns new Storage definitions and the file must be a CLS const oldContent = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)); - const oldContentArray = oldContent.split(/\r?\n/g); - const newContentArray = updateStorage(oldContentArray, data.result.content); + const oldContentArray = oldContent.split(/\r?\n/); + const storage = Buffer.isBuffer(data.result.content) ? new TextDecoder().decode(data.result.content).split(/\r?\n/g) : data.result.content; + const newContentArray = updateStorage(oldContentArray, storage); if (oldContentArray.some((oldLine, index) => oldLine !== newContentArray[index])) { const EOL = ((file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n"; const newContent = newContentArray.join(EOL); @@ -304,14 +305,15 @@ function storageToMap(storage: string[]): Map { if (line.startsWith("Storage ")) { k = line.slice("Storage ".length, line.length); v = []; - } else if (line === "{") { - continue; - } else if (line === "}") { - map.set(k, v.join("\n")); - } else if (line === "") { - continue; - } else { - v.push(line); + } else if (k !== undefined) { + if (line === "{") { + continue; + } else if (line === "}") { + map.set(k, v.join("\n")); + k = undefined; + } else { + v.push(line); + } } } return map; @@ -392,7 +394,7 @@ export async function importAndCompile(document?: vscode.TextDocument, askFlags } else { await importFile(file, false); } - } catch (_) { + } catch { // importFile handles any server errors } } diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index ebbc9d54..ab53f6f0 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -276,7 +276,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr debouncedCompile(change.addedOrChanged); } } - } catch (_) { + } catch { // importFile handles any server errors } } From ae3b95511bb81ecf1cfef7cb258b884d912b953b Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Fri, 6 Mar 2026 11:25:17 -0500 Subject: [PATCH 10/14] lint --- src/commands/compile.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 519f1155..6104e409 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -133,7 +133,9 @@ export async function importFile( // If flags === 1, putDoc returns new Storage definitions and the file must be a CLS const oldContent = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)); const oldContentArray = oldContent.split(/\r?\n/); - const storage = Buffer.isBuffer(data.result.content) ? new TextDecoder().decode(data.result.content).split(/\r?\n/g) : data.result.content; + const storage = Buffer.isBuffer(data.result.content) + ? new TextDecoder().decode(data.result.content).split(/\r?\n/g) + : data.result.content; const newContentArray = updateStorage(oldContentArray, storage); if (oldContentArray.some((oldLine, index) => oldLine !== newContentArray[index])) { const EOL = ((file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n"; From 838d6cc242747dc45d4de3470a921eae7e167dd3 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Fri, 6 Mar 2026 11:53:59 -0500 Subject: [PATCH 11/14] debug --- src/commands/compile.ts | 5 +++-- src/utils/documentIndex.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 6104e409..2825ca89 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -129,8 +129,9 @@ export async function importFile( ignoreConflict ); workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); - if (data.result.flags === 1 && !willCompile) { - // If flags === 1, putDoc returns new Storage definitions and the file must be a CLS + if (data.result.flags !== undefined && !willCompile) { + // In this case, the file must be a CLS and data.result.content must be the new Storage definitions + // (with the rest of the class if flags === 1) const oldContent = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)); const oldContentArray = oldContent.split(/\r?\n/); const storage = Buffer.isBuffer(data.result.content) diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index ab53f6f0..f2c74026 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -263,14 +263,14 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr if (change.addedOrChanged) { // Create or update the document on the server try { - const willCompile = vscodeChange && vscode.window.activeTextEditor?.document.uri.toString() == uriString; + const willCompile = conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name); await importFile(change.addedOrChanged, willCompile); outputImport(change.addedOrChanged.name, uri); - if (conf.get("compileOnSave") && isCompilable(change.addedOrChanged.name)) { + if (willCompile) { // Compile right away if this document is in the active text editor. // 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 (willCompile) { + if (vscodeChange && vscode.window.activeTextEditor?.document.uri.toString() == uriString) { compile([change.addedOrChanged]); } else { debouncedCompile(change.addedOrChanged); From c02ca0d019732f1e377df99286cd24d14fcc25c2 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Fri, 6 Mar 2026 14:02:58 -0500 Subject: [PATCH 12/14] Brett's comments --- package.json | 4 ++-- src/api/atelier.d.ts | 2 +- src/commands/compile.ts | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 71a6029d..db4a85d8 100644 --- a/package.json +++ b/package.json @@ -1417,7 +1417,7 @@ "default": true }, "objectscript.refreshClassesOnSync": { - "description": "Automatically refresh classes after any save or change by compilation", + "description": "Controls whether the entire content of client-side classes is replaced with the server copy after synchronizing with the server. If `false`, only the contents of Storage definitions are replaced.", "type": "boolean", "default": false }, @@ -1761,4 +1761,4 @@ "extensionDependencies": [ "intersystems-community.servermanager" ] -} \ No newline at end of file +} diff --git a/src/api/atelier.d.ts b/src/api/atelier.d.ts index b261b552..d445c0ef 100644 --- a/src/api/atelier.d.ts +++ b/src/api/atelier.d.ts @@ -41,7 +41,7 @@ export interface Document { cat: "RTN" | "CLS" | "CSP" | "OTH"; status: string; enc: boolean; - flags?: 0 | 1; + flags: 0 | 1; content: string[] | Buffer; ext?: UserAction | UserAction[]; } diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 2825ca89..61f0b234 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -129,13 +129,13 @@ export async function importFile( ignoreConflict ); workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); - if (data.result.flags !== undefined && !willCompile) { + if (!willCompile && isClass(file.name) && data.result.content.length) { // In this case, the file must be a CLS and data.result.content must be the new Storage definitions // (with the rest of the class if flags === 1) - const oldContent = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)); + const oldContent = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri)); const oldContentArray = oldContent.split(/\r?\n/); const storage = Buffer.isBuffer(data.result.content) - ? new TextDecoder().decode(data.result.content).split(/\r?\n/g) + ? new TextDecoder().decode(data.result.content).split(/\r?\n/) : data.result.content; const newContentArray = updateStorage(oldContentArray, storage); if (oldContentArray.some((oldLine, index) => oldLine !== newContentArray[index])) { @@ -240,9 +240,9 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] content = (await api.getDoc(file.name, file.uri)).result.content; } else { // Insert/update the storage part of class definition. - content = new TextDecoder("utf-8").decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/g); + content = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/); let storage = (await api.getDoc(file.name, file.uri, undefined, true)).result.content; - storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/g) : storage; + storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/) : storage; content = updateStorage(content, storage); } exportedUris.add(file.uri.toString()); // Set optimistically From 5e997d411589db2558ff1ff67184efcff80798d7 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Fri, 6 Mar 2026 14:05:21 -0500 Subject: [PATCH 13/14] telemetry --- src/extension.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension.ts b/src/extension.ts index 389c26b1..6366a33c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -802,6 +802,7 @@ function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[] "config.syncLocalChanges": !serverSide ? conf.get("syncLocalChanges") : undefined, dockerCompose: !serverSide ? String(typeof conf.get("conn.docker-compose") == "object") : undefined, "config.conn.links": String(Object.keys(conf.get("conn.links", {})).length), + "config.refreshClassesOnSync": !serverSide ? conf.get("refreshClassesOnSync") : undefined, }); }); } From ce80a9d2b9da01d3a5f81a0384682ed213cb7f90 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Fri, 6 Mar 2026 14:46:39 -0500 Subject: [PATCH 14/14] a few bugs --- src/commands/compile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 61f0b234..f55edf77 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -131,7 +131,7 @@ export async function importFile( workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z"))); if (!willCompile && isClass(file.name) && data.result.content.length) { // In this case, the file must be a CLS and data.result.content must be the new Storage definitions - // (with the rest of the class if flags === 1) + // (with the rest of the class if flags === 0) const oldContent = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri)); const oldContentArray = oldContent.split(/\r?\n/); const storage = Buffer.isBuffer(data.result.content) @@ -291,7 +291,7 @@ function updateStorage(content: string[], storage: string[]): string[] { }); contentString = contentString // insert remaining Storages - .replace(/}\s*$/m, (m) => { + .replace(/}\s*$/, (m) => { for (const [name, content] of storageMap.entries()) { m = `Storage ${name}\n{\n${content}\n}\n\n${m}`; }