From 2b0b383e8f23318c942c4f15599766aa01b65d3f Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Fri, 28 Feb 2025 09:58:40 -0500 Subject: [PATCH] Implement custom workspace folder picker --- src/commands/addServerNamespaceToWorkspace.ts | 32 +- src/commands/compile.ts | 275 +++++++++--------- src/commands/export.ts | 123 ++++---- src/commands/newFile.ts | 22 +- src/extension.ts | 19 +- src/utils/index.ts | 40 +++ 6 files changed, 256 insertions(+), 255 deletions(-) diff --git a/src/commands/addServerNamespaceToWorkspace.ts b/src/commands/addServerNamespaceToWorkspace.ts index bd92f9c3..58d577d5 100644 --- a/src/commands/addServerNamespaceToWorkspace.ts +++ b/src/commands/addServerNamespaceToWorkspace.ts @@ -9,7 +9,7 @@ import { filesystemSchemas, smExtensionId, } from "../extension"; -import { cspAppsForUri, handleError, notIsfs } from "../utils"; +import { cspAppsForUri, getWsFolder, handleError, notIsfs } from "../utils"; import { pickProject } from "./project"; import { isfsConfig, IsfsUriParam } from "../utils/FileProviderUtil"; @@ -372,18 +372,12 @@ export async function modifyWsFolder(wsFolderUri?: vscode.Uri): Promise { let wsFolder: vscode.WorkspaceFolder; if (!wsFolderUri) { // Select a workspace folder to modify - if (vscode.workspace.workspaceFolders == undefined || vscode.workspace.workspaceFolders.length == 0) { - vscode.window.showErrorMessage("No workspace folders are open.", "Dismiss"); - return; - } else if (vscode.workspace.workspaceFolders.length == 1) { - wsFolder = vscode.workspace.workspaceFolders[0]; - } else { - wsFolder = await vscode.window.showWorkspaceFolderPick({ - placeHolder: "Pick the workspace folder to modify", - ignoreFocusOut: true, - }); - } + wsFolder = await getWsFolder("Pick the workspace folder to modify", false, true); if (!wsFolder) { + if (wsFolder === undefined) { + // Strict equality needed because undefined == null + vscode.window.showErrorMessage("No server-side workspace folders are open.", "Dismiss"); + } return; } } else { @@ -392,13 +386,13 @@ export async function modifyWsFolder(wsFolderUri?: vscode.Uri): Promise { if (!wsFolder) { return; } - } - if (notIsfs(wsFolder.uri)) { - vscode.window.showErrorMessage( - `Workspace folder '${wsFolder.name}' does not have scheme 'isfs' or 'isfs-readonly'.`, - "Dismiss" - ); - return; + if (notIsfs(wsFolder.uri)) { + vscode.window.showErrorMessage( + `Workspace folder '${wsFolder.name}' does not have scheme 'isfs' or 'isfs-readonly'.`, + "Dismiss" + ); + return; + } } // Prompt the user to modify the uri diff --git a/src/commands/compile.ts b/src/commands/compile.ts index f6d75310..58aeebe4 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -21,6 +21,7 @@ import { currentFileFromContent, CurrentTextFile, exportedUris, + getWsFolder, handleError, isClassDeployed, isClassOrRtn, @@ -746,162 +747,150 @@ interface XMLQuickPickItem extends vscode.QuickPickItem { export async function importXMLFiles(): Promise { try { // Use the server connection from a workspace folder - let connectionUri: vscode.Uri; - const workspaceFolders = vscode.workspace.workspaceFolders || []; - if (workspaceFolders.length == 0) { - vscode.window.showErrorMessage("'Import XML Files...' command requires an open workspace.", "Dismiss"); - } else if (workspaceFolders.length == 1) { - // Use the current connection - connectionUri = workspaceFolders[0].uri; - } else { - // Pick from the workspace folders - connectionUri = ( - await vscode.window.showWorkspaceFolderPick({ - ignoreFocusOut: true, - placeHolder: "Pick a workspace folder. Server-side folders import from the local file system.", - }) - )?.uri; + const wsFolder = await getWsFolder( + "Pick a workspace folder. Server-side folders import from the local file system." + ); + if (!wsFolder) { + if (wsFolder === undefined) { + // Strict equality needed because undefined == null + vscode.window.showErrorMessage("'Import XML Files...' command requires an open workspace.", "Dismiss"); + } + return; + } + const api = new AtelierAPI(wsFolder.uri); + // Make sure the server connection is active + if (!api.active || api.ns == "") { + vscode.window.showErrorMessage("'Import XML Files...' command requires an active server connection.", "Dismiss"); + return; + } + // Make sure the server has the xml endpoints + if (api.config.apiVersion < 7) { + vscode.window.showErrorMessage( + "'Import XML Files...' command requires InterSystems IRIS version 2023.2 or above.", + "Dismiss" + ); + return; } - if (connectionUri) { - const api = new AtelierAPI(connectionUri); - // Make sure the server connection is active - if (!api.active || api.ns == "") { + let defaultUri = wsFolder.uri; + if (defaultUri.scheme == FILESYSTEM_SCHEMA) { + // Need a default URI without the isfs scheme or the open dialog + // will show the server-side files instead of local ones + defaultUri = vscode.workspace.workspaceFile; + if (defaultUri.scheme != "file") { vscode.window.showErrorMessage( - "'Import XML Files...' command requires an active server connection.", + "'Import XML Files...' command is not supported for unsaved workspaces.", "Dismiss" ); return; } - // Make sure the server has the xml endpoints - if (api.config.apiVersion < 7) { - vscode.window.showErrorMessage( - "'Import XML Files...' command requires InterSystems IRIS version 2023.2 or above.", - "Dismiss" + // Remove the file name from the URI + defaultUri = defaultUri.with({ path: defaultUri.path.split("/").slice(0, -1).join("/") }); + } + // Prompt the user the file to import + let uris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: true, + openLabel: "Import", + filters: { + "XML Files": ["xml"], + }, + defaultUri, + }); + if (!Array.isArray(uris) || uris.length == 0) { + // No file to import + return; + } + // Filter out non-XML files + uris = uris.filter((uri) => uri.path.split(".").pop().toLowerCase() == "xml"); + if (uris.length == 0) { + vscode.window.showErrorMessage("No XML files were selected.", "Dismiss"); + return; + } + // Read the XML files + const fileTimestamps: Map = new Map(); + const filesToList = await Promise.allSettled( + uris.map(async (uri) => { + fileTimestamps.set( + uri.fsPath, + new Date((await vscode.workspace.fs.stat(uri)).mtime).toISOString().replace("T", " ").split(".")[0] ); - return; - } - let defaultUri = vscode.workspace.getWorkspaceFolder(connectionUri)?.uri ?? connectionUri; - if (defaultUri.scheme == FILESYSTEM_SCHEMA) { - // Need a default URI without the isfs scheme or the open dialog - // will show the server-side files instead of local ones - defaultUri = vscode.workspace.workspaceFile; - if (defaultUri.scheme != "file") { - vscode.window.showErrorMessage( - "'Import XML Files...' command is not supported for unsaved workspaces.", - "Dismiss" - ); - return; + return { + file: uri.fsPath, + content: new TextDecoder().decode(await vscode.workspace.fs.readFile(uri)).split(/\r?\n/), + }; + }) + ).then((results) => results.map((result) => (result.status == "fulfilled" ? result.value : null)).filter(notNull)); + if (filesToList.length == 0) { + return; + } + // List the documents in the XML files + const documentsPerFile = await api.actionXMLList(filesToList).then((data) => data.result.content); + // Prompt the user to select documents to import + const quickPickItems = documentsPerFile + .filter((file) => { + if (file.status != "") { + outputChannel.appendLine(`Failed to list documents in file '${file.file}': ${file.status}`); + return false; + } else { + return true; } - // Remove the file name from the URI - defaultUri = defaultUri.with({ path: defaultUri.path.split("/").slice(0, -1).join("/") }); - } - // Prompt the user the file to import - let uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: true, - openLabel: "Import", - filters: { - "XML Files": ["xml"], - }, - defaultUri, - }); - if (!Array.isArray(uris) || uris.length == 0) { - // No file to import - return; - } - // Filter out non-XML files - uris = uris.filter((uri) => uri.path.split(".").pop().toLowerCase() == "xml"); - if (uris.length == 0) { - vscode.window.showErrorMessage("No XML files were selected.", "Dismiss"); - return; - } - // Read the XML files - const fileTimestamps: Map = new Map(); - const filesToList = await Promise.allSettled( - uris.map(async (uri) => { - fileTimestamps.set( - uri.fsPath, - new Date((await vscode.workspace.fs.stat(uri)).mtime).toISOString().replace("T", " ").split(".")[0] - ); - return { - file: uri.fsPath, - content: new TextDecoder().decode(await vscode.workspace.fs.readFile(uri)).split(/\r?\n/), - }; - }) - ).then((results) => - results.map((result) => (result.status == "fulfilled" ? result.value : null)).filter(notNull) - ); - if (filesToList.length == 0) { - return; - } - // List the documents in the XML files - const documentsPerFile = await api.actionXMLList(filesToList).then((data) => data.result.content); - // Prompt the user to select documents to import - const quickPickItems = documentsPerFile - .filter((file) => { - if (file.status != "") { - outputChannel.appendLine(`Failed to list documents in file '${file.file}': ${file.status}`); - return false; - } else { - return true; - } - }) - .flatMap((file) => { - const items: XMLQuickPickItem[] = []; - if (file.documents.length > 0) { - // Add a separator for this file + }) + .flatMap((file) => { + const items: XMLQuickPickItem[] = []; + if (file.documents.length > 0) { + // Add a separator for this file + items.push({ + label: file.file, + kind: vscode.QuickPickItemKind.Separator, + file: file.file, + }); + file.documents.forEach((doc) => items.push({ - label: file.file, - kind: vscode.QuickPickItemKind.Separator, + label: doc.name, + picked: true, + detail: `${ + doc.ts.toString() != "-1" ? `Server timestamp: ${doc.ts.split(".")[0]}` : "Does not exist on server" + }, ${fileTimestamps.has(file.file) ? `File timestamp: ${fileTimestamps.get(file.file)}` : ""}`, file: file.file, - }); - file.documents.forEach((doc) => - items.push({ - label: doc.name, - picked: true, - detail: `${ - doc.ts.toString() != "-1" ? `Server timestamp: ${doc.ts.split(".")[0]}` : "Does not exist on server" - }, ${fileTimestamps.has(file.file) ? `File timestamp: ${fileTimestamps.get(file.file)}` : ""}`, - file: file.file, - }) - ); - } - return items; - }); - // Prompt the user for documents to import - 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}'`, - }); - if (docsToImport == undefined || docsToImport.length == 0) { - return; - } - if (filesystemSchemas.includes(connectionUri.scheme)) { - // The user is importing into a server-side folder, so fire source control hook - await new StudioActions().fireImportUserAction(api, [...new Set(docsToImport.map((qpi) => qpi.label))]); - } - // Import the selected documents - const filesToLoad: { file: string; content: string[]; selected: string[] }[] = filesToList.map((f) => { - return { selected: [], ...f }; - }); - docsToImport.forEach((qpi) => - // This is safe because every document came from a file - filesToLoad[filesToLoad.findIndex((f) => f.file == qpi.file)].selected.push(qpi.label) - ); - const importedPerFile = await api - .actionXMLLoad(filesToLoad.filter((f) => f.selected.length > 0)) - .then((data) => data.result.content); - const imported = importedPerFile.flatMap((file) => { - if (file.status != "") { - outputChannel.appendLine(`Importing documents from file '${file.file}' produced error: ${file.status}`); + }) + ); } - return file.imported; + return items; }); - // Prompt the user for compilation - promptForCompile([...new Set(imported)], api, filesystemSchemas.includes(connectionUri.scheme)); + // Prompt the user for documents to import + 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}'`, + }); + if (docsToImport == undefined || docsToImport.length == 0) { + return; } + const isIsfs = filesystemSchemas.includes(wsFolder.uri.scheme); + if (isIsfs) { + // The user is importing into a server-side folder, so fire source control hook + await new StudioActions().fireImportUserAction(api, [...new Set(docsToImport.map((qpi) => qpi.label))]); + } + // Import the selected documents + const filesToLoad: { file: string; content: string[]; selected: string[] }[] = filesToList.map((f) => { + return { selected: [], ...f }; + }); + docsToImport.forEach((qpi) => + // This is safe because every document came from a file + filesToLoad[filesToLoad.findIndex((f) => f.file == qpi.file)].selected.push(qpi.label) + ); + const importedPerFile = await api + .actionXMLLoad(filesToLoad.filter((f) => f.selected.length > 0)) + .then((data) => data.result.content); + const imported = importedPerFile.flatMap((file) => { + if (file.status != "") { + outputChannel.appendLine(`Importing documents from file '${file.file}' produced error: ${file.status}`); + } + return file.imported; + }); + // Prompt the user for compilation + promptForCompile([...new Set(imported)], api, isIsfs); } catch (error) { handleError(error, "Error executing 'Import XML Files...' command."); } diff --git a/src/commands/export.ts b/src/commands/export.ts index 2d94e3a6..d0fb6b92 100644 --- a/src/commands/export.ts +++ b/src/commands/export.ts @@ -5,6 +5,7 @@ import { config, explorerProvider, OBJECTSCRIPT_FILE_SCHEMA, schemas, workspaceS import { currentFile, exportedUris, + getWsFolder, handleError, isClassOrRtn, notNull, @@ -291,81 +292,71 @@ export async function exportCurrentFile(): Promise { export async function exportDocumentsToXMLFile(): Promise { try { // Use the server connection from a workspace folder - let connectionUri: vscode.Uri; - const workspaceFolders = vscode.workspace.workspaceFolders || []; - if (workspaceFolders.length == 0) { - vscode.window.showErrorMessage( - "'Export Documents to XML File...' command requires an open workspace.", - "Dismiss" - ); - } else if (workspaceFolders.length == 1) { - // Use the current connection - connectionUri = workspaceFolders[0].uri; - } else { - // Pick from the workspace folders - connectionUri = ( - await vscode.window.showWorkspaceFolderPick({ - ignoreFocusOut: true, - placeHolder: "Pick a workspace folder. Server-side folders export to the local file system.", - }) - )?.uri; - } - if (connectionUri) { - const api = new AtelierAPI(connectionUri); - // Make sure the server connection is active - if (!api.active || api.ns == "") { + const wsFolder = await getWsFolder("Pick a workspace folder. Server-side folders export to the local file system."); + if (!wsFolder) { + if (wsFolder === undefined) { + // Strict equality needed because undefined == null vscode.window.showErrorMessage( - "'Export Documents to XML File...' command requires an active server connection.", + "'Export Documents to XML File...' command requires an open workspace.", "Dismiss" ); - return; } - // Make sure the server has the xml endpoints - if (api.config.apiVersion < 7) { + return; + } + const api = new AtelierAPI(wsFolder.uri); + // Make sure the server connection is active + if (!api.active || api.ns == "") { + vscode.window.showErrorMessage( + "'Export Documents to XML File...' command requires an active server connection.", + "Dismiss" + ); + return; + } + // Make sure the server has the xml endpoints + if (api.config.apiVersion < 7) { + vscode.window.showErrorMessage( + "'Export Documents to XML File...' command requires InterSystems IRIS version 2023.2 or above.", + "Dismiss" + ); + return; + } + let defaultUri = wsFolder.uri; + if (schemas.includes(defaultUri.scheme)) { + // Need a default URI without the isfs scheme or the save dialog + // will show the virtual files from the workspace folder + defaultUri = vscode.workspace.workspaceFile; + if (defaultUri.scheme != "file") { vscode.window.showErrorMessage( - "'Export Documents to XML File...' command requires InterSystems IRIS version 2023.2 or above.", + "'Export Documents to XML File...' command is not supported for unsaved workspaces.", "Dismiss" ); return; } - let defaultUri = vscode.workspace.getWorkspaceFolder(connectionUri)?.uri ?? connectionUri; - if (schemas.includes(defaultUri.scheme)) { - // Need a default URI without the isfs scheme or the save dialog - // will show the virtual files from the workspace folder - defaultUri = vscode.workspace.workspaceFile; - if (defaultUri.scheme != "file") { - vscode.window.showErrorMessage( - "'Export Documents to XML File...' command is not supported for unsaved workspaces.", - "Dismiss" - ); - return; - } - // Remove the file name from the URI - defaultUri = defaultUri.with({ path: defaultUri.path.split("/").slice(0, -1).join("/") }); - } - if (!vscode.workspace.fs.isWritableFileSystem(defaultUri.scheme)) { - vscode.window.showErrorMessage(`Cannot export to read-only file system '${defaultUri.scheme}'.`, "Dismiss"); - return; - } - // Prompt the user for the documents to export - const documents = await pickDocuments(api, "to export"); - if (documents.length == 0) { - return; - } - // Prompt the user for the export destination - const uri = await vscode.window.showSaveDialog({ - saveLabel: "Export", - filters: { - "XML Files": ["xml"], - }, - defaultUri, - }); - if (uri) { - // Get the XML content - const xmlContent = await api.actionXMLExport(documents).then((data) => data.result.content); - // Save the file - await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(xmlContent.join("\n"))); - } + // Remove the file name from the URI + defaultUri = defaultUri.with({ path: defaultUri.path.split("/").slice(0, -1).join("/") }); + } + if (!vscode.workspace.fs.isWritableFileSystem(defaultUri.scheme)) { + vscode.window.showErrorMessage(`Cannot export to read-only file system '${defaultUri.scheme}'.`, "Dismiss"); + return; + } + // Prompt the user for the documents to export + const documents = await pickDocuments(api, "to export"); + if (documents.length == 0) { + return; + } + // Prompt the user for the export destination + const uri = await vscode.window.showSaveDialog({ + saveLabel: "Export", + filters: { + "XML Files": ["xml"], + }, + defaultUri, + }); + if (uri) { + // Get the XML content + const xmlContent = await api.actionXMLExport(documents).then((data) => data.result.content); + // Save the file + await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(xmlContent.join("\n"))); } } catch (error) { handleError(error, "Error executing 'Export Documents to XML File...' command."); diff --git a/src/commands/newFile.ts b/src/commands/newFile.ts index c98c9bc2..72534d08 100644 --- a/src/commands/newFile.ts +++ b/src/commands/newFile.ts @@ -3,7 +3,7 @@ import path = require("path"); import { AtelierAPI } from "../api"; import { FILESYSTEM_SCHEMA } from "../extension"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; -import { handleError } from "../utils"; +import { getWsFolder, handleError } from "../utils"; import { getFileName } from "./export"; import { getUrisForDocument } from "../utils/documentIndex"; @@ -239,22 +239,12 @@ interface RuleAssistClasses { export async function newFile(type: NewFileType): Promise { try { // Select a workspace folder - let wsFolder: vscode.WorkspaceFolder; - if (vscode.workspace.workspaceFolders == undefined || vscode.workspace.workspaceFolders.length == 0) { - vscode.window.showErrorMessage("No workspace folders are open.", "Dismiss"); - return; - } else if (vscode.workspace.workspaceFolders.length == 1) { - wsFolder = vscode.workspace.workspaceFolders[0]; - } else { - wsFolder = await vscode.window.showWorkspaceFolderPick({ - placeHolder: "Pick the workspace folder where you want to create the file", - }); - } + const wsFolder = await getWsFolder("Pick the workspace folder where you want to create the file", true); if (!wsFolder) { - return; - } - if (!vscode.workspace.fs.isWritableFileSystem(wsFolder.uri.scheme)) { - vscode.window.showErrorMessage(`Workspace folder '${wsFolder.name}' is read-only.`, "Dismiss"); + if (wsFolder === undefined) { + // Strict equality needed because undefined == null + vscode.window.showErrorMessage("No writable workspace folders are open.", "Dismiss"); + } return; } diff --git a/src/extension.ts b/src/extension.ts index d4240aa0..6ecf6ac0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -103,6 +103,7 @@ import { getWsServerConnection, isClassOrRtn, addWsServerRootFolderData, + getWsFolder, } from "./utils"; import { ObjectScriptDiagnosticProvider } from "./providers/ObjectScriptDiagnosticProvider"; import { DocumentLinkProvider } from "./providers/DocumentLinkProvider"; @@ -1540,18 +1541,14 @@ export async function activate(context: vscode.ExtensionContext): Promise { ), vscode.commands.registerCommand("vscode-objectscript.compileIsfs", (uri) => fileSystemProvider.compile(uri)), vscode.commands.registerCommand("vscode-objectscript.openISCDocument", async () => { - const workspaceFolders = vscode.workspace.workspaceFolders || []; - let wsFolder: vscode.WorkspaceFolder; - if (workspaceFolders.length == 1) { - // Use the current connection - wsFolder = workspaceFolders[0]; - } else if (workspaceFolders.length > 1) { - // Pick from the workspace folders - wsFolder = await vscode.window.showWorkspaceFolderPick({ - placeHolder: "Pick the workspace folder where you want to open a document", - }); + const wsFolder = await getWsFolder("Pick the workspace folder where you want to open a document"); + if (!wsFolder) { + if (wsFolder === undefined) { + // Strict equality needed because undefined == null + vscode.window.showErrorMessage("No workspace folders are open.", "Dismiss"); + } + return; } - if (!wsFolder) return; const api = new AtelierAPI(wsFolder.uri); if (!api.active) { vscode.window.showErrorMessage( diff --git a/src/utils/index.ts b/src/utils/index.ts index 9398aeaa..8b1a2a24 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -832,6 +832,46 @@ export async function getWsServerConnection(minVersion?: string): Promise c?.uri ?? null); } +/** + * Prompt the user to pick a workspace folder. + * Returns the chosen `vscode.WorkspaceFolder` object. + * If there is only one workspace folder, it will be returned without prompting the user. + * + * @param title An optional custom prompt title. + * @param writableOnly If `true`, only allow the user to pick from writeable folders. + * @param isfsOnly If `true`, only allow the user to pick from `isfs(-readonly)` folders. + * @returns `undefined` if there were no workspace folders and `null` if the + * user explicitly escaped from the QuickPick. + */ +export async function getWsFolder( + title = "", + writeableOnly = false, + isfsOnly = false +): Promise { + if (!vscode.workspace.workspaceFolders?.length) return; + // Apply the filters + const folders = vscode.workspace.workspaceFolders.filter( + (f) => + (!writeableOnly || (writeableOnly && vscode.workspace.fs.isWritableFileSystem(f.uri.scheme))) && + (!isfsOnly || (isfsOnly && filesystemSchemas.includes(f.uri.scheme))) + ); + if (!folders.length) return; + if (folders.length == 1) return folders[0]; + return vscode.window + .showQuickPick( + folders.map((f) => { + return { label: f.name, detail: f.uri.toString(true), f }; + }), + { + canPickMany: false, + ignoreFocusOut: true, + matchOnDetail: true, + title: title || "Pick a workspace folder", + } + ) + .then((i) => i?.f ?? null); +} + /** Convert `query` to a fuzzy LIKE compatible pattern */ export function queryToFuzzyLike(query: string): string { let p = "%";