diff --git a/package.json b/package.json index f0ad7432..9a44115d 100644 --- a/package.json +++ b/package.json @@ -271,10 +271,6 @@ "command": "vscode-objectscript.newFile.message", "when": "false" }, - { - "command": "vscode-objectscript.importLocalFilesServerSide", - "when": "false" - }, { "command": "vscode-objectscript.showRESTDebugWebview", "when": "editorLangId =~ /^objectscript/ && vscode-objectscript.connectActive && activeCustomEditorId == ''" @@ -573,11 +569,6 @@ "when": "vscode-objectscript.connectActive && resourceScheme =~ /^isfs(-readonly)?$/ && resource =~ /project%3D/ && resourcePath =~ /^\\/?$/ && !listMultiSelection", "group": "objectscript_prj@3" }, - { - "command": "vscode-objectscript.importLocalFilesServerSide", - "when": "vscode-objectscript.connectActive && resourceScheme == isfs && explorerResourceIsRoot && !listMultiSelection", - "group": "objectscript_modify@2" - }, { "command": "vscode-objectscript.modifyWsFolder", "when": "(!resourceScheme || resourceScheme =~ /^isfs(-readonly)?$/) && explorerResourceIsRoot && !listMultiSelection", @@ -1070,11 +1061,6 @@ "command": "vscode-objectscript.newFile.message", "title": "Interoperability Message" }, - { - "category": "ObjectScript", - "command": "vscode-objectscript.importLocalFilesServerSide", - "title": "Import Local Files..." - }, { "category": "ObjectScript", "command": "vscode-objectscript.showRESTDebugWebview", @@ -1108,7 +1094,7 @@ { "category": "ObjectScript", "command": "vscode-objectscript.importXMLFiles", - "title": "Import XML Files..." + "title": "Import Files..." }, { "category": "ObjectScript", diff --git a/src/commands/compile.ts b/src/commands/compile.ts index ae15b0ba..285a284e 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -2,7 +2,6 @@ import vscode = require("vscode"); import { isText } from "istextorbinary"; import { AtelierAPI } from "../api"; import { - config, documentContentProvider, FILESYSTEM_SCHEMA, OBJECTSCRIPT_FILE_SCHEMA, @@ -14,7 +13,6 @@ import { import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { base64EncodeContent, - classNameRegex, compileErrorMsg, cspAppsForUri, CurrentBinaryFile, @@ -22,7 +20,7 @@ import { currentFileFromContent, CurrentTextFile, exportedUris, - getWsFolder, + getWsServerConnection, handleError, isClass, isClassDeployed, @@ -33,7 +31,6 @@ import { notNull, outputChannel, RateLimiter, - routineNameTypeRegex, } from "../utils"; import { StudioActions } from "./studio"; import { NodeBase, PackageNode, RootNode } from "../explorer/nodes"; @@ -565,57 +562,6 @@ export async function compileExplorerItems(nodes: NodeBase[]): Promise { ); } -/** Import file `name` to server `api`. Used for importing local files that are not used as part of a client-side editing workspace. */ -async function importFileFromContent( - name: string, - content: string, - api: AtelierAPI, - ignoreConflict?: boolean, - skipDeplCheck = false -): Promise { - if (name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) { - if (await isClassDeployed(name, api)) { - vscode.window.showErrorMessage(`Cannot import ${name} because it is deployed on the server.`, "Dismiss"); - return Promise.reject(); - } - } - ignoreConflict = ignoreConflict || config("overwriteServerChanges"); - return api - .putDoc( - name, - { - content: content.split(/\r?\n/), - enc: false, - // We don't have an mtime for this file because it's outside a local workspace folder - mtime: 0, - }, - ignoreConflict - ) - .then(() => { - return; - }) - .catch((error) => { - if (error?.statusCode == 409) { - return vscode.window - .showErrorMessage( - `Failed to import '${name}' because it already exists on the server. Overwrite server copy?`, - "Yes", - "No" - ) - .then((action) => { - if (action == "Yes") { - return importFileFromContent(name, content, api, true, true); - } else { - return Promise.reject(); - } - }); - } else { - handleError(error, `Failed to save file '${name}' on the server.`); - return Promise.reject(); - } - }); -} - /** Prompt the user to compile documents after importing them */ 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 @@ -669,207 +615,57 @@ async function promptForCompile(imported: string[], api: AtelierAPI, isIsfs: boo } } -/** Import files from the local file system into a server-namespace from an `isfs` workspace folder. */ -export async function importLocalFilesToServerSideFolder(wsFolderUri: vscode.Uri): Promise { - if ( - !( - wsFolderUri instanceof vscode.Uri && - wsFolderUri.scheme == FILESYSTEM_SCHEMA && - (vscode.workspace.workspaceFolders != undefined - ? vscode.workspace.workspaceFolders.some((wsFolder) => wsFolder.uri.toString() == wsFolderUri.toString()) - : false) - ) - ) { - // Need an isfs workspace folder URI - return; - } - if (vscode.workspace.workspaceFile.scheme != "file") { - vscode.window.showErrorMessage( - "'Import Local Files...' command is not supported for unsaved workspaces.", - "Dismiss" - ); - return; - } - const api = new AtelierAPI(wsFolderUri); - // Get the default URI and remove the file anme - let defaultUri = lastUsedLocalUri() ?? vscode.workspace.workspaceFile; - defaultUri = defaultUri.with({ path: defaultUri.path.split("/").slice(0, -1).join("/") }); - // Prompt the user for files to import - let uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: true, - openLabel: "Import", - filters: { - "InterSystems Files": ["cls", "mac", "int", "inc"], - }, - // Need a default URI with file scheme or the open dialog - // will show the virtual files from the workspace folder - defaultUri, - }); - if (!Array.isArray(uris) || uris.length == 0) { - // No files to import - return; - } - // Filter out non-ISC files - uris = uris.filter((uri) => isClassOrRtn(uri.path)); - if (uris.length == 0) { - vscode.window.showErrorMessage("No classes or routines were selected.", "Dismiss"); - return; - } - lastUsedLocalUri(uris[0]); - // Get the name and content of the files to import - const textDecoder = new TextDecoder(); - const docs = await Promise.allSettled<{ name: string; content: string; uri: vscode.Uri }>( - uris.map((uri) => - vscode.workspace.fs - .readFile(uri) - .then((contentBytes) => textDecoder.decode(contentBytes)) - .then((content) => { - // Determine the name of this file - let docName = ""; - let ext = ""; - if (uri.path.split(".").pop().toLowerCase() == "cls") { - // Allow Unicode letters - const match = content.match(classNameRegex); - if (match) { - [, docName, ext = "cls"] = match; - } - } else { - const match = content.match(routineNameTypeRegex); - if (match) { - [, docName, ext = "mac"] = match; - } else { - const basename = uri.path.split("/").pop(); - docName = basename.slice(0, basename.lastIndexOf(".")); - ext = basename.slice(basename.lastIndexOf(".") + 1); - } - } - if (docName != "" && ext != "") { - return { - name: `${docName}.${ext.toLowerCase()}`, - content, - uri, - }; - } else { - return Promise.reject(); - } - }) - ) - ).then((results) => results.map((result) => (result.status == "fulfilled" ? result.value : null)).filter(notNull)); - // The user is importing into a server-side folder, so fire the import list User Action - const docNames = docs.map((e) => e.name).join(","); - await new StudioActions().fireImportUserAction(api, docNames); - // Check the status of the documents to be imported and skip any that are read-only - await api.actionQuery("select * from %Atelier_v1_Utils.Extension_GetStatus(?)", [docNames]).then((data) => - data?.result?.content?.forEach((e) => { - if (!e.editable) { - const idx = docs.findIndex((d) => { - const nameSplit = d.name.split("."); - return e.name == `${nameSplit.slice(0, -1).join(".")}.${nameSplit.pop().toUpperCase()}`; - }); - if (idx != -1) { - docs.splice(idx, 1); - outputChannel.appendLine( - `Skipping '${e.name}' because it has been marked read-only by server-side source control.` - ); - } - } - }) - ); - // Import the files - const rateLimiter = new RateLimiter(50); - return Promise.allSettled( - docs.map((doc) => - rateLimiter.call(async () => { - // Allow importing over deployed classes since the XML import - // command and SMP, terminal, and Studio imports allow it - return importFileFromContent(doc.name, doc.content, api, false, true).then(() => { - outputChannel.appendLine("Imported file: " + doc.uri.path.split("/").pop()); - return doc.name; - }); - }) - ) - ).then((results) => - promptForCompile( - results.map((result) => (result.status == "fulfilled" ? result.value : null)).filter(notNull), - api, - true - ) - ); -} - -interface XMLQuickPickItem extends vscode.QuickPickItem { +interface FileQuickPickItem extends vscode.QuickPickItem { file: string; } -export async function importXMLFiles(): Promise { +export async function importArbitraryFiles(): Promise { try { - // Use the server connection from a workspace folder - const wsFolder = await getWsFolder( - "Pick a workspace folder. Server-side folders import from the local file system.", - false, - false, - false, - true - ); - if (!wsFolder) { - if (wsFolder === undefined) { + const wsFolderUri = await getWsServerConnection("2023.2.0"); + if (!wsFolderUri) { + if (wsFolderUri === undefined) { // Strict equality needed because undefined == null vscode.window.showErrorMessage( - "'Import XML Files...' command requires a workspace folder with an active server connection.", + "'Import Files...' command requires an active server connection to InterSystems IRIS version 2023.2 or above.", "Dismiss" ); } return; } - const api = new AtelierAPI(wsFolder.uri); - // 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; - } - let defaultUri = wsFolder.uri; + const api = new AtelierAPI(wsFolderUri); + let defaultUri = wsFolderUri; 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 = lastUsedLocalUri() ?? vscode.workspace.workspaceFile; if (defaultUri.scheme != "file") { - vscode.window.showErrorMessage( - "'Import XML Files...' command is not supported for unsaved workspaces.", - "Dismiss" - ); + vscode.window.showErrorMessage("'Import Files...' 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("/") }); } // Prompt the user the file to import + const supportedExts = ["cls", "mac", "int", "inc", "xml", "rtn", "ro"]; let uris = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: true, openLabel: "Import", filters: { - "XML Files": ["xml"], + "InterSystems Files": supportedExts, }, 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) return; + // Filter out non-importable files + uris = uris.filter((uri) => supportedExts.includes(uri.path.split(".").pop().toLowerCase())); if (uris.length == 0) { - vscode.window.showErrorMessage("No XML files were selected.", "Dismiss"); + vscode.window.showErrorMessage("No selected files are importable.", "Dismiss"); return; } lastUsedLocalUri(uris[0]); - // Read the XML files + // Read the files const fileTimestamps: Map = new Map(); const filesToList = await Promise.allSettled( uris.map(async (uri) => { @@ -882,11 +678,23 @@ export async function importXMLFiles(): Promise { 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)); + ).then((results) => + results + .map((result) => { + if (result.status == "fulfilled") { + return result.value; + } else { + handleError(result.reason); + return null; + } + }) + .filter(notNull) + ); if (filesToList.length == 0) { + vscode.window.showErrorMessage("Failed to read the text of every selected file.", "Dismiss"); return; } - // List the documents in the XML files + // List the documents in the files const documentsPerFile = await api.actionXMLList(filesToList).then((data) => data.result.content); // Prompt the user to select documents to import const quickPickItems = documentsPerFile @@ -899,7 +707,7 @@ export async function importXMLFiles(): Promise { } }) .flatMap((file) => { - const items: XMLQuickPickItem[] = []; + const items: FileQuickPickItem[] = []; if (file.documents.length > 0) { // Add a separator for this file items.push({ @@ -926,10 +734,9 @@ export async function importXMLFiles(): Promise { ignoreFocusOut: true, title: `Select the documents to import into namespace '${api.ns}' on server '${api.serverId}'`, }); - if (docsToImport == undefined || docsToImport.length == 0) { - return; - } - const isIsfs = filesystemSchemas.includes(wsFolder.uri.scheme); + if (!docsToImport?.length) return; + outputChannel.show(true); + const isIsfs = filesystemSchemas.includes(wsFolderUri.scheme); if (isIsfs) { // The user is importing into a server-side folder const docNames = [...new Set(docsToImport.map((qpi) => qpi.label))].join(","); @@ -974,6 +781,6 @@ export async function importXMLFiles(): Promise { // Prompt the user for compilation promptForCompile([...new Set(imported)], api, isIsfs); } catch (error) { - handleError(error, "Error executing 'Import XML Files...' command."); + handleError(error, "Error executing 'Import Files...' command."); } } diff --git a/src/extension.ts b/src/extension.ts index 775b926c..3351ae9d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,9 +35,8 @@ import { compileExplorerItems, checkChangedOnServer, compileOnly, - importLocalFilesToServerSideFolder, loadChanges, - importXMLFiles, + importArbitraryFiles, } from "./commands/compile"; import { deleteExplorerItems } from "./commands/delete"; import { exportAll, exportCurrentFile, exportDocumentsToXMLFile, exportExplorerItems } from "./commands/export"; @@ -1646,19 +1645,6 @@ export async function activate(context: vscode.ExtensionContext): Promise { }), vscode.window.registerFileDecorationProvider(fileDecorationProvider), vscode.workspace.onDidOpenTextDocument((doc) => !doc.isUntitled && fileDecorationProvider.emitter.fire(doc.uri)), - vscode.commands.registerCommand("vscode-objectscript.importLocalFilesServerSide", (wsFolderUri) => { - sendCommandTelemetryEvent("importLocalFilesServerSide"); - if ( - wsFolderUri instanceof vscode.Uri && - wsFolderUri.scheme == FILESYSTEM_SCHEMA && - (vscode.workspace.workspaceFolders != undefined - ? vscode.workspace.workspaceFolders.some((wsFolder) => wsFolder.uri.toString() == wsFolderUri.toString()) - : false) - ) { - // wsFolderUri is an isfs workspace folder URI - return importLocalFilesToServerSideFolder(wsFolderUri); - } - }), vscode.commands.registerCommand("vscode-objectscript.modifyWsFolder", (wsFolderUri?: vscode.Uri) => { sendCommandTelemetryEvent("modifyWsFolder"); modifyWsFolder(wsFolderUri); @@ -1729,7 +1715,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { }), vscode.commands.registerCommand("vscode-objectscript.importXMLFiles", () => { sendCommandTelemetryEvent("importXMLFiles"); - importXMLFiles(); + importArbitraryFiles(); }), vscode.commands.registerCommand("vscode-objectscript.exportToXMLFile", () => { sendCommandTelemetryEvent("exportToXMLFile"); diff --git a/src/utils/index.ts b/src/utils/index.ts index 2b84b2c6..84927ab4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1002,7 +1002,7 @@ export function queryToFuzzyLike(query: string): string { let _lastUsedLocalUri: vscode.Uri; -/** Get or set the uri of last used local file for XML import/export or local file import from an `isfs(-readonly)` workspace folder */ +/** Get or set the uri of last used local file for arbitrary import/export */ export function lastUsedLocalUri(newValue?: vscode.Uri): vscode.Uri { if (newValue) _lastUsedLocalUri = newValue; return _lastUsedLocalUri;