diff --git a/package.json b/package.json index 4d8b5294..05a17994 100644 --- a/package.json +++ b/package.json @@ -105,15 +105,15 @@ }, { "command": "vscode-objectscript.compile", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive" + "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty" }, { "command": "vscode-objectscript.refreshLocalFile", - "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive" + "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty" }, { "command": "vscode-objectscript.compileWithFlags", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive" + "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty" }, { "command": "vscode-objectscript.compileAll", @@ -213,11 +213,11 @@ }, { "command": "vscode-objectscript.compileOnly", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive" + "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty" }, { "command": "vscode-objectscript.compileOnlyWithFlags", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive" + "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty" }, { "command": "vscode-objectscript.editOthers", @@ -478,7 +478,7 @@ }, { "command": "vscode-objectscript.compile", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive", + "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty", "group": "objectscript@3" }, { @@ -498,7 +498,7 @@ }, { "command": "vscode-objectscript.compileOnly", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive", + "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty", "group": "objectscript@7" }, { @@ -545,7 +545,7 @@ { "command": "vscode-objectscript.touchBar.compile", "group": "objectscript.compile", - "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive" + "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && !activeEditorIsDirty" }, { "command": "vscode-objectscript.touchBar.viewOthers", @@ -1183,7 +1183,7 @@ "command": "vscode-objectscript.compile", "key": "Ctrl+F7", "mac": "Cmd+F7", - "when": "editorLangId =~ /^objectscript/" + "when": "!(resourceScheme =~ /^isfs(-readonly)?$/) && editorLangId =~ /^objectscript/" }, { "command": "vscode-objectscript.compileAll", diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 7670633c..1c421af4 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -9,6 +9,7 @@ import { fileSystemProvider, workspaceState, filesystemSchemas, + schemas, } from "../extension"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { @@ -58,7 +59,7 @@ async function compileFlags(): Promise { * @return mtime timestamp or -1. */ export async function checkChangedOnServer(file: CurrentTextFile | CurrentBinaryFile, force = false): Promise { - if (!file || !file.uri) { + if (!file || !file.uri || schemas.includes(file.uri.scheme)) { return -1; } const api = new AtelierAPI(file.uri); @@ -93,6 +94,7 @@ export async function importFile( skipDeplCheck = false ): Promise { const api = new AtelierAPI(file.uri); + if (!api.active) return; if (file.name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) { if (await isClassDeployed(file.name, api)) { vscode.window.showErrorMessage(`Cannot import ${file.name} because it is deployed on the server.`, "Dismiss"); @@ -306,12 +308,8 @@ export async function importAndCompile( compileFile = true ): Promise { const file = currentFile(document); - if (!file) { - return; - } - - // Do nothing if it is a local file and objectscript.conn.active is false - if (notIsfs(file.uri) && !config("conn").active) { + if (!file || filesystemSchemas.includes(file.uri.scheme) || !new AtelierAPI(file.uri).active) { + // Not for server-side URIs or folders with inactive server connections return; } @@ -319,25 +317,10 @@ export async function importAndCompile( const flags = askFlags ? await compileFlags() : defaultFlags; return importFile(file) .catch((error) => { - // console.error(error); throw error; }) .then(() => { - if (!file.fileName.startsWith("\\.vscode\\")) { - if (compileFile) { - compile([file], flags); - } else { - if (filesystemSchemas.includes(file.uri.scheme)) { - // Fire the file changed event to avoid VSCode alerting the user on the next save that - // "The content of the file is newer." - fileSystemProvider.fireFileChanged(file.uri); - } - } - } else if (filesystemSchemas.includes(file.uri.scheme)) { - // Fire the file changed event to avoid VSCode alerting the user on the next folder-specific save (e.g. of settings.json) that - // "The content of the file is newer." - fileSystemProvider.fireFileChanged(file.unredirectedUri ?? file.uri); - } + if (compileFile) compile([file], flags); }); } @@ -463,6 +446,7 @@ async function importFiles(files: vscode.Uri[], noCompile = false) { } export async function importFolder(uri: vscode.Uri, noCompile = false): Promise { + if (filesystemSchemas.includes(uri.scheme)) return; // Not for server-side URIs if ((await vscode.workspace.fs.stat(uri)).type != vscode.FileType.Directory) { return importFiles([uri], noCompile); } @@ -573,7 +557,7 @@ async function importFileFromContent( } /** Prompt the user to compile documents after importing them */ -async function promptForCompile(imported: string[], api: AtelierAPI, refresh: boolean): Promise { +async function promptForCompile(imported: string[], api: AtelierAPI, isIsfs: boolean): Promise { // This cast is safe because the only two callers intialize api with a workspace folder URI const conf = vscode.workspace.getConfiguration("objectscript", api.wsOrFile); // Prompt the user for compilation @@ -608,14 +592,14 @@ async function promptForCompile(imported: string[], api: AtelierAPI, refresh: bo }) .catch(() => compileErrorMsg(conf)) .finally(() => { - if (refresh) { + if (isIsfs) { // Refresh the files explorer to show the new files vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); } }) ); } else { - if (refresh) { + if (isIsfs) { // Refresh the files explorer to show the new files vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); } @@ -867,7 +851,7 @@ export async function importXMLFiles(): Promise { const docsToImport = await vscode.window.showQuickPick(quickPickItems, { canPickMany: true, ignoreFocusOut: true, - title: `Select the documents to import into namespace '${api.ns.toUpperCase()}' on server '${api.serverId}'`, + title: `Select the documents to import into namespace '${api.ns}' on server '${api.serverId}'`, }); if (docsToImport == undefined || docsToImport.length == 0) { return; diff --git a/src/commands/studio.ts b/src/commands/studio.ts index 5a08408c..b82c569e 100644 --- a/src/commands/studio.ts +++ b/src/commands/studio.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../api"; import { iscIcon } from "../extension"; -import { outputChannel, outputConsole, notIsfs, handleError, openCustomEditors } from "../utils"; +import { outputChannel, outputConsole, notIsfs, handleError, openLowCodeEditors } from "../utils"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { UserAction } from "../api/atelier"; import { isfsDocumentName } from "../providers/FileSystemProvider/FileSystemProvider"; @@ -547,7 +547,7 @@ export async function fireOtherStudioAction( const studioActions = new StudioActions(uri); return ( studioActions && - !openCustomEditors.includes(uri.toString()) && // The custom editor will handle all server-side source control interactions + !openLowCodeEditors.has(uri.toString()) && // The low-code editor will handle all server-side source control interactions studioActions.fireOtherStudioAction(action, userAction) ); } diff --git a/src/providers/FileSystemProvider/FileSystemProvider.ts b/src/providers/FileSystemProvider/FileSystemProvider.ts index 4ce19dfc..486af274 100644 --- a/src/providers/FileSystemProvider/FileSystemProvider.ts +++ b/src/providers/FileSystemProvider/FileSystemProvider.ts @@ -13,13 +13,12 @@ import { outputChannel, handleError, redirectDotvscodeRoot, - workspaceFolderOfUri, stringifyError, base64EncodeContent, - openCustomEditors, + openLowCodeEditors, compileErrorMsg, } from "../../utils"; -import { FILESYSTEM_READONLY_SCHEMA, FILESYSTEM_SCHEMA, intLangId, macLangId, workspaceState } from "../../extension"; +import { FILESYSTEM_READONLY_SCHEMA, FILESYSTEM_SCHEMA, intLangId, macLangId } from "../../extension"; import { addIsfsFileToProject, modifyProject } from "../../commands/project"; import { DocumentContentProvider } from "../DocumentContentProvider"; import { Document, UserAction } from "../../api/atelier"; @@ -225,17 +224,12 @@ export class FileSystemProvider implements vscode.FileSystemProvider { // Used by import and compile to make sure we notice its changes public fireFileChanged(uri: vscode.Uri): void { - // Remove entry from our cache - this._lookupParentDirectory(uri).then((parent) => { - const name = path.basename(uri.path); - parent.entries.delete(name); - }); - // Queue the event this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); } public async stat(uri: vscode.Uri): Promise { - if (!new AtelierAPI(uri).active) throw vscode.FileSystemError.Unavailable("Server connection is inactive"); + const api = new AtelierAPI(uri); + if (!api.active) throw vscode.FileSystemError.Unavailable("Server connection is inactive"); let entryPromise: Promise; let result: Entry; const redirectedUri = redirectDotvscodeRoot(uri); @@ -263,7 +257,6 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } if (result instanceof File) { - const api = new AtelierAPI(uri); const serverName = isfsDocumentName(uri); if (serverName.slice(-4).toLowerCase() == ".cls") { if (await isClassDeployed(serverName, api)) { @@ -420,12 +413,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { public async readFile(uri: vscode.Uri): Promise { // Use _lookup() instead of _lookupAsFile() so we send // our cached mtime with the GET /doc request if we have it - return this._lookup(uri, true).then((file: File) => { - // Update cache entry - const uniqueId = `${workspaceFolderOfUri(uri)}:${file.fileName}`; - workspaceState.update(`${uniqueId}:mtime`, file.mtime); - return file.data; - }); + return this._lookup(uri, true).then((file: File) => file.data); } public writeFile( @@ -446,17 +434,14 @@ export class FileSystemProvider implements vscode.FileSystemProvider { return; } const api = new AtelierAPI(uri); + let created = false; // Use _lookup() instead of _lookupAsFile() so we send // our cached mtime with the GET /doc request if we have it return this._lookup(uri) .then( - async () => { + async (entry: File) => { // Check cases for which we should fail the write and leave the document dirty if changed if (!csp && fileName.split(".").pop().toLowerCase() == "cls") { - // Check if the class is deployed - if (await isClassDeployed(fileName, api)) { - throw new Error("Cannot overwrite a deployed class"); - } // Check if the class name and file name match let clsname = ""; const match = new TextDecoder().decode(content).match(classNameRegex); @@ -469,11 +454,15 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (fileName.slice(0, -4) != clsname) { throw new Error("Cannot save an isfs class where the class name and file name do not match"); } - } - if (openCustomEditors.includes(uri.toString())) { - // This class is open in a graphical editor, so any - // updates to the class will be handled by that editor - return; + if (openLowCodeEditors.has(uri.toString())) { + // This class is open in a low-code editor, so any + // updates to the class will be handled by that editor + return; + } + // Check if the class is deployed + if (await isClassDeployed(fileName, api)) { + throw new Error("Cannot overwrite a deployed class"); + } } const contentBuffer = Buffer.from(content); const putContent = isText(uri.path.split("/").pop(), contentBuffer) @@ -496,12 +485,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { }, true ) - .then((data) => { - workspaceState.update( - `${workspaceFolderOfUri(uri)}:${fileName}:mtime`, - Number(new Date(data.result.ts + "Z")) - ); - }) + .then(() => entry) .catch((error) => { // Throw all failures throw vscode.FileSystemError.Unavailable(stringifyError(error) || uri); @@ -541,15 +525,22 @@ export class FileSystemProvider implements vscode.FileSystemProvider { addIsfsFileToProject(project, fileName, api); } // Create an entry in our cache for the document - this._lookupAsFile(uri); + return this._lookupAsFile(uri).then((entry) => { + created = true; + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + return entry; + }); }); } ) - .then(() => { + .then((entry) => { // Compile the document if required - if (vscode.workspace.getConfiguration("objectscript", uri).get("compileOnSave")) { - this.compile(uri); - } else { + if ( + !uri.path.includes("/_vscode/") && + vscode.workspace.getConfiguration("objectscript", uri).get("compileOnSave") + ) { + this.compile(uri, entry); + } else if (!created) { this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); } }); @@ -703,6 +694,11 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (!options.overwrite) { // If it does and we can't overwrite it, throw an error throw vscode.FileSystemError.FileExists(newUri); + } else if (newFileStat.permissions & vscode.FilePermission.Readonly) { + // If the file is read-only, throw an error + // This can happen if the target class is deployed, + // or the document is marked read-only by source control + throw vscode.FileSystemError.NoPermissions(newUri); } } catch (error) { if (error instanceof vscode.FileSystemError && error.code == "FileExists") { @@ -736,7 +732,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { }) .then(async (response) => { // New file has been written - if (newFileStat != undefined && response && response.result.ext && response.result.ext[0]) { + if (!newFileStat && response && response.result.ext && response.result.ext[0]) { // We created a file fireOtherStudioAction(OtherStudioAction.CreatedNewDocument, newUri, response.result.ext[0]); fireOtherStudioAction(OtherStudioAction.FirstTimeDocumentSave, newUri, response.result.ext[1]); @@ -748,7 +744,10 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } // Sanity check that we find it there, then make client side update things this._lookupAsFile(newUri).then(() => { - this._fireSoon({ type: vscode.FileChangeType.Changed, uri: newUri }); + this._fireSoon({ + type: newFileStat ? vscode.FileChangeType.Changed : vscode.FileChangeType.Created, + uri: newUri, + }); }); }); // Delete the old file @@ -758,13 +757,14 @@ export class FileSystemProvider implements vscode.FileSystemProvider { /** * If `uri` is a file, compile it. * If `uri` is a directory, compile its contents. + * `file` is passed if called from `writeFile()`. */ - public async compile(uri: vscode.Uri): Promise { + public async compile(uri: vscode.Uri, file?: File): Promise { if (!uri || uri.scheme != FILESYSTEM_SCHEMA) return; uri = redirectDotvscodeRoot(uri); const compileList: string[] = []; try { - const entry = await this._lookup(uri, true); + const entry = file || (await this._lookup(uri, true)); if (!entry) return; if (entry instanceof Directory) { // Get the list of files to compile @@ -786,6 +786,7 @@ export class FileSystemProvider implements vscode.FileSystemProvider { if (!compileList.length) return; const api = new AtelierAPI(uri); const conf = vscode.workspace.getConfiguration("objectscript"); + const filesToUpdate: Set = new Set(compileList); // Compile the files await vscode.window.withProgress( { @@ -803,38 +804,34 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } else if (!conf.get("suppressCompileMessages")) { vscode.window.showInformationMessage(`${info}Compilation succeeded.`, "Dismiss"); } + data.result.content.forEach((f) => filesToUpdate.add(f.name)); }) .catch(() => compileErrorMsg(conf)) ); - // Tell the client to update all "other" files affected by compilation - const workspaceFolder = workspaceFolderOfUri(uri); - const otherList: string[] = await api - .actionIndex(compileList) - .then((data) => - data.result.content.flatMap((idx) => { - if (!idx.status.length) { - // Update the timestamp for this file - const mtime = Number(new Date(idx.ts + "Z")); - workspaceState.update(`${workspaceFolder}:${idx.name}:mtime`, mtime > 0 ? mtime : undefined); - // Tell the client that it changed - this.fireFileChanged(DocumentContentProvider.getUri(idx.name, undefined, undefined, undefined, uri)); - // Return the list of "other" documents - return idx.others; - } else { - // The server failed to index the document. This should never happen. - return []; - } + // Fire file changed events for all files affected by compilation, including "other" files + this._fireSoon( + ...filesToUpdate.values().map((f) => { + return { + type: vscode.FileChangeType.Changed, + uri: DocumentContentProvider.getUri(f, undefined, undefined, undefined, uri), + }; + }) + ); + ( + await api + .actionIndex(Array.from(filesToUpdate)) + .then((data) => data.result.content.flatMap((idx) => (!idx.status.length ? idx.others : []))) + .catch(() => { + // Index API returned an error. This should never happen. + return []; }) - ) - .catch(() => { - // Index API returned an error. This should never happen. - return []; - }); - // Only fire the event for files that weren't in the compile list - otherList.forEach( + ).forEach( (f) => - !compileList.includes(f) && - this.fireFileChanged(DocumentContentProvider.getUri(f, undefined, undefined, undefined, uri)) + !filesToUpdate.has(f) && + this._fireSoon({ + type: vscode.FileChangeType.Changed, + uri: DocumentContentProvider.getUri(f, undefined, undefined, undefined, uri), + }) ); } diff --git a/src/providers/LowCodeEditorProvider.ts b/src/providers/LowCodeEditorProvider.ts index 012556a9..7d52b968 100644 --- a/src/providers/LowCodeEditorProvider.ts +++ b/src/providers/LowCodeEditorProvider.ts @@ -4,7 +4,7 @@ import { AtelierAPI } from "../api"; import { loadChanges } from "../commands/compile"; import { StudioActions } from "../commands/studio"; import { clsLangId } from "../extension"; -import { currentFile, openCustomEditors, outputChannel } from "../utils"; +import { currentFile, notIsfs, openLowCodeEditors, outputChannel } from "../utils"; export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { private readonly _rule: string = "/ui/interop/rule-editor"; @@ -80,9 +80,10 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { return this._errorMessage(`${className} is neither a rule definition class nor a DTL transformation class.`); } - // Add this document to the array of open custom editors + // Add this document to the Set of open low-code editors const documentUriString = document.uri.toString(); - openCustomEditors.push(documentUriString); + openLowCodeEditors.add(documentUriString); + const isClientSide = notIsfs(document.uri); // Initialize the webview const targetOrigin = `${api.config.https ? "https" : "http"}://${api.config.host}:${api.config.port}`; @@ -230,7 +231,7 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { direction: "editor", type: "compile", }); - } else { + } else if (isClientSide) { // Load changes loadChanges([file]); } @@ -242,7 +243,7 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { vscode.commands.executeCommand("workbench.action.files.revert"); } // Load changes - loadChanges([file]); + if (isClientSide) loadChanges([file]); return; case "userAction": { // Process the source control user action @@ -302,11 +303,8 @@ export class LowCodeEditorProvider implements vscode.CustomTextEditorProvider { // Revert so document is clean vscode.commands.executeCommand("workbench.action.files.revert"); } - const idx = openCustomEditors.findIndex((elem) => elem == documentUriString); - if (idx >= 0) { - // Remove this document from the array of open custom editors - openCustomEditors.splice(idx, 1); - } + // Remove this document from the Set of open low-code editors + openLowCodeEditors.delete(documentUriString); }); } } diff --git a/src/utils/documentIndex.ts b/src/utils/documentIndex.ts index 6a5a40e1..d7b15955 100644 --- a/src/utils/documentIndex.ts +++ b/src/utils/documentIndex.ts @@ -9,7 +9,7 @@ import { isClassOrRtn, isImportableLocalFile, notIsfs, - openCustomEditors, + openLowCodeEditors, outputChannel, } from "."; import { isText } from "istextorbinary"; @@ -191,7 +191,7 @@ export async function indexWorkspaceFolder(wsFolder: vscode.WorkspaceFolder): Pr const uriString = uri.toString(); const lastFileChangeTime = lastFileChangeTimes.get(uriString) ?? 0; lastFileChangeTimes.set(uriString, Date.now()); - if (openCustomEditors.includes(uriString)) { + if (openLowCodeEditors.has(uriString)) { // This class is open in a low-code editor, so its name will not change // and any updates to the class will be handled by that editor touchedByVSCode.delete(uriString); diff --git a/src/utils/index.ts b/src/utils/index.ts index 999c37b7..7710fbf6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -35,9 +35,9 @@ export const cspApps: Map = new Map(); export const otherDocExts: Map = new Map(); /** - * The URI strings for all documents that are open in a custom editor. + * The URI strings for all documents that are open in a low-code editor. */ -export const openCustomEditors: string[] = []; +export const openLowCodeEditors: Set = new Set(); /** * Set of stringified `Uri`s that have been exported.