diff --git a/package.json b/package.json index c6fa197e8..bf7b2079a 100644 --- a/package.json +++ b/package.json @@ -156,26 +156,6 @@ "default": [], "description": "A collection of connection settings to easily switch between them on this system." }, - "commandProfiles": { - "type": "array", - "items": { - "type": "object", - "title": "Command Profile", - "properties": { - "name": { - "type": "string", - "description": "Profile name" - }, - "command": { - "type": "string", - "description": "Command" - } - }, - "additionalProperties": true - }, - "default": [], - "description": "A collection of commands to easily switch between library list configurations on this system." - }, "ifsShortcuts": { "type": "array", "items": { @@ -889,34 +869,6 @@ } }, "commands": [ - { - "command": "code-for-ibmi.setToDefault", - "title": "Reset to Default Profile", - "category": "IBM i", - "enablement": "code-for-ibmi:connected == true", - "icon": "$(arrow-circle-right)" - }, - { - "command": "code-for-ibmi.manageCommandProfile", - "title": "Create/edit command profile...", - "category": "IBM i", - "enablement": "code-for-ibmi:connected == true", - "icon": "$(library)" - }, - { - "command": "code-for-ibmi.deleteCommandProfile", - "title": "Delete command profile...", - "category": "IBM i", - "enablement": "code-for-ibmi:connected == true && code-for-ibmi:hasProfiles == true", - "icon": "$(remove)" - }, - { - "command": "code-for-ibmi.loadCommandProfile", - "title": "Load command profile", - "category": "IBM i", - "enablement": "code-for-ibmi:connected == true", - "icon": "$(arrow-circle-right)" - }, { "command": "code-for-ibmi.debug.setup.local", "title": "Import Client Certificate", @@ -1046,13 +998,6 @@ "category": "IBM i", "icon": "$(go-to-file)" }, - { - "command": "code-for-ibmi.showVariableMaintenance", - "enablement": "code-for-ibmi:connected", - "title": "Maintain Custom Variables...", - "category": "IBM i", - "icon": "$(variable)" - }, { "command": "code-for-ibmi.toggleSourceDateGutter", "enablement": "code-for-ibmi:connected", @@ -1165,7 +1110,7 @@ "enablement": "code-for-ibmi:connected && !code-for-ibmi:isSystemReadonly", "title": "Run Action...", "category": "IBM i", - "icon": "$(file-binary)" + "icon": "$(github-action)" }, { "command": "code-for-ibmi.userLibraryList.enable", @@ -1222,34 +1167,6 @@ "category": "IBM i", "icon": "$(check)" }, - { - "command": "code-for-ibmi.newConnectionProfile", - "enablement": "code-for-ibmi:connected", - "title": "Save Current Settings to New Profile...", - "category": "IBM i", - "icon": "$(save-as)" - }, - { - "command": "code-for-ibmi.saveConnectionProfile", - "enablement": "code-for-ibmi:connected", - "title": "Save Current Settings to Profile", - "category": "IBM i", - "icon": "$(save)" - }, - { - "command": "code-for-ibmi.deleteConnectionProfile", - "enablement": "code-for-ibmi:connected && code-for-ibmi:hasProfiles == true", - "title": "Delete Profile...", - "category": "IBM i", - "icon": "$(remove)" - }, - { - "command": "code-for-ibmi.loadConnectionProfile", - "enablement": "code-for-ibmi:connected", - "title": "Set Active Profile", - "category": "IBM i", - "icon": "$(arrow-circle-right)" - }, { "command": "code-for-ibmi.searchSourceFile", "enablement": "code-for-ibmi:connected", @@ -1310,12 +1227,6 @@ "title": "Refresh", "category": "IBM i" }, - { - "command": "code-for-ibmi.revealInIFSBrowser", - "enablement": "code-for-ibmi:connected", - "title": "Reveal Browser Item", - "category": "IBM i" - }, { "command": "code-for-ibmi.changeWorkingDirectory", "enablement": "code-for-ibmi:connected", @@ -1393,9 +1304,10 @@ }, { "command": "code-for-ibmi.launchActionsSetup", - "enablement": "code-for-ibmi:connected", - "title": "Launch Actions Setup...", - "category": "IBM i" + "enablement": "code-for-ibmi:connected && workspaceFolderCount > 0", + "title": "Launch Workspace Actions Setup...", + "category": "IBM i", + "icon": "$(new-folder)" }, { "command": "code-for-ibmi.addIFSShortcut", @@ -1529,12 +1441,6 @@ "title": "Refresh", "category": "IBM i" }, - { - "command": "code-for-ibmi.revealInObjectBrowser", - "enablement": "code-for-ibmi:connected", - "title": "Reveal Object Browser Item", - "category": "IBM i" - }, { "command": "code-for-ibmi.createSourceFile", "enablement": "code-for-ibmi:connected && !code-for-ibmi:isReadonly", @@ -1708,6 +1614,160 @@ "category": "IBM i", "icon": "$(plus)", "enablement": "code-for-ibmi:connected && !code-for-ibmi:isReadonly" + }, + { + "command": "code-for-ibmi.environment.refresh", + "enablement": "code-for-ibmi:connected", + "title": "Refresh", + "category": "IBM i", + "icon": "$(refresh)" + }, + { + "command": "code-for-ibmi.environment.action.search", + "enablement": "code-for-ibmi:connected", + "title": "Search action", + "category": "IBM i", + "icon": "$(search)" + }, + { + "command": "code-for-ibmi.environment.action.search.next", + "enablement": "code-for-ibmi:connected && code-for-ibmi:hasActionSearched", + "title": "Go to next search match", + "category": "IBM i", + "icon": "$(go-to-search)" + }, + { + "command": "code-for-ibmi.environment.action.search.clear", + "enablement": "code-for-ibmi:connected && code-for-ibmi:hasActionSearched", + "title": "Clear search", + "category": "IBM i", + "icon": "$(search-stop)" + }, + { + "command": "code-for-ibmi.environment.action.create", + "enablement": "code-for-ibmi:connected", + "title": "Create action", + "category": "IBM i", + "icon": "$(add)" + }, + { + "command": "code-for-ibmi.environment.action.rename", + "enablement": "code-for-ibmi:connected", + "title": "Rename...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.action.copy", + "enablement": "code-for-ibmi:connected", + "title": "Copy...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.action.delete", + "enablement": "code-for-ibmi:connected", + "title": "Delete...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.action.runOnEditor", + "title": "Run on active editor", + "category": "IBM i", + "icon": "$(debug-start)" + }, + { + "command": "code-for-ibmi.environment.variable.declare", + "enablement": "code-for-ibmi:connected", + "title": "Declare custom variable", + "category": "IBM i", + "icon": "$(add)" + }, + { + "command": "code-for-ibmi.environment.variable.edit", + "enablement": "code-for-ibmi:connected", + "title": "Change value...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.variable.rename", + "enablement": "code-for-ibmi:connected", + "title": "Rename...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.variable.copy", + "enablement": "code-for-ibmi:connected", + "title": "Copy...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.variable.delete", + "enablement": "code-for-ibmi:connected", + "title": "Delete...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.profile.create", + "enablement": "code-for-ibmi:connected", + "title": "Create new profile", + "category": "IBM i", + "icon": "$(add)" + }, + { + "command": "code-for-ibmi.environment.profile.fromCurrent", + "enablement": "code-for-ibmi:connected", + "title": "Create new profile from current context", + "category": "IBM i", + "icon": "$(diff-added)" + }, + { + "command": "code-for-ibmi.environment.profile.rename", + "enablement": "code-for-ibmi:connected", + "title": "Rename...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.profile.copy", + "enablement": "code-for-ibmi:connected", + "title": "Copy...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.profile.delete", + "enablement": "code-for-ibmi:connected", + "title": "Delete...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.environment.profile.activate", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Set as active profile", + "icon": "$(arrow-swap)" + }, + { + "command": "code-for-ibmi.environment.profile.runLiblistCommand", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Run Library List Command", + "icon": "$(play-circle)" + }, + { + "command": "code-for-ibmi.environment.profile.unload", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Unload active profile", + "icon": "$(sign-out)" + } + ], + "customEditors": [ + { + "viewType": "code-for-ibmi.editor", + "displayName": "Code for i editor", + "selector": [ + { + "filenamePattern": "code4i:*/**/*" + } + ] } ], "keybindings": [ @@ -1816,9 +1876,9 @@ "when": "!code-for-ibmi:connecting && !code-for-ibmi:connected && !code-for-ibmi:connectionBrowserDisabled" }, { - "id": "profilesView", - "name": "Profiles", - "when": "code-for-ibmi:connected && code-for-ibmi:hasProfiles && code-for-ibmi:profilesViewDisabled !== true", + "id": "environmentView", + "name": "Environment", + "when": "code-for-ibmi:connected && !(code-for-ibmi:profilesViewDisabled || code-for-ibmi:environmentViewDisabled)", "visibility": "collapsed" }, { @@ -2192,10 +2252,6 @@ "command": "code-for-ibmi.moveObject", "when": "never" }, - { - "command": "code-for-ibmi.revealInObjectBrowser", - "when": "never" - }, { "command": "code-for-ibmi.refreshObjectBrowserItem", "when": "never" @@ -2204,10 +2260,6 @@ "command": "code-for-ibmi.refreshIFSBrowser", "when": "never" }, - { - "command": "code-for-ibmi.revealInIFSBrowser", - "when": "never" - }, { "command": "code-for-ibmi.deleteIFS", "when": "never" @@ -2367,6 +2419,94 @@ { "command": "code-for-ibmi.generateBinderSource", "when": "never" + }, + { + "command": "code-for-ibmi.environment.refresh", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.search", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.search.next", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.search.clear", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.create", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.rename", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.copy", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.delete", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.action.runOnEditor", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.variable.declare", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.variable.edit", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.variable.rename", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.variable.copy", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.variable.delete", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.create", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.fromCurrent", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.rename", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.copy", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.delete", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.activate", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.runLiblistCommand", + "when": "never" + }, + { + "command": "code-for-ibmi.environment.profile.unload", + "when": "never" } ], "view/title": [ @@ -2400,21 +2540,6 @@ "group": "navigation", "when": "view == libraryListView" }, - { - "command": "code-for-ibmi.newConnectionProfile", - "group": "navigation", - "when": "view == libraryListView && code-for-ibmi:hasProfiles != true" - }, - { - "command": "code-for-ibmi.newConnectionProfile", - "group": "navigation@profile", - "when": "view == profilesView" - }, - { - "command": "code-for-ibmi.manageCommandProfile", - "group": "navigation@profile", - "when": "view == profilesView" - }, { "command": "code-for-ibmi.createFilter", "group": "navigation@1", @@ -2430,11 +2555,6 @@ "group": "navigation@4", "when": "view == objectBrowser" }, - { - "command": "code-for-ibmi.showVariableMaintenance", - "group": "", - "when": "view == libraryListView" - }, { "command": "code-for-ibmi.cleanupLibraryList", "group": "", @@ -2509,6 +2629,11 @@ "command": "code-for-ibmi.term5250.resetPosition", "group": "navigation", "when": "code-for-ibmi:term5250Halted" + }, + { + "command": "code-for-ibmi.environment.refresh", + "when": "view === environmentView", + "group": "navigation@99" } ], "editor/title": [ @@ -2590,19 +2715,29 @@ "group": "inline" }, { - "command": "code-for-ibmi.loadConnectionProfile", - "when": "view == profilesView && viewItem == profile", - "group": "inline" + "command": "code-for-ibmi.launchActionsSetup", + "when": "view === environmentView && viewItem =~ /^actionsNode/", + "group": "inline@02" }, { - "command": "code-for-ibmi.loadCommandProfile", - "when": "view == profilesView && viewItem == commandProfile", - "group": "inline" + "command": "code-for-ibmi.environment.action.search", + "when": "view === environmentView && viewItem === actionsNode", + "group": "inline@10" }, { - "command": "code-for-ibmi.setToDefault", - "when": "view == profilesView && viewItem == resetProfile", - "group": "inline" + "command": "code-for-ibmi.environment.action.search.next", + "when": "view === environmentView && viewItem === actionsNode", + "group": "inline@11" + }, + { + "command": "code-for-ibmi.environment.action.search.clear", + "when": "view === environmentView && viewItem === actionsNode", + "group": "inline@12" + }, + { + "command": "code-for-ibmi.environment.action.create", + "when": "view === environmentView && viewItem =~ /^(actionsNode|actionTypeNode)/", + "group": "inline@01" }, { "command": "code-for-ibmi.createMember", @@ -2629,26 +2764,6 @@ "when": "view == libraryListView && viewItem == library", "group": "02libraryActions@01" }, - { - "command": "code-for-ibmi.saveConnectionProfile", - "when": "view == profilesView && viewItem == profile", - "group": "profiles@1" - }, - { - "command": "code-for-ibmi.manageCommandProfile", - "when": "view == profilesView && viewItem == commandProfile", - "group": "profiles@1" - }, - { - "command": "code-for-ibmi.deleteConnectionProfile", - "when": "view == profilesView && viewItem == profile", - "group": "profiles@2" - }, - { - "command": "code-for-ibmi.deleteCommandProfile", - "when": "view == profilesView && viewItem == commandProfile", - "group": "profiles@2" - }, { "command": "code-for-ibmi.maintainFilter", "when": "view == objectBrowser && viewItem =~ /^filter.*$/", @@ -2978,6 +3093,86 @@ "command": "code-for-ibmi.generateBinderSource", "when": "view == objectBrowser && viewItem =~ /^object.(module|srvpgm).*/", "group": "1_objActions@6" + }, + { + "command": "code-for-ibmi.environment.action.runOnEditor", + "when": "view === environmentView && viewItem =~ /^actionItem_canrun/", + "group": "inline@00" + }, + { + "command": "code-for-ibmi.environment.action.rename", + "when": "view === environmentView && viewItem =~ /^actionItem/", + "group": "00_actionItemAction01" + }, + { + "command": "code-for-ibmi.environment.action.copy", + "when": "view === environmentView && viewItem =~ /^actionItem/", + "group": "10_actionItemAction01" + }, + { + "command": "code-for-ibmi.environment.action.delete", + "when": "view === environmentView && viewItem =~ /^actionItem/", + "group": "20_actionItemAction01" + }, + { + "command": "code-for-ibmi.environment.variable.declare", + "when": "view === environmentView && viewItem =~ /^customVariablesNode/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.environment.variable.rename", + "when": "view === environmentView && viewItem =~ /^customVariableItem/", + "group": "00_customVariableItemAction01" + }, + { + "command": "code-for-ibmi.environment.variable.copy", + "when": "view === environmentView && viewItem =~ /^customVariableItem/", + "group": "10_customVariableItemAction01" + }, + { + "command": "code-for-ibmi.environment.variable.delete", + "when": "view === environmentView && viewItem =~ /^customVariableItem/", + "group": "20_customVariableItemAction01" + }, + { + "command": "code-for-ibmi.environment.profile.create", + "when": "view === environmentView && viewItem =~ /^profilesNode/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.environment.profile.fromCurrent", + "when": "view === environmentView && viewItem =~ /^profilesNode/", + "group": "inline@02" + }, + { + "command": "code-for-ibmi.environment.profile.unload", + "when": "view === environmentView && viewItem =~ /^profilesNode/ && code-for-ibmi:activeProfile", + "group": "inline@03" + }, + { + "command": "code-for-ibmi.environment.profile.activate", + "when": "view === environmentView && viewItem =~ /^profileItem(?!_active)/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.environment.profile.runLiblistCommand", + "when": "view === environmentView && viewItem =~ /^profileItem_active_command/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.environment.profile.rename", + "when": "view === environmentView && viewItem =~ /^profileItem/", + "group": "00_profileItemAction01" + }, + { + "command": "code-for-ibmi.environment.profile.copy", + "when": "view === environmentView && viewItem =~ /^profileItem/", + "group": "10_profileItemAction01" + }, + { + "command": "code-for-ibmi.environment.profile.delete", + "when": "view === environmentView && viewItem =~ /^profileItem(?!_active)/", + "group": "20_profileItemAction01" } ], "explorer/context": [ @@ -3052,4 +3247,4 @@ "halcyontechltd.vscode-ibmi-walkthroughs", "vscode.git" ] -} +} \ No newline at end of file diff --git a/src/api/actions.ts b/src/api/actions.ts new file mode 100644 index 000000000..710a3e618 --- /dev/null +++ b/src/api/actions.ts @@ -0,0 +1,81 @@ +import vscode, { l10n } from "vscode"; +import IBMi from "./IBMi"; +import { Action } from "./types"; + +export async function getActions(workspace?: vscode.WorkspaceFolder) { + return workspace ? await getLocalActions(workspace) : (IBMi.connectionManager.get(`actions`) || []); +} + +export async function updateAction(action: Action, workspace?: vscode.WorkspaceFolder, options?: { newName?: string, delete?: boolean }) { + const actions = await getActions(workspace); + const currentIndex = actions.findIndex(a => action.name === a.name); + + action.name = options?.newName || action.name; + + if (options?.delete) { + if (currentIndex >= 0) { + actions.splice(currentIndex, 1); + } + else { + throw new Error(l10n.t("Cannot find action {0} for delete operation", action.name)); + } + } + else { + actions[currentIndex >= 0 ? currentIndex : actions.length] = action; + } + + if (workspace) { + const actionsFile = (await getLocalActionsFiles(workspace)).at(0); + if (actionsFile) { + await vscode.workspace.fs.writeFile(actionsFile, Buffer.from(JSON.stringify(actions, undefined, 2), "utf-8")); + } + else { + throw new Error(l10n.t("No local actions file defined in workspace {0}", workspace.name)); + } + } + else { + await IBMi.connectionManager.set(`actions`, actions); + } +} + +export async function getLocalActionsFiles(workspace: vscode.WorkspaceFolder) { + return workspace ? await vscode.workspace.findFiles(new vscode.RelativePattern(workspace, `**/.vscode/actions.json`)) : []; +} + +async function getLocalActions(currentWorkspace: vscode.WorkspaceFolder) { + const actions: Action[] = []; + + if (currentWorkspace) { + const actionsFiles = await getLocalActionsFiles(currentWorkspace); + + for (const file of actionsFiles) { + const actionsContent = await vscode.workspace.fs.readFile(file); + try { + const actionsJson: Action[] = JSON.parse(actionsContent.toString()); + + // Maybe one day replace this with real schema validation + if (Array.isArray(actionsJson)) { + actionsJson.forEach((action, index) => { + if ( + typeof action.name === `string` && + typeof action.command === `string` && + [`ile`, `pase`, `qsh`].includes(action.environment) && + (!action.extensions || Array.isArray(action.extensions)) + ) { + actions.push({ + ...action, + type: `file` + }); + } else { + throw new Error(`Invalid Action defined at index ${index}.`); + } + }) + } + } catch (e: any) { + vscode.window.showErrorMessage(`Error parsing ${file.fsPath}: ${e.message}\n`); + } + }; + } + + return actions; +} \ No newline at end of file diff --git a/src/api/configuration/config/ConnectionManager.ts b/src/api/configuration/config/ConnectionManager.ts index 6958c2602..44cc2c24b 100644 --- a/src/api/configuration/config/ConnectionManager.ts +++ b/src/api/configuration/config/ConnectionManager.ts @@ -14,7 +14,6 @@ function initialize(parameters: Partial): ConnectionConfig { autoClearTempData: parameters.autoClearTempData || false, customVariables: parameters.customVariables || [], connectionProfiles: parameters.connectionProfiles || [], - commandProfiles: parameters.commandProfiles || [], ifsShortcuts: parameters.ifsShortcuts || [], /** Default auto sorting of shortcuts to off */ autoSortIFSShortcuts: parameters.autoSortIFSShortcuts || false, @@ -87,7 +86,7 @@ export class ConnectionManager { } async storeNew(data: ConnectionData) { - const connections = await this.getAll(); + const connections = this.getAll(); const newId = connections.length; connections.push(data); await this.setAll(connections); diff --git a/src/api/configuration/config/types.ts b/src/api/configuration/config/types.ts index de91dc157..0493cc49a 100644 --- a/src/api/configuration/config/types.ts +++ b/src/api/configuration/config/types.ts @@ -1,5 +1,5 @@ import { FilterType } from "../../Filter"; -import { DeploymentMethod, ConnectionData } from "../../types"; +import { ConnectionData, DeploymentMethod } from "../../types"; export type DefaultOpenMode = "browse" | "edit"; export type ReconnectMode = "always" | "never" | "ask"; @@ -8,7 +8,6 @@ export interface ConnectionConfig extends ConnectionProfile { host: string; autoClearTempData: boolean; connectionProfiles: ConnectionProfile[]; - commandProfiles: CommandProfile[]; autoSortIFSShortcuts: boolean; tempLibrary: string; tempDir: string; @@ -33,6 +32,7 @@ export interface ConnectionConfig extends ConnectionProfile { protectedPaths: string[]; showHiddenFiles: boolean; lastDownloadLocation: string; + currentProfile?: string [name: string]: any; } @@ -64,11 +64,7 @@ export interface ConnectionProfile { objectFilters: ObjectFilters[] ifsShortcuts: string[] customVariables: CustomVariable[] -} - -export interface CommandProfile { - name: string; - command: string; + setLibraryListCommand?: string } export interface StoredConnection { diff --git a/src/api/configuration/storage/ConnectionStorage.ts b/src/api/configuration/storage/ConnectionStorage.ts index 59929f3ab..ba218c0e0 100644 --- a/src/api/configuration/storage/ConnectionStorage.ts +++ b/src/api/configuration/storage/ConnectionStorage.ts @@ -45,14 +45,6 @@ export class ConnectionStorage { await this.internalStorage.set(SOURCE_LIST_KEY, sourceList); } - getLastProfile() { - return this.internalStorage.get(LAST_PROFILE_KEY); - } - - async setLastProfile(lastProfile: string) { - await this.internalStorage.set(LAST_PROFILE_KEY, lastProfile); - } - getPreviousCurLibs() { return this.internalStorage.get(PREVIOUS_CUR_LIBS_KEY) || []; } @@ -94,6 +86,15 @@ export class ConnectionStorage { await this.internalStorage.set(RECENTLY_OPENED_FILES_KEY, undefined); } + /** @deprecated stored in ConnectionSettings now */ + getLastProfile() { + return this.internalStorage.get(LAST_PROFILE_KEY); + } + + async clearDeprecatedLastProfile() { + await this.internalStorage.set(LAST_PROFILE_KEY, undefined); + } + async grantExtensionAuthorisation(extensionId: string, displayName: string) { const extensions = this.getAuthorisedExtensions(); if (!this.getExtensionAuthorisation(extensionId)) { diff --git a/src/api/connectionProfiles.ts b/src/api/connectionProfiles.ts new file mode 100644 index 000000000..32cd15f4a --- /dev/null +++ b/src/api/connectionProfiles.ts @@ -0,0 +1,91 @@ +import { l10n } from "vscode"; +import { instance } from "../instantiate"; +import IBMi from "./IBMi"; +import { ConnectionProfile } from "./types"; + +export async function updateConnectionProfile(profile: ConnectionProfile, options?: { newName?: string, delete?: boolean }) { + const config = instance.getConnection()?.getConfig(); + if (config) { + const profiles = config.connectionProfiles; + const index = profiles.findIndex(p => p.name === profile.name); + + if (options?.delete) { + if (index < 0) { + throw new Error(l10n.t("Profile {0} not found for deletion.", profile.name)); + } + profiles.splice(index, 1); + } + else { + profile.name = options?.newName || profile.name; + profiles[index < 0 ? profiles.length : index] = profile; + } + + if (isActiveProfile(profile)) { + //Only update the setLibraryListCommand in the current config since the editor is the only place it can be changed + config.setLibraryListCommand = profile.setLibraryListCommand; + } + + await IBMi.connectionManager.update(config); + } +} + +/** + * @returns ann arry of {@link ConnectionProfile} stored in the config; except the default profile (with a blank name), only used internally + */ +export function getConnectionProfiles() { + const config = instance.getConnection()?.getConfig(); + if (config) { + return config.connectionProfiles.filter(profile => Boolean(profile.name)); + } + else { + throw new Error(l10n.t("Not connected to an IBM i")); + } +} + +export function getConnectionProfile(profileName: string) { + return getConnectionProfiles().filter(p => p.name === profileName).at(0); +} + +export function getDefaultProfile() { + const config = instance.getConnection()?.getConfig(); + if (config) { + let defaultProfile = config.connectionProfiles.filter(profile => !profile.name).at(0); + if (!defaultProfile) { + defaultProfile = { + name: '', + homeDirectory: '', + ifsShortcuts: [], + currentLibrary: '', + objectFilters: [], + customVariables: [], + libraryList: [] + }; + + config.connectionProfiles.push(defaultProfile); + } + + return defaultProfile; + } + else { + throw new Error(l10n.t("Not connected to an IBM i")); + } +} + +export function assignProfile(fromProfile: ConnectionProfile, toProfile: ConnectionProfile) { + toProfile.homeDirectory = fromProfile.homeDirectory; + toProfile.currentLibrary = fromProfile.currentLibrary; + toProfile.libraryList = fromProfile.libraryList; + toProfile.objectFilters = fromProfile.objectFilters; + toProfile.ifsShortcuts = fromProfile.ifsShortcuts; + toProfile.customVariables = fromProfile.customVariables; + toProfile.setLibraryListCommand = fromProfile.setLibraryListCommand; + return toProfile; +} + +export function cloneProfile(fromProfile: ConnectionProfile, newName: string): ConnectionProfile { + return assignProfile(fromProfile, { name: newName } as ConnectionProfile); +} + +export function isActiveProfile(profile: ConnectionProfile) { + return instance.getConnection()?.getConfig().currentProfile === profile.name; +} \ No newline at end of file diff --git a/src/api/types.ts b/src/api/types.ts index a79caa4a1..f77e537a3 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -37,7 +37,7 @@ export interface CommandData extends StandardIO { export interface CommandResult { code: number; - signal?: string|null; + signal?: string | null; stdout: string; stderr: string; command?: string; @@ -72,10 +72,6 @@ export interface Server { name: string } -export interface Profile { - profile: string -} - export interface QsysPath { asp?: string, library: string, diff --git a/src/api/variables.ts b/src/api/variables.ts index acb96ba1a..193a29c49 100644 --- a/src/api/variables.ts +++ b/src/api/variables.ts @@ -22,7 +22,7 @@ export class Variables extends Map { .set(`&WORKDIR`, config.homeDirectory); for (const variable of config.customVariables) { - this.set(`&${variable.name.toUpperCase()}`, variable.value); + this.set(`&${variable.name.toUpperCase()}`, variable.value || ''); } } } diff --git a/src/editors/actionEditor.ts b/src/editors/actionEditor.ts new file mode 100644 index 000000000..c889c03a8 --- /dev/null +++ b/src/editors/actionEditor.ts @@ -0,0 +1,196 @@ +import vscode from "vscode"; +import { updateAction } from "../api/actions"; +import { Tools } from "../api/Tools"; +import { Action, ActionEnvironment, ActionRefresh, ActionType } from "../typings"; +import { CustomVariables } from "../ui/views/environment/customVariables"; +import { Tab } from "../webviews/CustomUI"; +import { CustomEditor } from "./customEditorProvider"; + +// Used to list info about available variables +type VariableInfo = { + name: string + text: string +} + +type VariableInfoList = { + member: VariableInfo[] + streamFile: VariableInfo[] + object: VariableInfo[] +} + +type ActionData = { + name: string + command: string + extensions: string + type: ActionType + environment: ActionEnvironment + refresh: ActionRefresh + runOnProtected: boolean + outputToFile: string +} + +const editedActions: Set = new Set; + +export function isActionEdited(action: Action) { + return editedActions.has(action.name); +} + +export function editAction(targetAction: Action, doAfterSave?: () => Thenable, workspace?: vscode.WorkspaceFolder) { + const customVariables = CustomVariables.getAll().map(variable => `
  • &${variable.name}: ${variable.value}
  • `).join(``); + new CustomEditor(`${targetAction.name}.action`, (actionData) => save(targetAction, actionData, workspace).then(doAfterSave), () => editedActions.delete(targetAction.name)) + .addInput( + `command`, + vscode.l10n.t(`Command(s) to run`), + vscode.l10n.t(`Below are available variables based on the Type you have select below. You can specify different commands on each line. Each command run is stateless and run in their own job.`), + { rows: 5, default: targetAction.command } + ) + .addTabs( + Object.entries(getVariablesInfo()) + .map(([type, variables]) => ({ + label: Tools.capitalize(type), + value: `
      ${variables.map(variable => `
    • ${variable.name}: ${variable.text}
    • `).join(``)}${customVariables}
    ` + } as Tab)), getDefaultTabIndex(targetAction.type) + ) + .addHorizontalRule() + .addInput(`extensions`, vscode.l10n.t(`Extensions`), vscode.l10n.t(`A comma delimited list of extensions for this action. This can be a member extension, a streamfile extension, an object type or an object attribute`), { default: targetAction.extensions?.join(`, `) }) + .addSelect(`type`, vscode.l10n.t(`Type`), workspace ? [{ + selected: targetAction.type === `file`, + value: `file`, + description: vscode.l10n.t(`Local File (Workspace)`), + text: vscode.l10n.t(`Actions for local files in the VS Code Workspace.`) + }] : + [ + { + selected: targetAction.type === `member`, + value: `member`, + description: vscode.l10n.t(`Member`), + text: vscode.l10n.t(`Source members in the QSYS file system`), + }, + { + selected: targetAction.type === `streamfile`, + value: `streamfile`, + description: vscode.l10n.t(`Streamfile`), + text: vscode.l10n.t(`Streamfiles in the IFS`) + }, + { + selected: targetAction.type === `object`, + value: `object`, + description: vscode.l10n.t(`Object`), + text: vscode.l10n.t(`Objects in the QSYS file system`) + } + ], + vscode.l10n.t(`The types of files this action can support.`), + workspace ? true : false + ) + .addSelect(`environment`, vscode.l10n.t(`Environment`), [ + { + selected: targetAction.environment === `ile`, + value: `ile`, + description: vscode.l10n.t(`ILE`), + text: vscode.l10n.t(`Runs as an ILE command`) + }, + { + selected: targetAction.environment === `qsh`, + value: `qsh`, + description: vscode.l10n.t(`QShell`), + text: vscode.l10n.t(`Runs the command through QShell`) + }, + { + selected: targetAction.environment === `pase`, + value: `pase`, + description: vscode.l10n.t(`PASE`), + text: vscode.l10n.t(`Runs the command in the PASE environment`) + }], vscode.l10n.t(`Environment for command to be executed in.`) + ) + .addSelect(`refresh`, vscode.l10n.t(`Refresh`), [ + { + selected: targetAction.refresh === `no`, + value: `no`, + description: vscode.l10n.t(`No`), + text: vscode.l10n.t(`No refresh`) + }, + { + selected: targetAction.refresh === `parent`, + value: `parent`, + description: vscode.l10n.t(`Parent`), + text: vscode.l10n.t(`The parent container is refreshed`) + }, + { + selected: targetAction.refresh === `filter`, + value: `filter`, + description: vscode.l10n.t(`Filter`), + text: vscode.l10n.t(`The parent filter is refreshed`) + }, + { + selected: targetAction.refresh === `browser`, + value: `browser`, + description: vscode.l10n.t(`Browser`), + text: vscode.l10n.t(`The entire browser is refreshed`) + }], vscode.l10n.t(`The browser level to refresh after the action is done`) + ) + .addCheckbox("runOnProtected", vscode.l10n.t(`Run on protected/read only`), vscode.l10n.t(`Allows the execution of this Action on protected or read only targets`), targetAction.runOnProtected) + .addInput(`outputToFile`, vscode.l10n.t(`Copy output to file`), vscode.l10n.t(`Copy the action output to a file. Variables can be used to define the file's path; use &i to compute file index.
    Example: ~/outputs/&CURLIB_&OPENMBR&i.txt.`), { default: targetAction.outputToFile }) + .open(); + + editedActions.add(targetAction.name); +} + +async function save(targetAction: Action, actionData: ActionData, workspace?: vscode.WorkspaceFolder) { + Object.assign(targetAction, actionData); + // We don't want \r (Windows line endings) + targetAction.command = targetAction.command.replace(new RegExp(`\\\r`, `g`), ``); + targetAction.extensions = actionData.extensions.split(`,`).map(item => item.trim().toUpperCase()) + await updateAction(targetAction, workspace); +} + +const generic: () => VariableInfo[] = () => [ + { name: `&CURLIB`, text: vscode.l10n.t(`Current library, changeable in Library List`) }, + { name: `&USERNAME`, text: vscode.l10n.t(`Username for connection`) }, + { name: `&WORKDIR`, text: vscode.l10n.t(`Current working directory, changeable in IFS Browser`) }, + { name: `&HOST`, text: vscode.l10n.t(`Hostname or IP address from the current connection`) }, + { name: `&BUILDLIB`, text: vscode.l10n.t(`The same as &CURLIB`) }, + { name: `&LIBLC`, text: vscode.l10n.t(`Library list delimited by comma`) }, + { name: `&LIBLS`, text: vscode.l10n.t(`Library list delimited by space`) } +]; + +export function getVariablesInfo(): VariableInfoList { + return { + member: [ + { name: `&OPENLIB`, text: vscode.l10n.t(`Library name where the source member lives (&OPENLIBL for lowercase)`) }, + { name: `&OPENSPF`, text: vscode.l10n.t(`Source file name where the source member lives (&OPENSPFL for lowercase)`) }, + { name: `&OPENMBR`, text: vscode.l10n.t(`Name of the source member (&OPENMBRL for lowercase)`) }, + { name: `&EXT`, text: vscode.l10n.t(`Extension of the source member (&EXTL for lowercase)`) }, + ...generic() + ], + streamFile: [ + { name: `&FULLPATH`, text: vscode.l10n.t(`Full path of the file on the remote system`) }, + { name: `&FILEDIR`, text: vscode.l10n.t(`Directory of the file on the remote system`) }, + { name: `&RELATIVEPATH`, text: vscode.l10n.t(`Relative path of the streamfile from the working directory or workspace`) }, + { name: `&PARENT`, text: vscode.l10n.t(`Name of the parent directory or source file`) }, + { name: `&BASENAME`, text: vscode.l10n.t(`Name of the file, including the extension`) }, + { name: `&NAME`, text: vscode.l10n.t(`Name of the file (&NAMEL for lowercase)`) }, + { name: `&EXT`, text: vscode.l10n.t(`Extension of the file (&EXTL for lowercase)`) }, + ...generic() + ], + object: [ + { name: `&LIBRARY`, text: vscode.l10n.t(`Library name where the object lives (&LIBRARYL for lowercase)`) }, + { name: `&NAME`, text: vscode.l10n.t(`Name of the object (&NAMEL for lowercase)`) }, + { name: `&TYPE`, text: vscode.l10n.t(`Type of the object (&TYPEL for lowercase)`) }, + { name: `&EXT`, text: vscode.l10n.t(`Extension/attribute of the object (&EXTL for lowercase)`) }, + ...generic() + ] + } +} + +function getDefaultTabIndex(type?: ActionType) { + switch (type) { + case `file`: + case `streamfile`: + return 1; + case `object`: + return 2; + case `member`: + default: + return 0; + } +} \ No newline at end of file diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts new file mode 100644 index 000000000..eaecb8372 --- /dev/null +++ b/src/editors/connectionProfileEditor.ts @@ -0,0 +1,69 @@ +import vscode, { l10n } from "vscode"; +import { isActiveProfile, updateConnectionProfile } from "../api/connectionProfiles"; +import { instance } from "../instantiate"; +import { ConnectionProfile } from "../typings"; +import { CustomEditor } from "./customEditorProvider"; + +type ConnectionProfileData = { + homeDirectory: string + currentLibrary: string + libraryList: string + setLibraryListCommand: string +} + +const editedProfiles: Set = new Set; + +export function isProfileEdited(profile: ConnectionProfile) { + return editedProfiles.has(profile.name); +} + +export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: () => Thenable) { + const activeProfile = isActiveProfile(profile); + new CustomEditor(`${profile.name}.profile`, data => save(profile, data).then(doAfterSave), () => editedProfiles.delete(profile.name)) + .addInput("homeDirectory", l10n.t("Home Directory"), '', { minlength: 1, default: profile.homeDirectory, readonly: activeProfile }) + .addInput("currentLibrary", l10n.t("Current Library"), '', { minlength: 1, maxlength: 10, default: profile.currentLibrary, readonly: activeProfile }) + .addInput("libraryList", l10n.t("Library List"), l10n.t("A comma-separated list of libraries."), { default: profile.libraryList.join(","), readonly: activeProfile }) + .addInput("setLibraryListCommand", l10n.t("Library List Command"), l10n.t("Library List Command can be used to set your library list based on the result of a command like CHGLIBL, or your own command that sets the library list.
    Commands should be as explicit as possible.
    When refering to commands and objects, both should be qualified with a library.
    Put ? in front of the command to prompt it before execution."), { default: profile.setLibraryListCommand }) + .addHorizontalRule() + .addHeading(l10n.t("Object filters"), 3) + .addParagraph(profile.objectFilters.length ? `
      ${profile.objectFilters.map(filter => `
    • ${filter.name}
    • `).join('')}
    ` : l10n.t("None")) + .addHorizontalRule() + .addHeading(l10n.t("IFS shortcuts"), 3) + .addParagraph(profile.ifsShortcuts.length ? `
      ${profile.ifsShortcuts.map(shortcut => `
    • ${shortcut}
    • `).join('')}
    ` : l10n.t("None")) + .addHorizontalRule() + .addHeading(l10n.t("Custom variables"), 3) + .addParagraph(profile.customVariables.length ? `
      ${profile.customVariables.map(variable => `
    • &${variable.name}: ${variable.value}
    • `).join('')}
    ` : l10n.t("None")) + .open(); + + editedProfiles.add(profile.name); +} + +async function save(profile: ConnectionProfile, data: ConnectionProfileData) { + const content = instance.getConnection()?.getContent(); + if (content) { + profile.homeDirectory = data.homeDirectory.trim(); + profile.setLibraryListCommand = data.setLibraryListCommand.trim(); + + data.currentLibrary = data.currentLibrary.trim(); + if (data.currentLibrary) { + if (await content.checkObject({ library: "QSYS", name: data.currentLibrary, type: "*LIB" })) { + profile.currentLibrary = data.currentLibrary; + } + else { + throw new Error(l10n.t("Current library {0} is invalid", data.currentLibrary)); + } + } + + const libraryList = data.libraryList.split(',').map(library => library.trim()); + const badLibraries = await content.validateLibraryList(libraryList); + if (badLibraries.length && !await vscode.window.showWarningMessage(l10n.t("The following libraries are invalid. Do you still want to save that profile?"), { + modal: true, + detail: badLibraries.sort().map(library => `- ${library}`).join("\n") + }, l10n.t("Yes"))) { + throw new Error(l10n.t("Save aborted")); + } + profile.libraryList = libraryList; + + await updateConnectionProfile(profile); + } +} \ No newline at end of file diff --git a/src/editors/customEditorProvider.ts b/src/editors/customEditorProvider.ts new file mode 100644 index 000000000..f0e597d08 --- /dev/null +++ b/src/editors/customEditorProvider.ts @@ -0,0 +1,118 @@ +import vscode from "vscode"; +import { CustomHTML } from "../webviews/CustomUI"; + +const customEditors: Map> = new Map; +export class CustomEditor extends CustomHTML implements vscode.CustomDocument { + readonly uri: vscode.Uri; + private data: T = {} as T; + valid?: boolean; + dirty = false; + + constructor(target: string, private readonly onSave: (data: T) => Promise, private readonly onClosed?: () => void) { + super(); + this.uri = vscode.Uri.from({ scheme: "code4i", path: `/${target}` }); + } + + protected getSpecificScript() { + return /* javascript */ ` + for (const field of submitfields) { + const fieldElement = document.getElementById(field); + fieldElement.addEventListener(inputFields.some(f => f.id === field) ? 'input' : 'change', function(event) { + event?.preventDefault(); + const data = document.querySelector('#laforma').data; + for (const checkbox of checkboxes) { + data[checkbox] = data[checkbox]?.length >= 1; + } + + data.valid = validateInputs(); + + vscode.postMessage({ type: 'dataChange', data }); + }); + } + `; + } + + open() { + customEditors.set(this.uri.toString(), this); + vscode.commands.executeCommand("vscode.open", this.uri); + } + + load(webviewPanel: vscode.WebviewPanel) { + const webview = webviewPanel.webview; + webview.options = { + enableScripts: true, + enableCommandUris: true + }; + + webview.html = this.getHTML(webviewPanel, this.uri.path); + } + + onDataChange(data: T & { valid?: boolean }) { + this.dirty = true; + this.valid = data.valid; + delete data.valid; + this.data = data; + } + + async save() { + await this.onSave(this.data); + } + + dispose() { + this.onClosed?.(); + } +} + +export class CustomEditorProvider implements vscode.CustomEditorProvider> { + readonly eventEmitter = new vscode.EventEmitter>>(); + readonly onDidChangeCustomDocument = this.eventEmitter.event; + + async saveCustomDocument(document: CustomEditor, cancellation: vscode.CancellationToken) { + if (document.dirty) { + if (document.valid) { + await document.save(); + document.dirty = false; + } + else { + throw new Error("Can't save: some inputs are invalid"); + } + } + } + + async openCustomDocument(uri: vscode.Uri, openContext: vscode.CustomDocumentOpenContext, token: vscode.CancellationToken) { + const customEditor = customEditors.get(uri.toString()); + if (customEditor) { + customEditors.delete(uri.toString()); + return customEditor; + } + else { + //Fail safe: do not fail, return an empty editor asking to reopen the editor + //Throwing an error here prevents that URI to be opened until the editor is closed and VS Code is restarted + return new CustomEditor(uri.path.substring(1), async () => { }).addHeading("Please close this editor and re-open it.", 3); + } + } + + async resolveCustomEditor(document: CustomEditor, webviewPanel: vscode.WebviewPanel, token: vscode.CancellationToken) { + document.load(webviewPanel); + webviewPanel.webview.onDidReceiveMessage(async body => { + if (body.type === "dataChange") { + document.onDataChange(body.data); + this.eventEmitter.fire({ + document, + redo: () => { throw new Error("Redo not supported."); }, + undo: () => { throw new Error("Undo not supported."); } + }); + } + }); + } + + saveCustomDocumentAs(document: CustomEditor, destination: vscode.Uri, cancellation: vscode.CancellationToken): Thenable { + throw new Error("Save As is not supported."); + } + revertCustomDocument(document: CustomEditor, cancellation: vscode.CancellationToken): Thenable { + throw new Error("Revert is not supported."); + } + backupCustomDocument(document: CustomEditor, context: vscode.CustomDocumentBackupContext, cancellation: vscode.CancellationToken): Thenable { + throw new Error("Backup is not supported."); + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index f9545e6e5..bf0b266f3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,19 +15,21 @@ import { parseErrors } from "./api/errors/parser"; import { CustomCLI } from "./api/tests/components/customCli"; import { onCodeForIBMiConfigurationChange } from "./config/Configuration"; import * as Debug from './debug'; +import { CustomEditor, CustomEditorProvider } from "./editors/customEditorProvider"; import { IFSFS } from "./filesystems/ifsFs"; import { DeployTools } from "./filesystems/local/deployTools"; import { Deployment } from "./filesystems/local/deployment"; import { instance, loadAllofExtension } from './instantiate'; import { LocalActionCompletionItemProvider } from "./languages/actions/completion"; +import { mergeCommandProfiles } from "./mergeProfiles"; import { initialise } from "./testing"; import { CodeForIBMi } from "./typings"; import { VscodeTools } from "./ui/Tools"; import { registerActionTools } from "./ui/actions"; import { initializeConnectionBrowser } from "./ui/views/ConnectionBrowser"; import { initializeLibraryListView } from "./ui/views/LibraryListView"; -import { ProfilesView } from "./ui/views/ProfilesView"; import { initializeDebugBrowser } from "./ui/views/debugView"; +import { initializeEnvironmentView } from "./ui/views/environment/environmentView"; import { HelpView } from "./ui/views/helpView"; import { initializeIFSBrowser } from "./ui/views/ifsBrowser"; import { initializeObjectBrowser } from "./ui/views/objectBrowser"; @@ -61,16 +63,13 @@ export async function activate(context: ExtensionContext): Promise initializeDebugBrowser(context); initializeSearchView(context); initializeLibraryListView(context); + initializeEnvironmentView(context); context.subscriptions.push( window.registerTreeDataProvider( `helpView`, new HelpView(context) ), - window.registerTreeDataProvider( - `profilesView`, - new ProfilesView(context) - ), onCodeForIBMiConfigurationChange("connections", updateLastConnectionAndServerCache), onCodeForIBMiConfigurationChange("connectionSettings", async () => { @@ -85,7 +84,12 @@ export async function activate(context: ExtensionContext): Promise workspace.registerFileSystemProvider(`streamfile`, new IFSFS(), { isCaseSensitive: false }), - languages.registerCompletionItemProvider({ language: 'json', pattern: "**/.vscode/actions.json" }, new LocalActionCompletionItemProvider(), "&") + languages.registerCompletionItemProvider({ language: 'json', pattern: "**/.vscode/actions.json" }, new LocalActionCompletionItemProvider(), "&"), + window.registerCustomEditorProvider(`code-for-ibmi.editor`, new CustomEditorProvider(), { + webviewOptions: { + retainContextWhenHidden: true + } + }) ); registerActionTools(context); @@ -112,7 +116,7 @@ export async function activate(context: ExtensionContext): Promise commands.executeCommand("code-for-ibmi.refreshObjectBrowser"); commands.executeCommand("code-for-ibmi.refreshLibraryListView"); commands.executeCommand("code-for-ibmi.refreshIFSBrowser"); - commands.executeCommand("code-for-ibmi.refreshProfileView"); + commands.executeCommand("code-for-ibmi.environment.refresh"); }); const customQsh = new CustomQSh(); @@ -128,8 +132,12 @@ export async function activate(context: ExtensionContext): Promise openURIHandler ); + await mergeCommandProfiles(); + return { - instance, customUI: () => new CustomUI(), + instance, + customUI: () => new CustomUI(), + customEditor: (target, onSave, onClosed) => new CustomEditor(target, onSave, onClosed), deployTools: DeployTools, evfeventParser: parseErrors, tools: VscodeTools, @@ -140,4 +148,4 @@ export async function activate(context: ExtensionContext): Promise // this method is called when your extension is deactivated export async function deactivate() { await commands.executeCommand(`code-for-ibmi.disconnect`, true); -} +} \ No newline at end of file diff --git a/src/filesystems/local/actions.ts b/src/filesystems/local/actions.ts deleted file mode 100644 index 80c26a315..000000000 --- a/src/filesystems/local/actions.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { RelativePattern, window, workspace, WorkspaceFolder } from "vscode"; -import { Action } from "../../api/types"; - -export async function getLocalActionsFiles(currentWorkspace?: WorkspaceFolder) { - return currentWorkspace ? await workspace.findFiles(new RelativePattern(currentWorkspace, `**/.vscode/actions.json`)) : []; -} - -export async function getLocalActions(currentWorkspace: WorkspaceFolder) { - const actions: Action[] = []; - - if (currentWorkspace) { - const actionsFiles = await getLocalActionsFiles(currentWorkspace); - - for (const file of actionsFiles) { - const actionsContent = await workspace.fs.readFile(file); - try { - const actionsJson: Action[] = JSON.parse(actionsContent.toString()); - - // Maybe one day replace this with real schema validation - if (Array.isArray(actionsJson)) { - actionsJson.forEach((action, index) => { - if ( - typeof action.name === `string` && - typeof action.command === `string` && - [`ile`, `pase`, `qsh`].includes(action.environment) && - Array.isArray(action.extensions) - ) { - actions.push({ - ...action, - type: `file` - }); - } else { - throw new Error(`Invalid Action defined at index ${index}.`); - } - }) - } - } catch (e: any) { - // ignore - window.showErrorMessage(`Error parsing ${file.fsPath}: ${e.message}\n`); - } - }; - } - - return actions; -} - -export async function getEvfeventFiles(currentWorkspace: WorkspaceFolder) { - if (currentWorkspace) { - const relativeSearch = new RelativePattern(currentWorkspace, `**/.evfevent/*`); - const iprojectFiles = await workspace.findFiles(relativeSearch, null); - - return iprojectFiles; - } -} \ No newline at end of file diff --git a/src/filesystems/local/deployTools.ts b/src/filesystems/local/deployTools.ts index e8867d126..e5b8de72c 100644 --- a/src/filesystems/local/deployTools.ts +++ b/src/filesystems/local/deployTools.ts @@ -1,18 +1,20 @@ +import { existsSync } from 'fs'; import createIgnore, { Ignore } from 'ignore'; import path, { basename } from 'path'; -import vscode, { Uri, WorkspaceFolder } from 'vscode'; +import vscode, { l10n, Uri, WorkspaceFolder } from 'vscode'; +import { getActions } from '../../api/actions'; +import { DeploymentMethod } from '../../api/types'; import { instance } from '../../instantiate'; +import { BrowserItem, DeploymentParameters } from '../../typings'; +import { VscodeTools } from '../../ui/Tools'; import { LocalLanguageActions } from './LocalLanguageActions'; import { Deployment } from './deployment'; -import { VscodeTools } from '../../ui/Tools'; -import { DeploymentMethod } from '../../api/types'; -import { DeploymentParameters } from '../../typings'; type ServerFileChanges = { uploads: Uri[], relativeRemoteDeletes: string[] }; export namespace DeployTools { - export async function launchActionsSetup(workspaceFolder?: WorkspaceFolder) { - const chosenWorkspace = workspaceFolder || await Deployment.getWorkspaceFolder(); + export async function launchActionsSetup(workspaceFolder?: WorkspaceFolder | BrowserItem) { + const chosenWorkspace = !workspaceFolder || workspaceFolder instanceof BrowserItem ? await Deployment.getWorkspaceFolder() : workspaceFolder; if (chosenWorkspace) { const types = Object.entries(LocalLanguageActions).map(([type, actions]) => ({ label: type, actions })); @@ -25,16 +27,44 @@ export namespace DeployTools { if (chosenTypes) { const newActions = chosenTypes.flatMap(type => type.actions); const localActionsUri = vscode.Uri.file(path.join(chosenWorkspace.uri.fsPath, `.vscode`, `actions.json`)); - try { - await vscode.workspace.fs.writeFile( - localActionsUri, - Buffer.from(JSON.stringify(newActions, null, 2), `utf-8`) - ); - - vscode.workspace.openTextDocument(localActionsUri).then(doc => vscode.window.showTextDocument(doc)); - } catch (e) { - console.log(e); - vscode.window.showErrorMessage(`Unable to create actions.json file.`); + const overwrite = l10n.t("Overwrite"); + const append = l10n.t("Append"); + const exists = existsSync(localActionsUri.fsPath); + let action; + if (!exists || (action = await vscode.window.showWarningMessage(l10n.t("Local actions are already defined for this workspace."), { modal: true }, overwrite, append))) { + try { + const actions = []; + if (!exists || action === overwrite) { + actions.push(...newActions); + } + else if (action === append) { + const existingActions = await getActions(chosenWorkspace); + const existingActionNames = existingActions.map(action => action.name); + //Change names of new actions + let toRename = []; + while ((toRename = newActions.filter(action => existingActionNames.includes(action.name))).length) { + toRename.forEach(action => { + const index = / \((\d+)\)$/.exec(action.name)?.[1]; + if (index) { + action.name = action.name.substring(0, action.name.lastIndexOf(' ') + 1) + `(${Number(index) + 1})`; + } + else { + action.name += " (1)"; + } + }); + } + + actions.push(...existingActions, ...newActions); + } + await vscode.workspace.fs.writeFile( + localActionsUri, + Buffer.from(JSON.stringify(actions, null, 2), `utf-8`) + ); + vscode.workspace.openTextDocument(localActionsUri).then(doc => vscode.window.showTextDocument(doc)); + } catch (e) { + console.log(e); + vscode.window.showErrorMessage(`Unable to create actions.json file.`); + } } } } diff --git a/src/filesystems/local/deployment.ts b/src/filesystems/local/deployment.ts index ea2e8ef9d..520be1346 100644 --- a/src/filesystems/local/deployment.ts +++ b/src/filesystems/local/deployment.ts @@ -3,12 +3,12 @@ import path from 'path'; import tar from 'tar'; import tmp from 'tmp'; import vscode from 'vscode'; +import { getActions, getLocalActionsFiles } from '../../api/actions'; import IBMi from '../../api/IBMi'; import IBMiContent from '../../api/IBMiContent'; import { Tools } from '../../api/Tools'; import { instance } from '../../instantiate'; import { DeploymentParameters } from '../../typings'; -import { getLocalActions, getLocalActionsFiles } from './actions'; import { DeployTools } from './deployTools'; export namespace Deployment { @@ -82,7 +82,7 @@ export namespace Deployment { }); } - getLocalActions(workspace).then(result => { + getActions(workspace).then(result => { if (result.length === 0) { vscode.window.showInformationMessage( `There are no local Actions defined for this project.`, @@ -161,7 +161,7 @@ export namespace Deployment { workspace = uri; } - vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasLocalActions`, (await getLocalActionsFiles(workspace)).length > 0); + vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasLocalActions`, workspace ? (await getLocalActionsFiles(workspace)).length > 0 : false); }; watcher.onDidChange(uri => { diff --git a/src/instantiate.ts b/src/instantiate.ts index f323d207c..d4fa8a528 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; +import { RemoteConfigFile } from './api/configuration/config/types'; import { getDebugServiceDetails } from './api/configuration/DebugConfiguration'; import { registerActionsCommands } from './commands/actions'; import { registerCompareCommands } from './commands/compare'; @@ -12,9 +13,6 @@ import { setupGitEventHandler } from './filesystems/local/git'; import { QSysFS } from "./filesystems/qsys/QSysFs"; import Instance from "./Instance"; import { Terminal } from './ui/Terminal'; -import { ActionsUI } from './webviews/actions'; -import { VariablesUI } from "./webviews/variables"; -import { RemoteConfigFile } from './api/configuration/config/types'; export let instance: Instance; @@ -84,8 +82,6 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.updateConnectedBar", updateConnectedBar), ); - ActionsUI.initialize(context); - VariablesUI.initialize(context); instance.subscribe(context, 'connected', 'Load status bars', onConnected); instance.subscribe(context, 'disconnected', 'Unload status bars', onDisconnected); @@ -114,15 +110,15 @@ async function updateConnectedBar() { } const systemReadOnly = serverConfig?.codefori?.readOnlyMode || false; - connectedBarItem.text = `$(${systemReadOnly ? "shield" : (config.readOnlyMode ? "lock" : "settings-gear")}) ${config.name}`; + connectedBarItem.text = `$(${systemReadOnly ? "shield" : (config.readOnlyMode ? "lock" : "settings-gear")}) ${config.name}${config.currentProfile ? ` (${config.currentProfile})` : ''}`; const terminalMenuItem = systemReadOnly ? `` : `[$(terminal) Terminals](command:code-for-ibmi.launchTerminalPicker)`; - const actionsMenuItem = systemReadOnly ? `` : `[$(file-binary) Actions](command:code-for-ibmi.showActionsMaintenance)`; + const actionsMenuItem = systemReadOnly ? `` : `[$(file-binary) Actions](command:code-for-ibmi.environment.actions.focus)`; const debugRunning = await isDebugEngineRunning(); const connectedBarItemTooltips: String[] = systemReadOnly ? [`[System-wide read only](https://codefori.github.io/docs/settings/system/)`] : []; connectedBarItemTooltips.push( `[$(settings-gear) Settings](command:code-for-ibmi.showAdditionalSettings)`, - actionsMenuItem, terminalMenuItem, + actionsMenuItem, debugPTFInstalled(connection) ? `[$(${debugRunning ? "bug" : "debug"}) Debugger ${((await getDebugServiceDetails(connection)).version)} (${debugRunning ? "on" : "off"})](command:ibmiDebugBrowser.focus)` : @@ -137,16 +133,12 @@ async function updateConnectedBar() { } async function onConnected() { - const config = instance.getConnection()?.getConfig(); [ connectedBarItem, disconnectBarItem, ].forEach(barItem => barItem.show()); updateConnectedBar(); - - // Enable the profile view if profiles exist. - vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasProfiles`, (config?.connectionProfiles || []).length > 0); } async function onDisconnected() { diff --git a/src/mergeProfiles.ts b/src/mergeProfiles.ts new file mode 100644 index 000000000..f9132ee35 --- /dev/null +++ b/src/mergeProfiles.ts @@ -0,0 +1,29 @@ +import { l10n, window } from "vscode"; +import IBMi from "./api/IBMi"; + +export async function mergeCommandProfiles() { + const connectionSettings = IBMi.connectionManager.getConnectionSettings(); + let updateSettings = false; + for (const settings of connectionSettings.filter(setting => setting.commandProfiles)) { + for (const commandProfile of settings.commandProfiles) { + settings.connectionProfiles.push({ + name: commandProfile.name as string, + setLibraryListCommand: commandProfile.command as string, + currentLibrary: "QGPL", + customVariables: [], + homeDirectory: settings.homeDirectory, + ifsShortcuts: [], + libraryList: ["QGPL", "QTEMP"], + objectFilters: [] + }); + } + delete settings.commandProfiles; + updateSettings = true; + } + if (updateSettings) { + window.showInformationMessage( + l10n.t("Your Command Profiles have been turned into Profiles since these two concepts have been merged with this new version of the Code for IBM i extension."), + { modal: true, detail: l10n.t("Open the Context view once connected to find your profile(s) and run your library list command(s).") }); + await IBMi.connectionManager.updateAll(connectionSettings); + } +} \ No newline at end of file diff --git a/src/typings.ts b/src/typings.ts index 41de6df4a..ab15e87f8 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -1,15 +1,17 @@ -import { CustomUI } from "./webviews/CustomUI"; +import { Ignore } from "ignore"; +import { WorkspaceFolder } from "vscode"; import Instance from "./Instance"; -import { DeployTools } from "./filesystems/local/deployTools"; import { ComponentRegistry } from './api/components/manager'; import { DeploymentMethod, FileError } from "./api/types"; -import { Ignore } from "ignore"; -import { WorkspaceFolder } from "vscode"; +import { CustomEditor } from "./editors/customEditorProvider"; +import { DeployTools } from "./filesystems/local/deployTools"; import { VscodeTools } from "./ui/Tools"; +import { CustomUI } from "./webviews/CustomUI"; export interface CodeForIBMi { instance: Instance, customUI: () => CustomUI, + customEditor: (target: string, onSave: (data: T) => Promise, onClosed?: () => void) => CustomEditor, deployTools: typeof DeployTools, evfeventParser: (lines: string[]) => Map, tools: typeof VscodeTools, @@ -24,4 +26,5 @@ export interface DeploymentParameters { } export * from "./api/types"; -export * from "./ui/types"; \ No newline at end of file +export * from "./ui/types"; + diff --git a/src/ui/Tools.ts b/src/ui/Tools.ts index 7438272e5..9b9f78a3e 100644 --- a/src/ui/Tools.ts +++ b/src/ui/Tools.ts @@ -2,10 +2,10 @@ import Crypto from 'crypto'; import { readFileSync } from "fs"; import vscode, { MarkdownString } from "vscode"; -import { API, GitExtension } from "../filesystems/local/gitApi"; -import { IBMiObject, IBMiMember, IFSFile } from '../typings'; import IBMi from '../api/IBMi'; import { Tools } from '../api/Tools'; +import { API, GitExtension } from "../filesystems/local/gitApi"; +import { ConnectionProfile, IBMiMember, IBMiObject, IFSFile } from '../typings'; let gitLookedUp: boolean; let gitAPI: API | undefined; @@ -199,7 +199,19 @@ export namespace VscodeTools { return tooltip; } - + export function profileToToolTip(profile: ConnectionProfile) { + const tooltip = new MarkdownString(generateTooltipHtmlTable('', { + "Home directory": profile.homeDirectory, + "Current Library": profile.currentLibrary, + "Library List": profile.libraryList, + "Library List Command": profile.setLibraryListCommand, + "Object Filters": profile.objectFilters.length, + "IFS Shortcuts": profile.ifsShortcuts.length, + "Custom Variables": profile.customVariables.length, + })); + tooltip.supportHtml = true; + return tooltip; + } function safeIsoValue(date: Date | undefined) { try { diff --git a/src/ui/actions.ts b/src/ui/actions.ts index 45f8e744d..b33853e6a 100644 --- a/src/ui/actions.ts +++ b/src/ui/actions.ts @@ -4,7 +4,6 @@ import { CompileTools } from '../api/CompileTools'; import IBMi from '../api/IBMi'; import { Tools } from '../api/Tools'; import { Variables } from '../api/variables'; -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'; @@ -15,6 +14,7 @@ import { CustomUI, TreeListItem } from '../webviews/CustomUI'; import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; import { VscodeTools } from './Tools'; +import { getActions } from '../api/actions'; import { BrowserItem } from './types'; type CommandObject = { @@ -594,7 +594,7 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur export type AvailableAction = { label: string; action: Action; } export async function getAllAvailableActions(targets: ActionTarget[], scheme: string) { - const allActions = [...IBMi.connectionManager.get(`actions`) || []]; + const allActions = [...await getActions()]; // Then, if we're being called from a local file // we fetch the Actions defined from the workspace. @@ -606,7 +606,7 @@ export async function getAllAvailableActions(targets: ActionTarget[], scheme: st const allTargetsInOne = targets.every(t => t.workspaceFolder?.index === workspaceId); if (allTargetsInOne) { - const localActions = await getLocalActions(firstWorkspace); + const localActions = await getActions(firstWorkspace); allActions.push(...localActions); } } diff --git a/src/ui/diagnostics.ts b/src/ui/diagnostics.ts index a348c2398..fabd29a02 100644 --- a/src/ui/diagnostics.ts +++ b/src/ui/diagnostics.ts @@ -1,11 +1,10 @@ import * as vscode from "vscode"; -import { FileError } from "../typings"; import Instance from "../Instance"; -import { getEvfeventFiles } from "../filesystems/local/actions"; +import IBMi from "../api/IBMi"; import { parseErrors } from "../api/errors/parser"; +import { FileError } from "../typings"; import { VscodeTools } from "./Tools"; -import IBMi from "../api/IBMi"; const ileDiagnostics = vscode.languages.createDiagnosticCollection(`ILE`); @@ -78,7 +77,7 @@ export async function refreshDiagnosticsFromServer(instance: Instance, evfeventI export async function refreshDiagnosticsFromLocal(instance: Instance, evfeventInfo: EvfEventInfo) { if (evfeventInfo.workspace) { - const evfeventFiles = await getEvfeventFiles(evfeventInfo.workspace); + const evfeventFiles = await vscode.workspace.findFiles(new vscode.RelativePattern(evfeventInfo.workspace, `**/.evfevent/*`), null); if (evfeventFiles) { const filesContent = await Promise.all(evfeventFiles.map(uri => vscode.workspace.fs.readFile(uri))); @@ -153,7 +152,7 @@ export function handleEvfeventLines(lines: string[], instance: Instance, evfeven if (connection) { // Belive it or not, sometimes if the deploy directory is symlinked into as ASP, this can be a problem const aspNames = connection.getAllIAsps().map(asp => asp.name); - + for (const aspName of aspNames) { const aspRoot = `/${aspName}`; if (relativeCompilePath.startsWith(aspRoot)) { diff --git a/src/ui/types.ts b/src/ui/types.ts index 6dd0a1cfe..2d3f024c1 100644 --- a/src/ui/types.ts +++ b/src/ui/types.ts @@ -1,4 +1,4 @@ -import { TreeItemCollapsibleState, TreeItem, ThemeIcon, ThemeColor, ProviderResult, MarkdownString } from "vscode" +import { MarkdownString, ProviderResult, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode" import { FocusOptions, IBMiMember, IBMiObject, ObjectFilters, WithPath } from "../api/types" export type BrowserItemParameters = { @@ -31,7 +31,7 @@ export class BrowserItem extends TreeItem { } getChildren?(): ProviderResult; - refresh?(): void; + refresh?(): Thenable; reveal?(options?: FocusOptions): Thenable; getToolTip?(): Promise; } \ No newline at end of file diff --git a/src/ui/views/ConnectionBrowser.ts b/src/ui/views/ConnectionBrowser.ts index 991d7759e..08a0fc2fc 100644 --- a/src/ui/views/ConnectionBrowser.ts +++ b/src/ui/views/ConnectionBrowser.ts @@ -203,7 +203,6 @@ export function initializeConnectionBrowser(context: vscode.ExtensionContext) { { label: vscode.l10n.t(`Object filters`), picked: true, copy: (from, to) => to.objectFilters = from.objectFilters }, { label: vscode.l10n.t(`IFS shortcuts`), picked: true, copy: (from, to) => to.ifsShortcuts = from.ifsShortcuts }, { label: vscode.l10n.t(`Custom variables`), picked: true, copy: (from, to) => to.customVariables = from.customVariables }, - { label: vscode.l10n.t(`Command profiles`), picked: true, copy: (from, to) => to.commandProfiles = from.commandProfiles }, { label: vscode.l10n.t(`Connection profiles`), picked: true, copy: (from, to) => to.connectionProfiles = from.connectionProfiles } ], { @@ -226,7 +225,6 @@ export function initializeConnectionBrowser(context: vscode.ExtensionContext) { newConnectionSetting.objectFilters = []; newConnectionSetting.ifsShortcuts = []; newConnectionSetting.customVariables = []; - newConnectionSetting.commandProfiles = []; newConnectionSetting.connectionProfiles = []; copyOperations.forEach(operation => operation(connectionSetting, newConnectionSetting)); connectionSettings.push(newConnectionSetting); diff --git a/src/ui/views/ProfilesView.ts b/src/ui/views/ProfilesView.ts deleted file mode 100644 index da721961d..000000000 --- a/src/ui/views/ProfilesView.ts +++ /dev/null @@ -1,308 +0,0 @@ - -import vscode, { l10n, window } from 'vscode'; -import { GetNewLibl } from '../../api/components/getNewLibl'; -import { instance } from '../../instantiate'; -import { ConnectionProfile, Profile } from '../../typings'; -import { CommandProfileUi } from '../../webviews/commandProfile'; -import IBMi from '../../api/IBMi'; - -export class ProfilesView { - private _onDidChangeTreeData = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - - constructor(context: vscode.ExtensionContext) { - context.subscriptions.push( - vscode.commands.registerCommand(`code-for-ibmi.refreshProfileView`, async () => { - this.refresh(); - }), - - vscode.commands.registerCommand(`code-for-ibmi.newConnectionProfile`, () => { - // Call it with no profile parameter - vscode.commands.executeCommand(`code-for-ibmi.saveConnectionProfile`); - }), - - vscode.commands.registerCommand(`code-for-ibmi.saveConnectionProfile`, async (profileNode?: Profile) => { - const connection = instance.getConnection(); - const storage = instance.getStorage(); - if (connection && storage) { - const config = connection.getConfig(); - const currentProfile = storage.getLastProfile() || ''; - let currentProfiles = config.connectionProfiles; - - const savedProfileName = profileNode?.profile || await vscode.window.showInputBox({ - value: currentProfile, - prompt: l10n.t(`Name of profile`) - }); - - if (savedProfileName) { - let savedProfile = currentProfiles.find(profile => profile.name.toUpperCase() === savedProfileName.toUpperCase()); - if (savedProfile) { - assignProfile(config, savedProfile); - } else { - savedProfile = cloneProfile(config, savedProfileName); - currentProfiles.push(savedProfile); - } - - await Promise.all([ - IBMi.connectionManager.update(config), - storage.setLastProfile(savedProfileName) - ]); - this.refresh(); - - vscode.window.showInformationMessage(l10n.t(`Saved current settings to profile "{0}".`, savedProfileName)); - } - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.deleteConnectionProfile`, async (profileNode?: Profile) => { - const connection = instance.getConnection(); - if (connection) { - const config = connection.getConfig(); - const currentProfiles = config.connectionProfiles; - const chosenProfile = await getOrPickAvailableProfile(currentProfiles, profileNode); - if (chosenProfile) { - vscode.window.showWarningMessage(l10n.t(`Are you sure you want to delete the "{0}" profile?`, chosenProfile.name), l10n.t("Yes")).then(async result => { - if (result === l10n.t(`Yes`)) { - currentProfiles.splice(currentProfiles.findIndex(profile => profile === chosenProfile), 1); - config.connectionProfiles = currentProfiles; - await IBMi.connectionManager.update(config) - this.refresh(); - // TODO: Add message about deleted profile! - } - }) - } - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.loadConnectionProfile`, async (profileNode?: Profile) => { - const connection = instance.getConnection(); - const storage = instance.getStorage(); - if (connection && storage) { - const config = connection.getConfig(); - const chosenProfile = await getOrPickAvailableProfile(config.connectionProfiles, profileNode); - if (chosenProfile) { - assignProfile(chosenProfile, config); - await IBMi.connectionManager.update(config); - - await Promise.all([ - vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), - vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), - vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`), - storage.setLastProfile(chosenProfile.name) - ]); - - vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, chosenProfile.name)); - this.refresh(); - } - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.manageCommandProfile`, async (commandProfile?: CommandProfileItem) => { - CommandProfileUi.show(commandProfile ? commandProfile.profile : undefined); - }), - - vscode.commands.registerCommand(`code-for-ibmi.deleteCommandProfile`, async (commandProfile?: CommandProfileItem) => { - const connection = instance.getConnection(); - if (connection && commandProfile) { - const config = connection.getConfig(); - const storedProfile = config.commandProfiles.findIndex(profile => profile.name === commandProfile.profile); - if (storedProfile !== undefined) { - config.commandProfiles.splice(storedProfile, 1); - await IBMi.connectionManager.update(config); - // TODO: Add message about deleting! - this.refresh(); - } - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.loadCommandProfile`, async (commandProfile?: CommandProfileItem) => { - const connection = instance.getConnection(); - const storage = instance.getStorage(); - if (commandProfile && connection && storage) { - const config = connection.getConfig(); - const storedProfile = config.commandProfiles.find(profile => profile.name === commandProfile.profile); - - if (storedProfile) { - try { - const component = connection?.getComponent(GetNewLibl.ID) - const newSettings = await component?.getLibraryListFromCommand(connection, storedProfile.command); - - if (newSettings) { - config.libraryList = newSettings.libraryList; - config.currentLibrary = newSettings.currentLibrary; - await IBMi.connectionManager.update(config); - - await Promise.all([ - storage.setLastProfile(storedProfile.name), - vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), - ]); - - vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, storedProfile.name)); - this.refresh(); - } else { - window.showWarningMessage(l10n.t(`Failed to get library list from command. Feature not installed.`)); - } - - } catch (e: any) { - window.showErrorMessage(l10n.t(`Failed to get library list from command: {0}`, e.message)); - } - } - } - }), - - vscode.commands.registerCommand(`code-for-ibmi.setToDefault`, () => { - const connection = instance.getConnection(); - const storage = instance.getStorage(); - - if (connection && storage) { - const config = connection.getConfig(); - window.showInformationMessage(l10n.t(`Reset to default`), { - detail: l10n.t(`This will reset the User Library List, working directory and Custom Variables back to the defaults.`), - modal: true - }, l10n.t(`Continue`)).then(async result => { - if (result === l10n.t(`Continue`)) { - const defaultName = `Default`; - - assignProfile({ - name: defaultName, - libraryList: connection?.defaultUserLibraries || [], - currentLibrary: config.currentLibrary, - customVariables: [], - homeDirectory: config.homeDirectory, - ifsShortcuts: config.ifsShortcuts, - objectFilters: config.objectFilters, - }, config); - - await IBMi.connectionManager.update(config); - - await Promise.all([ - vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), - vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), - vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`), - storage.setLastProfile(defaultName) - ]); - } - }) - } - }) - - ) - } - - refresh() { - const config = instance.getConnection()?.getConfig(); - if (config) { - vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasProfiles`, config.connectionProfiles.length > 0 || config.commandProfiles.length > 0); - this._onDidChangeTreeData.fire(null); - } - } - - getTreeItem(element: vscode.TreeItem): vscode.TreeItem { - return element; - } - - async getChildren(): Promise { - const connection = instance.getConnection(); - - if (connection) { - const config = connection.getConfig(); - const storage = instance.getStorage(); - if (config && storage) { - const currentProfile = storage.getLastProfile(); - return [ - new ResetProfileItem(), - ...config.connectionProfiles - .map(profile => profile.name) - .map(name => new ProfileItem(name, name === currentProfile)), - ...config.commandProfiles - .map(profile => profile.name) - .map(name => new CommandProfileItem(name, name === currentProfile)), - ] - } - } - - return []; - } -} - -async function getOrPickAvailableProfile(availableProfiles: ConnectionProfile[], profileNode?: Profile): Promise { - if (availableProfiles.length > 0) { - if (profileNode) { - return availableProfiles.find(profile => profile.name === profileNode.profile); - } - else { - const items = availableProfiles.map(profile => { - return { - label: profile.name, - profile: profile - } - }); - return (await vscode.window.showQuickPick(items))?.profile; - } - } - else { - vscode.window.showInformationMessage(`No profiles exist for this system.`); - } -} - -function assignProfile(fromProfile: ConnectionProfile, toProfile: ConnectionProfile) { - toProfile.homeDirectory = fromProfile.homeDirectory; - toProfile.currentLibrary = fromProfile.currentLibrary; - toProfile.libraryList = fromProfile.libraryList; - toProfile.objectFilters = fromProfile.objectFilters; - toProfile.ifsShortcuts = fromProfile.ifsShortcuts; - toProfile.customVariables = fromProfile.customVariables; -} - -function cloneProfile(fromProfile: ConnectionProfile, newName: string): ConnectionProfile { - return { - name: newName, - homeDirectory: fromProfile.homeDirectory, - currentLibrary: fromProfile.currentLibrary, - libraryList: fromProfile.libraryList, - objectFilters: fromProfile.objectFilters, - ifsShortcuts: fromProfile.ifsShortcuts, - customVariables: fromProfile.customVariables - } -} - -class ProfileItem extends vscode.TreeItem implements Profile { - readonly profile; - constructor(name: string, active: boolean) { - super(name, vscode.TreeItemCollapsibleState.None); - - this.contextValue = `profile`; - this.iconPath = new vscode.ThemeIcon(active ? `layers-active` : `layers`); - this.description = active ? `Active` : ``; - this.tooltip = ``; - - this.profile = name; - } -} - -class CommandProfileItem extends vscode.TreeItem implements Profile { - readonly profile; - constructor(name: string, active: boolean) { - super(name, vscode.TreeItemCollapsibleState.None); - - this.contextValue = `commandProfile`; - this.iconPath = new vscode.ThemeIcon(active ? `layers-active` : `console`); - this.description = active ? `Active` : ``; - this.tooltip = ``; - - this.profile = name; - } -} - -class ResetProfileItem extends vscode.TreeItem implements Profile { - readonly profile; - constructor() { - super(`Reset to Default`, vscode.TreeItemCollapsibleState.None); - - this.contextValue = `resetProfile`; - this.iconPath = new vscode.ThemeIcon(`debug-restart`); - this.tooltip = ``; - - this.profile = `Default`; - } -} diff --git a/src/ui/views/debugView.ts b/src/ui/views/debugView.ts index e83fdc7bf..d2e79aace 100644 --- a/src/ui/views/debugView.ts +++ b/src/ui/views/debugView.ts @@ -1,7 +1,7 @@ import vscode from "vscode"; +import { DebugConfiguration, getDebugServiceDetails, SERVICE_CERTIFICATE } from "../../api/configuration/DebugConfiguration"; import { Tools } from "../../api/Tools"; import { checkClientCertificate, remoteCertificatesExists } from "../../debug/certificates"; -import { DebugConfiguration, getDebugServiceDetails, SERVICE_CERTIFICATE } from "../../api/configuration/DebugConfiguration"; import { DebugJob, getDebugServerJob, getDebugServiceJob, isDebugEngineRunning, readActiveJob, readJVMInfo, startServer, startService, stopServer, stopService } from "../../debug/server"; import { instance } from "../../instantiate"; import { VscodeTools } from "../Tools"; @@ -140,7 +140,7 @@ class DebugBrowser implements vscode.TreeDataProvider { } class DebugItem extends BrowserItem { - refresh() { + async refresh() { vscode.commands.executeCommand("code-for-ibmi.debug.refresh.item", this); } } diff --git a/src/ui/views/environment/actions.ts b/src/ui/views/environment/actions.ts new file mode 100644 index 000000000..66cda652c --- /dev/null +++ b/src/ui/views/environment/actions.ts @@ -0,0 +1,177 @@ +import { parse } from "path"; +import { stringify } from "querystring"; +import vscode, { l10n } from "vscode"; +import { getActions } from "../../../api/actions"; +import { parseFSOptions } from "../../../filesystems/qsys/QSysFs"; +import { instance } from "../../../instantiate"; +import { Action, ActionType } from "../../../typings"; +import { EnvironmentItem } from "./environmentItem"; + +type ActionContext = { + canRun?: boolean + matched?: boolean +} + +export namespace Actions { + export function validateName(name: string, names: string[]) { + if (!name) { + return l10n.t('Name cannot be empty'); + } + else if (names.includes(name.toLocaleUpperCase())) { + return l10n.t("This name is already used by another action"); + } + } +} + +export class ActionsNode extends EnvironmentItem { + private readonly foundActions: ActionItem[] = []; + private revealIndex = -1; + + private readonly children: ActionTypeNode[] = []; + + constructor() { + super(l10n.t("Actions"), { icon: "code-oss", state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = "actionsNode"; + } + + async getChildren() { + if (!this.children.length) { + await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasActionSearched`, false); + const actions = (await getActions()).sort(sortActions); + const localActions = new Map(); + for (const workspace of vscode.workspace.workspaceFolders || []) { + const workspaceActions = (await getActions(workspace)); + if (workspaceActions.length) { + localActions.set(workspace, workspaceActions.sort(sortActions)); + } + } + + this.children.push( + new ActionTypeNode(this, l10n.t("Member"), 'file-code', 'member', actions), + new ActionTypeNode(this, l10n.t("Object"), 'database', 'object', actions), + new ActionTypeNode(this, l10n.t("Streamfile"), 'file-text', 'streamfile', actions), + ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'folder', 'file', localActions, workspace))) + ); + + if (vscode.window.activeTextEditor) { + await this.activeEditorChanged(vscode.window.activeTextEditor) + } + } + return this.children; + } + + private async getAllActionItems() { + return (await this.getChildren()).flatMap(child => child.actionItems); + } + + async searchActions() { + const nameOrCommand = (await vscode.window.showInputBox({ title: l10n.t("Search action"), placeHolder: l10n.t("name or command...") }))?.toLocaleLowerCase(); + if (nameOrCommand) { + await this.clearSearch(); + const found = this.foundActions.push(...(await this.getAllActionItems()).filter(action => [action.action.name, action.action.command].some(text => text.toLocaleLowerCase().includes(nameOrCommand)))) > 0; + await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasActionSearched`, found); + if (found) { + this.foundActions.forEach(node => node.setContext({ matched: true })); + this.refresh(); + this.goToNextSearchMatch(); + } + } + } + + async activeEditorChanged(editor?: vscode.TextEditor) { + const uri = editor?.document.uri; + let activeEditorContext = undefined; + if (uri) { + const connection = instance.getConnection(); + activeEditorContext = { + scheme: uri.scheme, + extension: parse(uri.path).ext.substring(1).toLocaleUpperCase(), + protected: parseFSOptions(uri).readonly || connection?.getConfig()?.readOnlyMode || connection?.getContent().isProtectedPath(uri.path), + workspace: vscode.workspace.getWorkspaceFolder(uri) + }; + } + + const canRunOnEditor = (actionItem: ActionItem) => activeEditorContext && + activeEditorContext.scheme === actionItem.action.type && + activeEditorContext.workspace === actionItem.workspace && + (actionItem.action.runOnProtected || !activeEditorContext.protected) && + (!actionItem.action.extensions?.length || actionItem.action.extensions.includes('GLOBAL') || actionItem.action.extensions.includes(activeEditorContext.extension)); + + (await this.getAllActionItems()).forEach(item => item.setContext({ canRun: canRunOnEditor(item) })); + this.refresh(); + } + + forceRefresh() { + this.children.splice(0, this.children.length); + this.refresh(); + } + + goToNextSearchMatch() { + this.revealIndex += (this.revealIndex + 1) < this.foundActions.length ? 1 : -this.revealIndex; + const actionNode = this.foundActions[this.revealIndex]; + actionNode.reveal({ focus: true }); + } + + async clearSearch() { + (await this.getAllActionItems()).forEach(node => node.setContext({ matched: false })); + this.revealIndex = -1; + this.foundActions.splice(0, this.foundActions.length); + await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasActionSearched`, false); + await this.refresh(); + } +} + +export class ActionTypeNode extends EnvironmentItem { + readonly actionItems: ActionItem[]; + constructor(parent: EnvironmentItem, label: string, icon: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { + super(label, { parent, icon, state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `actionTypeNode_${type}`; + this.actionItems = actions.filter(action => action.type === type).map(action => new ActionItem(this, action, workspace)); + } + + getChildren() { + return this.actionItems; + } +} + +export class ActionItem extends EnvironmentItem { + static matchedColor = "charts.yellow"; + static canRunColor = "charts.blue"; + static matchedCanRunColor = "charts.green"; + static context = `actionItem`; + + private context: ActionContext = {} + + constructor(parent: ActionTypeNode, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { + super(action.name, { parent }); + this.setContext(); + this.command = { + title: "Edit action", + command: "code-for-ibmi.environment.action.edit", + arguments: [this] + } + } + + setContext(context?: ActionContext) { + if (context?.canRun !== undefined) { + this.context.canRun = context.canRun; + } + if (context?.matched !== undefined) { + this.context.matched = context.matched; + } + + this.iconPath = new vscode.ThemeIcon("github-action", this.context.matched ? new vscode.ThemeColor(ActionItem.matchedColor) : undefined); + this.description = this.context.matched ? l10n.t("search match") : undefined; + this.tooltip = this.action.command; + this.resourceUri = vscode.Uri.from({ + scheme: ActionItem.context, + authority: this.action.name, + query: stringify({ matched: this.context.matched || undefined, canRun: this.context.canRun || undefined }) + }); + this.contextValue = `${ActionItem.context}${this.context.canRun ? "_canrun" : ""}${this.context.matched ? '_matched' : ''}`; + } +} + +function sortActions(a1: Action, a2: Action) { + return a1.name.localeCompare(a2.name); +} \ No newline at end of file diff --git a/src/ui/views/environment/connectionProfiles.ts b/src/ui/views/environment/connectionProfiles.ts new file mode 100644 index 000000000..e166625b0 --- /dev/null +++ b/src/ui/views/environment/connectionProfiles.ts @@ -0,0 +1,51 @@ +import vscode, { l10n } from "vscode"; +import { getConnectionProfiles } from "../../../api/connectionProfiles"; +import { instance } from "../../../instantiate"; +import { ConnectionProfile } from "../../../typings"; +import { VscodeTools } from "../../Tools"; +import { EnvironmentItem } from "./environmentItem"; + +export namespace ConnectionProfiles { + export function validateName(name: string, names: string[]) { + if (!name) { + return l10n.t('Name cannot be empty'); + } + else if (names.includes(name.toLocaleUpperCase())) { + return l10n.t("Profile {0} already exists", name); + } + } +} + +export class ProfilesNode extends EnvironmentItem { + constructor() { + super(l10n.t("Profiles"), { icon: "account", state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = "profilesNode"; + } + + getChildren() { + const currentProfile = instance.getConnection()?.getConfig().currentProfile; + return getConnectionProfiles() + .sort((p1, p2) => p1.name.localeCompare(p2.name)) + .map(profile => new ProfileItem(this, profile, profile.name === currentProfile)); + } +} + +export class ProfileItem extends EnvironmentItem { + static contextValue = `profileItem`; + static activeColor = "charts.green"; + + constructor(parent: EnvironmentItem, readonly profile: ConnectionProfile, active: boolean) { + super(profile.name, { parent, icon: "person", color: active ? ProfileItem.activeColor : undefined }); + + this.contextValue = `${ProfileItem.contextValue}${active ? '_active' : ''}${profile.setLibraryListCommand ? '_command' : ''}`; + this.description = active ? l10n.t(`Active profile`) : ``; + this.resourceUri = vscode.Uri.from({ scheme: this.contextValue, authority: profile.name, query: active ? "active" : "" }); + this.tooltip = VscodeTools.profileToToolTip(profile) + + this.command = { + title: "Edit connection profile", + command: "code-for-ibmi.environment.profile.edit", + arguments: [this.profile] + } + } +} \ No newline at end of file diff --git a/src/ui/views/environment/customVariables.ts b/src/ui/views/environment/customVariables.ts new file mode 100644 index 000000000..c4b38650c --- /dev/null +++ b/src/ui/views/environment/customVariables.ts @@ -0,0 +1,72 @@ +import vscode, { l10n } from "vscode"; +import IBMi from "../../../api/IBMi"; +import { instance } from "../../../instantiate"; +import { CustomVariable } from "../../../typings"; +import { EnvironmentItem } from "./environmentItem"; + +export namespace CustomVariables { + export function getAll() { + return instance.getConnection()?.getConfig().customVariables || []; + } + + export function validateName(name: string, names: string[]) { + name = sanitizeVariableName(name); + if (!name) { + return l10n.t('Name cannot be empty'); + } + else if (names.includes(name.toLocaleUpperCase())) { + return l10n.t("Custom variable {0} already exists", name); + } + } + + function sanitizeVariableName(name: string) { + return name.replace(/ /g, '_').replace(/&/g, '').toUpperCase(); + } + + export async function update(targetVariable: CustomVariable, options?: { newName?: string, delete?: boolean }) { + const config = instance.getConnection()?.getConfig(); + if (config) { + targetVariable.name = sanitizeVariableName(targetVariable.name); + const variables = config.customVariables; + const index = variables.findIndex(v => v.name === targetVariable.name); + + if (options?.delete) { + if (index < 0) { + throw new Error(l10n.t("Custom variable {0} not found for deletion.", targetVariable.name)); + } + variables.splice(index, 1); + } + else { + const variable = { name: sanitizeVariableName(options?.newName || targetVariable.name), value: targetVariable.value }; + variables[index < 0 ? variables.length : index] = variable; + } + + await IBMi.connectionManager.update(config); + } + } +} + +export class CustomVariablesNode extends EnvironmentItem { + constructor() { + super(l10n.t("Custom Variables"), { icon: "variable-group", state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `customVariablesNode`; + } + + getChildren() { + return CustomVariables.getAll().map(customVariable => new CustomVariableItem(this, customVariable)); + } +} + +export class CustomVariableItem extends EnvironmentItem { + constructor(parent: EnvironmentItem, readonly customVariable: CustomVariable) { + super(customVariable.name, { parent, icon: "symbol-variable" }); + this.contextValue = `customVariableItem`; + this.description = customVariable.value; + + this.command = { + title: "Change value", + command: "code-for-ibmi.environment.variable.edit", + arguments: [this.customVariable] + } + } +} \ No newline at end of file diff --git a/src/ui/views/environment/environmentItem.ts b/src/ui/views/environment/environmentItem.ts new file mode 100644 index 000000000..a8f432546 --- /dev/null +++ b/src/ui/views/environment/environmentItem.ts @@ -0,0 +1,13 @@ +import vscode from "vscode"; +import { FocusOptions } from "../../../typings"; +import { BrowserItem } from "../../types"; + +export class EnvironmentItem extends BrowserItem { + async refresh() { + await vscode.commands.executeCommand("code-for-ibmi.environment.refresh.item", this); + } + + reveal(options?: FocusOptions) { + return vscode.commands.executeCommand(`code-for-ibmi.environment.reveal`, this, options); + } +} \ No newline at end of file diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts new file mode 100644 index 000000000..5a05d8661 --- /dev/null +++ b/src/ui/views/environment/environmentView.ts @@ -0,0 +1,422 @@ + +import { parse as parseQuery } from "querystring"; +import vscode, { l10n, QuickPickItem } from 'vscode'; +import { getActions, updateAction } from '../../../api/actions'; +import { GetNewLibl } from '../../../api/components/getNewLibl'; +import { assignProfile, cloneProfile, getConnectionProfile, getConnectionProfiles, getDefaultProfile, updateConnectionProfile } from '../../../api/connectionProfiles'; +import IBMi from '../../../api/IBMi'; +import { editAction, isActionEdited } from '../../../editors/actionEditor'; +import { editConnectionProfile, isProfileEdited } from '../../../editors/connectionProfileEditor'; +import { instance } from '../../../instantiate'; +import { Action, ActionEnvironment, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../../typings'; +import { uriToActionTarget } from '../../actions'; +import { ActionItem, Actions, ActionsNode, ActionTypeNode } from './actions'; +import { ConnectionProfiles, ProfileItem, ProfilesNode } from './connectionProfiles'; +import { CustomVariableItem, CustomVariables, CustomVariablesNode } from './customVariables'; + +export function initializeEnvironmentView(context: vscode.ExtensionContext) { + const environmentView = new EnvironmentView(); + const environmentTreeViewer = vscode.window.createTreeView( + `environmentView`, { + treeDataProvider: environmentView, + showCollapseAll: true + }); + + const updateUIContext = async (profileName?: string) => { + await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", profileName); + environmentTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); + vscode.commands.executeCommand("code-for-ibmi.updateConnectedBar"); + }; + + const localActionsWatcher = vscode.workspace.createFileSystemWatcher(`**/.vscode/actions.json`); + localActionsWatcher.onDidCreate(() => environmentView.actionsNode?.forceRefresh()); + localActionsWatcher.onDidChange(() => environmentView.actionsNode?.forceRefresh()); + localActionsWatcher.onDidDelete(() => environmentView.actionsNode?.forceRefresh()); + + context.subscriptions.push( + environmentTreeViewer, + localActionsWatcher, + vscode.window.onDidChangeActiveTextEditor(async editor => environmentView.actionsNode?.activeEditorChanged(editor)), + vscode.window.registerFileDecorationProvider({ + provideFileDecoration(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { + if (uri.scheme.startsWith(ProfileItem.contextValue) && uri.query === "active") { + return { color: new vscode.ThemeColor(ProfileItem.activeColor) }; + } + else if (uri.scheme === ActionItem.context) { + const query = parseQuery(uri.query); + if (query.matched && query.canRun) { + return { color: new vscode.ThemeColor(ActionItem.matchedCanRunColor) }; + } + if (query.matched) { + return { color: new vscode.ThemeColor(ActionItem.matchedColor) }; + } + if (query.canRun) { + return { color: new vscode.ThemeColor(ActionItem.canRunColor) }; + } + } + } + }), + + vscode.commands.registerCommand("code-for-ibmi.environment.refresh", () => environmentView.refresh()), + vscode.commands.registerCommand("code-for-ibmi.environment.refresh.item", (item: BrowserItem) => environmentView.refresh(item)), + vscode.commands.registerCommand("code-for-ibmi.environment.reveal", (item: BrowserItem, options?: FocusOptions) => environmentTreeViewer.reveal(item, options)), + + vscode.commands.registerCommand("code-for-ibmi.environment.action.search", (node: ActionsNode) => node.searchActions()), + vscode.commands.registerCommand("code-for-ibmi.environment.action.search.next", (node: ActionsNode) => node.goToNextSearchMatch()), + vscode.commands.registerCommand("code-for-ibmi.environment.action.search.clear", (node: ActionsNode) => node.clearSearch()), + vscode.commands.registerCommand("code-for-ibmi.environment.action.create", async (node: ActionsNode | ActionTypeNode, from?: ActionItem) => { + const typeNode = "type" in node ? node : (await vscode.window.showQuickPick((await node.getChildren()).map(typeNode => ({ label: typeNode.label as string, description: typeNode.description ? typeNode.description as string : undefined, typeNode })), { title: l10n.t("Select an action type") }))?.typeNode; + if (typeNode) { + const existingNames = (await getActions(typeNode.workspace)).map(act => act.name); + + const name = await vscode.window.showInputBox({ + title: from ? l10n.t("Copy action '{0}'", from.action.name) : l10n.t("New action"), + placeHolder: l10n.t("action name..."), + value: from?.action.name, + validateInput: name => Actions.validateName(name, existingNames) + }); + + if (name) { + const action: Action = from ? { ...from.action, name } : { + name, + type: typeNode.type, + environment: "ile" as ActionEnvironment, + command: '' + }; + await updateAction(action, typeNode.workspace); + environmentView.actionsNode?.forceRefresh(); + vscode.commands.executeCommand("code-for-ibmi.environment.action.edit", { action, workspace: typeNode.workspace }); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.action.rename", async (node: ActionItem) => { + const action = node.action; + if (isActionEdited(node.action)) { + vscode.window.showWarningMessage(l10n.t("Action '{0}' is being edited. Please close its editor first.", action.name)); + } + else { + const existingNames = (await getActions(node.workspace)).filter(act => act.name === action.name).map(act => act.name); + + const newName = await vscode.window.showInputBox({ + title: l10n.t("Rename action"), + placeHolder: l10n.t("action name..."), + value: action.name, + validateInput: newName => Actions.validateName(newName, existingNames) + }); + + if (newName) { + await updateAction(action, node.workspace, { newName }); + environmentView.actionsNode?.forceRefresh(); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.action.edit", (node: ActionItem) => { + editAction(node.action, async () => environmentView.actionsNode?.forceRefresh(), node.workspace); + }), + vscode.commands.registerCommand("code-for-ibmi.environment.action.copy", async (node: ActionItem) => { + vscode.commands.executeCommand('code-for-ibmi.environment.action.create', node.parent, node); + }), + vscode.commands.registerCommand("code-for-ibmi.environment.action.delete", async (node: ActionItem) => { + if (isActionEdited(node.action)) { + vscode.window.showWarningMessage(l10n.t("Action '{0}' is being edited. Please close its editor first.", node.action.name)); + } + else if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete action '{0}' ?", node.action.name), { modal: true }, l10n.t("Yes"))) { + await updateAction(node.action, node.workspace, { delete: true }); + environmentView.actionsNode?.forceRefresh(); + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.action.runOnEditor", (node: ActionItem) => { + const uri = vscode.window.activeTextEditor?.document.uri; + if (uri) { + const editAction = () => vscode.commands.executeCommand("code-for-ibmi.environment.action.edit", node); + const editActionLabel = l10n.t("Edit action"); + const action = node.action; + if (action.type !== uri.scheme) { + vscode.window.showErrorMessage(l10n.t("This action cannot run on a {0}.", uri.scheme), editActionLabel).then(edit => edit ? editAction() : ''); + return; + } + + const workspace = vscode.workspace.getWorkspaceFolder(uri); + if (workspace && node.workspace && node.workspace !== workspace) { + vscode.window.showErrorMessage(l10n.t("This action belongs to workspace {0} and cannot be run on a file from workspace {1}", node.workspace.name, workspace.name)) + return; + } + + const actionTarget = uriToActionTarget(uri); + if (action.extensions && !action.extensions.includes('GLOBAL') && !action.extensions.includes(actionTarget.extension) && !action.extensions.includes(actionTarget.fragment)) { + vscode.window.showErrorMessage(l10n.t("This action cannot run on a file with the {0} extension.", actionTarget.extension), editActionLabel).then(edit => edit ? editAction() : ''); + return; + } + + vscode.commands.executeCommand(`code-for-ibmi.runAction`, uri, undefined, action, undefined, workspace); + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.actions.focus", () => environmentView.actionsNode?.reveal({ focus: true, expand: true })), + + vscode.commands.registerCommand("code-for-ibmi.environment.variable.declare", async (variablesNode: CustomVariablesNode, from?: CustomVariable) => { + const existingNames = CustomVariables.getAll().map(v => v.name); + const name = (await vscode.window.showInputBox({ + title: l10n.t('Enter new Custom Variable name'), + prompt: l10n.t("The name will automatically be uppercased"), + placeHolder: l10n.t('new custom variable name...'), + validateInput: name => CustomVariables.validateName(name, existingNames) + })); + + if (name) { + const variable = { name, value: from?.value } as CustomVariable; + if (from) { + await CustomVariables.update(variable); + environmentView.refresh(variablesNode); + } else { + vscode.commands.executeCommand("code-for-ibmi.environment.variable.edit", variable, variablesNode); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.variable.edit", async (variable: CustomVariable, variablesNode?: CustomVariablesNode) => { + const value = await vscode.window.showInputBox({ title: l10n.t('Enter {0} value', variable.name), value: variable.value }); + if (value !== undefined) { + variable.value = value; + await CustomVariables.update(variable); + environmentView.refresh(variablesNode); + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.variable.rename", async (variableItem: CustomVariableItem) => { + const variable = variableItem.customVariable; + const existingNames = CustomVariables.getAll().map(v => v.name).filter(name => name !== variable.name); + const newName = (await vscode.window.showInputBox({ + title: l10n.t('Enter Custom Variable {0} new name', variable.name), + prompt: l10n.t("The name will automatically be uppercased"), + validateInput: name => CustomVariables.validateName(name, existingNames) + })); + + if (newName) { + await CustomVariables.update(variable, { newName }); + environmentView.refresh(variableItem.parent); + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.variable.copy", async (variableItem: CustomVariableItem) => { + vscode.commands.executeCommand("code-for-ibmi.environment.variable.declare", variableItem.parent, variableItem.customVariable); + }), + vscode.commands.registerCommand("code-for-ibmi.environment.variable.delete", async (variableItem: CustomVariableItem) => { + const variable = variableItem.customVariable; + if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete Custom Variable '{0}' ?", variable.name), { modal: true }, l10n.t("Yes"))) { + await CustomVariables.update(variable, { delete: true }); + environmentView.refresh(variableItem.parent); + } + }), + + vscode.commands.registerCommand("code-for-ibmi.environment.profile.create", async (node?: ProfilesNode, from?: ConnectionProfile) => { + const existingNames = getConnectionProfiles().map(profile => profile.name); + + const name = await vscode.window.showInputBox({ + title: l10n.t("Enter new profile name"), + placeHolder: l10n.t("profile name..."), + value: from?.name, + validateInput: name => Actions.validateName(name, existingNames) + }); + + if (name) { + const connection = instance.getConnection(); + const homeDirectory = connection?.getConfig().homeDirectory || `/home/${connection?.currentUser || 'QPGMR'}`; //QPGMR case should not happen, but better be safe here + const profile: ConnectionProfile = from ? cloneProfile(from, name) : { + name, + homeDirectory, + currentLibrary: 'QGPL', + libraryList: ["QGPL", "QTEMP"], + customVariables: [], + ifsShortcuts: [homeDirectory], + objectFilters: [], + }; + await updateConnectionProfile(profile); + environmentView.refresh(environmentView.profilesNode); + if (!from) { + vscode.commands.executeCommand("code-for-ibmi.environment.profile.edit", profile); + } + else { + vscode.window.showInformationMessage(l10n.t("Created connection Profile '{0}'.", profile.name), l10n.t("Activate profile {0}", profile.name)) + .then(doSwitch => { + if (doSwitch) { + vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", profile); + } + }) + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.fromCurrent", async (profilesNode: ProfilesNode) => { + const config = instance.getConnection()?.getConfig(); + + if (config) { + const current = cloneProfile(config, ""); + vscode.commands.executeCommand("code-for-ibmi.environment.profile.create", undefined, current); + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.edit", async (profile: ConnectionProfile) => { + editConnectionProfile(profile, async () => environmentView.refresh(environmentView.profilesNode)) + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.rename", async (item: ProfileItem) => { + if (isProfileEdited(item.profile)) { + vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor first.", item.profile.name)); + } + else { + const currentName = item.profile.name; + const existingNames = getConnectionProfiles().map(profile => profile.name).filter(name => name !== currentName); + const newName = await vscode.window.showInputBox({ + title: l10n.t('Enter Profile {0} new name', item.profile.name), + placeHolder: l10n.t("profile name..."), + validateInput: name => ConnectionProfiles.validateName(name, existingNames) + }); + + if (newName) { + await updateConnectionProfile(item.profile, { newName }); + const config = instance.getConnection()?.getConfig(); + if (config?.currentProfile === currentName) { + config.currentProfile = newName; + await IBMi.connectionManager.update(config); + updateUIContext(newName); + } + environmentView.refresh(environmentView.profilesNode); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.copy", async (item: ProfileItem) => { + vscode.commands.executeCommand("code-for-ibmi.environment.profile.create", undefined, item.profile); + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.delete", async (item: ProfileItem) => { + if (isProfileEdited(item.profile)) { + vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor first.", item.profile.name)); + } + else if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete profile '{0}' ?", item.profile.name), { modal: true }, l10n.t("Yes"))) { + await updateConnectionProfile(item.profile, { delete: true }); + environmentView.refresh(environmentView.profilesNode); + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.activate", async (item: ProfileItem | ConnectionProfile) => { + const connection = instance.getConnection(); + const storage = instance.getStorage(); + if (connection && storage) { + const profile = "profile" in item ? item.profile : item; + const config = connection.getConfig(); + const profileToBackup = config.currentProfile ? getConnectionProfile(config.currentProfile) : getDefaultProfile(); + + if (isProfileEdited(profile)) { + vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor before activating it.", profile.name)); + return; + } + else if (profileToBackup && isProfileEdited(profileToBackup)) { + vscode.window.showWarningMessage(l10n.t("Profile {0} is being edited. Please close its editor before unloading it.", profileToBackup.name)); + return; + } + + if (profileToBackup) { + assignProfile(config, profileToBackup); + } + assignProfile(profile, config); + config.currentProfile = profile.name || undefined; + await IBMi.connectionManager.update(config); + + await Promise.all([ + vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`), + vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), + vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`) + ]); + environmentView.refresh(); + + if (profile.name && profile.setLibraryListCommand) { + await vscode.commands.executeCommand("code-for-ibmi.environment.profile.runLiblistCommand", profile); + } + + await updateUIContext(profile.name); + vscode.window.showInformationMessage(config.currentProfile ? l10n.t(`Switched to profile "{0}".`, profile.name) : l10n.t("Active profile unloaded")); + } + }), + + vscode.commands.registerCommand("code-for-ibmi.environment.profile.runLiblistCommand", async (profileItem?: ProfileItem | ConnectionProfile) => { + const connection = instance.getConnection(); + const storage = instance.getStorage(); + if (connection && storage) { + const config = connection.getConfig(); + const profile = profileItem && ("profile" in profileItem ? profileItem?.profile : profileItem) || getConnectionProfile(config.get); + + if (profile?.setLibraryListCommand) { + const command = profile.setLibraryListCommand.startsWith(`?`) ? + await vscode.window.showInputBox({ title: l10n.t(`Run Library List Command`), value: profile.setLibraryListCommand.substring(1) }) : + profile.setLibraryListCommand; + + if (command) { + return await vscode.window.withProgress({ title: l10n.t("Running {0} profile's Library List Command...", profile.name), location: vscode.ProgressLocation.Notification }, async () => { + try { + const component = connection.getComponent(GetNewLibl.ID) + const newSettings = await component?.getLibraryListFromCommand(connection, command); + + if (newSettings) { + config.libraryList = newSettings.libraryList; + config.currentLibrary = newSettings.currentLibrary; + await IBMi.connectionManager.update(config); + await vscode.commands.executeCommand(`code-for-ibmi.refreshLibraryListView`); + } else { + vscode.window.showWarningMessage(l10n.t(`Failed to get library list from command. Feature not installed; try to reload settings when connecting.`)); + } + } catch (e: any) { + vscode.window.showErrorMessage(l10n.t(`Failed to get library list from command: {0}`, e.message)); + } + }); + } + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.environment.profile.unload", async () => { + vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", getDefaultProfile()); + }) + ); + + instance.subscribe(context, 'connected', 'Update context view description', async () => { + const config = instance.getConnection()?.getConfig(); + const storage = instance.getStorage(); + if (config && storage) { + //Retrieve and clear old value for last used profile + const deprecatedLastProfile = storage.getLastProfile(); + if (deprecatedLastProfile) { + if (deprecatedLastProfile.toLocaleLowerCase() !== 'default') { + config.currentProfile = deprecatedLastProfile; + await IBMi.connectionManager.update(config); + } + await storage.clearDeprecatedLastProfile(); + } + updateUIContext(config.currentProfile); + } + }); +} + +class EnvironmentView implements vscode.TreeDataProvider { + private readonly emitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.emitter.event; + actionsNode?: ActionsNode + profilesNode?: ProfilesNode + + refresh(target?: BrowserItem) { + this.emitter.fire(target); + } + + getTreeItem(element: BrowserItem): vscode.TreeItem { + return element; + } + + getParent(element: BrowserItem) { + return element?.parent; + } + + async getChildren(item?: BrowserItem) { + if (item) { + return item.getChildren?.(); + } + else { + this.actionsNode = new ActionsNode(); + this.profilesNode = new ProfilesNode(); + return [ + this.actionsNode, + new CustomVariablesNode(), + this.profilesNode + ]; + } + } +} \ No newline at end of file diff --git a/src/ui/views/ifsBrowser.ts b/src/ui/views/ifsBrowser.ts index 81dcfc0c4..e9317efc9 100644 --- a/src/ui/views/ifsBrowser.ts +++ b/src/ui/views/ifsBrowser.ts @@ -8,7 +8,7 @@ import { SortOptions } from "../../api/IBMiContent"; import { Search } from "../../api/Search"; import { Tools } from "../../api/Tools"; import { instance } from "../../instantiate"; -import { FocusOptions, IFSFile, IFS_BROWSER_MIMETYPE, OBJECT_BROWSER_MIMETYPE, SearchHit, SearchResults, URI_LIST_MIMETYPE, URI_LIST_SEPARATOR, WithPath } from "../../typings"; +import { FocusOptions, IFS_BROWSER_MIMETYPE, IFSFile, OBJECT_BROWSER_MIMETYPE, SearchHit, SearchResults, URI_LIST_MIMETYPE, URI_LIST_SEPARATOR, WithPath } from "../../typings"; import { VscodeTools } from "../Tools"; import { BrowserItem, BrowserItemParameters } from "../types"; @@ -116,7 +116,7 @@ class IFSItem extends BrowserItem implements WithPath { this.refresh(); } - refresh(): void { + async refresh() { vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowserItem`, this); } diff --git a/src/ui/views/objectBrowser.ts b/src/ui/views/objectBrowser.ts index d34cbd2c2..d5e4c937c 100644 --- a/src/ui/views/objectBrowser.ts +++ b/src/ui/views/objectBrowser.ts @@ -55,7 +55,7 @@ abstract class ObjectBrowserItem extends BrowserItem { super(label, params); } - refresh(): void { + async refresh() { vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowserItem`, this); } diff --git a/src/webviews/CustomUI.ts b/src/webviews/CustomUI.ts index b7246026b..645fadabc 100644 --- a/src/webviews/CustomUI.ts +++ b/src/webviews/CustomUI.ts @@ -110,9 +110,10 @@ export class Section { return this; } - addSelect(id: string, label: string, items: SelectItem[], description?: string) { + addSelect(id: string, label: string, items: SelectItem[], description?: string, readonly?: boolean) { const select = new Field('select', id, label, description); select.items = items; + select.readonly = readonly; this.addField(select); return this; } @@ -152,90 +153,19 @@ export class Section { const openedWebviews: Map = new Map; -export class CustomUI extends Section { +export class CustomHTML extends Section { private options?: PanelOptions; - /** - * If no callback is provided, a Promise will be returned. - * If the page is already opened, it grabs the focus and return no Promise (as it's alreay handled by the first call). - * - * @param title - * @param callback - * @returns a Promise> if no callback is provided - */ - loadPage(title: string): Promise> | undefined { - const webview = openedWebviews.get(title); - if (webview) { - webview.reveal(); - } - else { - return this.createPage(title); - } - } setOptions(options: PanelOptions) { this.options = options; return this; } - private createPage(title: string): Promise> | undefined { - const panel = vscode.window.createWebviewPanel( - `custom`, - title, - vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - enableFindWidget: true - } - ); - - panel.webview.html = this.getHTML(panel, title); - - let didSubmit = false; - - openedWebviews.set(title, panel); - - const page = new Promise>((resolve) => { - panel.webview.onDidReceiveMessage( - (message: WebviewMessageRequest) => { - if (message.type && message.data) { - switch (message.type) { - case `submit`: - didSubmit = true; - resolve({ panel, data: message.data }); - break; - - case `file`: - const resultField = message.data.field; - if (resultField) { - vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectMany: false, - canSelectFolders: false, - }).then(result => { - if (result) { - panel.webview.postMessage({ type: `update`, field: resultField, value: result[0].fsPath }); - } - }); - } - break; - } - } - } - ); - - panel.onDidDispose(() => { - openedWebviews.delete(title); - if (!didSubmit) { - resolve({ panel }); - } - }); - }); - - return page; + protected getSpecificScript() { + return ""; } - private getHTML(panel: vscode.WebviewPanel, title: string) { + protected getHTML(panel: vscode.WebviewPanel, title: string) { const notInputFields = [`submit`, `buttons`, `tree`, `hr`, `paragraph`, `tabs`, `complexTabs`, 'browser'] as FieldType[]; const trees = this.fields.filter(field => [`tree`, 'browser'].includes(field.type)); @@ -342,7 +272,7 @@ export class CustomUI extends Section { } } validateInputs(response.field); - } + } } }); @@ -398,27 +328,7 @@ export class CustomUI extends Section { } return isValid; - } - - - const doDone = (event, buttonId) => { - console.log('submit now!!', buttonId) - if (event) - event.preventDefault(); - - var data = document.querySelector('#laforma').data; - - if (buttonId) { - data['buttons'] = buttonId; - } - - // Convert the weird array value of checkboxes to boolean - for (const checkbox of checkboxes) { - data[checkbox] = (data[checkbox] && data[checkbox].length >= 1); - } - - vscode.postMessage({ type: 'submit', data }); - }; + } const treeItemClick = (treeId, type, value) => { if(type === "browse"){ @@ -440,50 +350,8 @@ export class CustomUI extends Section { // Setup the input fields for validation for (const field of inputFields) { const fieldElement = document.getElementById(field.id); - fieldElement.addEventListener("change", (e) => {validateInputs()}); - } - - // Now many buttons can be pressed to submit - for (const fieldData of groupButtons) { - const field = fieldData.id; - - console.log('group button', fieldData, document.getElementById(field)); - var button = document.getElementById(field); - - const submitButtonAction = (event) => { - const isValid = fieldData.requiresValidation ? validateInputs() : true; - console.log({requiresValidation: fieldData.requiresValidation, isValid}); - if (isValid) doDone(event, field); - } - - button.onclick = submitButtonAction; - button.onKeyDown = submitButtonAction; - } - - for (const field of submitfields) { - const currentElement = document.getElementById(field); - if (currentElement.hasAttribute('rows')) { - currentElement - .addEventListener('keyup', function(event) { - event.preventDefault(); - if (event.keyCode === 13 && event.altKey) { - if (validateInputs()) { - doDone(); - } - } - }); - } else { - currentElement - .addEventListener('keyup', function(event) { - event.preventDefault(); - if (event.keyCode === 13) { - if (validateInputs()) { - doDone(); - } - } - }); - } - } + fieldElement.addEventListener("change", (e) => validateInputs()); + } // This is used to read the file in order to get the real path. for (const field of filefields) { @@ -507,7 +375,7 @@ export class CustomUI extends Section { });` )} }); - + ${this.getSpecificScript()} }()) @@ -515,6 +383,149 @@ export class CustomUI extends Section { } } +export class CustomUI extends CustomHTML { + /** + * If no callback is provided, a Promise will be returned. + * If the page is already opened, it grabs the focus and return no Promise (as it's alreay handled by the first call). + * + * @param title + * @param callback + * @returns a Promise> if no callback is provided + */ + loadPage(title: string): Promise> | undefined { + const webview = openedWebviews.get(title); + if (webview) { + webview.reveal(); + } + else { + return this.createPage(title); + } + } + + private createPage(title: string): Promise> | undefined { + const panel = vscode.window.createWebviewPanel( + `custom`, + title, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + enableFindWidget: true + } + ); + + panel.webview.html = this.getHTML(panel, title); + + let didSubmit = false; + + openedWebviews.set(title, panel); + + const page = new Promise>((resolve) => { + panel.webview.onDidReceiveMessage( + (message: WebviewMessageRequest) => { + if (message.type && message.data) { + switch (message.type) { + case `submit`: + didSubmit = true; + resolve({ panel, data: message.data }); + break; + + case `file`: + const resultField = message.data.field; + if (resultField) { + vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectMany: false, + canSelectFolders: false, + }).then(result => { + if (result) { + panel.webview.postMessage({ type: `update`, field: resultField, value: result[0].fsPath }); + } + }); + } + break; + } + } + } + ); + + panel.onDidDispose(() => { + openedWebviews.delete(title); + if (!didSubmit) { + resolve({ panel }); + } + }); + }); + + return page; + } + + protected getSpecificScript() { + return /* javascript */ ` + const doDone = (event, buttonId) => { + console.log('submit now!!', buttonId) + if (event) + event.preventDefault(); + + var data = document.querySelector('#laforma').data; + + if (buttonId) { + data['buttons'] = buttonId; + } + + // Convert the weird array value of checkboxes to boolean + for (const checkbox of checkboxes) { + data[checkbox] = (data[checkbox] && data[checkbox].length >= 1); + } + + vscode.postMessage({ type: 'submit', data }); + }; + + // Now many buttons can be pressed to submit + for (const fieldData of groupButtons) { + const field = fieldData.id; + + console.log('group button', fieldData, document.getElementById(field)); + var button = document.getElementById(field); + + const submitButtonAction = (event) => { + const isValid = fieldData.requiresValidation ? validateInputs() : true; + console.log({requiresValidation: fieldData.requiresValidation, isValid}); + if (isValid) doDone(event, field); + } + + button.onclick = submitButtonAction; + button.onKeyDown = submitButtonAction; + } + + for (const field of submitfields) { + const currentElement = document.getElementById(field); + if (currentElement.hasAttribute('rows')) { + currentElement + .addEventListener('keyup', function(event) { + event.preventDefault(); + if (event.keyCode === 13 && event.altKey) { + if (validateInputs()) { + doDone(); + } + } + }); + } else { + currentElement + .addEventListener('keyup', function(event) { + event.preventDefault(); + if (event.keyCode === 13) { + if (validateInputs()) { + doDone(); + } + } + }); + } + } + `; + } +} + export type FieldType = "input" | "password" | "buttons" | "checkbox" | "file" | "complexTabs" | "tabs" | "tree" | "select" | "paragraph" | "hr" | "heading" | "browser"; export interface TreeListItemIcon { @@ -669,7 +680,7 @@ export class Field { ${this.renderLabel()} ${this.renderDescription()} - + ${this.items?.map(item => /* html */`${item.description}`)} `; diff --git a/src/webviews/actions/index.ts b/src/webviews/actions/index.ts deleted file mode 100644 index ef5693d62..000000000 --- a/src/webviews/actions/index.ts +++ /dev/null @@ -1,326 +0,0 @@ -import vscode from "vscode"; - -import { CustomUI, Tab } from "../CustomUI"; - -import IBMi from "../../api/IBMi"; -import { Tools } from "../../api/Tools"; -import { instance } from "../../instantiate"; -import { Action, ActionEnvironment, ActionRefresh, ActionType } from "../../typings"; -import { getVariablesInfo } from "./varinfo"; - -type MainMenuPage = { - buttons?: 'newAction' | 'duplicateAction' - value: string -} - -type ActionPage = { - name: string - command: string - extensions: string - type: ActionType - environment: ActionEnvironment - refresh: ActionRefresh - runOnProtected: boolean - outputToFile: string - buttons: "saveAction" | "deleteAction" | "cancelAction" -} - -export namespace ActionsUI { - - export function initialize(context: vscode.ExtensionContext) { - context.subscriptions.push( - vscode.commands.registerCommand(`code-for-ibmi.showActionsMaintenance`, showMainMenu) - ) - } - - async function showMainMenu() { - const allActions = loadActions().map((action, index) => ({ - ...action, - index, - })); - - const icons = { - branch: `folder`, - leaf: `file`, - open: `folder-opened`, - }; - const ui = new CustomUI() - .addTree(`actions`, vscode.l10n.t(`Work with Actions`), - Array.from(new Set(allActions.map(action => action.type))) - .map(type => ({ - icons, - open: true, - label: `📦 ${Tools.capitalize(type || '?')}`, - type, - subItems: allActions.filter(action => action.type === type) - .map(action => ({ - icons, - label: `🔨 ${action.name} (${action.extensions?.map(ext => ext.toLowerCase()).join(`, `)})`, - value: String(action.index), - })) - })), - vscode.l10n.t(`Create or maintain Actions. Actions are grouped by the type of file/object they target.`)) - .addButtons( - { id: `newAction`, label: vscode.l10n.t(`New Action`) }, - { id: `duplicateAction`, label: vscode.l10n.t(`Duplicate`) } - ); - - const page = await ui.loadPage(vscode.l10n.t(`Work with Actions`)); - if (page && page.data) { - page.panel.dispose(); - - switch (page.data.buttons) { - case `newAction`: - workAction(-1); - break; - case `duplicateAction`: - duplicateAction(); - break; - default: - workAction(Number(page.data.value)); - break; - } - } - } - - /** - * Show item picker to duplicate an existing action - */ - async function duplicateAction() { - const actions = loadActions(); - - const action = (await vscode.window.showQuickPick( - actions.map((action, index) => ({ - label: `${action.name} (${action.type}: ${action.extensions?.join(`, `)})`, - value: index, - action - })).sort((a, b) => a.label.localeCompare(b.label)), - { - placeHolder: vscode.l10n.t(`Select an action to duplicate`) - } - ))?.action; - - if (action) { - //Duplicate the selected action - workAction(-1, { ...action }); - } else { - showMainMenu(); - } - } - - /** - * Edit an existing action - */ - async function workAction(id: number, actionDefault?: Action) { - const config = instance.getConfig(); - if (config) { - const allActions = loadActions(); - let currentAction: Action; - let uiTitle: string; - let stayOnPanel = true; - - if (id >= 0) { - //Fetch existing action - currentAction = allActions[id]; - uiTitle = vscode.l10n.t(`Edit action "{0}"`, currentAction.name); - } else if (actionDefault) { - currentAction = actionDefault; - uiTitle = vscode.l10n.t(`Duplicate action "{0}"`, currentAction.name); - } else { - //Otherwise.. prefill with defaults - currentAction = { - type: `member`, - extensions: [ - `RPGLE`, - `RPG` - ], - environment: `ile`, - name: ``, - command: ``, - refresh: `no` - } - uiTitle = vscode.l10n.t(`Create action`); - } - - if (!currentAction.environment) { - currentAction.environment = `ile`; - } - - // Our custom variables as HTML - const custom = config.customVariables.map(variable => `
  • &${variable.name}: ${variable.value}
  • `).join(``); - - const ui = new CustomUI() - .addInput(`name`, vscode.l10n.t(`Action name`), undefined, { default: currentAction.name }) - .addHorizontalRule() - .addInput( - `command`, - vscode.l10n.t(`Command(s) to run`), - vscode.l10n.t(`Below are available variables based on the Type you have select below. You can specify different commands on each line. Each command run is stateless and run in their own job.`), - { rows: 5, default: currentAction.command } - ) - .addTabs( - Object.entries(getVariablesInfo()) - .map(([type, variables]) => ({ - label: Tools.capitalize(type), - value: `
      ${variables.map(variable => `
    • ${variable.name}: ${variable.text}
    • `).join(``)}${custom}
    ` - } as Tab)), getDefaultTabIndex(currentAction.type) - ) - .addHorizontalRule() - .addInput(`extensions`, vscode.l10n.t(`Extensions`), vscode.l10n.t(`A comma delimited list of extensions for this action. This can be a member extension, a streamfile extension, an object type or an object attribute`), { default: currentAction.extensions?.join(`, `) }) - .addSelect(`type`, vscode.l10n.t(`Type`), [ - { - selected: currentAction.type === `member`, - value: `member`, - description: vscode.l10n.t(`Member`), - text: vscode.l10n.t(`Source members in the QSYS file system`), - }, - { - selected: currentAction.type === `streamfile`, - value: `streamfile`, - description: vscode.l10n.t(`Streamfile`), - text: vscode.l10n.t(`Streamfiles in the IFS`) - }, - { - selected: currentAction.type === `object`, - value: `object`, - description: vscode.l10n.t(`Object`), - text: vscode.l10n.t(`Objects in the QSYS file system`) - }, - { - selected: currentAction.type === `file`, - value: `file`, - description: vscode.l10n.t(`Local File (Workspace)`), - text: vscode.l10n.t(`Actions for local files in the VS Code Workspace.`) - }], vscode.l10n.t(`The types of files this action can support.`) - ) - .addSelect(`environment`, vscode.l10n.t(`Environment`), [ - { - selected: currentAction.environment === `ile`, - value: `ile`, - description: vscode.l10n.t(`ILE`), - text: vscode.l10n.t(`Runs as an ILE command`) - }, - { - selected: currentAction.environment === `qsh`, - value: `qsh`, - description: vscode.l10n.t(`QShell`), - text: vscode.l10n.t(`Runs the command through QShell`) - }, - { - selected: currentAction.environment === `pase`, - value: `pase`, - description: vscode.l10n.t(`PASE`), - text: vscode.l10n.t(`Runs the command in the PASE environment`) - }], vscode.l10n.t(`Environment for command to be executed in.`) - ) - .addSelect(`refresh`, vscode.l10n.t(`Refresh`), [ - { - selected: currentAction.refresh === `no`, - value: `no`, - description: vscode.l10n.t(`No`), - text: vscode.l10n.t(`No refresh`) - }, - { - selected: currentAction.refresh === `parent`, - value: `parent`, - description: vscode.l10n.t(`Parent`), - text: vscode.l10n.t(`The parent container is refreshed`) - }, - { - selected: currentAction.refresh === `filter`, - value: `filter`, - description: vscode.l10n.t(`Filter`), - text: vscode.l10n.t(`The parent filter is refreshed`) - }, - { - selected: currentAction.refresh === `browser`, - value: `browser`, - description: vscode.l10n.t(`Browser`), - text: vscode.l10n.t(`The entire browser is refreshed`) - }], vscode.l10n.t(`The browser level to refresh after the action is done`) - ) - .addCheckbox("runOnProtected", vscode.l10n.t(`Run on protected/read only`), vscode.l10n.t(`Allows the execution of this Action on protected or read only targets`), currentAction.runOnProtected) - .addInput(`outputToFile`, vscode.l10n.t(`Copy output to file`), vscode.l10n.t(`Copy the action output to a file. Variables can be used to define the file's path; use &i to compute file index.
    Example: ~/outputs/&CURLIB_&OPENMBR&i.txt.`), { default: currentAction.outputToFile }) - .addHorizontalRule() - .addButtons( - { id: `saveAction`, label: vscode.l10n.t(`Save`) }, - id >= 0 ? { id: `deleteAction`, label: vscode.l10n.t(`Delete`) } : undefined, - { id: `cancelAction`, label: vscode.l10n.t(`Cancel`) } - ); - - while (stayOnPanel) { - const page = await ui.loadPage(uiTitle); - if (page && page.data) { - const data = page.data; - switch (data.buttons) { - case `deleteAction`: - const yes = vscode.l10n.t(`Yes`); - const result = await vscode.window.showInformationMessage(vscode.l10n.t(`Are you sure you want to delete the action "{0}"?`, currentAction.name), { modal: true }, yes, vscode.l10n.t("No")) - if (result === yes) { - allActions.splice(id, 1); - await saveActions(allActions); - stayOnPanel = false; - } - break; - - case `cancelAction`: - stayOnPanel = false; - break; - - default: - // We don't want \r (Windows line endings) - data.command = data.command.replace(new RegExp(`\\\r`, `g`), ``); - - const newAction: Action = { - type: data.type, - extensions: data.extensions.split(`,`).map(item => item.trim().toUpperCase()), - environment: data.environment, - name: data.name, - command: data.command, - refresh: data.refresh, - runOnProtected: data.runOnProtected, - outputToFile: data.outputToFile - }; - - if (id >= 0) { - allActions[id] = newAction; - } else { - allActions.push(newAction); - } - - await saveActions(allActions); - stayOnPanel = false; - break; - } - - page.panel.dispose(); - } - else { - stayOnPanel = false; - } - } - } - showMainMenu(); - } -} - -function saveActions(actions: Action[]) { - return IBMi.connectionManager.set(`actions`, actions); -} - -function loadActions(): Action[] { - return IBMi.connectionManager.get(`actions`) || []; -} - -function getDefaultTabIndex(type?: ActionType) { - switch (type) { - case `file`: - case `streamfile`: - return 1; - case `object`: - return 2; - case `member`: - default: - return 0; - } -} \ No newline at end of file diff --git a/src/webviews/actions/varinfo.ts b/src/webviews/actions/varinfo.ts deleted file mode 100644 index 644f8f4ee..000000000 --- a/src/webviews/actions/varinfo.ts +++ /dev/null @@ -1,52 +0,0 @@ -import vscode from "vscode"; - -// Used to list info about available variables -type VariableInfo = { - name: string - text: string -} - -type VariableInfoList = { - member: VariableInfo[] - streamFile: VariableInfo[] - object: VariableInfo[] -} - -const generic: () => VariableInfo[] = () => [ - { name: `&CURLIB`, text: vscode.l10n.t(`Current library, changeable in Library List`) }, - { name: `&USERNAME`, text: vscode.l10n.t(`Username for connection`)}, - { name: `&WORKDIR`, text: vscode.l10n.t(`Current working directory, changeable in IFS Browser`)}, - { name: `&HOST`, text: vscode.l10n.t(`Hostname or IP address from the current connection`)}, - { name: `&BUILDLIB`, text: vscode.l10n.t(`The same as &CURLIB`)}, - { name: `&LIBLC`, text: vscode.l10n.t(`Library list delimited by comma`)}, - { name: `&LIBLS`, text: vscode.l10n.t(`Library list delimited by space`) } -]; - -export function getVariablesInfo(): VariableInfoList { - return { - member : [ - { name: `&OPENLIB`, text: vscode.l10n.t(`Library name where the source member lives (&OPENLIBL for lowercase)`)}, - { name: `&OPENSPF`, text: vscode.l10n.t(`Source file name where the source member lives (&OPENSPFL for lowercase)`)}, - { name: `&OPENMBR`, text: vscode.l10n.t(`Name of the source member (&OPENMBRL for lowercase)`)}, - { name: `&EXT`, text: vscode.l10n.t(`Extension of the source member (&EXTL for lowercase)`)}, - ...generic() - ], - streamFile: [ - { name: `&FULLPATH`, text: vscode.l10n.t(`Full path of the file on the remote system`)}, - { name: `&FILEDIR`, text: vscode.l10n.t(`Directory of the file on the remote system`)}, - { name: `&RELATIVEPATH`, text: vscode.l10n.t(`Relative path of the streamfile from the working directory or workspace`)}, - { name: `&PARENT`, text: vscode.l10n.t(`Name of the parent directory or source file`)}, - { name: `&BASENAME`, text: vscode.l10n.t(`Name of the file, including the extension`)}, - { name: `&NAME`, text: vscode.l10n.t(`Name of the file (&NAMEL for lowercase)`)}, - { name: `&EXT`, text: vscode.l10n.t(`Extension of the file (&EXTL for lowercase)`)}, - ...generic() - ], - object: [ - { name: `&LIBRARY`, text: vscode.l10n.t(`Library name where the object lives (&LIBRARYL for lowercase)`)}, - { name: `&NAME`, text: vscode.l10n.t(`Name of the object (&NAMEL for lowercase)`)}, - { name: `&TYPE`, text: vscode.l10n.t(`Type of the object (&TYPEL for lowercase)`)}, - { name: `&EXT`, text: vscode.l10n.t(`Extension/attribute of the object (&EXTL for lowercase)`)}, - ...generic() - ] - } -} \ No newline at end of file diff --git a/src/webviews/commandProfile/index.ts b/src/webviews/commandProfile/index.ts deleted file mode 100644 index 6b20e6808..000000000 --- a/src/webviews/commandProfile/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { commands, window } from "vscode"; - -import { CustomUI } from "../CustomUI"; -import { instance } from "../../instantiate"; -import IBMi from "../../api/IBMi"; -import { CommandProfile } from "../../typings"; - -export class CommandProfileUi { - static async show(currentName?: string) { - let config = instance.getConfig(); - const connection = instance.getConnection(); - - let currentSettings: CommandProfile = { - name: ``, - command: `` - }; - - if (currentName) { - const storedSettings = config?.commandProfiles.find(profile => profile.name === currentName); - if (storedSettings) { - currentSettings = storedSettings; - } - } - - const page = await new CustomUI() - .addParagraph(`Command Profiles can be used to set your library list based on the result of a command like CHGLIBL, or your own command that sets the library list. Commands should be as explicit as possible. When refering to commands and objects, both should be qualified with a library.`) - .addInput(`name`, `Name`, `Name of the Command Profile`, {default: currentSettings.name}) - .addInput(`command`, `Command`, `Command to be executed that will set the library list`, {default: currentSettings.command}) - .addButtons( - { id: `save`, label: `Save` }, - { id: `cancel`, label: `Cancel` } - ) - .loadPage(`Command Profile`); - - if (page && page.data) { - if (page.data.buttons !== `cancel`) { - if (page.data.name && page.data.command) { - if (currentName) { - const oldIndex = config?.commandProfiles.findIndex(profile => profile.name === currentName); - - if (oldIndex !== undefined) { - config!.commandProfiles[oldIndex] = page.data; - } else { - config!.commandProfiles.push(page.data); - } - } else { - config!.commandProfiles.push(page.data); - } - - await IBMi.connectionManager.update(config!); - commands.executeCommand(`code-for-ibmi.refreshProfileView`); - - } else { - // Bad name. Do nothing? - window.showWarningMessage(`A valid name and command is required for Command Profiles.`); - } - } - - page.panel.dispose(); - } - } -} \ No newline at end of file diff --git a/src/webviews/variables/index.ts b/src/webviews/variables/index.ts deleted file mode 100644 index 65b5e5802..000000000 --- a/src/webviews/variables/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -import vscode from "vscode"; -import IBMi from "../../api/IBMi"; -import { instance } from "../../instantiate"; -import { CustomVariable } from "../../typings"; -import { CustomUI } from "../CustomUI"; - -type VariablesListPage = { - value?: string - buttons?: "newVariable" -} - -type EditVariablePage = { - name: string - value: string - buttons?: "save" | "delete" -} - -export namespace VariablesUI { - export function initialize(context: vscode.ExtensionContext) { - context.subscriptions.push( - vscode.commands.registerCommand(`code-for-ibmi.showVariableMaintenance`, openVariablesList) - ) - } - - async function openVariablesList() { - const config = instance.getConnection()?.getConfig(); - if (config) { - const variables = config.customVariables; - - const ui = new CustomUI() - .addTree(`variable`, `Work with Variables`, [ - ...variables.map((variable, index) => ({ - label: `&${variable.name}: '${variable.value}'`, - value: String(index) - })).sort((a, b) => a.label.localeCompare(b.label)) - ], `Create or maintain custom variables. Custom variables can be used in any Action in this connection.`) - .addButtons({ id: `newVariable`, label: `New Variable` }); - - const page = await ui.loadPage(`Work with Variables`); - if (page && page.data) { - page.panel.dispose(); - - const data = page.data; - switch (data.buttons) { - case `newVariable`: - editVariable(); - break; - default: - editVariable(Number(data.value)); - break; - } - } - } - } - - async function editVariable(id?: number) { - const config = instance.getConnection()?.getConfig(); - if (config) { - const allVariables = config.customVariables; - const currentVariable: CustomVariable = id !== undefined ? allVariables[id] : { name: ``, value: `` }; - - const ui = new CustomUI() - .addInput(`name`, `Variable name`, `& not required. Will be forced uppercase.`, { default: currentVariable.name }) - .addInput(`value`, `Variable value`, ``, { default: currentVariable.value }) - .addButtons({ id: `save`, label: `Save` }, { id: `delete`, label: `Delete` }); - - const page = await ui.loadPage(`Work with Variable`); - if (page && page.data) { - page.panel.dispose(); - - const data = page.data; - switch (data.buttons) { - case `delete`: - if (id !== undefined) { - allVariables.splice(id, 1); - config.customVariables = allVariables; - await IBMi.connectionManager.update(config); - } - break; - - case "save": - default: - data.name = data.name.replace(/ /g, '_') - .replace(/&/g, '') - .toUpperCase(); - - const newAction = { ...data }; - if (id !== undefined) { - allVariables[id] = newAction; - } else { - allVariables.push(newAction); - } - - config.customVariables = allVariables; - await IBMi.connectionManager.update(config); - break; - } - } - - openVariablesList(); - } - } - -} \ No newline at end of file