From 5e220c951c61dbe877b8cd3f10bfa71478a29b7a Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 12 Apr 2025 18:09:11 +0200 Subject: [PATCH 01/23] Restrict multi-selection on commands Signed-off-by: Seb Julliand --- package.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 8e799a771..0c82b91fb 100644 --- a/package.json +++ b/package.json @@ -2647,7 +2647,7 @@ }, { "command": "code-for-ibmi.copyFilter", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "4_filters@2" }, { @@ -2657,27 +2657,27 @@ }, { "command": "code-for-ibmi.moveFilterUp", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "inline" }, { "command": "code-for-ibmi.moveFilterDown", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "inline" }, { "command": "code-for-ibmi.moveFilterToTop", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "5_filters@1" }, { "command": "code-for-ibmi.moveFilterToBottom", - "when": "view == objectBrowser && viewItem =~ /^filter.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^filter.*$/", "group": "5_filters@2" }, { "command": "code-for-ibmi.createMember", - "when": "view == objectBrowser && viewItem == SPF", + "when": "view == objectBrowser && !listMultiSelection && viewItem == SPF", "group": "3_sourceFileStuff@1" }, { @@ -2717,7 +2717,7 @@ }, { "submenu": "code-for-ibmi.debug.group", - "when": "view == objectBrowser && viewItem =~ /^object.(pgm|srvpgm).*/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^object.(pgm|srvpgm).*/", "group": "2_debug@1" }, { @@ -2727,17 +2727,17 @@ }, { "command": "code-for-ibmi.setCurrentLibrary", - "when": "!code-for-ibmi:libraryListDisabled && view == objectBrowser && viewItem =~ /library/", + "when": "!code-for-ibmi:libraryListDisabled && !listMultiSelection && view == objectBrowser && viewItem =~ /library/", "group": "2_library@2" }, { "command": "code-for-ibmi.updateMemberText", - "when": "view == objectBrowser && viewItem == member", + "when": "view == objectBrowser && !listMultiSelection && viewItem == member", "group": "2_memberStuff@1" }, { "command": "code-for-ibmi.copyMember", - "when": "view == objectBrowser && viewItem =~ /^member.*$/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^member.*$/", "group": "2_memberStuff@2" }, { @@ -2747,7 +2747,7 @@ }, { "command": "code-for-ibmi.renameMember", - "when": "view == objectBrowser && viewItem == member", + "when": "view == objectBrowser && !listMultiSelection && viewItem == member", "group": "2_memberStuff@3" }, { @@ -2757,7 +2757,7 @@ }, { "command": "code-for-ibmi.uploadAndReplaceMemberAsFile", - "when": "view == objectBrowser && viewItem == member", + "when": "view == objectBrowser && !listMultiSelection && viewItem == member", "group": "3_memberTransfer@2" }, { @@ -2912,12 +2912,12 @@ }, { "command": "code-for-ibmi.changeObjectDesc", - "when": "view == objectBrowser && (viewItem =~ /^object/ || viewItem == SPF)", + "when": "view == objectBrowser && !listMultiSelection && (viewItem =~ /^object/ || viewItem == SPF)", "group": "1_objActions@1" }, { "command": "code-for-ibmi.copyObject", - "when": "view == objectBrowser && (viewItem =~ /^object/ || viewItem == SPF)", + "when": "view == objectBrowser && !listMultiSelection && (viewItem =~ /^object/ || viewItem == SPF)", "group": "1_objActions@2" }, { @@ -2927,17 +2927,17 @@ }, { "command": "code-for-ibmi.renameObject", - "when": "view == objectBrowser && viewItem =~ /^object/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^object/", "group": "1_objActions@3" }, { "command": "code-for-ibmi.moveObject", - "when": "view == objectBrowser && viewItem =~ /^object(?!.lib)/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /^object(?!.lib)/", "group": "1_objActions@4" }, { "command": "code-for-ibmi.createSourceFile", - "when": "view == objectBrowser && viewItem =~ /library/", + "when": "view == objectBrowser && !listMultiSelection && viewItem =~ /library/", "group": "1_LibActions@1" }, { From b19d5cf2b18e36180eb38225cbb617e4259d6e74 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 12 Apr 2025 18:30:59 +0200 Subject: [PATCH 02/23] Muli-selection support for generateBinderSource command Signed-off-by: Seb Julliand --- src/ui/views/objectBrowser.ts | 51 +++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index d9d207327..f20396124 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -1,7 +1,7 @@ import fs, { existsSync } from "fs"; import os from "os"; import path, { basename, dirname } from "path"; -import vscode from "vscode"; +import vscode, { l10n } from "vscode"; import { parseFilter, singleGenericName } from "../../api/Filter"; import IBMi, { MemberParts } from "../../api/IBMi"; import { SortOptions, SortOrder } from "../../api/IBMiContent"; @@ -9,7 +9,7 @@ import { Search } from "../../api/Search"; import { Tools } from "../../api/Tools"; import { getMemberUri } from "../../filesystems/qsys/QSysFs"; import { instance } from "../../instantiate"; -import { CommandResult, DefaultOpenMode, FilteredItem, FocusOptions, IBMiMember, IBMiObject, MemberItem, ModuleExport, ObjectFilters, ObjectItem, ProgramExportImportInfo, WithLibrary } from "../../typings"; +import { CommandResult, DefaultOpenMode, FilteredItem, FocusOptions, IBMiMember, IBMiObject, MemberItem, ObjectFilters, ObjectItem, WithLibrary } from "../../typings"; import { editFilter } from "../../webviews/filters"; import { VscodeTools } from "../Tools"; import { BrowserItem, BrowserItemParameters } from "../types"; @@ -539,25 +539,36 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { objectTreeViewer.reveal(item, options); }), - vscode.commands.registerCommand(`code-for-ibmi.generateBinderSource`, async (node: ObjectBrowserObjectItem) => { + vscode.commands.registerCommand(`code-for-ibmi.generateBinderSource`, async (node: ObjectBrowserObjectItem, nodes: ObjectBrowserObjectItem[]) => { + nodes = (nodes || [node]); const contentApi = getContent(); - let exports: ProgramExportImportInfo[] | ModuleExport[] = []; - if (node.object.type === '*MODULE') { - exports = (await contentApi.getModuleExports(node.object.library, node.object.name)) - .filter(exp => exp.symbolType === 'PROCEDURE'); - } else { - exports = (await contentApi.getProgramExportImportInfo(node.object.library, node.object.name, node.object.type)) - .filter(info => info.symbolUsage === '*PROCEXP'); - } - const content = [ - `/* Binder source generated from ${node} */`, - ``, - `STRPGMEXP PGMLVL(*CURRENT) /* SIGNATURE("") */`, - ...exports.map(info => ` EXPORT SYMBOL("${info.symbolName}")`), - `ENDPGMEXP`, - ].join("\n"); - const textDoc = await vscode.workspace.openTextDocument({ language: 'bnd', content }); - await vscode.window.showTextDocument(textDoc); + const increment = 100 / nodes.length; + vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: true, title: l10n.t("Generating binder source") }, async (progress, cancel) => { + for (const node of nodes) { + if (cancel.isCancellationRequested) { + return; + } + progress.report({ message: node.toString(), increment }); + + const exports = []; + if (node.object.type === '*MODULE') { + exports.push(...(await contentApi.getModuleExports(node.object.library, node.object.name)) + .filter(exp => exp.symbolType === 'PROCEDURE')); + } else { + exports.push(...(await contentApi.getProgramExportImportInfo(node.object.library, node.object.name, node.object.type)) + .filter(info => info.symbolUsage === '*PROCEXP')); + } + const content = [ + `/* Binder source generated from ${node} */`, + ``, + `STRPGMEXP PGMLVL(*CURRENT) /* SIGNATURE("") */`, + ...exports.map(info => ` EXPORT SYMBOL("${info.symbolName}")`), + `ENDPGMEXP`, + ].join("\n"); + const textDoc = await vscode.workspace.openTextDocument({ language: 'bnd', content }); + await vscode.window.showTextDocument(textDoc); + } + }); }), vscode.commands.registerCommand(`code-for-ibmi.createMember`, async (node: ObjectBrowserSourcePhysicalFileItem, fullName?: string) => { From 357742ef7ebb4351ccd2c46c1b586ab0442fb1f7 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 12 Apr 2025 18:41:30 +0200 Subject: [PATCH 03/23] Muli-selection support for code-for-ibmi.browse/edit commands Signed-off-by: Seb Julliand --- src/commands/open.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/commands/open.ts b/src/commands/open.ts index ad412086d..d2f04fd3a 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -47,10 +47,10 @@ export function registerOpenCommands(instance: Instance): Disposable[] { } try { - if(options.position){ + if (options.position) { await commands.executeCommand(`vscode.openWith`, uri, 'default', { selection: options.position } as TextDocumentShowOptions); } - else{ + else { await commands.executeCommand(`vscode.open`, uri); } @@ -71,17 +71,17 @@ export function registerOpenCommands(instance: Instance): Disposable[] { } }), - commands.registerCommand("code-for-ibmi.browse", (item: WithPath | MemberItem) => { - return commands.executeCommand("code-for-ibmi.openWithDefaultMode", item, "browse" as DefaultOpenMode); + commands.registerCommand("code-for-ibmi.browse", (item: WithPath | MemberItem, items?: WithPath | MemberItem) => { + return commands.executeCommand("code-for-ibmi.openWithDefaultMode", items || item, "browse" as DefaultOpenMode); }), - commands.registerCommand("code-for-ibmi.edit", (item: WithPath | MemberItem) => { - return commands.executeCommand("code-for-ibmi.openWithDefaultMode", item, "edit" as DefaultOpenMode); + commands.registerCommand("code-for-ibmi.edit", (item: WithPath | MemberItem, items?: WithPath | MemberItem) => { + return commands.executeCommand("code-for-ibmi.openWithDefaultMode", items || item, "edit" as DefaultOpenMode); }), - commands.registerCommand("code-for-ibmi.openWithDefaultMode", (item: WithPath, overrideMode?: DefaultOpenMode, position?: Range) => { + commands.registerCommand("code-for-ibmi.openWithDefaultMode", (items: WithPath | WithPath[], overrideMode?: DefaultOpenMode, position?: Range) => { const readonly = (overrideMode || IBMi.connectionManager.get("defaultOpenMode")) === "browse"; - commands.executeCommand(`code-for-ibmi.openEditable`, item.path, { readonly, position } as OpenEditableOptions); + (Array.isArray(items) ? items : [items]).forEach(item => commands.executeCommand(`code-for-ibmi.openEditable`, item.path, { readonly, position } as OpenEditableOptions)); }), @@ -121,7 +121,7 @@ export function registerOpenCommands(instance: Instance): Disposable[] { const LOADING_LABEL = `Please wait`; const connection = instance.getConnection(); if (!connection) return; - + const storage = instance.getStorage(); const content = connection?.getContent(); let starRemoved: boolean = false; @@ -367,11 +367,11 @@ export function registerOpenCommands(instance: Instance): Disposable[] { window.showInformationMessage(`Cleared cached files.`); } else { const selectionSplit = connection!.upperCaseName(selection).split('/') - if ([3,4].includes(selectionSplit.length) || selection.startsWith(`/`)) { + if ([3, 4].includes(selectionSplit.length) || selection.startsWith(`/`)) { // When selection is QSYS path if (!selection.startsWith(`/`) && connection) { - if(selectionSplit.length === 4){ + if (selectionSplit.length === 4) { //Remove the iASP part selectionSplit.shift(); } From 5a36e20e8c54a39ab677fb93920dc92830d74411 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 12 Apr 2025 18:43:39 +0200 Subject: [PATCH 04/23] Enabled multi selection for openIFSFile menu Signed-off-by: Seb Julliand --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c82b91fb..bf1714871 100644 --- a/package.json +++ b/package.json @@ -2777,7 +2777,7 @@ }, { "submenu": "code-for-ibmi.openIFSFile", - "when": "view == ifsBrowser && !listMultiSelection && viewItem == streamfile", + "when": "view == ifsBrowser && viewItem == streamfile", "group": "0_open@1" }, { From 8fdaf08360f129f979ca9280783e9c797cdf5a01 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 12 Apr 2025 18:59:09 +0200 Subject: [PATCH 05/23] Enabled multi selection for maintainFilter command Signed-off-by: Seb Julliand --- src/ui/views/objectBrowser.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index f20396124..bf906f79c 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -512,9 +512,14 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { objectBrowser.refresh(); }), - vscode.commands.registerCommand(`code-for-ibmi.maintainFilter`, async (node?: FilteredItem) => { - await editFilter(node?.filter); - objectBrowser.refresh(); + vscode.commands.registerCommand(`code-for-ibmi.maintainFilter`, async (node?: FilteredItem, nodes?: FilteredItem[]) => { + if (node) { + (nodes || [node]).map(n => n.filter).forEach(filter => editFilter(filter).then(() => objectBrowser.refresh())); + } + else{ + await editFilter(); + objectBrowser.refresh(); + } }), vscode.commands.registerCommand(`code-for-ibmi.moveFilterUp`, (node: ObjectBrowserFilterItem) => objectBrowser.moveFilterInList(node, `UP`)), From 00a1903b3fcf9611680c99836e3848d3c79dc9d9 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 12 Apr 2025 18:59:30 +0200 Subject: [PATCH 06/23] Fixed filter update when multiple filters are opened simultaneously Signed-off-by: Seb Julliand --- src/webviews/filters/index.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 38059d88d..51633ef9a 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -1,14 +1,14 @@ -import { CustomUI } from "../CustomUI"; +import IBMi from "../../api/IBMi"; import { Tools } from "../../api/Tools"; import { instance } from "../../instantiate"; -import IBMi from "../../api/IBMi"; import { ObjectFilters } from "../../typings"; +import { CustomUI } from "../CustomUI"; export async function editFilter(filter?: ObjectFilters, copy = false) { const connection = instance.getConnection(); - const config = instance.getConfig(); + const config = connection?.getConfig(); if (config) { - const objectFilters = config.objectFilters; + let objectFilters = config.objectFilters; const filterIndex = filter ? objectFilters.findIndex(f => f.name === filter!.name) : -1; let newFilter = false; @@ -17,7 +17,7 @@ export async function editFilter(filter?: ObjectFilters, copy = false) { filter = { name: `${filter.name} - copy`, filterType: 'simple', - library: filter.library, + library: filter.library, object: filter.object, types: [...filter.types], member: filter.member, @@ -64,11 +64,11 @@ export async function editFilter(filter?: ObjectFilters, copy = false) { for (const key in data) { const useRegexFilters = data.filterType === "regex"; - + //In case we need to play with the data switch (key) { case `name`: - case `filterType`: + case `filterType`: data[key] = String(data[key]).trim(); break; case `types`: @@ -82,7 +82,7 @@ export async function editFilter(filter?: ObjectFilters, copy = false) { .filter(Tools.distinct) .join(","); break; - case `member`: + case `member`: case `memberType`: data[key] = String(data[key].trim()) || `*`; break; @@ -95,6 +95,8 @@ export async function editFilter(filter?: ObjectFilters, copy = false) { } } + //Re-read filters in case another filter was changed before this one + objectFilters = config.objectFilters if (newFilter) { if (objectFilters.some(f => f.name === data.name)) { data.name = `${data.name.trim()} (2)`; From 9930c5824e0458fe9b565cf2dd0bcf632eccb8d3 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 17 Apr 2025 16:31:55 +0200 Subject: [PATCH 07/23] multi selection actions (work in progress) Signed-off-by: Seb Julliand --- src/commands/actions.ts | 102 +++--- src/ui/actions.ts | 774 ++++++++++++++++++++-------------------- 2 files changed, 445 insertions(+), 431 deletions(-) diff --git a/src/commands/actions.ts b/src/commands/actions.ts index 008357249..feb4c4efe 100644 --- a/src/commands/actions.ts +++ b/src/commands/actions.ts @@ -1,67 +1,81 @@ import path from "path"; -import { commands, TreeItem, Uri, WorkspaceFolder, window, Disposable } from "vscode"; -import { refreshDiagnosticsFromServer } from "../ui/diagnostics"; +import { commands, Disposable, l10n, TreeItem, Uri, window, WorkspaceFolder } from "vscode"; +import IBMi from "../api/IBMi"; import Instance from "../Instance"; import { Action, DeploymentMethod } from "../typings"; import { runAction } from "../ui/actions"; -import IBMi from "../api/IBMi"; +import { refreshDiagnosticsFromServer } from "../ui/diagnostics"; import { BrowserItem } from "../ui/types"; export function registerActionsCommands(instance: Instance): Disposable[] { return [ - commands.registerCommand(`code-for-ibmi.runAction`, async (target: TreeItem | BrowserItem | Uri, group?: any, action?: Action, method?: DeploymentMethod, workspaceFolder?: WorkspaceFolder) => { + commands.registerCommand(`code-for-ibmi.runAction`, async (item?: (TreeItem | BrowserItem | Uri), items?: (TreeItem | BrowserItem | Uri)[], action?: Action, method?: DeploymentMethod, workspaceFolder?: WorkspaceFolder) => { const connection = instance.getConnection()!; - const editor = window.activeTextEditor; - let uri; - let browserItem; - if (target) { - if ("fsPath" in target) { - uri = target; + if (connection) { + const editor = window.activeTextEditor; + const browserItems: BrowserItem[] = []; + const uris: Uri[] = []; + if (!item) { + if (editor?.document.uri) { + uris.push(editor?.document.uri); + } } else { - uri = target?.resourceUri; - if ("refresh" in target) { - browserItem = target; + for (const target of (items || [item])) { + if (target instanceof Uri) { + uris.push(target); + } + else if (target.resourceUri) { + uris.push(target.resourceUri); + if (target instanceof BrowserItem) { + browserItems.push(target); + } + } } } - } - uri = uri || editor?.document.uri; + const scheme = uris[0]?.scheme; + if (scheme) { + if (!uris.every(uri => uri.scheme === scheme)) { + window.showWarningMessage(l10n.t("Actions can't be run on multiple items of different types.")) + return false; + } - if (uri) { - if (connection) { - const config = connection.getConfig(); - let canRun = true; - if (editor && uri.path === editor.document.uri.path && editor.document.isDirty) { - if (config.autoSaveBeforeAction) { - await editor.document.save(); - } else { - const result = await window.showWarningMessage(`The file must be saved to run Actions.`, `Save`, `Save automatically`, `Cancel`); - switch (result) { - case `Save`: - await editor.document.save(); - canRun = true; - break; - case `Save automatically`: - config.autoSaveBeforeAction = true; - await IBMi.connectionManager.update(config); - await editor.document.save(); - canRun = true; - break; - default: - canRun = false; - break; + const config = connection.getConfig(); + + for (const openedEditor of window.visibleTextEditors) { + const path = openedEditor.document.uri.path; + if (uris.some(uri => uri.path === path) && openedEditor.document.isDirty) { + if (config.autoSaveBeforeAction) { + await openedEditor.document.save(); + } else { + const result = await window.showWarningMessage(`File ${path} must be saved to run Actions.`, `Save`, `Save automatically`, `Cancel`); + switch (result) { + case `Save`: + await openedEditor.document.save(); + break; + + case `Save automatically`: + config.autoSaveBeforeAction = true; + await IBMi.connectionManager.update(config); + await openedEditor.document.save(); + break; + + default: + return; + } } } } - if (canRun && [`member`, `streamfile`, `file`, 'object'].includes(uri.scheme)) { - return await runAction(instance, uri, action, method, browserItem, workspaceFolder); + if ([`member`, `streamfile`, `file`, 'object'].includes(scheme)) { + return await runAction(instance, uris, action, method, browserItems, workspaceFolder); } } - else { - window.showErrorMessage('Please connect to an IBM i first'); - } + + } + else { + window.showErrorMessage('Please connect to an IBM i first'); } return false; @@ -94,7 +108,7 @@ export function registerActionsCommands(instance: Instance): Disposable[] { let initialPath = ``; const editor = window.activeTextEditor; const connection = instance.getConnection(); - + if (editor && connection) { const config = connection.getConfig(); const uri = editor.document.uri; diff --git a/src/ui/actions.ts b/src/ui/actions.ts index 570c7cc94..da704755d 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -1,19 +1,19 @@ import path from 'path'; -import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; import { getLocalActions } from '../filesystems/local/actions'; import { DeployTools } from '../filesystems/local/deployTools'; import { getBranchLibraryName, getEnvConfig } from '../filesystems/local/env'; import { getGitBranch } from '../filesystems/local/git'; -import Instance from '../Instance'; import { parseFSOptions } from '../filesystems/qsys/QSysFs'; +import Instance from '../Instance'; import { Action, DeploymentMethod, Variable } from '../typings'; +import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; import vscode, { CustomExecution, Pseudoterminal, TaskGroup, TaskRevealKind, WorkspaceFolder, commands, tasks } from 'vscode'; -import { CustomUI } from '../webviews/CustomUI'; -import { Tools } from '../api/Tools'; import { CompileTools } from '../api/CompileTools'; import IBMi from '../api/IBMi'; +import { Tools } from '../api/Tools'; +import { CustomUI } from '../webviews/CustomUI'; import { BrowserItem } from './types'; interface CommandObject { @@ -30,22 +30,26 @@ export function registerActionTools(context: vscode.ExtensionContext) { ); } -export async function runAction(instance: Instance, uri: vscode.Uri, customAction?: Action, method?: DeploymentMethod, browserItem?: BrowserItem, workspaceFolder?: WorkspaceFolder): Promise { +export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Uri[], customAction?: Action, method?: DeploymentMethod, browserItems?: BrowserItem[], workspaceFolder?: WorkspaceFolder): Promise { + uris = Array.isArray(uris) ? uris : [uris]; + //Global scheme: all URIs share the same + const scheme = uris[0].scheme; const connection = instance.getConnection(); - - const uriOptions = parseFSOptions(uri); if (connection) { const config = connection.getConfig(); const content = connection.getContent(); - const extension = uri.path.substring(uri.path.lastIndexOf(`.`) + 1).toUpperCase(); - const fragment = uri.fragment.toUpperCase(); + const targets = uris.map(uri => ({ + uri, + extension: uri.path.substring(uri.path.lastIndexOf(`.`) + 1).toUpperCase(), + fragment: uri.fragment.toUpperCase(), + protected: parseFSOptions(uri).readonly || config?.readOnlyMode, + workspaceFolder: workspaceFolder || vscode.workspace.getWorkspaceFolder(uri), + executionOK: false + })); - const isProtected = uriOptions.readonly || config?.readOnlyMode; + workspaceFolder = targets[0].workspaceFolder; - if (!workspaceFolder) { - workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); - } let remoteCwd = config?.homeDirectory || `.`; let availableActions: { label: string; action: Action; }[] = []; @@ -55,8 +59,8 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio // Then, if we're being called from a local file // we fetch the Actions defined from the workspace. - if (workspaceFolder && uri.scheme === `file`) { - const localActions = await getLocalActions(workspaceFolder); + if (targets[0].workspaceFolder && scheme === `file`) { + const localActions = await getLocalActions(targets[0].workspaceFolder); allActions.push(...localActions); } @@ -68,7 +72,9 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio }); // Then we get all the available Actions for the current context - availableActions = allActions.filter(action => action.type === uri.scheme && (!action.extensions || action.extensions.includes(extension) || action.extensions.includes(fragment) || action.extensions.includes(`GLOBAL`)) && (!isProtected || action.runOnProtected)) + availableActions = allActions.filter(action => action.type === scheme) + .filter(action => !action.extensions || targets.every(t => action.extensions!.includes(t.extension) || action.extensions!.includes(t.fragment)) || action.extensions.includes(`GLOBAL`)) + .filter(action => action.runOnProtected || !targets.every(t => !t.protected)) .sort((a, b) => (actionUsed.get(b.name) || 0) - (actionUsed.get(a.name) || 0)) .map(action => ({ label: action.name, @@ -108,434 +114,428 @@ export async function runAction(instance: Instance, uri: vscode.Uri, customActio } } - let fromWorkspace: WorkspaceFolder | undefined; - - if (chosenAction.type === `file` && vscode.workspace.workspaceFolders) { - fromWorkspace = vscode.workspace.workspaceFolders[workspaceId || 0]; - } - - const variables: Variable = {}; - const evfeventInfo: EvfEventInfo = { - object: '', - library: '', - extension, - workspace: fromWorkspace - }; - - if (workspaceFolder) { - const envFileVars = await getEnvConfig(workspaceFolder); + const fromWorkspace = (chosenAction.type === `file` && vscode.workspace.workspaceFolders) ? vscode.workspace.workspaceFolders[workspaceId || 0] : undefined; + const envFileVars = workspaceFolder ? await getEnvConfig(workspaceFolder) : {}; + for (const target of targets) { + const variables: Variable = {}; Object.entries(envFileVars).forEach(([key, value]) => variables[`&${key}`] = value); - } - - switch (chosenAction.type) { - case `member`: - const memberDetail = connection.parserMemberPath(uri.path); - evfeventInfo.library = memberDetail.library; - evfeventInfo.object = memberDetail.name; - evfeventInfo.extension = memberDetail.extension; - evfeventInfo.asp = memberDetail.asp; - - variables[`&OPENLIBL`] = memberDetail.library.toLowerCase(); - variables[`&OPENLIB`] = memberDetail.library; - - variables[`&OPENSPFL`] = memberDetail.file.toLowerCase(); - variables[`&OPENSPF`] = memberDetail.file; - - variables[`&OPENMBRL`] = memberDetail.name.toLowerCase(); - variables[`&OPENMBR`] = memberDetail.name; - - variables[`&EXTL`] = memberDetail.extension.toLowerCase(); - variables[`&EXT`] = memberDetail.extension; - break; - - case `file`: - case `streamfile`: - const pathData = path.parse(uri.path); - const basename = pathData.base; - const ext = pathData.ext ? (pathData.ext.startsWith(`.`) ? pathData.ext.substring(1) : pathData.ext) : ``; - const parent = path.parse(pathData.dir).base; - let name = pathData.name; - - // Logic to handle second extension, caused by bob. - const bobTypes = [`.PGM`, `.SRVPGM`]; - const secondName = path.parse(name); - if (secondName.ext && bobTypes.includes(secondName.ext.toUpperCase())) { - name = secondName.name; - } - - // Remove bob text convention - if (name.includes(`-`)) { - name = name.substring(0, name.indexOf(`-`)); - } - - if (variables[`&CURLIB`]) { - evfeventInfo.library = variables[`&CURLIB`]; - - } else { - evfeventInfo.library = config.currentLibrary; - } - - evfeventInfo.library = evfeventInfo.library.toUpperCase(); - evfeventInfo.object = name.toUpperCase(); - evfeventInfo.extension = ext; - - if (chosenAction.command.includes(`&SRCFILE`)) { - variables[`&SRCLIB`] = evfeventInfo.library; - variables[`&SRCPF`] = `QTMPSRC`; - variables[`&SRCFILE`] = `${evfeventInfo.library}/QTMPSRC`; - } - - switch (chosenAction.type) { - case `file`: - variables[`&LOCALPATH`] = uri.fsPath; - if (fromWorkspace) { - const relativePath = path.relative(fromWorkspace.uri.path, uri.path).split(path.sep).join(path.posix.sep); - variables[`&RELATIVEPATH`] = relativePath; + const evfeventInfo: EvfEventInfo = { + object: '', + library: '', + extension: target.extension, + workspace: fromWorkspace + }; - // We need to make sure the remote path is posix - const fullPath = path.posix.join(remoteCwd, relativePath); - variables[`&FULLPATH`] = fullPath; - variables[`{path}`] = fullPath; - variables[`&WORKDIR`] = remoteCwd; - variables[`&FILEDIR`] = path.posix.parse(fullPath).dir; - - const branch = getGitBranch(fromWorkspace); - if (branch) { - variables[`&BRANCHLIB`] = getBranchLibraryName(branch); - variables[`&BRANCH`] = branch; - variables[`{branch}`] = branch; + switch (chosenAction.type) { + case `member`: + const memberDetail = connection.parserMemberPath(target.uri.path); + evfeventInfo.library = memberDetail.library; + evfeventInfo.object = memberDetail.name; + evfeventInfo.extension = memberDetail.extension; + evfeventInfo.asp = memberDetail.asp; + + variables[`&OPENLIBL`] = memberDetail.library.toLowerCase(); + variables[`&OPENLIB`] = memberDetail.library; + + variables[`&OPENSPFL`] = memberDetail.file.toLowerCase(); + variables[`&OPENSPF`] = memberDetail.file; + + variables[`&OPENMBRL`] = memberDetail.name.toLowerCase(); + variables[`&OPENMBR`] = memberDetail.name; + + variables[`&EXTL`] = memberDetail.extension.toLowerCase(); + variables[`&EXT`] = memberDetail.extension; + break; + + case `file`: + case `streamfile`: + const pathData = path.parse(target.uri.path); + const basename = pathData.base; + const ext = pathData.ext ? (pathData.ext.startsWith(`.`) ? pathData.ext.substring(1) : pathData.ext) : ``; + const parent = path.parse(pathData.dir).base; + let name = pathData.name; + + // Logic to handle second extension, caused by bob. + const bobTypes = [`.PGM`, `.SRVPGM`]; + const secondName = path.parse(name); + if (secondName.ext && bobTypes.includes(secondName.ext.toUpperCase())) { + name = secondName.name; + } + + // Remove bob text convention + if (name.includes(`-`)) { + name = name.substring(0, name.indexOf(`-`)); + } + + if (variables[`&CURLIB`]) { + evfeventInfo.library = variables[`&CURLIB`]; + + } else { + evfeventInfo.library = config.currentLibrary; + } + + evfeventInfo.library = evfeventInfo.library.toUpperCase(); + evfeventInfo.object = name.toUpperCase(); + evfeventInfo.extension = ext; + + if (chosenAction.command.includes(`&SRCFILE`)) { + variables[`&SRCLIB`] = evfeventInfo.library; + variables[`&SRCPF`] = `QTMPSRC`; + variables[`&SRCFILE`] = `${evfeventInfo.library}/QTMPSRC`; + } + + switch (chosenAction.type) { + case `file`: + variables[`&LOCALPATH`] = target.uri.fsPath; + if (fromWorkspace) { + const relativePath = path.relative(fromWorkspace.uri.path, target.uri.path).split(path.sep).join(path.posix.sep); + variables[`&RELATIVEPATH`] = relativePath; + + // We need to make sure the remote path is posix + const fullPath = path.posix.join(remoteCwd, relativePath); + variables[`&FULLPATH`] = fullPath; + variables[`{path}`] = fullPath; + variables[`&WORKDIR`] = remoteCwd; + variables[`&FILEDIR`] = path.posix.parse(fullPath).dir; + + const branch = getGitBranch(fromWorkspace); + if (branch) { + variables[`&BRANCHLIB`] = getBranchLibraryName(branch); + variables[`&BRANCH`] = branch; + variables[`{branch}`] = branch; + } } - } - break; + break; - case `streamfile`: - const relativePath = path.posix.relative(remoteCwd, uri.path); - variables[`&RELATIVEPATH`] = relativePath; + case `streamfile`: + const relativePath = path.posix.relative(remoteCwd, target.uri.path); + variables[`&RELATIVEPATH`] = relativePath; - const fullName = uri.path; - variables[`&FULLPATH`] = fullName; - variables[`&FILEDIR`] = path.parse(fullName).dir; - break; - } + const fullName = target.uri.path; + variables[`&FULLPATH`] = fullName; + variables[`&FILEDIR`] = path.parse(fullName).dir; + break; + } - variables[`&PARENT`] = parent; + variables[`&PARENT`] = parent; - variables[`&BASENAME`] = basename; - variables[`{filename}`] = basename; + variables[`&BASENAME`] = basename; + variables[`{filename}`] = basename; - variables[`&NAMEL`] = name.toLowerCase(); - variables[`&NAME`] = name; + variables[`&NAMEL`] = name.toLowerCase(); + variables[`&NAME`] = name; - variables[`&EXTL`] = extension.toLowerCase(); - variables[`&EXT`] = extension; - break; + variables[`&EXTL`] = target.extension.toLowerCase(); + variables[`&EXT`] = target.extension; + break; - case `object`: - const [_, library, fullName] = uri.path.toUpperCase().split(`/`); - const object = fullName.substring(0, fullName.lastIndexOf(`.`)); + case `object`: + const [_, library, fullName] = target.uri.path.toUpperCase().split(`/`); + const object = fullName.substring(0, fullName.lastIndexOf(`.`)); - evfeventInfo.library = library; - evfeventInfo.object = object; + evfeventInfo.library = library; + evfeventInfo.object = object; - variables[`&LIBRARYL`] = library.toLowerCase(); - variables[`&LIBRARY`] = library; + variables[`&LIBRARYL`] = library.toLowerCase(); + variables[`&LIBRARY`] = library; - variables[`&NAMEL`] = object.toLowerCase(); - variables[`&NAME`] = object; + variables[`&NAMEL`] = object.toLowerCase(); + variables[`&NAME`] = object; - variables[`&TYPEL`] = extension.toLowerCase(); - variables[`&TYPE`] = extension; + variables[`&TYPEL`] = target.extension.toLowerCase(); + variables[`&TYPE`] = target.extension; - variables[`&EXTL`] = extension.toLowerCase(); - variables[`&EXT`] = extension; - break; - } + variables[`&EXTL`] = target.extension.toLowerCase(); + variables[`&EXT`] = target.extension; + break; + } - const viewControl = IBMi.connectionManager.get(`postActionView`) || "none"; - const outputBuffer: string[] = []; - let actionName = chosenAction.name; - let hasRun = false; - - const commandConfirm = async (commandString: string): Promise => { - const commands = commandString.split(`\n`).filter(command => command.trim().length > 0); - const promptedCommands = []; - for (let command of commands) { - if (command.startsWith(`?`)) { - command = await vscode.window.showInputBox({ prompt: `Run Command`, value: command.substring(1) }) || ''; - } else { - command = await showCustomInputs(`Run Command`, command, chosenAction.name || `Command`); + const viewControl = IBMi.connectionManager.get(`postActionView`) || "none"; + const outputBuffer: string[] = []; + let actionName = chosenAction.name; + let hasRun = false; + + const commandConfirm = async (commandString: string): Promise => { + const commands = commandString.split(`\n`).filter(command => command.trim().length > 0); + const promptedCommands = []; + for (let command of commands) { + if (command.startsWith(`?`)) { + command = await vscode.window.showInputBox({ prompt: `Run Command`, value: command.substring(1) }) || ''; + } else { + command = await showCustomInputs(`Run Command`, command, chosenAction.name || `Command`); + } + promptedCommands.push(command); + if (!command) break; } - promptedCommands.push(command); - if (!command) break; - } - return !promptedCommands.includes(``) ? promptedCommands.join(`\n`) : ``; - } + return !promptedCommands.includes(``) ? promptedCommands.join(`\n`) : ``; + } - const exitCode = await new Promise(resolve => - tasks.executeTask({ - isBackground: true, - name: chosenAction.name, - definition: { type: `ibmi` }, - scope: workspaceFolder, - source: 'IBM i', - presentationOptions: { - showReuseMessage: true, - clear: IBMi.connectionManager.get(`clearOutputEveryTime`), - focus: false, - reveal: (viewControl === `task` ? TaskRevealKind.Always : TaskRevealKind.Never), - }, - problemMatchers: [], - runOptions: {}, - group: TaskGroup.Build, - execution: new CustomExecution(async (e) => { - const writeEmitter = new vscode.EventEmitter(); - const closeEmitter = new vscode.EventEmitter(); - - writeEmitter.event(s => outputBuffer.push(s)); - closeEmitter.event(resolve); - - const term: Pseudoterminal = { - onDidWrite: writeEmitter.event, - onDidClose: closeEmitter.event, - open: async (initialDimensions: vscode.TerminalDimensions | undefined) => { - let successful = false; - let problemsFetched = false; - - try { - writeEmitter.fire(`Running Action: ${chosenAction.name} (${new Date().toLocaleTimeString()})` + CompileTools.NEWLINE); - - // If &SRCFILE is set, we need to copy the file to a temporary source file from the IFS - if (variables[`&FULLPATH`] && variables[`&SRCFILE`] && evfeventInfo.object) { - const [lib, srcpf] = variables[`&SRCFILE`].split(`/`); - - const createSourceFile = content.toCl(`CRTSRCPF`, { - rcdlen: 112, //NICE: this configurable in a VS Code setting? - file: `${lib}/${srcpf}`, - }); - - const copyFromStreamfile = content.toCl(`CPYFRMSTMF`, { - fromstmf: variables[`&FULLPATH`], - tombr: `'${Tools.qualifyPath(lib, srcpf, evfeventInfo.object)}'`, - mbropt: `*REPLACE`, - dbfccsid: `*FILE`, - stmfccsid: 1208, - }); - - // We don't care if this fails. Usually it's because the source file already exists. - await CompileTools.runCommand(connection, { command: createSourceFile, environment: `ile`, noLibList: true }); - - // Attempt to copy to member - const copyResult = await CompileTools.runCommand(connection, { command: copyFromStreamfile, environment: `ile`, noLibList: true }); - - if (copyResult.code !== 0) { - writeEmitter.fire(`Failed to copy file to a temporary member.\n\t${copyResult.stderr}\n\n`); - closeEmitter.fire(copyResult.code || 1); + const exitCode = await new Promise(resolve => + tasks.executeTask({ + isBackground: true, + name: chosenAction.name, + definition: { type: `ibmi` }, + scope: workspaceFolder, + source: 'IBM i', + presentationOptions: { + showReuseMessage: true, + clear: IBMi.connectionManager.get(`clearOutputEveryTime`), + focus: false, + reveal: (viewControl === `task` ? TaskRevealKind.Always : TaskRevealKind.Never), + }, + problemMatchers: [], + runOptions: {}, + group: TaskGroup.Build, + execution: new CustomExecution(async (e) => { + const writeEmitter = new vscode.EventEmitter(); + const closeEmitter = new vscode.EventEmitter(); + + writeEmitter.event(s => outputBuffer.push(s)); + closeEmitter.event(resolve); + + const term: Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: async (initialDimensions: vscode.TerminalDimensions | undefined) => { + let successful = false; + let problemsFetched = false; + + try { + writeEmitter.fire(`Running Action: ${chosenAction.name} (${new Date().toLocaleTimeString()})` + CompileTools.NEWLINE); + + // If &SRCFILE is set, we need to copy the file to a temporary source file from the IFS + if (variables[`&FULLPATH`] && variables[`&SRCFILE`] && evfeventInfo.object) { + const [lib, srcpf] = variables[`&SRCFILE`].split(`/`); + + const createSourceFile = content.toCl(`CRTSRCPF`, { + rcdlen: 112, //NICE: this configurable in a VS Code setting? + file: `${lib}/${srcpf}`, + }); + + const copyFromStreamfile = content.toCl(`CPYFRMSTMF`, { + fromstmf: variables[`&FULLPATH`], + tombr: `'${Tools.qualifyPath(lib, srcpf, evfeventInfo.object)}'`, + mbropt: `*REPLACE`, + dbfccsid: `*FILE`, + stmfccsid: 1208, + }); + + // We don't care if this fails. Usually it's because the source file already exists. + await CompileTools.runCommand(connection, { command: createSourceFile, environment: `ile`, noLibList: true }); + + // Attempt to copy to member + const copyResult = await CompileTools.runCommand(connection, { command: copyFromStreamfile, environment: `ile`, noLibList: true }); + + if (copyResult.code !== 0) { + writeEmitter.fire(`Failed to copy file to a temporary member.\n\t${copyResult.stderr}\n\n`); + closeEmitter.fire(copyResult.code || 1); + } } - } - const commandResult = await CompileTools.runCommand(connection, - { - title: chosenAction.name, - environment, - command: chosenAction.command, - cwd: remoteCwd, - env: variables, - }, { + const commandResult = await CompileTools.runCommand(connection, + { + title: chosenAction.name, + environment, + command: chosenAction.command, + cwd: remoteCwd, + env: variables, + }, { writeEvent: (content) => writeEmitter.fire(content), commandConfirm } - ); + ); - if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { - hasRun = true; - const isIleCommand = environment === `ile`; + if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { + hasRun = true; + const isIleCommand = environment === `ile`; - const useLocalEvfevent = - fromWorkspace && chosenAction.postDownload && - (chosenAction.postDownload.includes(`.evfevent`) || chosenAction.postDownload.includes(`.evfevent/`)); + const useLocalEvfevent = + fromWorkspace && chosenAction.postDownload && + (chosenAction.postDownload.includes(`.evfevent`) || chosenAction.postDownload.includes(`.evfevent/`)); - const possibleObject = getObjectFromCommand(commandResult.command); - if (isIleCommand && possibleObject) { - Object.assign(evfeventInfo, possibleObject); - } + const possibleObject = getObjectFromCommand(commandResult.command); + if (isIleCommand && possibleObject) { + Object.assign(evfeventInfo, possibleObject); + } - actionName = (isIleCommand && possibleObject ? `${chosenAction.name} for ${evfeventInfo.library}/${evfeventInfo.object}` : actionName); - successful = (commandResult.code === 0 || commandResult.code === null); + actionName = (isIleCommand && possibleObject ? `${chosenAction.name} for ${evfeventInfo.library}/${evfeventInfo.object}` : actionName); + successful = (commandResult.code === 0 || commandResult.code === null); - writeEmitter.fire(CompileTools.NEWLINE); + writeEmitter.fire(CompileTools.NEWLINE); - if (useLocalEvfevent) { - writeEmitter.fire(`Fetching errors from .evfevent.${CompileTools.NEWLINE}`); + if (useLocalEvfevent) { + writeEmitter.fire(`Fetching errors from .evfevent.${CompileTools.NEWLINE}`); - } - else if (evfeventInfo.object && evfeventInfo.library) { - if (chosenAction.command.includes(`*EVENTF`)) { - writeEmitter.fire(`Fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); - refreshDiagnosticsFromServer(instance, evfeventInfo); - problemsFetched = true; - } else if (chosenAction.command.trimStart().toUpperCase().startsWith(`CRT`)) { - writeEmitter.fire(`*EVENTF not found in command string. Not fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); } - } - - if (chosenAction.type === `file` && chosenAction.postDownload?.length) { - if (fromWorkspace) { - const remoteDir = remoteCwd; - const localDir = fromWorkspace.uri; - - const postDownloads: { type: vscode.FileType, localPath: string, remotePath: string }[] = []; - const downloadDirectories = new Set(); - for (const download of chosenAction.postDownload) { - const remotePath = path.posix.join(remoteDir, download); - const localPath = vscode.Uri.joinPath(localDir, download).path; - - let type: vscode.FileType; - if (await content.isDirectory(remotePath)) { - downloadDirectories.add(vscode.Uri.joinPath(localDir, download)); - type = vscode.FileType.Directory; - } - else { - const directory = path.parse(download).dir; - if (directory) { - downloadDirectories.add(vscode.Uri.joinPath(localDir, directory)); - } - type = vscode.FileType.File; - } - - postDownloads.push({ remotePath, localPath, type }) + else if (evfeventInfo.object && evfeventInfo.library) { + if (chosenAction.command.includes(`*EVENTF`)) { + writeEmitter.fire(`Fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); + refreshDiagnosticsFromServer(instance, evfeventInfo); + problemsFetched = true; + } else if (chosenAction.command.trimStart().toUpperCase().startsWith(`CRT`)) { + writeEmitter.fire(`*EVENTF not found in command string. Not fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); } + } - //Clear and create every local download directories - for (const downloadPath of downloadDirectories) { - try { - const stat = await vscode.workspace.fs.stat(downloadPath); //Check if target exists - if (stat.type !== vscode.FileType.Directory) { - if (await vscode.window.showWarningMessage(`${downloadPath} exists but is a file.`, "Delete and create directory")) { - await vscode.workspace.fs.delete(downloadPath); - throw new Error("Create directory"); - } + if (chosenAction.type === `file` && chosenAction.postDownload?.length) { + if (fromWorkspace) { + const remoteDir = remoteCwd; + const localDir = fromWorkspace.uri; + + const postDownloads: { type: vscode.FileType, localPath: string, remotePath: string }[] = []; + const downloadDirectories = new Set(); + for (const download of chosenAction.postDownload) { + const remotePath = path.posix.join(remoteDir, download); + const localPath = vscode.Uri.joinPath(localDir, download).path; + + let type: vscode.FileType; + if (await content.isDirectory(remotePath)) { + downloadDirectories.add(vscode.Uri.joinPath(localDir, download)); + type = vscode.FileType.Directory; } - else if (stat.type === vscode.FileType.Directory) { - await vscode.workspace.fs.delete(downloadPath, { recursive: true }); - throw new Error("Create directory"); + else { + const directory = path.parse(download).dir; + if (directory) { + downloadDirectories.add(vscode.Uri.joinPath(localDir, directory)); + } + type = vscode.FileType.File; } + + postDownloads.push({ remotePath, localPath, type }) } - catch (e) { - //Either fs.stat did not find the folder or it wasn't a folder and it's been deleted above + + //Clear and create every local download directories + for (const downloadPath of downloadDirectories) { try { - await vscode.workspace.fs.createDirectory(downloadPath) + const stat = await vscode.workspace.fs.stat(downloadPath); //Check if target exists + if (stat.type !== vscode.FileType.Directory) { + if (await vscode.window.showWarningMessage(`${downloadPath} exists but is a file.`, "Delete and create directory")) { + await vscode.workspace.fs.delete(downloadPath); + throw new Error("Create directory"); + } + } + else if (stat.type === vscode.FileType.Directory) { + await vscode.workspace.fs.delete(downloadPath, { recursive: true }); + throw new Error("Create directory"); + } } - catch (error) { - vscode.window.showWarningMessage(`Failed to create download path ${downloadPath}: ${error}`); - console.log(error); - closeEmitter.fire(1); + catch (e) { + //Either fs.stat did not find the folder or it wasn't a folder and it's been deleted above + try { + await vscode.workspace.fs.createDirectory(downloadPath) + } + catch (error) { + vscode.window.showWarningMessage(`Failed to create download path ${downloadPath}: ${error}`); + console.log(error); + closeEmitter.fire(1); + } } } - } - // Then we download the files that is specified. - const downloads = postDownloads.map( - async (postDownload) => { - const content = connection.getContent(); - if (postDownload.type === vscode.FileType.Directory) { - return content.downloadDirectory(postDownload.localPath, postDownload.remotePath, { recursive: true, concurrency: 5 }); - } else { - return content.downloadFile(postDownload.localPath, postDownload.remotePath); + // Then we download the files that is specified. + const downloads = postDownloads.map( + async (postDownload) => { + const content = connection.getContent(); + if (postDownload.type === vscode.FileType.Directory) { + return content.downloadDirectory(postDownload.localPath, postDownload.remotePath, { recursive: true, concurrency: 5 }); + } else { + return content.downloadFile(postDownload.localPath, postDownload.remotePath); + } } - } - ); + ); - await Promise.all(downloads) - .then(async result => { - // Done! - writeEmitter.fire(`Downloaded files as part of Action: ${chosenAction.postDownload!.join(`, `)}\n`); + await Promise.all(downloads) + .then(async result => { + // Done! + writeEmitter.fire(`Downloaded files as part of Action: ${chosenAction.postDownload!.join(`, `)}\n`); - // Process locally downloaded evfevent files: - if (useLocalEvfevent) { - refreshDiagnosticsFromLocal(instance, evfeventInfo); - problemsFetched = true; - } - }) - .catch(error => { - vscode.window.showErrorMessage(`Failed to download files as part of Action.`); - writeEmitter.fire(`Failed to download a file after Action: ${error.message}\n`); - closeEmitter.fire(1); - }); + // Process locally downloaded evfevent files: + if (useLocalEvfevent) { + refreshDiagnosticsFromLocal(instance, evfeventInfo); + problemsFetched = true; + } + }) + .catch(error => { + vscode.window.showErrorMessage(`Failed to download files as part of Action.`); + writeEmitter.fire(`Failed to download a file after Action: ${error.message}\n`); + closeEmitter.fire(1); + }); + } } - } - if (problemsFetched && viewControl === `problems`) { - commands.executeCommand(`workbench.action.problems.focus`); + if (problemsFetched && viewControl === `problems`) { + commands.executeCommand(`workbench.action.problems.focus`); + } + } else { + writeEmitter.fire(`Command did not run.` + CompileTools.NEWLINE); } - } else { - writeEmitter.fire(`Command did not run.` + CompileTools.NEWLINE); + + } catch (e) { + writeEmitter.fire(`${e}\n`); + vscode.window.showErrorMessage(`Action ${chosenAction} for ${evfeventInfo.library}/${evfeventInfo.object} failed. (internal error).`); + successful = false; } - } catch (e) { - writeEmitter.fire(`${e}\n`); - vscode.window.showErrorMessage(`Action ${chosenAction} for ${evfeventInfo.library}/${evfeventInfo.object} failed. (internal error).`); - successful = false; + closeEmitter.fire(successful ? 0 : 1); + }, + close: function (): void { } + }; + + return term; + }) + }) + ); + + target.executionOK = (exitCode === 0); + if (hasRun) { + if (target.executionOK && browserItem) { + switch (chosenAction.refresh) { + case 'browser': + if (chosenAction.type === 'streamfile') { + vscode.commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); + } + else if (chosenAction.type !== 'file') { + vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); } + break; - closeEmitter.fire(successful ? 0 : 1); - }, - close: function (): void { } - }; + case 'filter': + //Filter is a top level item so it has no parent (like Batman) + let filter: BrowserItem = browserItem; + while (filter.parent) { + filter = filter.parent; + } + filter.refresh?.(); + break; - return term; - }) - }) - ); - - const executionOK = (exitCode === 0); - if (hasRun) { - if (executionOK && browserItem) { - switch (chosenAction.refresh) { - case 'browser': - if (chosenAction.type === 'streamfile') { - vscode.commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); - } - else if (chosenAction.type !== 'file') { - vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); - } - break; - - case 'filter': - //Filter is a top level item so it has no parent (like Batman) - let filter: BrowserItem = browserItem; - while (filter.parent) { - filter = filter.parent; - } - filter.refresh?.(); - break; - - case 'parent': - browserItem.parent?.refresh?.(); - break; - - default: - //No refresh + case 'parent': + browserItem.parent?.refresh?.(); + break; + + default: + //No refresh + } } + + const openOutputAction = "Open output"; //TODO: will be translated in the future + const uiPromise = target.executionOK ? + vscode.window.showInformationMessage(`Action ${actionName} was successful.`, openOutputAction) : + vscode.window.showErrorMessage(`Action ${actionName} was not successful.`, openOutputAction); + + uiPromise.then(openOutput => { + if (openOutput) { + const now = new Date(); + new CustomUI() + .addParagraph(`
${outputBuffer.join("")}
`) + .setOptions({ fullWidth: true }) + .loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); + } + }) } - const openOutputAction = "Open output"; //TODO: will be translated in the future - const uiPromise = executionOK ? - vscode.window.showInformationMessage(`Action ${actionName} was successful.`, openOutputAction) : - vscode.window.showErrorMessage(`Action ${actionName} was not successful.`, openOutputAction); - - uiPromise.then(openOutput => { - if (openOutput) { - const now = new Date(); - new CustomUI() - .addParagraph(`
${outputBuffer.join("")}
`) - .setOptions({ fullWidth: true }) - .loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); - } - }) + return executionOK; } - - return executionOK; } else { return false; From 111aa605807342bf777a46778fe11fbd923fc8d4 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Fri, 18 Apr 2025 22:05:53 +0200 Subject: [PATCH 08/23] Multi selection actions working Signed-off-by: Seb Julliand --- src/ui/actions.ts | 776 +++++++++++++++++++++++----------------------- 1 file changed, 390 insertions(+), 386 deletions(-) diff --git a/src/ui/actions.ts b/src/ui/actions.ts index da704755d..b204a13a7 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -1,5 +1,5 @@ -import path from 'path'; +import path, { basename } from 'path'; import { getLocalActions } from '../filesystems/local/actions'; import { DeployTools } from '../filesystems/local/deployTools'; import { getBranchLibraryName, getEnvConfig } from '../filesystems/local/env'; @@ -9,11 +9,11 @@ import Instance from '../Instance'; import { Action, DeploymentMethod, Variable } from '../typings'; import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; -import vscode, { CustomExecution, Pseudoterminal, TaskGroup, TaskRevealKind, WorkspaceFolder, commands, tasks } from 'vscode'; +import vscode, { CustomExecution, Pseudoterminal, TaskGroup, TaskRevealKind, WorkspaceFolder, commands, l10n, tasks } from 'vscode'; import { CompileTools } from '../api/CompileTools'; import IBMi from '../api/IBMi'; import { Tools } from '../api/Tools'; -import { CustomUI } from '../webviews/CustomUI'; +import { CustomUI, Tab } from '../webviews/CustomUI'; import { BrowserItem } from './types'; interface CommandObject { @@ -45,7 +45,9 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur fragment: uri.fragment.toUpperCase(), protected: parseFSOptions(uri).readonly || config?.readOnlyMode, workspaceFolder: workspaceFolder || vscode.workspace.getWorkspaceFolder(uri), - executionOK: false + executionOK: false, + hasRun: false, + output: [] as string[] })); workspaceFolder = targets[0].workspaceFolder; @@ -73,8 +75,8 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur // Then we get all the available Actions for the current context availableActions = allActions.filter(action => action.type === scheme) - .filter(action => !action.extensions || targets.every(t => action.extensions!.includes(t.extension) || action.extensions!.includes(t.fragment)) || action.extensions.includes(`GLOBAL`)) - .filter(action => action.runOnProtected || !targets.every(t => !t.protected)) + .filter(action => !action.extensions || action.extensions.every(e => !e) || targets.every(t => action.extensions!.includes(t.extension) || action.extensions!.includes(t.fragment)) || action.extensions.includes(`GLOBAL`)) + .filter(action => action.runOnProtected || !targets.some(t => t.protected)) .sort((a, b) => (actionUsed.get(b.name) || 0) - (actionUsed.get(a.name) || 0)) .map(action => ({ label: action.name, @@ -116,437 +118,407 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur const fromWorkspace = (chosenAction.type === `file` && vscode.workspace.workspaceFolders) ? vscode.workspace.workspaceFolders[workspaceId || 0] : undefined; const envFileVars = workspaceFolder ? await getEnvConfig(workspaceFolder) : {}; - for (const target of targets) { - const variables: Variable = {}; - Object.entries(envFileVars).forEach(([key, value]) => variables[`&${key}`] = value); - const evfeventInfo: EvfEventInfo = { - object: '', - library: '', - extension: target.extension, - workspace: fromWorkspace - }; - - switch (chosenAction.type) { - case `member`: - const memberDetail = connection.parserMemberPath(target.uri.path); - evfeventInfo.library = memberDetail.library; - evfeventInfo.object = memberDetail.name; - evfeventInfo.extension = memberDetail.extension; - evfeventInfo.asp = memberDetail.asp; - - variables[`&OPENLIBL`] = memberDetail.library.toLowerCase(); - variables[`&OPENLIB`] = memberDetail.library; - - variables[`&OPENSPFL`] = memberDetail.file.toLowerCase(); - variables[`&OPENSPF`] = memberDetail.file; - - variables[`&OPENMBRL`] = memberDetail.name.toLowerCase(); - variables[`&OPENMBR`] = memberDetail.name; - - variables[`&EXTL`] = memberDetail.extension.toLowerCase(); - variables[`&EXT`] = memberDetail.extension; - break; - - case `file`: - case `streamfile`: - const pathData = path.parse(target.uri.path); - const basename = pathData.base; - const ext = pathData.ext ? (pathData.ext.startsWith(`.`) ? pathData.ext.substring(1) : pathData.ext) : ``; - const parent = path.parse(pathData.dir).base; - let name = pathData.name; - - // Logic to handle second extension, caused by bob. - const bobTypes = [`.PGM`, `.SRVPGM`]; - const secondName = path.parse(name); - if (secondName.ext && bobTypes.includes(secondName.ext.toUpperCase())) { - name = secondName.name; - } - - // Remove bob text convention - if (name.includes(`-`)) { - name = name.substring(0, name.indexOf(`-`)); - } - - if (variables[`&CURLIB`]) { - evfeventInfo.library = variables[`&CURLIB`]; - - } else { - evfeventInfo.library = config.currentLibrary; - } - - evfeventInfo.library = evfeventInfo.library.toUpperCase(); - evfeventInfo.object = name.toUpperCase(); - evfeventInfo.extension = ext; - - if (chosenAction.command.includes(`&SRCFILE`)) { - variables[`&SRCLIB`] = evfeventInfo.library; - variables[`&SRCPF`] = `QTMPSRC`; - variables[`&SRCFILE`] = `${evfeventInfo.library}/QTMPSRC`; - } - switch (chosenAction.type) { - case `file`: - variables[`&LOCALPATH`] = target.uri.fsPath; - if (fromWorkspace) { - const relativePath = path.relative(fromWorkspace.uri.path, target.uri.path).split(path.sep).join(path.posix.sep); - variables[`&RELATIVEPATH`] = relativePath; - - // We need to make sure the remote path is posix - const fullPath = path.posix.join(remoteCwd, relativePath); - variables[`&FULLPATH`] = fullPath; - variables[`{path}`] = fullPath; - variables[`&WORKDIR`] = remoteCwd; - variables[`&FILEDIR`] = path.posix.parse(fullPath).dir; - - const branch = getGitBranch(fromWorkspace); - if (branch) { - variables[`&BRANCHLIB`] = getBranchLibraryName(branch); - variables[`&BRANCH`] = branch; - variables[`{branch}`] = branch; + await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: true, title: l10n.t("Running action {0} on {1} item(s)", chosenAction.name, targets.length) }, async (task, canceled) => { + const increment = 100 / targets.length; + let done = 1; + for (const target of targets) { + task.report({ message: `${done++}/${targets.length}`, increment }) + const variables: Variable = {}; + Object.entries(envFileVars).forEach(([key, value]) => variables[`&${key}`] = value); + const evfeventInfo: EvfEventInfo = { + object: '', + library: '', + extension: target.extension, + workspace: fromWorkspace + }; + + switch (chosenAction.type) { + case `member`: + const memberDetail = connection.parserMemberPath(target.uri.path); + evfeventInfo.library = memberDetail.library; + evfeventInfo.object = memberDetail.name; + evfeventInfo.extension = memberDetail.extension; + evfeventInfo.asp = memberDetail.asp; + + variables[`&OPENLIBL`] = memberDetail.library.toLowerCase(); + variables[`&OPENLIB`] = memberDetail.library; + + variables[`&OPENSPFL`] = memberDetail.file.toLowerCase(); + variables[`&OPENSPF`] = memberDetail.file; + + variables[`&OPENMBRL`] = memberDetail.name.toLowerCase(); + variables[`&OPENMBR`] = memberDetail.name; + + variables[`&EXTL`] = memberDetail.extension.toLowerCase(); + variables[`&EXT`] = memberDetail.extension; + break; + + case `file`: + case `streamfile`: + const pathData = path.parse(target.uri.path); + const basename = pathData.base; + const ext = pathData.ext ? (pathData.ext.startsWith(`.`) ? pathData.ext.substring(1) : pathData.ext) : ``; + const parent = path.parse(pathData.dir).base; + let name = pathData.name; + + // Logic to handle second extension, caused by bob. + const bobTypes = [`.PGM`, `.SRVPGM`]; + const secondName = path.parse(name); + if (secondName.ext && bobTypes.includes(secondName.ext.toUpperCase())) { + name = secondName.name; + } + + // Remove bob text convention + if (name.includes(`-`)) { + name = name.substring(0, name.indexOf(`-`)); + } + + if (variables[`&CURLIB`]) { + evfeventInfo.library = variables[`&CURLIB`]; + + } else { + evfeventInfo.library = config.currentLibrary; + } + + evfeventInfo.library = evfeventInfo.library.toUpperCase(); + evfeventInfo.object = name.toUpperCase(); + evfeventInfo.extension = ext; + + if (chosenAction.command.includes(`&SRCFILE`)) { + variables[`&SRCLIB`] = evfeventInfo.library; + variables[`&SRCPF`] = `QTMPSRC`; + variables[`&SRCFILE`] = `${evfeventInfo.library}/QTMPSRC`; + } + + switch (chosenAction.type) { + case `file`: + variables[`&LOCALPATH`] = target.uri.fsPath; + if (fromWorkspace) { + const relativePath = path.relative(fromWorkspace.uri.path, target.uri.path).split(path.sep).join(path.posix.sep); + variables[`&RELATIVEPATH`] = relativePath; + + // We need to make sure the remote path is posix + const fullPath = path.posix.join(remoteCwd, relativePath); + variables[`&FULLPATH`] = fullPath; + variables[`{path}`] = fullPath; + variables[`&WORKDIR`] = remoteCwd; + variables[`&FILEDIR`] = path.posix.parse(fullPath).dir; + + const branch = getGitBranch(fromWorkspace); + if (branch) { + variables[`&BRANCHLIB`] = getBranchLibraryName(branch); + variables[`&BRANCH`] = branch; + variables[`{branch}`] = branch; + } } - } - break; + break; - case `streamfile`: - const relativePath = path.posix.relative(remoteCwd, target.uri.path); - variables[`&RELATIVEPATH`] = relativePath; + case `streamfile`: + const relativePath = path.posix.relative(remoteCwd, target.uri.path); + variables[`&RELATIVEPATH`] = relativePath; - const fullName = target.uri.path; - variables[`&FULLPATH`] = fullName; - variables[`&FILEDIR`] = path.parse(fullName).dir; - break; - } + const fullName = target.uri.path; + variables[`&FULLPATH`] = fullName; + variables[`&FILEDIR`] = path.parse(fullName).dir; + break; + } - variables[`&PARENT`] = parent; + variables[`&PARENT`] = parent; - variables[`&BASENAME`] = basename; - variables[`{filename}`] = basename; + variables[`&BASENAME`] = basename; + variables[`{filename}`] = basename; - variables[`&NAMEL`] = name.toLowerCase(); - variables[`&NAME`] = name; + variables[`&NAMEL`] = name.toLowerCase(); + variables[`&NAME`] = name; - variables[`&EXTL`] = target.extension.toLowerCase(); - variables[`&EXT`] = target.extension; - break; + variables[`&EXTL`] = target.extension.toLowerCase(); + variables[`&EXT`] = target.extension; + break; - case `object`: - const [_, library, fullName] = target.uri.path.toUpperCase().split(`/`); - const object = fullName.substring(0, fullName.lastIndexOf(`.`)); + case `object`: + const [_, library, fullName] = target.uri.path.toUpperCase().split(`/`); + const object = fullName.substring(0, fullName.lastIndexOf(`.`)); - evfeventInfo.library = library; - evfeventInfo.object = object; + evfeventInfo.library = library; + evfeventInfo.object = object; - variables[`&LIBRARYL`] = library.toLowerCase(); - variables[`&LIBRARY`] = library; + variables[`&LIBRARYL`] = library.toLowerCase(); + variables[`&LIBRARY`] = library; - variables[`&NAMEL`] = object.toLowerCase(); - variables[`&NAME`] = object; + variables[`&NAMEL`] = object.toLowerCase(); + variables[`&NAME`] = object; - variables[`&TYPEL`] = target.extension.toLowerCase(); - variables[`&TYPE`] = target.extension; + variables[`&TYPEL`] = target.extension.toLowerCase(); + variables[`&TYPE`] = target.extension; - variables[`&EXTL`] = target.extension.toLowerCase(); - variables[`&EXT`] = target.extension; - break; - } + variables[`&EXTL`] = target.extension.toLowerCase(); + variables[`&EXT`] = target.extension; + break; + } - const viewControl = IBMi.connectionManager.get(`postActionView`) || "none"; - const outputBuffer: string[] = []; - let actionName = chosenAction.name; - let hasRun = false; - - const commandConfirm = async (commandString: string): Promise => { - const commands = commandString.split(`\n`).filter(command => command.trim().length > 0); - const promptedCommands = []; - for (let command of commands) { - if (command.startsWith(`?`)) { - command = await vscode.window.showInputBox({ prompt: `Run Command`, value: command.substring(1) }) || ''; - } else { - command = await showCustomInputs(`Run Command`, command, chosenAction.name || `Command`); + const viewControl = IBMi.connectionManager.get(`postActionView`) || "none"; + let actionName = chosenAction.name; + + const commandConfirm = async (commandString: string): Promise => { + const commands = commandString.split(`\n`).filter(command => command.trim().length > 0); + const promptedCommands = []; + for (let command of commands) { + if (command.startsWith(`?`)) { + command = await vscode.window.showInputBox({ prompt: `Run Command`, value: command.substring(1) }) || ''; + } else { + command = await showCustomInputs(`Run Command`, command, chosenAction.name || `Command`); + } + promptedCommands.push(command); + if (!command) break; } - promptedCommands.push(command); - if (!command) break; - } - return !promptedCommands.includes(``) ? promptedCommands.join(`\n`) : ``; - } + return !promptedCommands.includes(``) ? promptedCommands.join(`\n`) : ``; + } - const exitCode = await new Promise(resolve => - tasks.executeTask({ - isBackground: true, - name: chosenAction.name, - definition: { type: `ibmi` }, - scope: workspaceFolder, - source: 'IBM i', - presentationOptions: { - showReuseMessage: true, - clear: IBMi.connectionManager.get(`clearOutputEveryTime`), - focus: false, - reveal: (viewControl === `task` ? TaskRevealKind.Always : TaskRevealKind.Never), - }, - problemMatchers: [], - runOptions: {}, - group: TaskGroup.Build, - execution: new CustomExecution(async (e) => { - const writeEmitter = new vscode.EventEmitter(); - const closeEmitter = new vscode.EventEmitter(); - - writeEmitter.event(s => outputBuffer.push(s)); - closeEmitter.event(resolve); - - const term: Pseudoterminal = { - onDidWrite: writeEmitter.event, - onDidClose: closeEmitter.event, - open: async (initialDimensions: vscode.TerminalDimensions | undefined) => { - let successful = false; - let problemsFetched = false; - - try { - writeEmitter.fire(`Running Action: ${chosenAction.name} (${new Date().toLocaleTimeString()})` + CompileTools.NEWLINE); - - // If &SRCFILE is set, we need to copy the file to a temporary source file from the IFS - if (variables[`&FULLPATH`] && variables[`&SRCFILE`] && evfeventInfo.object) { - const [lib, srcpf] = variables[`&SRCFILE`].split(`/`); - - const createSourceFile = content.toCl(`CRTSRCPF`, { - rcdlen: 112, //NICE: this configurable in a VS Code setting? - file: `${lib}/${srcpf}`, - }); - - const copyFromStreamfile = content.toCl(`CPYFRMSTMF`, { - fromstmf: variables[`&FULLPATH`], - tombr: `'${Tools.qualifyPath(lib, srcpf, evfeventInfo.object)}'`, - mbropt: `*REPLACE`, - dbfccsid: `*FILE`, - stmfccsid: 1208, - }); - - // We don't care if this fails. Usually it's because the source file already exists. - await CompileTools.runCommand(connection, { command: createSourceFile, environment: `ile`, noLibList: true }); - - // Attempt to copy to member - const copyResult = await CompileTools.runCommand(connection, { command: copyFromStreamfile, environment: `ile`, noLibList: true }); - - if (copyResult.code !== 0) { - writeEmitter.fire(`Failed to copy file to a temporary member.\n\t${copyResult.stderr}\n\n`); - closeEmitter.fire(copyResult.code || 1); + const exitCode = await new Promise(resolve => + tasks.executeTask({ + isBackground: true, + name: chosenAction.name, + definition: { type: `ibmi` }, + scope: workspaceFolder, + source: 'IBM i', + presentationOptions: { + showReuseMessage: true, + clear: IBMi.connectionManager.get(`clearOutputEveryTime`), + focus: false, + reveal: (viewControl === `task` ? TaskRevealKind.Always : TaskRevealKind.Never), + }, + problemMatchers: [], + runOptions: {}, + group: TaskGroup.Build, + execution: new CustomExecution(async (e) => { + const writeEmitter = new vscode.EventEmitter(); + const closeEmitter = new vscode.EventEmitter(); + + writeEmitter.event(s => target.output.push(s)); + closeEmitter.event(resolve); + + const term: Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: async () => { + let successful = false; + let problemsFetched = false; + + try { + writeEmitter.fire(`Running Action: ${chosenAction.name} (${new Date().toLocaleTimeString()})` + CompileTools.NEWLINE); + + // If &SRCFILE is set, we need to copy the file to a temporary source file from the IFS + if (variables[`&FULLPATH`] && variables[`&SRCFILE`] && evfeventInfo.object) { + const [lib, srcpf] = variables[`&SRCFILE`].split(`/`); + + const createSourceFile = content.toCl(`CRTSRCPF`, { + rcdlen: 112, //NICE: this configurable in a VS Code setting? + file: `${lib}/${srcpf}`, + }); + + const copyFromStreamfile = content.toCl(`CPYFRMSTMF`, { + fromstmf: variables[`&FULLPATH`], + tombr: `'${Tools.qualifyPath(lib, srcpf, evfeventInfo.object)}'`, + mbropt: `*REPLACE`, + dbfccsid: `*FILE`, + stmfccsid: 1208, + }); + + // We don't care if this fails. Usually it's because the source file already exists. + await CompileTools.runCommand(connection, { command: createSourceFile, environment: `ile`, noLibList: true }); + + // Attempt to copy to member + const copyResult = await CompileTools.runCommand(connection, { command: copyFromStreamfile, environment: `ile`, noLibList: true }); + + if (copyResult.code !== 0) { + writeEmitter.fire(`Failed to copy file to a temporary member.\n\t${copyResult.stderr}\n\n`); + closeEmitter.fire(copyResult.code || 1); + } } - } - const commandResult = await CompileTools.runCommand(connection, - { - title: chosenAction.name, - environment, - command: chosenAction.command, - cwd: remoteCwd, - env: variables, - }, { - writeEvent: (content) => writeEmitter.fire(content), - commandConfirm - } - ); + const commandResult = await CompileTools.runCommand(connection, + { + title: chosenAction.name, + environment, + command: chosenAction.command, + cwd: remoteCwd, + env: variables, + }, { + writeEvent: (content) => writeEmitter.fire(content), + commandConfirm + } + ); - if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { - hasRun = true; - const isIleCommand = environment === `ile`; + if (commandResult && commandResult.code !== CompileTools.DID_NOT_RUN) { + target.hasRun = true; + const isIleCommand = environment === `ile`; - const useLocalEvfevent = - fromWorkspace && chosenAction.postDownload && - (chosenAction.postDownload.includes(`.evfevent`) || chosenAction.postDownload.includes(`.evfevent/`)); + const useLocalEvfevent = + fromWorkspace && chosenAction.postDownload && + (chosenAction.postDownload.includes(`.evfevent`) || chosenAction.postDownload.includes(`.evfevent/`)); - const possibleObject = getObjectFromCommand(commandResult.command); - if (isIleCommand && possibleObject) { - Object.assign(evfeventInfo, possibleObject); - } + const possibleObject = getObjectFromCommand(commandResult.command); + if (isIleCommand && possibleObject) { + Object.assign(evfeventInfo, possibleObject); + } - actionName = (isIleCommand && possibleObject ? `${chosenAction.name} for ${evfeventInfo.library}/${evfeventInfo.object}` : actionName); - successful = (commandResult.code === 0 || commandResult.code === null); + actionName = (isIleCommand && possibleObject ? `${chosenAction.name} for ${evfeventInfo.library}/${evfeventInfo.object}` : actionName); + successful = (commandResult.code === 0 || commandResult.code === null); - writeEmitter.fire(CompileTools.NEWLINE); + writeEmitter.fire(CompileTools.NEWLINE); - if (useLocalEvfevent) { - writeEmitter.fire(`Fetching errors from .evfevent.${CompileTools.NEWLINE}`); + if (useLocalEvfevent) { + writeEmitter.fire(`Fetching errors from .evfevent.${CompileTools.NEWLINE}`); - } - else if (evfeventInfo.object && evfeventInfo.library) { - if (chosenAction.command.includes(`*EVENTF`)) { - writeEmitter.fire(`Fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); - refreshDiagnosticsFromServer(instance, evfeventInfo); - problemsFetched = true; - } else if (chosenAction.command.trimStart().toUpperCase().startsWith(`CRT`)) { - writeEmitter.fire(`*EVENTF not found in command string. Not fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); } - } - - if (chosenAction.type === `file` && chosenAction.postDownload?.length) { - if (fromWorkspace) { - const remoteDir = remoteCwd; - const localDir = fromWorkspace.uri; - - const postDownloads: { type: vscode.FileType, localPath: string, remotePath: string }[] = []; - const downloadDirectories = new Set(); - for (const download of chosenAction.postDownload) { - const remotePath = path.posix.join(remoteDir, download); - const localPath = vscode.Uri.joinPath(localDir, download).path; - - let type: vscode.FileType; - if (await content.isDirectory(remotePath)) { - downloadDirectories.add(vscode.Uri.joinPath(localDir, download)); - type = vscode.FileType.Directory; - } - else { - const directory = path.parse(download).dir; - if (directory) { - downloadDirectories.add(vscode.Uri.joinPath(localDir, directory)); - } - type = vscode.FileType.File; - } - - postDownloads.push({ remotePath, localPath, type }) + else if (evfeventInfo.object && evfeventInfo.library) { + if (chosenAction.command.includes(`*EVENTF`)) { + writeEmitter.fire(`Fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); + refreshDiagnosticsFromServer(instance, evfeventInfo); + problemsFetched = true; + } else if (chosenAction.command.trimStart().toUpperCase().startsWith(`CRT`)) { + writeEmitter.fire(`*EVENTF not found in command string. Not fetching errors for ${evfeventInfo.library}/${evfeventInfo.object}.` + CompileTools.NEWLINE); } + } - //Clear and create every local download directories - for (const downloadPath of downloadDirectories) { - try { - const stat = await vscode.workspace.fs.stat(downloadPath); //Check if target exists - if (stat.type !== vscode.FileType.Directory) { - if (await vscode.window.showWarningMessage(`${downloadPath} exists but is a file.`, "Delete and create directory")) { - await vscode.workspace.fs.delete(downloadPath); - throw new Error("Create directory"); - } + if (chosenAction.type === `file` && chosenAction.postDownload?.length) { + if (fromWorkspace) { + const remoteDir = remoteCwd; + const localDir = fromWorkspace.uri; + + const postDownloads: { type: vscode.FileType, localPath: string, remotePath: string }[] = []; + const downloadDirectories = new Set(); + for (const download of chosenAction.postDownload) { + const remotePath = path.posix.join(remoteDir, download); + const localPath = vscode.Uri.joinPath(localDir, download).path; + + let type: vscode.FileType; + if (await content.isDirectory(remotePath)) { + downloadDirectories.add(vscode.Uri.joinPath(localDir, download)); + type = vscode.FileType.Directory; } - else if (stat.type === vscode.FileType.Directory) { - await vscode.workspace.fs.delete(downloadPath, { recursive: true }); - throw new Error("Create directory"); + else { + const directory = path.parse(download).dir; + if (directory) { + downloadDirectories.add(vscode.Uri.joinPath(localDir, directory)); + } + type = vscode.FileType.File; } + + postDownloads.push({ remotePath, localPath, type }) } - catch (e) { - //Either fs.stat did not find the folder or it wasn't a folder and it's been deleted above + + //Clear and create every local download directories + for (const downloadPath of downloadDirectories) { try { - await vscode.workspace.fs.createDirectory(downloadPath) + const stat = await vscode.workspace.fs.stat(downloadPath); //Check if target exists + if (stat.type !== vscode.FileType.Directory) { + if (await vscode.window.showWarningMessage(`${downloadPath} exists but is a file.`, "Delete and create directory")) { + await vscode.workspace.fs.delete(downloadPath); + throw new Error("Create directory"); + } + } + else if (stat.type === vscode.FileType.Directory) { + await vscode.workspace.fs.delete(downloadPath, { recursive: true }); + throw new Error("Create directory"); + } } - catch (error) { - vscode.window.showWarningMessage(`Failed to create download path ${downloadPath}: ${error}`); - console.log(error); - closeEmitter.fire(1); + catch (e) { + //Either fs.stat did not find the folder or it wasn't a folder and it's been deleted above + try { + await vscode.workspace.fs.createDirectory(downloadPath) + } + catch (error) { + vscode.window.showWarningMessage(`Failed to create download path ${downloadPath}: ${error}`); + console.log(error); + closeEmitter.fire(1); + } } } - } - // Then we download the files that is specified. - const downloads = postDownloads.map( - async (postDownload) => { - const content = connection.getContent(); - if (postDownload.type === vscode.FileType.Directory) { - return content.downloadDirectory(postDownload.localPath, postDownload.remotePath, { recursive: true, concurrency: 5 }); - } else { - return content.downloadFile(postDownload.localPath, postDownload.remotePath); + // Then we download the files that is specified. + const downloads = postDownloads.map( + async (postDownload) => { + const content = connection.getContent(); + if (postDownload.type === vscode.FileType.Directory) { + return content.downloadDirectory(postDownload.localPath, postDownload.remotePath, { recursive: true, concurrency: 5 }); + } else { + return content.downloadFile(postDownload.localPath, postDownload.remotePath); + } } - } - ); + ); - await Promise.all(downloads) - .then(async result => { - // Done! - writeEmitter.fire(`Downloaded files as part of Action: ${chosenAction.postDownload!.join(`, `)}\n`); + await Promise.all(downloads) + .then(async result => { + // Done! + writeEmitter.fire(`Downloaded files as part of Action: ${chosenAction.postDownload!.join(`, `)}\n`); - // Process locally downloaded evfevent files: - if (useLocalEvfevent) { - refreshDiagnosticsFromLocal(instance, evfeventInfo); - problemsFetched = true; - } - }) - .catch(error => { - vscode.window.showErrorMessage(`Failed to download files as part of Action.`); - writeEmitter.fire(`Failed to download a file after Action: ${error.message}\n`); - closeEmitter.fire(1); - }); + // Process locally downloaded evfevent files: + if (useLocalEvfevent) { + refreshDiagnosticsFromLocal(instance, evfeventInfo); + problemsFetched = true; + } + }) + .catch(error => { + vscode.window.showErrorMessage(`Failed to download files as part of Action.`); + writeEmitter.fire(`Failed to download a file after Action: ${error.message}\n`); + closeEmitter.fire(1); + }); + } } - } - if (problemsFetched && viewControl === `problems`) { - commands.executeCommand(`workbench.action.problems.focus`); + if (problemsFetched && viewControl === `problems`) { + commands.executeCommand(`workbench.action.problems.focus`); + } + } else { + writeEmitter.fire(`Command did not run.` + CompileTools.NEWLINE); } - } else { - writeEmitter.fire(`Command did not run.` + CompileTools.NEWLINE); - } - } catch (e) { - writeEmitter.fire(`${e}\n`); - vscode.window.showErrorMessage(`Action ${chosenAction} for ${evfeventInfo.library}/${evfeventInfo.object} failed. (internal error).`); - successful = false; - } + } catch (e) { + writeEmitter.fire(`${e}\n`); + vscode.window.showErrorMessage(`Action ${chosenAction} for ${evfeventInfo.library}/${evfeventInfo.object} failed. (internal error).`); + successful = false; + } - closeEmitter.fire(successful ? 0 : 1); - }, - close: function (): void { } - }; + closeEmitter.fire(successful ? 0 : 1); + }, + close: function (): void { } + }; - return term; + return term; + }) }) - }) - ); - - target.executionOK = (exitCode === 0); - if (hasRun) { - if (target.executionOK && browserItem) { - switch (chosenAction.refresh) { - case 'browser': - if (chosenAction.type === 'streamfile') { - vscode.commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); - } - else if (chosenAction.type !== 'file') { - vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); - } - break; - - case 'filter': - //Filter is a top level item so it has no parent (like Batman) - let filter: BrowserItem = browserItem; - while (filter.parent) { - filter = filter.parent; - } - filter.refresh?.(); - break; - - case 'parent': - browserItem.parent?.refresh?.(); - break; - - default: - //No refresh - } - } + ); - const openOutputAction = "Open output"; //TODO: will be translated in the future - const uiPromise = target.executionOK ? - vscode.window.showInformationMessage(`Action ${actionName} was successful.`, openOutputAction) : - vscode.window.showErrorMessage(`Action ${actionName} was not successful.`, openOutputAction); - - uiPromise.then(openOutput => { - if (openOutput) { - const now = new Date(); - new CustomUI() - .addParagraph(`
${outputBuffer.join("")}
`) - .setOptions({ fullWidth: true }) - .loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); - } - }) + target.executionOK = (exitCode === 0); + + if (target.hasRun && target.executionOK && target.executionOK) { + doRefresh(chosenAction, browserItems?.find(item => item.resourceUri?.path === target.uri.path)); + } } + }); - return executionOK; - } - } - else { - return false; + const openOutputAction = l10n.t("Open output(s)"); + const uiPromise = targets.every(target => target.executionOK) ? + vscode.window.showInformationMessage(l10n.t(`Action {0} was successful.`, chosenAction.name), openOutputAction) : + vscode.window.showErrorMessage(l10n.t(`Action {0} was not successful ({1}/{2} failed).`, chosenAction.name, targets.filter(target => !target.executionOK).length, targets.length), openOutputAction); + + uiPromise.then(openOutput => { + if (openOutput) { + const now = new Date(); + new CustomUI() + .addTabs([...targets.map(target => ({ label: basename(target.uri.path), value: `
${target.output.join("")}
` }) as Tab)]) + .setOptions({ fullWidth: true }) + .loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); + } + }) } - } else if (isProtected) { - //when a member is protected(read only) - vscode.window.showErrorMessage(`Action cannot be applied on a read only member.`); - return false; - } else { - //No compile commands - vscode.window.showErrorMessage(`No compile commands found for ${uri.scheme}-${extension}.`); + return targets.every(target => target.executionOK); + } + else { + vscode.window.showErrorMessage(l10n.t(`No suitable actions found for {0} - {1}`, scheme, targets.map(t => t.extension).filter(Tools.distinct).join(", "))); return false; } } @@ -555,6 +527,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur } } + function getObjectFromCommand(baseCommand?: string): CommandObject | undefined { if (baseCommand) { const regex = PARM_REGEX.exec(baseCommand.toUpperCase()); @@ -655,4 +628,35 @@ async function showCustomInputs(name: string, command: string, title?: string): } return command; -} \ No newline at end of file +} + +function doRefresh(chosenAction: Action, browserItem?: BrowserItem) { + if (browserItem) { + switch (chosenAction.refresh) { + case 'browser': + if (chosenAction.type === 'streamfile') { + vscode.commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); + } + else if (chosenAction.type !== 'file') { + vscode.commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); + } + break; + + case 'filter': + //Filter is a top level item so it has no parent (like Batman) + let filter: BrowserItem = browserItem; + while (filter.parent) { + filter = filter.parent; + } + filter.refresh?.(); + break; + + case 'parent': + browserItem.parent?.refresh?.(); + break; + + default: + //No refresh + } + } +} From f6be325afb02ec1cd2ba1213890f52f1893c74ef Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Fri, 18 Apr 2025 22:07:40 +0200 Subject: [PATCH 09/23] Fixed names uppercasing Signed-off-by: Seb Julliand --- src/ui/actions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/actions.ts b/src/ui/actions.ts index b204a13a7..0b49da5a3 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -181,8 +181,8 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur evfeventInfo.library = config.currentLibrary; } - evfeventInfo.library = evfeventInfo.library.toUpperCase(); - evfeventInfo.object = name.toUpperCase(); + evfeventInfo.library = connection.upperCaseName(evfeventInfo.library); + evfeventInfo.object = connection.upperCaseName(name); evfeventInfo.extension = ext; if (chosenAction.command.includes(`&SRCFILE`)) { @@ -237,7 +237,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur break; case `object`: - const [_, library, fullName] = target.uri.path.toUpperCase().split(`/`); + const [_, library, fullName] = connection.upperCaseName(target.uri.path).split(`/`); const object = fullName.substring(0, fullName.lastIndexOf(`.`)); evfeventInfo.library = library; From ab03174b376aa3838743b357699a8982044d78b4 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 19 Apr 2025 10:50:06 +0200 Subject: [PATCH 10/23] Enhanced actions result panel for multi executions Signed-off-by: Seb Julliand --- package-lock.json | 248 ++++++++++++++++++++++++--------------- package.json | 2 +- src/ui/actions.ts | 76 ++++++++++-- src/webviews/CustomUI.ts | 100 ++++++++++++---- 4 files changed, 294 insertions(+), 132 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d9f2e54d..cddb57685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1891,12 +1891,14 @@ } }, "node_modules/esbuild-loader": { - "version": "3.0.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-3.2.0.tgz", + "integrity": "sha512-lnIdRMQpk50alCa0QoW0ozc0D3rjJXl02mtMsk9INIcW25RPZhDja332bu85ixwVNbhQ7VfBRcQyZ/qza8mWiA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.17.6", - "get-tsconfig": "^4.4.0", + "esbuild": "^0.19.0", + "get-tsconfig": "^4.6.2", "loader-utils": "^2.0.4", "webpack-sources": "^1.4.3" }, @@ -1907,14 +1909,32 @@ "webpack": "^4.40.0 || ^5.0.0" } }, + "node_modules/esbuild-loader/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/esbuild-loader/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1924,13 +1944,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1940,13 +1961,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1956,13 +1978,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1972,13 +1995,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1988,13 +2012,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2004,13 +2029,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -2020,13 +2046,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2036,13 +2063,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2052,13 +2080,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2068,13 +2097,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2084,13 +2114,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2100,13 +2131,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2116,13 +2148,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2132,13 +2165,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2148,7 +2182,9 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "cpu": [ "x64" ], @@ -2163,13 +2199,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -2179,13 +2216,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -2195,13 +2233,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -2211,13 +2250,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2227,13 +2267,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2243,13 +2284,14 @@ } }, "node_modules/esbuild-loader/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2259,7 +2301,9 @@ } }, "node_modules/esbuild-loader/node_modules/esbuild": { - "version": "0.17.19", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2270,28 +2314,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, "node_modules/esbuild-loader/node_modules/webpack-sources": { @@ -3050,9 +3095,14 @@ "license": "MIT" }, "node_modules/get-tsconfig": { - "version": "4.4.0", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, "funding": { "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } @@ -3869,6 +3919,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rimraf": { "version": "3.0.2", "license": "ISC", @@ -4508,9 +4568,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.17", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz", - "integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==", + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index bf1714871..2952ae28e 100644 --- a/package.json +++ b/package.json @@ -3037,7 +3037,7 @@ }, "dependencies": { "@ibm/ibmi-eventf-parser": "^1.0.2", - "@vscode-elements/elements": "^1.9.1", + "@vscode-elements/elements": "^1.9.1", "adm-zip": "^0.5.14", "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", "csv": "^6.2.1", diff --git a/src/ui/actions.ts b/src/ui/actions.ts index 0b49da5a3..7d10ccc80 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -13,14 +13,26 @@ import vscode, { CustomExecution, Pseudoterminal, TaskGroup, TaskRevealKind, Wor import { CompileTools } from '../api/CompileTools'; import IBMi from '../api/IBMi'; import { Tools } from '../api/Tools'; -import { CustomUI, Tab } from '../webviews/CustomUI'; +import { CustomUI, TreeListItem } from '../webviews/CustomUI'; import { BrowserItem } from './types'; -interface CommandObject { +type CommandObject = { object: string library?: string } +type ActionTarget = { + uri: vscode.Uri + extension: string + fragment: string + protected: boolean + workspaceFolder: vscode.WorkspaceFolder + executionOK: boolean, + hasRun: boolean, + processed: boolean, + output: string[] +} + const actionUsed: Map = new Map; const PARM_REGEX = /(PNLGRP|OBJ|PGM|MODULE)\((?.+?)\)/; @@ -47,8 +59,9 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur workspaceFolder: workspaceFolder || vscode.workspace.getWorkspaceFolder(uri), executionOK: false, hasRun: false, - output: [] as string[] - })); + processed: false, + output: [] + }) as ActionTarget); workspaceFolder = targets[0].workspaceFolder; @@ -119,11 +132,17 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur const fromWorkspace = (chosenAction.type === `file` && vscode.workspace.workspaceFolders) ? vscode.workspace.workspaceFolders[workspaceId || 0] : undefined; const envFileVars = workspaceFolder ? await getEnvConfig(workspaceFolder) : {}; + let cancelled = false; await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: true, title: l10n.t("Running action {0} on {1} item(s)", chosenAction.name, targets.length) }, async (task, canceled) => { const increment = 100 / targets.length; let done = 1; for (const target of targets) { + if (canceled.isCancellationRequested) { + cancelled = true; + return; + } task.report({ message: `${done++}/${targets.length}`, increment }) + target.processed = true; const variables: Variable = {}; Object.entries(envFileVars).forEach(([key, value]) => variables[`&${key}`] = value); const evfeventInfo: EvfEventInfo = { @@ -501,17 +520,43 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur }); const openOutputAction = l10n.t("Open output(s)"); - const uiPromise = targets.every(target => target.executionOK) ? - vscode.window.showInformationMessage(l10n.t(`Action {0} was successful.`, chosenAction.name), openOutputAction) : - vscode.window.showErrorMessage(l10n.t(`Action {0} was not successful ({1}/{2} failed).`, chosenAction.name, targets.filter(target => !target.executionOK).length, targets.length), openOutputAction); + let uiPromise; + if (cancelled) { + uiPromise = vscode.window.showWarningMessage(l10n.t(`Action {0} was cancelled; ({1} processed).`, chosenAction.name, targets.filter(target => target.processed).length), openOutputAction); + } + else if (targets.every(target => target.executionOK)) { + uiPromise = vscode.window.showInformationMessage(l10n.t(`Action {0} was successful.`, chosenAction.name), openOutputAction); + } + else { + uiPromise = vscode.window.showErrorMessage(l10n.t(`Action {0} was not successful ({1}/{2} failed).`, chosenAction.name, targets.filter(target => !target.executionOK).length, targets.length), openOutputAction); + } uiPromise.then(openOutput => { if (openOutput) { const now = new Date(); - new CustomUI() - .addTabs([...targets.map(target => ({ label: basename(target.uri.path), value: `
${target.output.join("")}
` }) as Tab)]) - .setOptions({ fullWidth: true }) - .loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); + const resultsPanel = new CustomUI(); + if (targets.length === 1) { + resultsPanel.addParagraph(`
${targets[0].output.join("")}
`) + .setOptions({ fullPage: true }); + } + else { + resultsPanel.addBrowser("results", targets.filter(target => target.processed).map(target => ({ label: `${getTargetResultIcon(target)} ${basename(target.uri.path)}`, value: `
${target.output.join("")}
` } as TreeListItem))) + .setOptions({ + fullPage: true, + css: /* css */ ` + body{ + margin: 0; + padding: 0; + overflow: hidden; + } + + pre { + margin: 1em; + } + ` + }); + } + resultsPanel.loadPage(`${chosenAction.name} [${now.toLocaleString()}]`); } }) } @@ -660,3 +705,12 @@ function doRefresh(chosenAction: Action, browserItem?: BrowserItem) { } } } +function getTargetResultIcon(target: ActionTarget) { + if (target.hasRun) { + return target.executionOK ? '✔️' : '❌'; + } + else { + return '❔'; + } +} + diff --git a/src/webviews/CustomUI.ts b/src/webviews/CustomUI.ts index dfafe79b4..11111106f 100644 --- a/src/webviews/CustomUI.ts +++ b/src/webviews/CustomUI.ts @@ -6,8 +6,12 @@ const vscodeweb = require(`@vscode-elements/elements/dist/bundled`); type PanelOptions = { fullWidth?: boolean + fullPage?: boolean + css?: string }; +type TreeLeafAction = "submit" | "browse"; + export interface Page { panel: vscode.WebviewPanel data?: T @@ -37,7 +41,7 @@ export interface ComplexTab { } interface WebviewMessageRequest { - type: "submit"|"file"; + type: "submit" | "file"; data?: any; } @@ -63,7 +67,7 @@ export class Section { return this; } - addInput(id: string, label: string, description?: string, options?: { default?: string, readonly?: boolean, rows?: number, minlength?: number, maxlength?: number, regexTest?: string, inputType?: InputType, min?:number, max?:number }) { + addInput(id: string, label: string, description?: string, options?: { default?: string, readonly?: boolean, rows?: number, minlength?: number, maxlength?: number, regexTest?: string, inputType?: InputType, min?: number, max?: number }) { const input = Object.assign(new Field('input', id, label, description), options); this.addField(input); return this; @@ -113,8 +117,9 @@ export class Section { return this; } - addTree(id: string, label: string, treeItems: TreeListItem[], description?: string) { + addTree(id: string, label: string, treeItems: TreeListItem[], description?: string, onClick: TreeLeafAction = "submit") { const tree = new Field('tree', id, label, description); + tree.treeLeafAction = onClick; tree.treeList = treeItems; this.addField(tree); return this; @@ -132,6 +137,17 @@ export class Section { this.fields.push(field); return this; } + + addBrowser(id: string, items: TreeListItem[]) { + const browser = new Field('browser', id, ''); + browser.treeList = items; + if (browser.treeList[0]) { + browser.treeList[0].selected = true; + } + browser.treeLeafAction = 'browse'; + this.addField(browser); + return this; + } } const openedWebviews: Map = new Map; @@ -220,8 +236,8 @@ export class CustomUI extends Section { } private getHTML(panel: vscode.WebviewPanel, title: string) { - const notInputFields = [`submit`, `buttons`, `tree`, `hr`, `paragraph`, `tabs`, `complexTabs`]; - const trees = this.fields.filter(field => field.type == `tree`); + const notInputFields = [`submit`, `buttons`, `tree`, `hr`, `paragraph`, `tabs`, `complexTabs`, 'browser'] as FieldType[]; + const trees = this.fields.filter(field => [`tree`, 'browser'].includes(field.type)); const complexTabFields = this.fields.filter(field => field.type === `complexTabs`).map(tabs => tabs.complexTabItems?.map(tab => tab.fields)); const allFields = [...this.fields, ...complexTabFields.flat(2)].filter(cField => cField) as Field[]; @@ -239,13 +255,13 @@ export class CustomUI extends Section { - - ${this.fields.map(field => field.getHTML()).join(``)} - + ${this.options?.fullPage ? + this.fields.map(field => field.getHTML()).join(``) : + /* html */ ` + + ${this.fields.map(field => field.getHTML()).join(``)} + + ` + }