From fffe9d1924721bd5f829dfba79c9751cc61911fd Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Fri, 28 Jun 2024 08:19:52 -0400 Subject: [PATCH] Add `Compile` command to server-side file explorer --- package.json | 18 ++- src/commands/compile.ts | 69 ++++------- src/extension.ts | 3 +- .../FileSystemProvider/FileSystemProvider.ts | 112 +++++++++++++++++- src/utils/FileProviderUtil.ts | 8 +- 5 files changed, 154 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 9d00ca30..86c51835 100644 --- a/package.json +++ b/package.json @@ -195,10 +195,6 @@ "command": "vscode-objectscript.explorer.project.refresh", "when": "vscode-objectscript.connectActive" }, - { - "command": "vscode-objectscript.compileFolder", - "when": "false" - }, { "command": "vscode-objectscript.serverCommands.sourceControl", "when": "vscode-objectscript.connectActive && resourceScheme == isfs || (vscode-objectscript.connectActive && !editorIsOpen)" @@ -338,6 +334,10 @@ { "command": "vscode-objectscript.extractXMLFileContents", "when": "vscode-objectscript.connectActive && workspaceFolderCount != 0" + }, + { + "command": "vscode-objectscript.compileIsfs", + "when": "false" } ], "view/title": [ @@ -622,6 +622,11 @@ "command": "vscode-objectscript.extractXMLFileContents", "when": "vscode-objectscript.connectActive && resourceExtname =~ /^\\.xml$/i && !(resourceScheme =~ /^isfs(-readonly)?$/)", "group": "objectscript_modify@4" + }, + { + "command": "vscode-objectscript.compileIsfs", + "when": "vscode-objectscript.connectActive && resourceScheme == isfs && resourcePath && !(resourcePath =~ /^\\/?$/) && !listMultiSelection", + "group": "objectscript_modify@1" } ], "file/newFile": [ @@ -1174,6 +1179,11 @@ "category": "ObjectScript", "command": "vscode-objectscript.extractXMLFileContents", "title": "Extract Documents from XML File..." + }, + { + "category": "ObjectScript", + "command": "vscode-objectscript.compileIsfs", + "title": "Compile" } ], "keybindings": [ diff --git a/src/commands/compile.ts b/src/commands/compile.ts index ec461310..fd17b595 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -5,7 +5,6 @@ import { config, documentContentProvider, FILESYSTEM_SCHEMA, - FILESYSTEM_READONLY_SCHEMA, OBJECTSCRIPT_FILE_SCHEMA, fileSystemProvider, workspaceState, @@ -213,24 +212,14 @@ What do you want to do?`, function updateOthers(others: string[], baseUri: vscode.Uri) { let workspaceFolder = vscode.workspace.getWorkspaceFolder(baseUri); - if (!workspaceFolder && (baseUri.scheme === FILESYSTEM_SCHEMA || baseUri.scheme === FILESYSTEM_READONLY_SCHEMA)) { + if (!workspaceFolder && filesystemSchemas.includes(baseUri.scheme)) { // hack to deal with problem seen with isfs* schemes workspaceFolder = vscode.workspace.getWorkspaceFolder(baseUri.with({ path: "" })); } - const workspaceFolderName = workspaceFolder ? workspaceFolder.name : ""; others.forEach((item) => { - const uri = DocumentContentProvider.getUri(item, workspaceFolderName); - if (uri.scheme === FILESYSTEM_SCHEMA || uri.scheme === FILESYSTEM_READONLY_SCHEMA) { - // Massage uri.path to change the first N-1 dots to slashes, where N is the number of slashes in baseUri.path - // For example, when baseUri.path is /Foo/Bar.cls and uri.path is /Foo.Bar.1.int - const partsToConvert = baseUri.path.split("/").length - 1; - const dotParts = uri.path.split("."); - const correctPath = - dotParts.length <= partsToConvert - ? uri.path - : dotParts.slice(0, partsToConvert).join("/") + "." + dotParts.slice(partsToConvert).join("."); - //console.log(`updateOthers: uri.path=${uri.path} baseUri.path=${baseUri.path} correctPath=${correctPath}`); - fileSystemProvider.fireFileChanged(uri.with({ path: correctPath })); + const uri = DocumentContentProvider.getUri(item, undefined, undefined, undefined, workspaceFolder?.uri); + if (filesystemSchemas.includes(uri.scheme)) { + fileSystemProvider.fireFileChanged(uri); } else { documentContentProvider.update(uri); } @@ -242,33 +231,25 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[] return; } const api = new AtelierAPI(files[0].uri); - return Promise.all( - files.map((file) => - api - .getDoc(file.name) - .then(async (data) => { - const mtime = Number(new Date(data.result.ts + "Z")); - workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined); - if (file.uri.scheme === "file") { - if (Buffer.isBuffer(data.result.content)) { - // This is a binary file - await vscode.workspace.fs.writeFile(file.uri, data.result.content); - } else { - // This is a text file - const content = (data.result.content || []).join( - (file as CurrentTextFile).eol === vscode.EndOfLine.LF ? "\n" : "\r\n" - ); - await vscode.workspace.fs.writeFile(file.uri, new TextEncoder().encode(content)); - } - } else if (file.uri.scheme === FILESYSTEM_SCHEMA || file.uri.scheme === FILESYSTEM_READONLY_SCHEMA) { - fileSystemProvider.fireFileChanged(file.uri); - } - }) - .then(() => api.actionIndex([file.name])) - .then((data) => data.result.content[0].others) - .then((others) => { - updateOthers(others, file.uri); - }) + // Use allSettled so we attempt to load changes for all files, even if some fail + return api.actionIndex(files.map((f) => f.name)).then((data) => + Promise.allSettled( + data.result.content.map(async (doc) => { + if (doc.status.length) return; + const file = files.find((f) => f.name == doc.name); + const mtime = Number(new Date(doc.ts + "Z")); + workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined); + if (file.uri.scheme === "file") { + const content = await api.getDoc(file.name).then((data) => data.result.content); + await vscode.workspace.fs.writeFile( + file.uri, + Buffer.isBuffer(content) ? content : new TextEncoder().encode(content.join("\n")) + ); + } else if (filesystemSchemas.includes(file.uri.scheme)) { + fileSystemProvider.fireFileChanged(file.uri); + } + updateOthers(doc.others, file.uri); + }) ) ); } @@ -347,13 +328,13 @@ export async function importAndCompile( if (compileFile) { compile([file], flags); } else { - if (file.uri.scheme === FILESYSTEM_SCHEMA || file.uri.scheme === FILESYSTEM_READONLY_SCHEMA) { + 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 (file.uri.scheme === FILESYSTEM_SCHEMA || file.uri.scheme === FILESYSTEM_READONLY_SCHEMA) { + } 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); diff --git a/src/extension.ts b/src/extension.ts index e7e2b9f6..e2b4f23b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -877,7 +877,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { vscode.commands.registerCommand("vscode-objectscript.compileWithFlags", () => importAndCompile(true)), vscode.commands.registerCommand("vscode-objectscript.compileAll", () => namespaceCompile(false)), vscode.commands.registerCommand("vscode-objectscript.compileAllWithFlags", () => namespaceCompile(true)), - vscode.commands.registerCommand("vscode-objectscript.refreshLocalFile", async (_file, files) => { + vscode.commands.registerCommand("vscode-objectscript.refreshLocalFile", async () => { const file = currentFile(); if (!file) { return; @@ -1404,6 +1404,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { } } ), + vscode.commands.registerCommand("vscode-objectscript.compileIsfs", (uri) => fileSystemProvider.compile(uri)), ...setUpTestController(), /* Anything we use from the VS Code proposed API */ diff --git a/src/providers/FileSystemProvider/FileSystemProvider.ts b/src/providers/FileSystemProvider/FileSystemProvider.ts index e4048263..37454a2b 100644 --- a/src/providers/FileSystemProvider/FileSystemProvider.ts +++ b/src/providers/FileSystemProvider/FileSystemProvider.ts @@ -13,7 +13,7 @@ import { redirectDotvscodeRoot, workspaceFolderOfUri, } from "../../utils/index"; -import { config, intLangId, macLangId, workspaceState } from "../../extension"; +import { config, FILESYSTEM_SCHEMA, intLangId, macLangId, workspaceState } from "../../extension"; import { addIsfsFileToProject, modifyProject } from "../../commands/project"; import { DocumentContentProvider } from "../DocumentContentProvider"; import { Document, UserAction } from "../../api/atelier"; @@ -512,10 +512,12 @@ export class FileSystemProvider implements vscode.FileSystemProvider { // Ignore the recursive flag for project folders toDeletePromise = projectContentsFromUri(uri, true); } else { - toDeletePromise = studioOpenDialogFromURI(uri, options.recursive ? { flat: true } : undefined); + toDeletePromise = studioOpenDialogFromURI(uri, options.recursive ? { flat: true } : undefined).then( + (data) => data.result.content + ); } const toDelete: string[] = await toDeletePromise.then((data) => - data.result.content + data .map((entry) => { if (options.recursive || project) { return entry.Name; @@ -658,6 +660,110 @@ export class FileSystemProvider implements vscode.FileSystemProvider { await vscode.workspace.fs.delete(oldUri); } + /** + * If `uri` is a file, compile it. + * If `uri` is a directory, compile its contents. + */ + public async compile(uri: vscode.Uri): Promise { + if (!uri || uri.scheme != FILESYSTEM_SCHEMA) return; + uri = redirectDotvscodeRoot(uri); + const compileList: string[] = []; + try { + const entry = await this._lookup(uri, true); + if (!entry) return; + if (entry instanceof Directory) { + // Get the list of files to compile + let compileListPromise: Promise; + if (new URLSearchParams(uri.query).get("project")?.length) { + compileListPromise = projectContentsFromUri(uri, true); + } else { + compileListPromise = studioOpenDialogFromURI(uri, { flat: true }).then((data) => data.result.content); + } + compileList.push(...(await compileListPromise.then((data) => data.map((e) => e.Name)))); + } else { + // Compile this file + compileList.push(isCSPFile(uri) ? uri.path : uri.path.slice(1).replace(/\//g, ".")); + } + } catch (error) { + console.log(error); + let errorMsg = "Error determining documents to compile."; + if (error && error.errorText && error.errorText !== "") { + outputChannel.appendLine("\n" + error.errorText); + outputChannel.show(true); + errorMsg += " Check 'ObjectScript' output channel for details."; + } + vscode.window.showErrorMessage(errorMsg, "Dismiss"); + return; + } + if (!compileList.length) return; + const api = new AtelierAPI(uri); + const conf = vscode.workspace.getConfiguration("objectscript"); + // Compile the files + await vscode.window.withProgress( + { + cancellable: true, + location: vscode.ProgressLocation.Notification, + title: `Compiling: ${compileList.length == 1 ? compileList[0] : compileList.length + " files"}`, + }, + (progress, token: vscode.CancellationToken) => + api + .asyncCompile(compileList, token, conf.get("compileFlags")) + .then((data) => { + const info = compileList.length > 1 ? "" : `${compileList}: `; + if (data.status && data.status.errors && data.status.errors.length) { + throw new Error(`${info}Compile error`); + } else if (!conf.get("suppressCompileMessages")) { + vscode.window.showInformationMessage(`${info}Compilation succeeded.`, "Dismiss"); + } + }) + .catch(() => { + if (!conf.get("suppressCompileErrorMessages")) { + vscode.window + .showErrorMessage( + "Compilation failed. Check 'ObjectScript' output channel for details.", + "Show", + "Dismiss" + ) + .then((action) => { + if (action === "Show") { + outputChannel.show(true); + } + }); + } + }) + ); + // 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 []; + } + }) + ) + .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( + (f) => + !compileList.includes(f) && + this.fireFileChanged(DocumentContentProvider.getUri(f, undefined, undefined, undefined, uri)) + ); + } + public watch(uri: vscode.Uri): vscode.Disposable { return new vscode.Disposable(() => { return; diff --git a/src/utils/FileProviderUtil.ts b/src/utils/FileProviderUtil.ts index bcfa171a..e4bad5ce 100644 --- a/src/utils/FileProviderUtil.ts +++ b/src/utils/FileProviderUtil.ts @@ -25,16 +25,16 @@ export async function projectContentsFromUri(uri: vscode.Uri, overrideFlat?: boo "SELECT CASE " + "WHEN Type = 'CLS' THEN Name||'.cls' " + "ELSE Name END Name, Type FROM %Studio.Project_ProjectItemsList(?) " + - "WHERE (Name %STARTSWITH ? OR Name %STARTSWITH ?) AND (" + + "WHERE (Name %STARTSWITH ? OR Name %STARTSWITH ?) AND ((" + "(Type = 'MAC' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.mac,*.int,*.inc,*.bas,*.mvi',1,1,1,1,0,1) AS sod WHERE Name = sod.Name)) OR " + "(Type = 'CSP' AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,1,0,1) AS sod WHERE '/'||Name = sod.Name)) OR " + "(Type NOT IN ('CLS','PKG','MAC','CSP','DIR','GBL') AND EXISTS (SELECT sod.Size FROM %Library.RoutineMgr_StudioOpenDialog('*.other',1,1,1,1,0,1) AS sod WHERE Name = sod.Name))) OR " + - "(Type = 'CLS' AND (Package IS NOT NULL OR (Package IS NULL AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID = Name)))) " + + "(Type = 'CLS' AND (Package IS NOT NULL OR (Package IS NULL AND EXISTS (SELECT dcd.ID FROM %Dictionary.ClassDefinition AS dcd WHERE dcd.ID = Name))))) " + "UNION " + - "SELECT SUBSTR(sod.Name,2) AS Name, pil.Type FROM %Library.RoutineMgr_StudioOpenDialog(?,1,1,1,1,0,1) AS sod " + + "SELECT SUBSTR(sod.Name,2) AS Name, pil.Type FROM %Library.RoutineMgr_StudioOpenDialog('*.cspall',1,1,1,1,0,1,?) AS sod " + "JOIN %Studio.Project_ProjectItemsList(?,1) AS pil ON " + "pil.Type = 'DIR' AND SUBSTR(sod.Name,2) %STARTSWITH ? AND SUBSTR(sod.Name,2) %STARTSWITH pil.Name||'/'"; - parameters = [project, folderDots, folder, folder + "*.cspall", project, folder]; + parameters = [project, folderDots, folder, `Name %STARTSWITH '/${folder}'`, project, folder]; } else { if (folder.length) { const l = String(folder.length + 1); // Need the + 1 because SUBSTR is 1 indexed