From ad5eadcc93611e3cebf022d7eee1fa215d42dd25 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 20 Oct 2025 14:00:34 +0200 Subject: [PATCH 01/38] Initialized context view work Signed-off-by: Seb Julliand --- package.json | 44 +++++++++++------ src/extension.ts | 7 +-- src/instantiate.ts | 5 +- .../views/{ProfilesView.ts => contextView.ts} | 47 +++++++++++-------- 4 files changed, 61 insertions(+), 42 deletions(-) rename src/ui/views/{ProfilesView.ts => contextView.ts} (90%) diff --git a/package.json b/package.json index 1055f9dc0..e63051cb6 100644 --- a/package.json +++ b/package.json @@ -1599,7 +1599,7 @@ }, { "command": "code-for-ibmi.openTerminalHere", - "enablement": "code-for-ibmi:connected && !code-for-ibmi:isSystemReadonly", + "enablement": "code-for-ibmi:connected && !code-for-ibmi:isSystemReadonly", "title": "Open Terminal Here", "category": "IBM i" }, @@ -1708,6 +1708,13 @@ "category": "IBM i", "icon": "$(plus)", "enablement": "code-for-ibmi:connected && !code-for-ibmi:isReadonly" + }, + { + "command": "code-for-ibmi.context.refresh", + "enablement": "code-for-ibmi:connected", + "title": "Refresh", + "category": "IBM i", + "icon": "$(refresh)" } ], "keybindings": [ @@ -1816,9 +1823,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": "contextView", + "name": "Context", + "when": "code-for-ibmi:connected && !(code-for-ibmi:profilesViewDisabled || code-for-ibmi:contextViewDisabled)", "visibility": "collapsed" }, { @@ -2367,6 +2374,10 @@ { "command": "code-for-ibmi.generateBinderSource", "when": "never" + }, + { + "command": "code-for-ibmi.context.refresh", + "when": "never" } ], "view/title": [ @@ -2408,12 +2419,12 @@ { "command": "code-for-ibmi.newConnectionProfile", "group": "navigation@profile", - "when": "view == profilesView" + "when": "view == contextView" }, { "command": "code-for-ibmi.manageCommandProfile", "group": "navigation@profile", - "when": "view == profilesView" + "when": "view == contextView" }, { "command": "code-for-ibmi.createFilter", @@ -2509,6 +2520,11 @@ "command": "code-for-ibmi.term5250.resetPosition", "group": "navigation", "when": "code-for-ibmi:term5250Halted" + }, + { + "command": "code-for-ibmi.context.refresh", + "when": "view === contextView", + "group": "navigation@99" } ], "editor/title": [ @@ -2591,17 +2607,17 @@ }, { "command": "code-for-ibmi.loadConnectionProfile", - "when": "view == profilesView && viewItem == profile", + "when": "view === contextView && viewItem == profile", "group": "inline" }, { "command": "code-for-ibmi.loadCommandProfile", - "when": "view == profilesView && viewItem == commandProfile", + "when": "view === contextView && viewItem == commandProfile", "group": "inline" }, { "command": "code-for-ibmi.setToDefault", - "when": "view == profilesView && viewItem == resetProfile", + "when": "view === contextView && viewItem == resetProfile", "group": "inline" }, { @@ -2631,22 +2647,22 @@ }, { "command": "code-for-ibmi.saveConnectionProfile", - "when": "view == profilesView && viewItem == profile", + "when": "view === contextView && viewItem == profile", "group": "profiles@1" }, { "command": "code-for-ibmi.manageCommandProfile", - "when": "view == profilesView && viewItem == commandProfile", + "when": "view === contextView && viewItem == commandProfile", "group": "profiles@1" }, { "command": "code-for-ibmi.deleteConnectionProfile", - "when": "view == profilesView && viewItem == profile", + "when": "view === contextView && viewItem == profile", "group": "profiles@2" }, { "command": "code-for-ibmi.deleteCommandProfile", - "when": "view == profilesView && viewItem == commandProfile", + "when": "view === contextView && viewItem == commandProfile", "group": "profiles@2" }, { @@ -3052,4 +3068,4 @@ "halcyontechltd.vscode-ibmi-walkthroughs", "vscode.git" ] -} +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index f9545e6e5..c9f3c4e4a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,7 +26,7 @@ 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 { initializeContextView } from "./ui/views/contextView"; import { initializeDebugBrowser } from "./ui/views/debugView"; import { HelpView } from "./ui/views/helpView"; import { initializeIFSBrowser } from "./ui/views/ifsBrowser"; @@ -61,16 +61,13 @@ export async function activate(context: ExtensionContext): Promise initializeDebugBrowser(context); initializeSearchView(context); initializeLibraryListView(context); + initializeContextView(context); context.subscriptions.push( window.registerTreeDataProvider( `helpView`, new HelpView(context) ), - window.registerTreeDataProvider( - `profilesView`, - new ProfilesView(context) - ), onCodeForIBMiConfigurationChange("connections", updateLastConnectionAndServerCache), onCodeForIBMiConfigurationChange("connectionSettings", async () => { diff --git a/src/instantiate.ts b/src/instantiate.ts index d374194ae..6de9c7463 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'; @@ -14,7 +15,6 @@ 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; @@ -144,9 +144,6 @@ async function onConnected() { ].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/ui/views/ProfilesView.ts b/src/ui/views/contextView.ts similarity index 90% rename from src/ui/views/ProfilesView.ts rename to src/ui/views/contextView.ts index da721961d..501125e58 100644 --- a/src/ui/views/ProfilesView.ts +++ b/src/ui/views/contextView.ts @@ -1,14 +1,27 @@ import vscode, { l10n, window } from 'vscode'; import { GetNewLibl } from '../../api/components/getNewLibl'; +import IBMi from '../../api/IBMi'; import { instance } from '../../instantiate'; -import { ConnectionProfile, Profile } from '../../typings'; +import { BrowserItem, 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; +export function initializeContextView(context: vscode.ExtensionContext) { + const contextView = new ContextView(context); + const contextTreeViewer = vscode.window.createTreeView( + `contextView`, { + treeDataProvider: contextView, + showCollapseAll: true + }); + + context.subscriptions.push( + contextTreeViewer, + vscode.commands.registerCommand("code-for-ibmi.context.refresh", () => contextView.refresh()) + ); +} +class ContextView implements vscode.TreeDataProvider { + private readonly emitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.emitter.event; constructor(context: vscode.ExtensionContext) { context.subscriptions.push( @@ -189,19 +202,15 @@ export class ProfilesView { ) } - 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); - } + refresh(target?: BrowserItem) { + this.emitter.fire(target); } - getTreeItem(element: vscode.TreeItem): vscode.TreeItem { + getTreeItem(element: BrowserItem): vscode.TreeItem { return element; } - async getChildren(): Promise { + async getChildren() { const connection = instance.getConnection(); if (connection) { @@ -266,10 +275,10 @@ function cloneProfile(fromProfile: ConnectionProfile, newName: string): Connecti } } -class ProfileItem extends vscode.TreeItem implements Profile { +class ProfileItem extends BrowserItem implements Profile { readonly profile; constructor(name: string, active: boolean) { - super(name, vscode.TreeItemCollapsibleState.None); + super(name); this.contextValue = `profile`; this.iconPath = new vscode.ThemeIcon(active ? `layers-active` : `layers`); @@ -280,10 +289,10 @@ class ProfileItem extends vscode.TreeItem implements Profile { } } -class CommandProfileItem extends vscode.TreeItem implements Profile { +class CommandProfileItem extends BrowserItem implements Profile { readonly profile; constructor(name: string, active: boolean) { - super(name, vscode.TreeItemCollapsibleState.None); + super(name); this.contextValue = `commandProfile`; this.iconPath = new vscode.ThemeIcon(active ? `layers-active` : `console`); @@ -294,10 +303,10 @@ class CommandProfileItem extends vscode.TreeItem implements Profile { } } -class ResetProfileItem extends vscode.TreeItem implements Profile { +class ResetProfileItem extends BrowserItem implements Profile { readonly profile; constructor() { - super(`Reset to Default`, vscode.TreeItemCollapsibleState.None); + super(`Reset to Default`); this.contextValue = `resetProfile`; this.iconPath = new vscode.ThemeIcon(`debug-restart`); From 941a4f8baabb5d37576902a5ce379f281d10bfc5 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 23 Oct 2025 22:19:44 +0200 Subject: [PATCH 02/38] Added CustomEditor Signed-off-by: Seb Julliand --- package.json | 11 ++ src/editors/customEditorProvider.ts | 113 +++++++++++ src/extension.ts | 12 +- src/typings.ts | 12 +- src/webviews/CustomUI.ts | 294 ++++++++++++++-------------- 5 files changed, 293 insertions(+), 149 deletions(-) create mode 100644 src/editors/customEditorProvider.ts diff --git a/package.json b/package.json index e63051cb6..29017a182 100644 --- a/package.json +++ b/package.json @@ -1717,6 +1717,17 @@ "icon": "$(refresh)" } ], + "customEditors": [ + { + "viewType": "code-for-ibmi.editor", + "displayName": "Code for i editor", + "selector": [ + { + "filenamePattern": "code4i:*/**/*" + } + ] + } + ], "keybindings": [ { "command": "code-for-ibmi.runAction", diff --git a/src/editors/customEditorProvider.ts b/src/editors/customEditorProvider.ts new file mode 100644 index 000000000..c7a32a8bf --- /dev/null +++ b/src/editors/customEditorProvider.ts @@ -0,0 +1,113 @@ +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; + + constructor(target: string, private readonly onSave: (data: T) => Promise) { + 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.valid = data.valid; + delete data.valid; + this.data = data; + } + + async save() { + await this.onSave(this.data); + } + + dispose(): void { + //nothing to dispose of + } +} + +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.valid) { + await document.save(); + } + 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 c9f3c4e4a..b90a0ba66 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ 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"; @@ -82,7 +83,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); @@ -126,7 +132,9 @@ export async function activate(context: ExtensionContext): Promise ); return { - instance, customUI: () => new CustomUI(), + instance, + customUI: () => new CustomUI(), + customEditor: (target, onSave) => new CustomEditor(target, onSave), deployTools: DeployTools, evfeventParser: parseErrors, tools: VscodeTools, diff --git a/src/typings.ts b/src/typings.ts index 41de6df4a..7e963faa3 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) => CustomEditor, deployTools: typeof DeployTools, evfeventParser: (lines: string[]) => Map, tools: typeof VscodeTools, @@ -24,4 +26,4 @@ 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/webviews/CustomUI.ts b/src/webviews/CustomUI.ts index b7246026b..c2e532d59 100644 --- a/src/webviews/CustomUI.ts +++ b/src/webviews/CustomUI.ts @@ -152,90 +152,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 +271,7 @@ export class CustomUI extends Section { } } validateInputs(response.field); - } + } } }); @@ -398,27 +327,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 +349,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 +374,7 @@ export class CustomUI extends Section { });` )} }); - + ${this.getSpecificScript()} }()) @@ -515,6 +382,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 { From a978131774f36dd92866e67a032a4f4ee12b4c63 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 25 Oct 2025 18:51:17 +0200 Subject: [PATCH 03/38] Made resfresh() async Signed-off-by: Seb Julliand --- src/ui/types.ts | 4 ++-- src/ui/views/ifsBrowser.ts | 4 ++-- src/ui/views/objectBrowser.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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/ifsBrowser.ts b/src/ui/views/ifsBrowser.ts index b5290ff63..98b5e0af4 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 f652c2df9..49b935a30 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); } From 626817c84c851a65dad04452d298425697f85136 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 25 Oct 2025 19:38:15 +0200 Subject: [PATCH 04/38] Actions management overhaul Signed-off-by: Seb Julliand --- package.json | 219 +++--- src/api/actions.ts | 81 +++ .../configuration/config/ConnectionManager.ts | 3 +- src/api/configuration/config/types.ts | 9 +- src/editors/actionEditor.ts | 188 +++++ src/extension.ts | 31 +- src/filesystems/local/actions.ts | 54 -- src/filesystems/local/deployment.ts | 6 +- src/instantiate.ts | 7 +- src/ui/actions.ts | 9 +- src/ui/diagnostics.ts | 9 +- src/ui/views/ConnectionBrowser.ts | 6 +- src/ui/views/contextView.ts | 641 ++++++++++++------ src/ui/views/debugView.ts | 4 +- src/webviews/CustomUI.ts | 5 +- src/webviews/actions/index.ts | 326 --------- src/webviews/actions/varinfo.ts | 52 -- src/webviews/commandProfile/index.ts | 62 -- 18 files changed, 892 insertions(+), 820 deletions(-) create mode 100644 src/api/actions.ts create mode 100644 src/editors/actionEditor.ts delete mode 100644 src/filesystems/local/actions.ts delete mode 100644 src/webviews/actions/index.ts delete mode 100644 src/webviews/actions/varinfo.ts delete mode 100644 src/webviews/commandProfile/index.ts diff --git a/package.json b/package.json index 29017a182..5b9531de7 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": { @@ -896,27 +876,6 @@ "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", @@ -1165,7 +1124,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", @@ -1310,12 +1269,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 +1346,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 +1483,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", @@ -1715,6 +1663,58 @@ "title": "Refresh", "category": "IBM i", "icon": "$(refresh)" + }, + { + "command": "code-for-ibmi.context.action.search", + "enablement": "code-for-ibmi:connected", + "title": "Search action", + "category": "IBM i", + "icon": "$(search)" + }, + { + "command": "code-for-ibmi.context.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.context.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.context.action.create", + "enablement": "code-for-ibmi:connected", + "title": "Create action", + "category": "IBM i", + "icon": "$(add)" + }, + { + "command": "code-for-ibmi.context.action.rename", + "enablement": "code-for-ibmi:connected", + "title": "Rename...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.action.copy", + "enablement": "code-for-ibmi:connected", + "title": "Copy...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.action.delete", + "enablement": "code-for-ibmi:connected", + "title": "Delete...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.action.runOnEditor", + "title": "Run on active editor", + "category": "IBM i", + "icon": "$(debug-start)" } ], "customEditors": [ @@ -2210,10 +2210,6 @@ "command": "code-for-ibmi.moveObject", "when": "never" }, - { - "command": "code-for-ibmi.revealInObjectBrowser", - "when": "never" - }, { "command": "code-for-ibmi.refreshObjectBrowserItem", "when": "never" @@ -2222,10 +2218,6 @@ "command": "code-for-ibmi.refreshIFSBrowser", "when": "never" }, - { - "command": "code-for-ibmi.revealInIFSBrowser", - "when": "never" - }, { "command": "code-for-ibmi.deleteIFS", "when": "never" @@ -2389,6 +2381,38 @@ { "command": "code-for-ibmi.context.refresh", "when": "never" + }, + { + "command": "code-for-ibmi.context.action.search", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.search.next", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.search.clear", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.create", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.rename", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.copy", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.delete", + "when": "never" + }, + { + "command": "code-for-ibmi.context.action.runOnEditor", + "when": "never" } ], "view/title": [ @@ -2432,11 +2456,6 @@ "group": "navigation@profile", "when": "view == contextView" }, - { - "command": "code-for-ibmi.manageCommandProfile", - "group": "navigation@profile", - "when": "view == contextView" - }, { "command": "code-for-ibmi.createFilter", "group": "navigation@1", @@ -2621,16 +2640,36 @@ "when": "view === contextView && viewItem == profile", "group": "inline" }, - { - "command": "code-for-ibmi.loadCommandProfile", - "when": "view === contextView && viewItem == commandProfile", - "group": "inline" - }, { "command": "code-for-ibmi.setToDefault", "when": "view === contextView && viewItem == resetProfile", "group": "inline" }, + { + "command": "code-for-ibmi.context.action.create", + "when": "view === contextView && viewItem =~ /^(actionsNode|actionTypeNode)/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.launchActionsSetup", + "when": "view === contextView && viewItem =~ /^actionsNode/", + "group": "inline@02" + }, + { + "command": "code-for-ibmi.context.action.search", + "when": "view === contextView && viewItem === actionsNode", + "group": "inline@10" + }, + { + "command": "code-for-ibmi.context.action.search.next", + "when": "view === contextView && viewItem === actionsNode", + "group": "inline@11" + }, + { + "command": "code-for-ibmi.context.action.search.clear", + "when": "view === contextView && viewItem === actionsNode", + "group": "inline@12" + }, { "command": "code-for-ibmi.createMember", "when": "view == objectBrowser && viewItem == SPF", @@ -2661,21 +2700,11 @@ "when": "view === contextView && viewItem == profile", "group": "profiles@1" }, - { - "command": "code-for-ibmi.manageCommandProfile", - "when": "view === contextView && viewItem == commandProfile", - "group": "profiles@1" - }, { "command": "code-for-ibmi.deleteConnectionProfile", "when": "view === contextView && viewItem == profile", "group": "profiles@2" }, - { - "command": "code-for-ibmi.deleteCommandProfile", - "when": "view === contextView && viewItem == commandProfile", - "group": "profiles@2" - }, { "command": "code-for-ibmi.maintainFilter", "when": "view == objectBrowser && viewItem =~ /^filter.*$/", @@ -3005,6 +3034,26 @@ "command": "code-for-ibmi.generateBinderSource", "when": "view == objectBrowser && viewItem =~ /^object.(module|srvpgm).*/", "group": "1_objActions@6" + }, + { + "command": "code-for-ibmi.context.action.runOnEditor", + "when": "view === contextView && ((viewItem =~ /^actionItemRemote/ && code-for-ibmi:editorCanRunRemoteAction) || (viewItem =~ /^actionItemLocal/ && code-for-ibmi:editorCanRunLocalAction))", + "group": "inline@00" + }, + { + "command": "code-for-ibmi.context.action.rename", + "when": "view === contextView && viewItem =~ /^actionItem/", + "group": "00_actionItemAction01" + }, + { + "command": "code-for-ibmi.context.action.copy", + "when": "view === contextView && viewItem =~ /^actionItem/", + "group": "10_actionItemAction01" + }, + { + "command": "code-for-ibmi.context.action.delete", + "when": "view === contextView && viewItem =~ /^actionItem/", + "group": "20_actionItemAction01" } ], "explorer/context": [ diff --git a/src/api/actions.ts b/src/api/actions.ts new file mode 100644 index 000000000..6b1f2f684 --- /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 saveAction(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) && + 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..a3478aa33 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; @@ -64,11 +63,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/editors/actionEditor.ts b/src/editors/actionEditor.ts new file mode 100644 index 000000000..2d2a502ed --- /dev/null +++ b/src/editors/actionEditor.ts @@ -0,0 +1,188 @@ +import vscode from "vscode"; +import { saveAction } from "../api/actions"; +import { Tools } from "../api/Tools"; +import { instance } from "../instantiate"; +import { Action, ActionEnvironment, ActionRefresh, ActionType } from "../typings"; +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 +} + +export function editAction(targetAction: Action, doAfterSave?: () => Thenable, workspace?: vscode.WorkspaceFolder) { + const customVariables = instance.getConnection()?.getConfig().customVariables.map(variable => `
  • &${variable.name}: ${variable.value}
  • `).join(``); + new CustomEditor(`${targetAction.name}.action`, (actionData) => save(targetAction, actionData, workspace).then(doAfterSave)) + .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(); +} + +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 saveAction(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/extension.ts b/src/extension.ts index b90a0ba66..d83496e19 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ // The module 'vscode' contains the VS Code extensibility API -import { commands, ExtensionContext, languages, window, workspace } from "vscode"; +import { commands, ExtensionContext, l10n, languages, window, workspace } from "vscode"; // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -131,6 +131,8 @@ export async function activate(context: ExtensionContext): Promise openURIHandler ); + await mergeCommandProfiles(); + return { instance, customUI: () => new CustomUI(), @@ -146,3 +148,30 @@ export async function activate(context: ExtensionContext): Promise export async function deactivate() { await commands.executeCommand(`code-for-ibmi.disconnect`, true); } + +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/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/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 6de9c7463..7cbfde1e1 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -13,7 +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"; export let instance: Instance; @@ -84,7 +83,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); @@ -108,7 +106,7 @@ async function updateConnectedBar() { const remoteConnectionConfig = connection.getConfigFile(`settings`); const serverConfigOk = remoteConnectionConfig.getState().server === `ok`; - let serverConfig: RemoteConfigFile|undefined; + let serverConfig: RemoteConfigFile | undefined; if (serverConfigOk) { serverConfig = await remoteConnectionConfig.get(); } @@ -116,12 +114,10 @@ async function updateConnectedBar() { const systemReadOnly = serverConfig?.codefori?.readOnlyMode || false; connectedBarItem.text = `$(${systemReadOnly ? "shield" : (config.readOnlyMode ? "lock" : "settings-gear")}) ${config.name}`; const terminalMenuItem = systemReadOnly ? `` : `[$(terminal) Terminals](command:code-for-ibmi.launchTerminalPicker)`; - const actionsMenuItem = systemReadOnly ? `` : `[$(file-binary) Actions](command:code-for-ibmi.showActionsMaintenance)`; 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, debugPTFInstalled(connection) ? `[$(${debugRunning ? "bug" : "debug"}) Debugger ${((await getDebugServiceDetails(connection)).version)} (${debugRunning ? "on" : "off"})](command:ibmiDebugBrowser.focus)` @@ -137,7 +133,6 @@ async function updateConnectedBar() { } async function onConnected() { - const config = instance.getConnection()?.getConfig(); [ connectedBarItem, disconnectBarItem, diff --git a/src/ui/actions.ts b/src/ui/actions.ts index 5edf6664c..c2d55058c 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'; @@ -14,6 +13,7 @@ import { Action, DeploymentMethod } from '../typings'; import { CustomUI, TreeListItem } from '../webviews/CustomUI'; import { EvfEventInfo, refreshDiagnosticsFromLocal, refreshDiagnosticsFromServer, registerDiagnostics } from './diagnostics'; +import { getActions } from '../api/actions'; import { BrowserItem } from './types'; type CommandObject = { @@ -542,7 +542,8 @@ export async function runAction(instance: Instance, uris: vscode.Uri | vscode.Ur const resultsPanel = new CustomUI(); if (targets.length === 1) { resultsPanel.addParagraph(`
    ${targets[0].output.join("")}
    `) - .setOptions({ fullPage: true , + .setOptions({ + fullPage: true, css: /* css */ ` pre{ background-color: transparent; @@ -587,7 +588,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. @@ -599,7 +600,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/views/ConnectionBrowser.ts b/src/ui/views/ConnectionBrowser.ts index 8247c2110..ffc6db91c 100644 --- a/src/ui/views/ConnectionBrowser.ts +++ b/src/ui/views/ConnectionBrowser.ts @@ -1,10 +1,10 @@ import vscode from 'vscode'; import { ConnectionConfig, ConnectionData, Server } from '../../typings'; -import { instance } from '../../instantiate'; -import { Login } from '../../webviews/login'; import IBMi from '../../api/IBMi'; import { deleteStoredPassword, getStoredPassword, setStoredPassword } from '../../config/passwords'; +import { instance } from '../../instantiate'; +import { Login } from '../../webviews/login'; type CopyOperationItem = { label: string @@ -199,7 +199,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 } ], { @@ -222,7 +221,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/contextView.ts b/src/ui/views/contextView.ts index 501125e58..789d728c0 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -1,13 +1,15 @@ import vscode, { l10n, window } from 'vscode'; +import { getActions, saveAction } from '../../api/actions'; import { GetNewLibl } from '../../api/components/getNewLibl'; import IBMi from '../../api/IBMi'; +import { editAction } from '../../editors/actionEditor'; import { instance } from '../../instantiate'; -import { BrowserItem, ConnectionProfile, Profile } from '../../typings'; -import { CommandProfileUi } from '../../webviews/commandProfile'; +import { Action, ActionEnvironment, ActionType, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions, Profile } from '../../typings'; +import { uriToActionTarget } from '../actions'; export function initializeContextView(context: vscode.ExtensionContext) { - const contextView = new ContextView(context); + const contextView = new ContextView(); const contextTreeViewer = vscode.window.createTreeView( `contextView`, { treeDataProvider: contextView, @@ -16,191 +18,299 @@ export function initializeContextView(context: vscode.ExtensionContext) { context.subscriptions.push( contextTreeViewer, - vscode.commands.registerCommand("code-for-ibmi.context.refresh", () => contextView.refresh()) - ); -} -class ContextView implements vscode.TreeDataProvider { - private readonly emitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this.emitter.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) => { + vscode.window.onDidChangeActiveTextEditor(async editor => { + let editorCanRunAction = false; + let editorCanRunLocalAction = false; + if (editor) { 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); - } + if (connection) { + const uri = editor.document.uri; + if (uri) { + editorCanRunAction = ['streamfile', 'member', 'object'].includes(uri.scheme); + editorCanRunLocalAction = uri.scheme === 'file'; + } + } + } + vscode.commands.executeCommand(`setContext`, "code-for-ibmi:editorCanRunRemoteAction", editorCanRunAction); + vscode.commands.executeCommand(`setContext`, "code-for-ibmi:editorCanRunLocalAction", editorCanRunLocalAction); + }), + vscode.window.registerFileDecorationProvider({ + provideFileDecoration(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { + if (uri.scheme === ProfileItem.contextValue && uri.query === "active") { + return { color: new vscode.ThemeColor(ProfileItem.activeColor) }; + } + else if (uri.scheme === ActionItem.contextValue && uri.query === "matched") { + return { color: new vscode.ThemeColor(ActionItem.matchedColor) }; + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.refresh", () => contextView.refresh()), + vscode.commands.registerCommand("code-for-ibmi.context.refresh.item", (item: BrowserItem) => contextView.refresh(item)), + vscode.commands.registerCommand("code-for-ibmi.context.reveal", (item: BrowserItem, options?: FocusOptions) => contextTreeViewer.reveal(item, options)), + + 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.context.action.search", (node: ActionsNode) => node.searchActions()), + vscode.commands.registerCommand("code-for-ibmi.context.action.search.next", (node: ActionsNode) => node.goToNextSearchMatch()), + vscode.commands.registerCommand("code-for-ibmi.context.action.search.clear", (node: ActionsNode) => node.clearSearch()), + vscode.commands.registerCommand("code-for-ibmi.context.action.create", async (node: ActionsNode | ActionTypeNode) => { + const typeNode = "type" in node ? node : (await vscode.window.showQuickPick(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: l10n.t("Enter new action name"), + placeHolder: l10n.t("action name..."), + validateInput: (newName) => existingNames.includes(newName) ? l10n.t("This name is already used by another action") : undefined + }); + + if (name) { + const action = { + name, + type: typeNode.type, + environment: "ile" as ActionEnvironment, + command: '' + }; + await saveAction(action, typeNode.workspace); + contextView.refresh(); + vscode.commands.executeCommand("code-for-ibmi.context.action.edit", { action, workspace: typeNode.workspace }); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.action.rename", async (node: ActionItem) => { + const action = node.action; + 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"), + value: action.name, + validateInput: (newName) => existingNames.includes(newName) ? l10n.t("This name is already used by another action") : undefined + }); - await Promise.all([ - IBMi.connectionManager.update(config), - storage.setLastProfile(savedProfileName) - ]); - this.refresh(); + if (newName) { + await saveAction(action, node.workspace, { newName }); + contextView.refresh(); + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.action.edit", (node: ActionItem) => { + editAction(node.action, async () => contextView.refresh(), node.workspace); + }), + vscode.commands.registerCommand("code-for-ibmi.context.action.copy", async (node: ActionItem) => { + const action = node.action; + const existingNames = (await getActions(node.workspace)).map(act => act.name); + + const copyName = await vscode.window.showInputBox({ + title: l10n.t("Copy action '{0}'", action.name), + placeHolder: l10n.t("new action name..."), + validateInput: (newName) => existingNames.includes(newName) ? l10n.t("This name is already used by another action") : undefined + }); - vscode.window.showInformationMessage(l10n.t(`Saved current settings to profile "{0}".`, savedProfileName)); - } + if (copyName) { + const newCopyAction = { ...action, name: copyName } as Action; + await saveAction(newCopyAction, node.workspace); + contextView.refresh(); + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.action.delete", async (node: ActionItem) => { + if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete action '{0}' ?", node.action.name), { modal: true }, l10n.t("Yes"))) { + await saveAction(node.action, node.workspace, { delete: true }); + contextView.refresh(); + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.action.runOnEditor", (node: ActionItem) => { + const uri = vscode.window.activeTextEditor?.document.uri; + if (uri) { + const editAction = () => vscode.commands.executeCommand("code-for-ibmi.context.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; } - }), - 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! - } - }) - } + 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; } - }), - 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); + 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; + } - 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.commands.executeCommand(`code-for-ibmi.runAction`, uri, undefined, action, undefined, workspace); + } + }), - vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, chosenProfile.name)); - this.refresh(); + 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); } - } - }), - vscode.commands.registerCommand(`code-for-ibmi.manageCommandProfile`, async (commandProfile?: CommandProfileItem) => { - CommandProfileUi.show(commandProfile ? commandProfile.profile : undefined); - }), + await Promise.all([ + IBMi.connectionManager.update(config), + storage.setLastProfile(savedProfileName) + ]); + contextView.refresh(); - 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.window.showInformationMessage(l10n.t(`Saved current settings to profile "{0}".`, savedProfileName)); } - }), - - 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.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) + contextView.refresh(); + // TODO: Add message about deleted profile! } - } + }) } - }), + } + }), - 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); + 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)); + contextView.refresh(); + } + } + }), + vscode.commands.registerCommand(`code-for-ibmi.loadCommandProfile`, async (commandProfile?: any) => { + //TODO + const connection = instance.getConnection(); + const storage = instance.getStorage(); + if (commandProfile && connection && storage) { + const config = connection.getConfig(); + const storedProfile = config.connectionProfiles.find(profile => profile.name === commandProfile.profile); + + if (storedProfile && storedProfile.setLibraryListCommand) { + try { + const component = connection?.getComponent(GetNewLibl.ID) + const newSettings = await component?.getLibraryListFromCommand(connection, storedProfile.setLibraryListCommand); + + 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.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), - vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`), - storage.setLastProfile(defaultName) ]); + + vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, storedProfile.name)); + contextView.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) + ]); + } + }) + } + }) + + ) +} + +class ContextIem extends BrowserItem { + async refresh() { + await vscode.commands.executeCommand("code-for-ibmi.context.refresh.item", this); + } - ) + reveal(options?: FocusOptions) { + return vscode.commands.executeCommand(`code-for-ibmi.context.reveal`, this, options); } +} + +class ContextView implements vscode.TreeDataProvider { + private readonly emitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.emitter.event; refresh(target?: BrowserItem) { this.emitter.fire(target); @@ -210,27 +320,166 @@ class ContextView implements vscode.TreeDataProvider { return element; } - async getChildren() { - const connection = instance.getConnection(); + getParent(element: BrowserItem) { + return element?.parent; + } - 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)), - ] + async getChildren(item?: BrowserItem) { + if (item) { + return item.getChildren?.(); + } + else { + const sortActions = (a1: Action, a2: Action) => a1.name.localeCompare(a2.name); + + const actions = (await getActions()).sort(sortActions); + const localActions = new Map(); + for (const workspace of vscode.workspace.workspaceFolders || []) { + localActions.set(workspace, (await getActions(workspace)).sort(sortActions)); + } + + return [ + new ActionsNode(actions, localActions), + new CustomVariablesNode(), + new ProfilesNode() + ]; + } + } +} + +class ActionsNode extends ContextIem { + private readonly foundActions: ActionItem[] = []; + private revealIndex = -1; + + private readonly children; + + constructor(actions: Action[], localActions: Map) { + super(l10n.t("Actions"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = "actionsNode"; + this.children = [ + new ActionTypeNode(this, l10n.t("Member"), 'member', actions), + new ActionTypeNode(this, l10n.t("Object"), 'object', actions), + new ActionTypeNode(this, l10n.t("Streamfile"), 'streamfile', actions), + ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'file', localActions, workspace))) + ] + } + + getChildren() { + return this.children; + } + + getAllActionItems() { + return this.children.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(...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(true)); + this.refresh(); + this.goToNextSearchMatch(); } } + } + + goToNextSearchMatch() { + this.revealIndex += (this.revealIndex + 1) < this.foundActions.length ? 1 : -this.revealIndex; + const actionNode = this.foundActions[this.revealIndex]; + actionNode.reveal({ focus: true }); + } - return []; + async clearSearch() { + this.getAllActionItems().forEach(node => node.setContext(false)); + this.revealIndex = -1; + this.foundActions.splice(0, this.foundActions.length); + await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasActionSearched`, false); + await this.refresh(); + } +} + +class ActionTypeNode extends ContextIem { + readonly actionItems: ActionItem[]; + constructor(parent: BrowserItem, label: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { + super(label, { parent, state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `actionTypeNode_${type}`; + this.description = workspace ? l10n.t("workspace actions") : undefined; + this.actionItems = actions.filter(action => action.type === type).map(action => new ActionItem(this, action, workspace)); + } + + getChildren() { + return this.actionItems; + } +} + +class ActionItem extends ContextIem { + static matchedColor = "charts.yellow"; + static contextValue = `actionItem`; + + constructor(parent: BrowserItem, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { + super(action.name, { parent }); + this.setContext(); + this.command = { + title: "Edit action", + command: "code-for-ibmi.context.action.edit", + arguments: [this] + } + } + + setContext(matched?: boolean) { + this.contextValue = `${ActionItem.contextValue}${this.workspace ? "Local" : "Remote"}${matched ? '_matched' : ''}`; + this.iconPath = new vscode.ThemeIcon("github-action", matched ? new vscode.ThemeColor(ActionItem.matchedColor) : undefined); + this.resourceUri = vscode.Uri.from({ scheme: ActionItem.contextValue, authority: this.action.name, query: matched ? "matched" : "" }); + this.description = matched ? l10n.t("search match") : undefined; + this.tooltip = this.action.command; + } +} + +class ProfilesNode extends ContextIem { + constructor() { + super(l10n.t("Profiles"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = "profilesNode"; + } + + getChildren() { + const currentProfile = instance.getStorage()?.getLastProfile(); + return instance.getConnection()?.getConfig().connectionProfiles + .sort((p1, p2) => p1.name.localeCompare(p2.name)) + .map(profile => new ProfileItem(this, profile, profile.name === currentProfile)); + } +} + +class ProfileItem extends ContextIem { + static contextValue = `profileItem`; + static activeColor = "charts.green"; + + constructor(parent: BrowserItem, readonly profile: ConnectionProfile, active: boolean) { + super(profile.name, { parent, icon: "person", color: active ? ProfileItem.activeColor : undefined }); + + this.contextValue = `${ProfileItem.contextValue}${active ? '_active' : ''}`; + this.description = active ? l10n.t(`Active`) : ``; + this.resourceUri = vscode.Uri.from({ scheme: this.contextValue, authority: profile.name, query: active ? "active" : "" }); + } +} + +class CustomVariablesNode extends ContextIem { + constructor() { + super(l10n.t("Custom Variables"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `customVariablesNode`; + } + + getChildren() { + return instance.getConnection()?.getConfig().customVariables.map(customVariable => new CustomVariableItem(this, customVariable)); + } +} + +class CustomVariableItem extends ContextIem { + constructor(parent: BrowserItem, readonly customVariable: CustomVariable) { + super(customVariable.name, { parent, icon: "symbol-variable" }); + this.contextValue = `customVariableItem`; + this.description = customVariable.value; } } @@ -275,33 +524,7 @@ function cloneProfile(fromProfile: ConnectionProfile, newName: string): Connecti } } -class ProfileItem extends BrowserItem implements Profile { - readonly profile; - constructor(name: string, active: boolean) { - super(name); - - this.contextValue = `profile`; - this.iconPath = new vscode.ThemeIcon(active ? `layers-active` : `layers`); - this.description = active ? `Active` : ``; - this.tooltip = ``; - - this.profile = name; - } -} - -class CommandProfileItem extends BrowserItem implements Profile { - readonly profile; - constructor(name: string, active: boolean) { - super(name); - this.contextValue = `commandProfile`; - this.iconPath = new vscode.ThemeIcon(active ? `layers-active` : `console`); - this.description = active ? `Active` : ``; - this.tooltip = ``; - - this.profile = name; - } -} class ResetProfileItem extends BrowserItem implements Profile { readonly profile; @@ -315,3 +538,11 @@ class ResetProfileItem extends BrowserItem implements Profile { this.profile = `Default`; } } + + + +/* saved for later +.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(`setLibraryListCommand`, `Library list command`, `Command to be executed that will set the library list`, {default: currentSettings.command}) + */ \ No newline at end of file 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/webviews/CustomUI.ts b/src/webviews/CustomUI.ts index c2e532d59..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; } @@ -679,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 From c502113c1efaeafe4602df9bafe54329bc96ca00 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 26 Oct 2025 00:29:29 +0200 Subject: [PATCH 05/38] Call new refresh context command Signed-off-by: Seb Julliand --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index d83496e19..0a4ff3d9e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -115,7 +115,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.context.refresh"); }); const customQsh = new CustomQSh(); From 3fcea1b3edd05d50b4cb80fa3db8dc9b3563ebc1 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 26 Oct 2025 00:41:41 +0200 Subject: [PATCH 06/38] Added custom variable management to context view Signed-off-by: Seb Julliand --- package.json | 83 +++++++++++++++++--- src/ui/views/contextView.ts | 129 +++++++++++++++++++++++++++++--- src/webviews/variables/index.ts | 104 ------------------------- 3 files changed, 189 insertions(+), 127 deletions(-) delete mode 100644 src/webviews/variables/index.ts diff --git a/package.json b/package.json index 5b9531de7..31411420d 100644 --- a/package.json +++ b/package.json @@ -1005,13 +1005,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", @@ -1715,6 +1708,37 @@ "title": "Run on active editor", "category": "IBM i", "icon": "$(debug-start)" + }, + { + "command": "code-for-ibmi.context.variable.declare", + "enablement": "code-for-ibmi:connected", + "title": "Declare custom variable", + "category": "IBM i", + "icon": "$(add)" + }, + { + "command": "code-for-ibmi.context.variable.edit", + "enablement": "code-for-ibmi:connected", + "title": "Change value...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.variable.rename", + "enablement": "code-for-ibmi:connected", + "title": "Rename...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.variable.copy", + "enablement": "code-for-ibmi:connected", + "title": "Copy...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.variable.delete", + "enablement": "code-for-ibmi:connected", + "title": "Delete...", + "category": "IBM i" } ], "customEditors": [ @@ -2413,6 +2437,26 @@ { "command": "code-for-ibmi.context.action.runOnEditor", "when": "never" + }, + { + "command": "code-for-ibmi.context.variable.declare", + "when": "never" + }, + { + "command": "code-for-ibmi.context.variable.edit", + "when": "never" + }, + { + "command": "code-for-ibmi.context.variable.rename", + "when": "never" + }, + { + "command": "code-for-ibmi.context.variable.copy", + "when": "never" + }, + { + "command": "code-for-ibmi.context.variable.delete", + "when": "never" } ], "view/title": [ @@ -2471,11 +2515,6 @@ "group": "navigation@4", "when": "view == objectBrowser" }, - { - "command": "code-for-ibmi.showVariableMaintenance", - "group": "", - "when": "view == libraryListView" - }, { "command": "code-for-ibmi.cleanupLibraryList", "group": "", @@ -3054,6 +3093,26 @@ "command": "code-for-ibmi.context.action.delete", "when": "view === contextView && viewItem =~ /^actionItem/", "group": "20_actionItemAction01" + }, + { + "command": "code-for-ibmi.context.variable.declare", + "when": "view === contextView && viewItem =~ /^customVariablesNode/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.context.variable.rename", + "when": "view === contextView && viewItem =~ /^customVariableItem/", + "group": "00_customVariableItemAction01" + }, + { + "command": "code-for-ibmi.context.variable.copy", + "when": "view === contextView && viewItem =~ /^customVariableItem/", + "group": "10_customVariableItemAction01" + }, + { + "command": "code-for-ibmi.context.variable.delete", + "when": "view === contextView && viewItem =~ /^customVariableItem/", + "group": "20_customVariableItemAction01" } ], "explorer/context": [ diff --git a/src/ui/views/contextView.ts b/src/ui/views/contextView.ts index 789d728c0..1a8faeac6 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -1,5 +1,5 @@ -import vscode, { l10n, window } from 'vscode'; +import vscode, { l10n } from 'vscode'; import { getActions, saveAction } from '../../api/actions'; import { GetNewLibl } from '../../api/components/getNewLibl'; import IBMi from '../../api/IBMi'; @@ -8,6 +8,56 @@ import { instance } from '../../instantiate'; import { Action, ActionEnvironment, ActionType, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions, Profile } from '../../typings'; import { uriToActionTarget } from '../actions'; +function validateActionName(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("This name is already used by another action"); + } +} + +function getCustomVariables() { + return instance.getConnection()?.getConfig().customVariables || []; +} + +function sanitizeVariableName(name: string) { + return name.replace(/ /g, '_').replace(/&/g, '').toUpperCase(); +} + +function validateVariableName(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); + } +} + +async function updateCustomVariable(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 function initializeContextView(context: vscode.ExtensionContext) { const contextView = new ContextView(); const contextTreeViewer = vscode.window.createTreeView( @@ -64,7 +114,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { const name = await vscode.window.showInputBox({ title: l10n.t("Enter new action name"), placeHolder: l10n.t("action name..."), - validateInput: (newName) => existingNames.includes(newName) ? l10n.t("This name is already used by another action") : undefined + validateInput: name => validateActionName(name, existingNames) }); if (name) { @@ -75,7 +125,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { command: '' }; await saveAction(action, typeNode.workspace); - contextView.refresh(); + contextView.refresh(typeNode.parent); vscode.commands.executeCommand("code-for-ibmi.context.action.edit", { action, workspace: typeNode.workspace }); } } @@ -87,12 +137,12 @@ export function initializeContextView(context: vscode.ExtensionContext) { const newName = await vscode.window.showInputBox({ title: l10n.t("Rename action"), value: action.name, - validateInput: (newName) => existingNames.includes(newName) ? l10n.t("This name is already used by another action") : undefined + validateInput: newName => validateActionName(newName, existingNames) }); if (newName) { await saveAction(action, node.workspace, { newName }); - contextView.refresh(); + contextView.refresh(node.parent?.parent); } }), vscode.commands.registerCommand("code-for-ibmi.context.action.edit", (node: ActionItem) => { @@ -111,13 +161,13 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (copyName) { const newCopyAction = { ...action, name: copyName } as Action; await saveAction(newCopyAction, node.workspace); - contextView.refresh(); + contextView.refresh(node.parent?.parent); } }), vscode.commands.registerCommand("code-for-ibmi.context.action.delete", async (node: ActionItem) => { if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete action '{0}' ?", node.action.name), { modal: true }, l10n.t("Yes"))) { await saveAction(node.action, node.workspace, { delete: true }); - contextView.refresh(); + contextView.refresh(node.parent?.parent); } }), vscode.commands.registerCommand("code-for-ibmi.context.action.runOnEditor", (node: ActionItem) => { @@ -147,6 +197,57 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), + vscode.commands.registerCommand("code-for-ibmi.context.variable.declare", async (variablesNode: CustomVariablesNode, value?: string) => { + const existingNames = getCustomVariables().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 => validateVariableName(name, existingNames) + })); + + if (name) { + const variable = { name, value } as CustomVariable; + await updateCustomVariable(variable); + contextView.refresh(variablesNode); + if (!value) { + vscode.commands.executeCommand("code-for-ibmi.context.variable.edit", variable, variablesNode); + } + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.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 updateCustomVariable(variable); + contextView.refresh(variablesNode); + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.variable.rename", async (variableItem: CustomVariableItem) => { + const variable = variableItem.customVariable; + const existingNames = getCustomVariables().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 => validateVariableName(name, existingNames) + })); + + if (newName) { + await updateCustomVariable(variable, { newName }); + contextView.refresh(variableItem.parent); + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.variable.copy", async (variableItem: CustomVariableItem) => { + vscode.commands.executeCommand("code-for-ibmi.context.variable.declare", variableItem.parent, variableItem.customVariable.value); + }), + vscode.commands.registerCommand("code-for-ibmi.context.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 updateCustomVariable(variable, { delete: true }); + contextView.refresh(variableItem.parent); + } + }), + vscode.commands.registerCommand(`code-for-ibmi.saveConnectionProfile`, async (profileNode?: Profile) => { const connection = instance.getConnection(); const storage = instance.getStorage(); @@ -249,11 +350,11 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, storedProfile.name)); contextView.refresh(); } else { - window.showWarningMessage(l10n.t(`Failed to get library list from command. Feature not installed.`)); + vscode.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.window.showErrorMessage(l10n.t(`Failed to get library list from command: {0}`, e.message)); } } } @@ -265,7 +366,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (connection && storage) { const config = connection.getConfig(); - window.showInformationMessage(l10n.t(`Reset to default`), { + vscode.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 => { @@ -471,7 +572,7 @@ class CustomVariablesNode extends ContextIem { } getChildren() { - return instance.getConnection()?.getConfig().customVariables.map(customVariable => new CustomVariableItem(this, customVariable)); + return getCustomVariables().map(customVariable => new CustomVariableItem(this, customVariable)); } } @@ -480,6 +581,12 @@ class CustomVariableItem extends ContextIem { super(customVariable.name, { parent, icon: "symbol-variable" }); this.contextValue = `customVariableItem`; this.description = customVariable.value; + + this.command = { + title: "Change value", + command: "code-for-ibmi.context.variable.edit", + arguments: [this.customVariable] + } } } 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 From 638095caa16b59ea3e0321a857c980813e77bc36 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 26 Oct 2025 22:37:59 +0100 Subject: [PATCH 07/38] Implemented Connection Profile management Signed-off-by: Seb Julliand --- package.json | 200 +++++--- src/api/actions.ts | 2 +- src/api/configuration/config/types.ts | 1 + .../storage/ConnectionStorage.ts | 8 - src/api/connectionProfiles.ts | 82 +++ src/api/types.ts | 6 +- src/editors/actionEditor.ts | 4 +- src/editors/connectionProfileEditor.ts | 60 +++ src/instantiate.ts | 2 - src/ui/Tools.ts | 18 +- src/ui/views/contextView.ts | 479 ++++++++---------- 11 files changed, 503 insertions(+), 359 deletions(-) create mode 100644 src/api/connectionProfiles.ts create mode 100644 src/editors/connectionProfileEditor.ts diff --git a/package.json b/package.json index 31411420d..770c7a952 100644 --- a/package.json +++ b/package.json @@ -869,13 +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.debug.setup.local", "title": "Import Client Certificate", @@ -1174,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", @@ -1739,6 +1704,59 @@ "enablement": "code-for-ibmi:connected", "title": "Delete...", "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.profile.create", + "enablement": "code-for-ibmi:connected", + "title": "Create new profile", + "category": "IBM i", + "icon": "$(add)" + }, + { + "command": "code-for-ibmi.context.profile.fromCurrent", + "enablement": "code-for-ibmi:connected", + "title": "Create new profile from current context", + "category": "IBM i", + "icon": "$(diff-added)" + }, + { + "command": "code-for-ibmi.context.profile.rename", + "enablement": "code-for-ibmi:connected", + "title": "Rename...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.profile.copy", + "enablement": "code-for-ibmi:connected", + "title": "Copy...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.profile.delete", + "enablement": "code-for-ibmi:connected", + "title": "Delete...", + "category": "IBM i" + }, + { + "command": "code-for-ibmi.context.profile.activate", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Set as active profile", + "icon": "$(arrow-circle-right)" + }, + { + "command": "code-for-ibmi.context.profile.runLiblistCommand", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Run Library List Command", + "icon": "$(play-circle)" + }, + { + "command": "code-for-ibmi.context.profile.unload", + "enablement": "code-for-ibmi:connected", + "category": "IBM i", + "title": "Unload active profile", + "icon": "$(debug-restart)" } ], "customEditors": [ @@ -2457,6 +2475,38 @@ { "command": "code-for-ibmi.context.variable.delete", "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.create", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.fromCurrent", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.rename", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.copy", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.delete", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.activate", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.runLiblistCommand", + "when": "never" + }, + { + "command": "code-for-ibmi.context.profile.unload", + "when": "never" } ], "view/title": [ @@ -2490,16 +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 == contextView" - }, { "command": "code-for-ibmi.createFilter", "group": "navigation@1", @@ -2674,21 +2714,6 @@ "when": "view == libraryListView && viewItem == library", "group": "inline" }, - { - "command": "code-for-ibmi.loadConnectionProfile", - "when": "view === contextView && viewItem == profile", - "group": "inline" - }, - { - "command": "code-for-ibmi.setToDefault", - "when": "view === contextView && viewItem == resetProfile", - "group": "inline" - }, - { - "command": "code-for-ibmi.context.action.create", - "when": "view === contextView && viewItem =~ /^(actionsNode|actionTypeNode)/", - "group": "inline@01" - }, { "command": "code-for-ibmi.launchActionsSetup", "when": "view === contextView && viewItem =~ /^actionsNode/", @@ -2709,6 +2734,11 @@ "when": "view === contextView && viewItem === actionsNode", "group": "inline@12" }, + { + "command": "code-for-ibmi.context.action.create", + "when": "view === contextView && viewItem =~ /^(actionsNode|actionTypeNode)/", + "group": "inline@01" + }, { "command": "code-for-ibmi.createMember", "when": "view == objectBrowser && viewItem == SPF", @@ -2734,16 +2764,6 @@ "when": "view == libraryListView && viewItem == library", "group": "02libraryActions@01" }, - { - "command": "code-for-ibmi.saveConnectionProfile", - "when": "view === contextView && viewItem == profile", - "group": "profiles@1" - }, - { - "command": "code-for-ibmi.deleteConnectionProfile", - "when": "view === contextView && viewItem == profile", - "group": "profiles@2" - }, { "command": "code-for-ibmi.maintainFilter", "when": "view == objectBrowser && viewItem =~ /^filter.*$/", @@ -3113,6 +3133,46 @@ "command": "code-for-ibmi.context.variable.delete", "when": "view === contextView && viewItem =~ /^customVariableItem/", "group": "20_customVariableItemAction01" + }, + { + "command": "code-for-ibmi.context.profile.create", + "when": "view === contextView && viewItem =~ /^profilesNode/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.context.profile.fromCurrent", + "when": "view === contextView && viewItem =~ /^profilesNode/", + "group": "inline@02" + }, + { + "command": "code-for-ibmi.context.profile.unload", + "when": "view === contextView && viewItem =~ /^profilesNode/ && code-for-ibmi:activeProfile", + "group": "inline@03" + }, + { + "command": "code-for-ibmi.context.profile.activate", + "when": "view === contextView && viewItem =~ /^profileItem(?!_active)/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.context.profile.runLiblistCommand", + "when": "view === contextView && viewItem =~ /^profileItem_active/", + "group": "inline@01" + }, + { + "command": "code-for-ibmi.context.profile.rename", + "when": "view === contextView && viewItem =~ /^profileItem/", + "group": "00_profileItemAction01" + }, + { + "command": "code-for-ibmi.context.profile.copy", + "when": "view === contextView && viewItem =~ /^profileItem/", + "group": "10_profileItemAction01" + }, + { + "command": "code-for-ibmi.context.profile.delete", + "when": "view === contextView && viewItem =~ /^profileItem(?!_active)/", + "group": "20_profileItemAction01" } ], "explorer/context": [ diff --git a/src/api/actions.ts b/src/api/actions.ts index 6b1f2f684..1d2f12353 100644 --- a/src/api/actions.ts +++ b/src/api/actions.ts @@ -6,7 +6,7 @@ export async function getActions(workspace?: vscode.WorkspaceFolder) { return workspace ? await getLocalActions(workspace) : (IBMi.connectionManager.get(`actions`) || []); } -export async function saveAction(action: Action, workspace?: vscode.WorkspaceFolder, options?: { newName?: string, delete?: boolean }) { +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); diff --git a/src/api/configuration/config/types.ts b/src/api/configuration/config/types.ts index a3478aa33..0493cc49a 100644 --- a/src/api/configuration/config/types.ts +++ b/src/api/configuration/config/types.ts @@ -32,6 +32,7 @@ export interface ConnectionConfig extends ConnectionProfile { protectedPaths: string[]; showHiddenFiles: boolean; lastDownloadLocation: string; + currentProfile?: string [name: string]: any; } diff --git a/src/api/configuration/storage/ConnectionStorage.ts b/src/api/configuration/storage/ConnectionStorage.ts index 59929f3ab..e83ce17b7 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) || []; } diff --git a/src/api/connectionProfiles.ts b/src/api/connectionProfiles.ts new file mode 100644 index 000000000..dbad80b74 --- /dev/null +++ b/src/api/connectionProfiles.ts @@ -0,0 +1,82 @@ +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; + } + + 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); +} \ 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/editors/actionEditor.ts b/src/editors/actionEditor.ts index 2d2a502ed..7fbb0ba85 100644 --- a/src/editors/actionEditor.ts +++ b/src/editors/actionEditor.ts @@ -1,5 +1,5 @@ import vscode from "vscode"; -import { saveAction } from "../api/actions"; +import { updateAction } from "../api/actions"; import { Tools } from "../api/Tools"; import { instance } from "../instantiate"; import { Action, ActionEnvironment, ActionRefresh, ActionType } from "../typings"; @@ -132,7 +132,7 @@ async function save(targetAction: Action, actionData: ActionData, workspace?: vs // 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 saveAction(targetAction, workspace); + await updateAction(targetAction, workspace); } const generic: () => VariableInfo[] = () => [ diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts new file mode 100644 index 000000000..6eb255bce --- /dev/null +++ b/src/editors/connectionProfileEditor.ts @@ -0,0 +1,60 @@ +import vscode, { l10n } from "vscode"; +import { 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 +} + +export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: () => Thenable) { + new CustomEditor(`${profile.name}.profile`, data => save(profile, data).then(doAfterSave)) + .addInput("homeDirectory", l10n.t("Home Directory"), '', { minlength: 1, default: profile.homeDirectory }) + .addInput("currentLibrary", l10n.t("Current Library"), '', { minlength: 1, maxlength: 10, default: profile.currentLibrary }) + .addInput("libraryList", l10n.t("Library List"), l10n.t("A comma-separated list of libraries."), { default: profile.libraryList.join(",") }) + .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."), { 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(); +} + +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/instantiate.ts b/src/instantiate.ts index 7cbfde1e1..abc6a766d 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -13,7 +13,6 @@ import { setupGitEventHandler } from './filesystems/local/git'; import { QSysFS } from "./filesystems/qsys/QSysFs"; import Instance from "./Instance"; import { Terminal } from './ui/Terminal'; -import { VariablesUI } from "./webviews/variables"; export let instance: Instance; @@ -83,7 +82,6 @@ export async function loadAllofExtension(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.updateConnectedBar", updateConnectedBar), ); - VariablesUI.initialize(context); instance.subscribe(context, 'connected', 'Load status bars', onConnected); instance.subscribe(context, 'disconnected', 'Unload status bars', onDisconnected); diff --git a/src/ui/Tools.ts b/src/ui/Tools.ts index 43bad16b4..4d9619095 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; @@ -189,7 +189,19 @@ export namespace VscodeTools { return tooltip; } - + export function profileToToolTip(profile: ConnectionProfile) { + const tooltip = new MarkdownString(generateTooltipHtmlTable(profile.name, { + "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/views/contextView.ts b/src/ui/views/contextView.ts index 1a8faeac6..877665f95 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -1,60 +1,77 @@ import vscode, { l10n } from 'vscode'; -import { getActions, saveAction } from '../../api/actions'; +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 } from '../../editors/actionEditor'; +import { editConnectionProfile } from '../../editors/connectionProfileEditor'; import { instance } from '../../instantiate'; -import { Action, ActionEnvironment, ActionType, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions, Profile } from '../../typings'; +import { Action, ActionEnvironment, ActionType, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../typings'; import { uriToActionTarget } from '../actions'; +import { VscodeTools } from '../Tools'; -function validateActionName(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("This name is already used by another action"); +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"); + } } } -function getCustomVariables() { - return instance.getConnection()?.getConfig().customVariables || []; +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); + } + } } -function sanitizeVariableName(name: string) { - return name.replace(/ /g, '_').replace(/&/g, '').toUpperCase(); -} +namespace CustomVariables { + export function getAll() { + return instance.getConnection()?.getConfig().customVariables || []; + } -function validateVariableName(name: string, names: string[]) { - name = sanitizeVariableName(name); - if (!name) { - return l10n.t('Name cannot be empty'); + 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); + } } - 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(); } -} -async function updateCustomVariable(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); + 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)); + 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; } - 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); + await IBMi.connectionManager.update(config); + } } } @@ -66,6 +83,8 @@ export function initializeContextView(context: vscode.ExtensionContext) { showCollapseAll: true }); + const updateContextViewDescription = (profileName?: string) => contextTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); + context.subscriptions.push( contextTreeViewer, vscode.window.onDidChangeActiveTextEditor(async editor => { @@ -86,7 +105,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { }), vscode.window.registerFileDecorationProvider({ provideFileDecoration(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { - if (uri.scheme === ProfileItem.contextValue && uri.query === "active") { + if (uri.scheme.startsWith(ProfileItem.contextValue) && uri.query === "active") { return { color: new vscode.ThemeColor(ProfileItem.activeColor) }; } else if (uri.scheme === ActionItem.contextValue && uri.query === "matched") { @@ -94,37 +113,34 @@ export function initializeContextView(context: vscode.ExtensionContext) { } } }), + vscode.commands.registerCommand("code-for-ibmi.context.refresh", () => contextView.refresh()), vscode.commands.registerCommand("code-for-ibmi.context.refresh.item", (item: BrowserItem) => contextView.refresh(item)), vscode.commands.registerCommand("code-for-ibmi.context.reveal", (item: BrowserItem, options?: FocusOptions) => contextTreeViewer.reveal(item, options)), - 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.context.action.search", (node: ActionsNode) => node.searchActions()), vscode.commands.registerCommand("code-for-ibmi.context.action.search.next", (node: ActionsNode) => node.goToNextSearchMatch()), vscode.commands.registerCommand("code-for-ibmi.context.action.search.clear", (node: ActionsNode) => node.clearSearch()), - vscode.commands.registerCommand("code-for-ibmi.context.action.create", async (node: ActionsNode | ActionTypeNode) => { + vscode.commands.registerCommand("code-for-ibmi.context.action.create", async (node: ActionsNode | ActionTypeNode, from?: ActionItem) => { const typeNode = "type" in node ? node : (await vscode.window.showQuickPick(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: l10n.t("Enter new action name"), + title: from ? l10n.t("Copy action '{0}'", from.action.name) : l10n.t("New action"), placeHolder: l10n.t("action name..."), - validateInput: name => validateActionName(name, existingNames) + value: from?.action.name, + validateInput: name => Actions.validateName(name, existingNames) }); if (name) { - const action = { + const action = from ? { ...from.action, name } : { name, type: typeNode.type, environment: "ile" as ActionEnvironment, command: '' }; - await saveAction(action, typeNode.workspace); + await updateAction(action, typeNode.workspace); contextView.refresh(typeNode.parent); vscode.commands.executeCommand("code-for-ibmi.context.action.edit", { action, workspace: typeNode.workspace }); } @@ -136,37 +152,25 @@ export function initializeContextView(context: vscode.ExtensionContext) { const newName = await vscode.window.showInputBox({ title: l10n.t("Rename action"), + placeHolder: l10n.t("action name..."), value: action.name, - validateInput: newName => validateActionName(newName, existingNames) + validateInput: newName => Actions.validateName(newName, existingNames) }); if (newName) { - await saveAction(action, node.workspace, { newName }); + await updateAction(action, node.workspace, { newName }); contextView.refresh(node.parent?.parent); } }), vscode.commands.registerCommand("code-for-ibmi.context.action.edit", (node: ActionItem) => { - editAction(node.action, async () => contextView.refresh(), node.workspace); + editAction(node.action, async () => contextView.refresh(node.parent?.parent), node.workspace); }), vscode.commands.registerCommand("code-for-ibmi.context.action.copy", async (node: ActionItem) => { - const action = node.action; - const existingNames = (await getActions(node.workspace)).map(act => act.name); - - const copyName = await vscode.window.showInputBox({ - title: l10n.t("Copy action '{0}'", action.name), - placeHolder: l10n.t("new action name..."), - validateInput: (newName) => existingNames.includes(newName) ? l10n.t("This name is already used by another action") : undefined - }); - - if (copyName) { - const newCopyAction = { ...action, name: copyName } as Action; - await saveAction(newCopyAction, node.workspace); - contextView.refresh(node.parent?.parent); - } + vscode.commands.executeCommand('code-for-ibmi.context.action.create', node.parent, node); }), vscode.commands.registerCommand("code-for-ibmi.context.action.delete", async (node: ActionItem) => { if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete action '{0}' ?", node.action.name), { modal: true }, l10n.t("Yes"))) { - await saveAction(node.action, node.workspace, { delete: true }); + await updateAction(node.action, node.workspace, { delete: true }); contextView.refresh(node.parent?.parent); } }), @@ -197,20 +201,20 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.variable.declare", async (variablesNode: CustomVariablesNode, value?: string) => { - const existingNames = getCustomVariables().map(v => v.name); + vscode.commands.registerCommand("code-for-ibmi.context.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 => validateVariableName(name, existingNames) + validateInput: name => CustomVariables.validateName(name, existingNames) })); if (name) { - const variable = { name, value } as CustomVariable; - await updateCustomVariable(variable); + const variable = { name, value: from?.value } as CustomVariable; + await CustomVariables.update(variable); contextView.refresh(variablesNode); - if (!value) { + if (!from) { vscode.commands.executeCommand("code-for-ibmi.context.variable.edit", variable, variablesNode); } } @@ -219,184 +223,178 @@ export function initializeContextView(context: vscode.ExtensionContext) { const value = await vscode.window.showInputBox({ title: l10n.t('Enter {0} value', variable.name), value: variable.value }); if (value !== undefined) { variable.value = value; - await updateCustomVariable(variable); + await CustomVariables.update(variable); contextView.refresh(variablesNode); } }), vscode.commands.registerCommand("code-for-ibmi.context.variable.rename", async (variableItem: CustomVariableItem) => { const variable = variableItem.customVariable; - const existingNames = getCustomVariables().map(v => v.name).filter(name => name !== variable.name); + 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 => validateVariableName(name, existingNames) + validateInput: name => CustomVariables.validateName(name, existingNames) })); if (newName) { - await updateCustomVariable(variable, { newName }); + await CustomVariables.update(variable, { newName }); contextView.refresh(variableItem.parent); } }), vscode.commands.registerCommand("code-for-ibmi.context.variable.copy", async (variableItem: CustomVariableItem) => { - vscode.commands.executeCommand("code-for-ibmi.context.variable.declare", variableItem.parent, variableItem.customVariable.value); + vscode.commands.executeCommand("code-for-ibmi.context.variable.declare", variableItem.parent, variableItem.customVariable); }), vscode.commands.registerCommand("code-for-ibmi.context.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 updateCustomVariable(variable, { delete: true }); + await CustomVariables.update(variable, { delete: true }); contextView.refresh(variableItem.parent); } }), - 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; + vscode.commands.registerCommand("code-for-ibmi.context.profile.create", async (profilesNode: ProfilesNode, from?: ConnectionProfile) => { + const existingNames = getConnectionProfiles().map(profile => profile.name); - 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) - ]); - contextView.refresh(); + 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) + }); - vscode.window.showInformationMessage(l10n.t(`Saved current settings to profile "{0}".`, savedProfileName)); + 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); + contextView.refresh(profilesNode); + if (!from) { + vscode.commands.executeCommand("code-for-ibmi.context.profile.edit", profile, profilesNode); + } + 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.context.profile.activate", profile); + } + }) } } }), + vscode.commands.registerCommand("code-for-ibmi.context.profile.fromCurrent", async (profilesNode: ProfilesNode) => { + const config = instance.getConnection()?.getConfig(); - 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) - contextView.refresh(); - // TODO: Add message about deleted profile! - } - }) - } + if (config) { + const current = cloneProfile(config, ""); + vscode.commands.executeCommand("code-for-ibmi.context.profile.create", profilesNode, current); } }), + vscode.commands.registerCommand("code-for-ibmi.context.profile.edit", async (profile: ConnectionProfile, parentNode?: BrowserItem) => { + editConnectionProfile(profile, async () => contextView.refresh(parentNode)) + }), + vscode.commands.registerCommand("code-for-ibmi.context.profile.rename", async (item: ProfileItem) => { + 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) + }); - 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); + if (newName) { + await updateConnectionProfile(item.profile, { newName }); + const config = instance.getConnection()?.getConfig(); + if (config?.currentProfile === currentName) { + config.currentProfile = newName; 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)); - contextView.refresh(); + updateContextViewDescription(newName); } + contextView.refresh(item.parent); } }), - - vscode.commands.registerCommand(`code-for-ibmi.loadCommandProfile`, async (commandProfile?: any) => { - //TODO + vscode.commands.registerCommand("code-for-ibmi.context.profile.copy", async (item: ProfileItem) => { + vscode.commands.executeCommand("code-for-ibmi.context.profile.create", item.parent, item.profile); + }), + vscode.commands.registerCommand("code-for-ibmi.context.profile.delete", async (item: ProfileItem) => { + 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 }); + contextView.refresh(item.parent); + } + }), + vscode.commands.registerCommand("code-for-ibmi.context.profile.activate", async (item: ProfileItem | ConnectionProfile) => { const connection = instance.getConnection(); const storage = instance.getStorage(); - if (commandProfile && connection && storage) { + if (connection && storage) { + const profile = "profile" in item ? item.profile : item; const config = connection.getConfig(); - const storedProfile = config.connectionProfiles.find(profile => profile.name === commandProfile.profile); - - if (storedProfile && storedProfile.setLibraryListCommand) { - try { - const component = connection?.getComponent(GetNewLibl.ID) - const newSettings = await component?.getLibraryListFromCommand(connection, storedProfile.setLibraryListCommand); - - 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)); - contextView.refresh(); - } else { - vscode.window.showWarningMessage(l10n.t(`Failed to get library list from command. Feature not installed.`)); - } - } catch (e: any) { - vscode.window.showErrorMessage(l10n.t(`Failed to get library list from command: {0}`, e.message)); - } + const profileToBackup = config.currentProfile ? getConnectionProfile(config.currentProfile) : getDefaultProfile(); + if (profileToBackup) { + assignProfile(config, profileToBackup); + } + assignProfile(profile, config); + config.currentProfile = profile.name || undefined; + await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", config.currentProfile); + 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`) + ]); + contextView.refresh(); + + if (profile.name && profile.setLibraryListCommand) { + await vscode.commands.executeCommand("code-for-ibmi.context.profile.runLiblistCommand", profile); } + + updateContextViewDescription(profile.name); + vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, profile.name)); } }), - vscode.commands.registerCommand(`code-for-ibmi.setToDefault`, () => { + vscode.commands.registerCommand("code-for-ibmi.context.profile.runLiblistCommand", async (profileItem?: ProfileItem) => { const connection = instance.getConnection(); const storage = instance.getStorage(); - if (connection && storage) { const config = connection.getConfig(); - vscode.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) - ]); - } - }) + const profile = profileItem?.profile || getConnectionProfile(config.get); + + if (profile?.setLibraryListCommand) { + 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, profile.setLibraryListCommand!); + + 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.context.profile.unload", async () => { + vscode.commands.executeCommand("code-for-ibmi.context.profile.activate", getDefaultProfile()); }) + ); - ) + instance.subscribe(context, 'connected', 'Update context view description', () => updateContextViewDescription(instance.getConnection()?.getConfig().currentProfile)); } class ContextIem extends BrowserItem { @@ -545,8 +543,8 @@ class ProfilesNode extends ContextIem { } getChildren() { - const currentProfile = instance.getStorage()?.getLastProfile(); - return instance.getConnection()?.getConfig().connectionProfiles + 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)); } @@ -560,8 +558,17 @@ class ProfileItem extends ContextIem { super(profile.name, { parent, icon: "person", color: active ? ProfileItem.activeColor : undefined }); this.contextValue = `${ProfileItem.contextValue}${active ? '_active' : ''}`; - this.description = active ? l10n.t(`Active`) : ``; + 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) + + if (!active) { + this.command = { + title: "Edit connection profile", + command: "code-for-ibmi.context.profile.edit", + arguments: [this.profile, this.parent] + } + } } } @@ -572,7 +579,7 @@ class CustomVariablesNode extends ContextIem { } getChildren() { - return getCustomVariables().map(customVariable => new CustomVariableItem(this, customVariable)); + return CustomVariables.getAll().map(customVariable => new CustomVariableItem(this, customVariable)); } } @@ -588,68 +595,4 @@ class CustomVariableItem extends ContextIem { arguments: [this.customVariable] } } -} - -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 ResetProfileItem extends BrowserItem implements Profile { - readonly profile; - constructor() { - super(`Reset to Default`); - - this.contextValue = `resetProfile`; - this.iconPath = new vscode.ThemeIcon(`debug-restart`); - this.tooltip = ``; - - this.profile = `Default`; - } -} - - - -/* saved for later -.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(`setLibraryListCommand`, `Library list command`, `Command to be executed that will set the library list`, {default: currentSettings.command}) - */ \ No newline at end of file +} \ No newline at end of file From a83d44aa7d684e3c4c0baa010525a9850a8a8b4c Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 26 Oct 2025 22:45:59 +0100 Subject: [PATCH 08/38] Removed header in profile tooltip Signed-off-by: Seb Julliand --- src/ui/Tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/Tools.ts b/src/ui/Tools.ts index 4d9619095..b5537c6bc 100644 --- a/src/ui/Tools.ts +++ b/src/ui/Tools.ts @@ -190,7 +190,7 @@ export namespace VscodeTools { } export function profileToToolTip(profile: ConnectionProfile) { - const tooltip = new MarkdownString(generateTooltipHtmlTable(profile.name, { + const tooltip = new MarkdownString(generateTooltipHtmlTable('', { "Home directory": profile.homeDirectory, "Current Library": profile.currentLibrary, "Library List": profile.libraryList, From ff974db78fb3f94461e06f6419d56e568be6247a Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 26 Oct 2025 22:47:06 +0100 Subject: [PATCH 09/38] Fixed code-for-ibmi:activeProfile not being set Signed-off-by: Seb Julliand --- src/ui/views/contextView.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ui/views/contextView.ts b/src/ui/views/contextView.ts index 877665f95..dd4be41ae 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -83,7 +83,10 @@ export function initializeContextView(context: vscode.ExtensionContext) { showCollapseAll: true }); - const updateContextViewDescription = (profileName?: string) => contextTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); + const updateUIContext = async (profileName?: string) => { + await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", profileName); + contextTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); + }; context.subscriptions.push( contextTreeViewer, @@ -315,7 +318,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (config?.currentProfile === currentName) { config.currentProfile = newName; await IBMi.connectionManager.update(config); - updateContextViewDescription(newName); + updateUIContext(newName); } contextView.refresh(item.parent); } @@ -342,7 +345,6 @@ export function initializeContextView(context: vscode.ExtensionContext) { } assignProfile(profile, config); config.currentProfile = profile.name || undefined; - await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", config.currentProfile); await IBMi.connectionManager.update(config); await Promise.all([ @@ -356,8 +358,8 @@ export function initializeContextView(context: vscode.ExtensionContext) { await vscode.commands.executeCommand("code-for-ibmi.context.profile.runLiblistCommand", profile); } - updateContextViewDescription(profile.name); - vscode.window.showInformationMessage(l10n.t(`Switched to profile "{0}".`, profile.name)); + await updateUIContext(profile.name); + vscode.window.showInformationMessage(config.currentProfile ? l10n.t(`Switched to profile "{0}".`, profile.name) : l10n.t("Active profile unloaded")); } }), @@ -394,7 +396,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { }) ); - instance.subscribe(context, 'connected', 'Update context view description', () => updateContextViewDescription(instance.getConnection()?.getConfig().currentProfile)); + instance.subscribe(context, 'connected', 'Update context view description', () => updateUIContext(instance.getConnection()?.getConfig().currentProfile)); } class ContextIem extends BrowserItem { From 1e2c035f9d2c66eddb8874b0b0c43e523937a11b Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 09:46:18 +0100 Subject: [PATCH 10/38] Fixed library list command exec + retrieve and clear old storage value Signed-off-by: Seb Julliand --- package.json | 2 +- .../storage/ConnectionStorage.ts | 9 ++++++++ src/ui/views/contextView.ts | 23 +++++++++++++++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 770c7a952..5182725db 100644 --- a/package.json +++ b/package.json @@ -3156,7 +3156,7 @@ }, { "command": "code-for-ibmi.context.profile.runLiblistCommand", - "when": "view === contextView && viewItem =~ /^profileItem_active/", + "when": "view === contextView && viewItem =~ /^profileItem_active_command/", "group": "inline@01" }, { diff --git a/src/api/configuration/storage/ConnectionStorage.ts b/src/api/configuration/storage/ConnectionStorage.ts index e83ce17b7..ba218c0e0 100644 --- a/src/api/configuration/storage/ConnectionStorage.ts +++ b/src/api/configuration/storage/ConnectionStorage.ts @@ -86,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/ui/views/contextView.ts b/src/ui/views/contextView.ts index dd4be41ae..69427183a 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -363,12 +363,12 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.runLiblistCommand", async (profileItem?: ProfileItem) => { + vscode.commands.registerCommand("code-for-ibmi.context.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 || getConnectionProfile(config.get); + const profile = profileItem && ("profile" in profileItem ? profileItem?.profile : profileItem) || getConnectionProfile(config.get); if (profile?.setLibraryListCommand) { return await vscode.window.withProgress({ title: l10n.t("Running {0} profile's Library List Command...", profile.name), location: vscode.ProgressLocation.Notification }, async () => { @@ -396,7 +396,22 @@ export function initializeContextView(context: vscode.ExtensionContext) { }) ); - instance.subscribe(context, 'connected', 'Update context view description', () => updateUIContext(instance.getConnection()?.getConfig().currentProfile)); + 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 ContextIem extends BrowserItem { @@ -559,7 +574,7 @@ class ProfileItem extends ContextIem { constructor(parent: BrowserItem, readonly profile: ConnectionProfile, active: boolean) { super(profile.name, { parent, icon: "person", color: active ? ProfileItem.activeColor : undefined }); - this.contextValue = `${ProfileItem.contextValue}${active ? '_active' : ''}`; + 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) From 8dc45ddeb7a8b0dcd3b85c35f8ae55b20d6982f1 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 14:53:42 +0100 Subject: [PATCH 11/38] Fixed filters display in profile editor Signed-off-by: Seb Julliand --- src/editors/connectionProfileEditor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts index 6eb255bce..154ef8cf9 100644 --- a/src/editors/connectionProfileEditor.ts +++ b/src/editors/connectionProfileEditor.ts @@ -19,13 +19,13 @@ export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: .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."), { default: profile.setLibraryListCommand }) .addHorizontalRule() .addHeading(l10n.t("Object filters"), 3) - .addParagraph(profile.objectFilters.length ? `
      ${profile.objectFilters.map(filter => `
    • ${filter.name}
    • `).join()}
    ` : l10n.t("None")) + .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")) + .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")) + .addParagraph(profile.customVariables.length ? `
      ${profile.customVariables.map(variable => `
    • &${variable.name}: ${variable.value}
    • `).join('')}
    ` : l10n.t("None")) .open(); } From 131550df789fbd4fcd0c10523f4955a23009ff16 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 14:54:13 +0100 Subject: [PATCH 12/38] Only show workspace action node if there are actions in it Signed-off-by: Seb Julliand --- src/ui/views/contextView.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/views/contextView.ts b/src/ui/views/contextView.ts index 69427183a..657b1fa7a 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -450,7 +450,10 @@ class ContextView implements vscode.TreeDataProvider { const actions = (await getActions()).sort(sortActions); const localActions = new Map(); for (const workspace of vscode.workspace.workspaceFolders || []) { - localActions.set(workspace, (await getActions(workspace)).sort(sortActions)); + const workspaceActions = (await getActions(workspace)); + if (workspaceActions.length) { + localActions.set(workspace, workspaceActions.sort(sortActions)); + } } return [ From f9950922ddc8f76fcc9301b6047cb063030a69eb Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 18:55:35 +0100 Subject: [PATCH 13/38] Put back Actions status command + give focus to Actions node Signed-off-by: Seb Julliand --- src/instantiate.ts | 2 ++ src/ui/views/contextView.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/instantiate.ts b/src/instantiate.ts index abc6a766d..6ea967442 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -112,11 +112,13 @@ async function updateConnectedBar() { const systemReadOnly = serverConfig?.codefori?.readOnlyMode || false; connectedBarItem.text = `$(${systemReadOnly ? "shield" : (config.readOnlyMode ? "lock" : "settings-gear")}) ${config.name}`; const terminalMenuItem = systemReadOnly ? `` : `[$(terminal) Terminals](command:code-for-ibmi.launchTerminalPicker)`; + const actionsMenuItem = systemReadOnly ? `` : `[$(file-binary) Actions](command:code-for-ibmi.context.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)`, terminalMenuItem, + actionsMenuItem, debugPTFInstalled(connection) ? `[$(${debugRunning ? "bug" : "debug"}) Debugger ${((await getDebugServiceDetails(connection)).version)} (${debugRunning ? "on" : "off"})](command:ibmiDebugBrowser.focus)` : diff --git a/src/ui/views/contextView.ts b/src/ui/views/contextView.ts index 657b1fa7a..a72b0ccff 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/contextView.ts @@ -203,6 +203,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.executeCommand(`code-for-ibmi.runAction`, uri, undefined, action, undefined, workspace); } }), + vscode.commands.registerCommand("code-for-ibmi.context.actions.focus", () => contextView.actionsNode?.reveal({ focus: true, expand: true })), vscode.commands.registerCommand("code-for-ibmi.context.variable.declare", async (variablesNode: CustomVariablesNode, from?: CustomVariable) => { const existingNames = CustomVariables.getAll().map(v => v.name); @@ -427,6 +428,7 @@ class ContextIem extends BrowserItem { class ContextView implements vscode.TreeDataProvider { private readonly emitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.emitter.event; + actionsNode?: ActionsNode refresh(target?: BrowserItem) { this.emitter.fire(target); @@ -456,8 +458,10 @@ class ContextView implements vscode.TreeDataProvider { } } + this.actionsNode = new ActionsNode(actions, localActions); + return [ - new ActionsNode(actions, localActions), + this.actionsNode, new CustomVariablesNode(), new ProfilesNode() ]; From 744abdd58044bf028de04e6c33751655a19820ea Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 19:11:11 +0100 Subject: [PATCH 14/38] Moved node classes in their own files Signed-off-by: Seb Julliand --- src/extension.ts | 2 +- src/ui/views/context/actions.ts | 105 +++++++++ src/ui/views/context/connectionProfiles.ts | 53 +++++ src/ui/views/context/contextItem.ts | 13 ++ src/ui/views/{ => context}/contextView.ts | 248 +-------------------- src/ui/views/context/customVariables.ts | 72 ++++++ 6 files changed, 256 insertions(+), 237 deletions(-) create mode 100644 src/ui/views/context/actions.ts create mode 100644 src/ui/views/context/connectionProfiles.ts create mode 100644 src/ui/views/context/contextItem.ts rename src/ui/views/{ => context}/contextView.ts (68%) create mode 100644 src/ui/views/context/customVariables.ts diff --git a/src/extension.ts b/src/extension.ts index 0a4ff3d9e..3c66a45bc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,7 +27,7 @@ import { VscodeTools } from "./ui/Tools"; import { registerActionTools } from "./ui/actions"; import { initializeConnectionBrowser } from "./ui/views/ConnectionBrowser"; import { initializeLibraryListView } from "./ui/views/LibraryListView"; -import { initializeContextView } from "./ui/views/contextView"; +import { initializeContextView } from "./ui/views/context/contextView"; import { initializeDebugBrowser } from "./ui/views/debugView"; import { HelpView } from "./ui/views/helpView"; import { initializeIFSBrowser } from "./ui/views/ifsBrowser"; diff --git a/src/ui/views/context/actions.ts b/src/ui/views/context/actions.ts new file mode 100644 index 000000000..ed8a80304 --- /dev/null +++ b/src/ui/views/context/actions.ts @@ -0,0 +1,105 @@ +import vscode, { l10n } from "vscode"; +import { Action, ActionType } from "../../../typings"; +import { ContextItem } from "./contextItem"; + +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 ContextItem { + private readonly foundActions: ActionItem[] = []; + private revealIndex = -1; + + private readonly children; + + constructor(actions: Action[], localActions: Map) { + super(l10n.t("Actions"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = "actionsNode"; + this.children = [ + new ActionTypeNode(this, l10n.t("Member"), 'member', actions), + new ActionTypeNode(this, l10n.t("Object"), 'object', actions), + new ActionTypeNode(this, l10n.t("Streamfile"), 'streamfile', actions), + ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'file', localActions, workspace))) + ] + } + + getChildren() { + return this.children; + } + + getAllActionItems() { + return this.children.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(...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(true)); + this.refresh(); + this.goToNextSearchMatch(); + } + } + } + + goToNextSearchMatch() { + this.revealIndex += (this.revealIndex + 1) < this.foundActions.length ? 1 : -this.revealIndex; + const actionNode = this.foundActions[this.revealIndex]; + actionNode.reveal({ focus: true }); + } + + async clearSearch() { + this.getAllActionItems().forEach(node => node.setContext(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 ContextItem { + readonly actionItems: ActionItem[]; + constructor(parent: ContextItem, label: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { + super(label, { parent, state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `actionTypeNode_${type}`; + this.description = workspace ? l10n.t("workspace actions") : undefined; + this.actionItems = actions.filter(action => action.type === type).map(action => new ActionItem(this, action, workspace)); + } + + getChildren() { + return this.actionItems; + } +} + +export class ActionItem extends ContextItem { + static matchedColor = "charts.yellow"; + static contextValue = `actionItem`; + + constructor(parent: ContextItem, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { + super(action.name, { parent }); + this.setContext(); + this.command = { + title: "Edit action", + command: "code-for-ibmi.context.action.edit", + arguments: [this] + } + } + + setContext(matched?: boolean) { + this.contextValue = `${ActionItem.contextValue}${this.workspace ? "Local" : "Remote"}${matched ? '_matched' : ''}`; + this.iconPath = new vscode.ThemeIcon("github-action", matched ? new vscode.ThemeColor(ActionItem.matchedColor) : undefined); + this.resourceUri = vscode.Uri.from({ scheme: ActionItem.contextValue, authority: this.action.name, query: matched ? "matched" : "" }); + this.description = matched ? l10n.t("search match") : undefined; + this.tooltip = this.action.command; + } +} \ No newline at end of file diff --git a/src/ui/views/context/connectionProfiles.ts b/src/ui/views/context/connectionProfiles.ts new file mode 100644 index 000000000..2de113245 --- /dev/null +++ b/src/ui/views/context/connectionProfiles.ts @@ -0,0 +1,53 @@ +import vscode, { l10n } from "vscode"; +import { getConnectionProfiles } from "../../../api/connectionProfiles"; +import { instance } from "../../../instantiate"; +import { ConnectionProfile } from "../../../typings"; +import { VscodeTools } from "../../Tools"; +import { ContextItem } from "./contextItem"; + +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 ContextItem { + constructor() { + super(l10n.t("Profiles"), { 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 ContextItem { + static contextValue = `profileItem`; + static activeColor = "charts.green"; + + constructor(parent: ContextItem, 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) + + if (!active) { + this.command = { + title: "Edit connection profile", + command: "code-for-ibmi.context.profile.edit", + arguments: [this.profile, this.parent] + } + } + } +} \ No newline at end of file diff --git a/src/ui/views/context/contextItem.ts b/src/ui/views/context/contextItem.ts new file mode 100644 index 000000000..555c05395 --- /dev/null +++ b/src/ui/views/context/contextItem.ts @@ -0,0 +1,13 @@ +import vscode from "vscode"; +import { FocusOptions } from "../../../typings"; +import { BrowserItem } from "../../types"; + +export class ContextItem extends BrowserItem { + async refresh() { + await vscode.commands.executeCommand("code-for-ibmi.context.refresh.item", this); + } + + reveal(options?: FocusOptions) { + return vscode.commands.executeCommand(`code-for-ibmi.context.reveal`, this, options); + } +} \ No newline at end of file diff --git a/src/ui/views/contextView.ts b/src/ui/views/context/contextView.ts similarity index 68% rename from src/ui/views/contextView.ts rename to src/ui/views/context/contextView.ts index a72b0ccff..d0bc6335d 100644 --- a/src/ui/views/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -1,79 +1,17 @@ import vscode, { l10n } 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 } from '../../editors/actionEditor'; -import { editConnectionProfile } from '../../editors/connectionProfileEditor'; -import { instance } from '../../instantiate'; -import { Action, ActionEnvironment, ActionType, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../typings'; -import { uriToActionTarget } from '../actions'; -import { VscodeTools } from '../Tools'; - -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"); - } - } -} - -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); - } - } -} - -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); - } - } -} +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 } from '../../../editors/actionEditor'; +import { editConnectionProfile } 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 initializeContextView(context: vscode.ExtensionContext) { const contextView = new ContextView(); @@ -415,16 +353,6 @@ export function initializeContextView(context: vscode.ExtensionContext) { }); } -class ContextIem extends BrowserItem { - async refresh() { - await vscode.commands.executeCommand("code-for-ibmi.context.refresh.item", this); - } - - reveal(options?: FocusOptions) { - return vscode.commands.executeCommand(`code-for-ibmi.context.reveal`, this, options); - } -} - class ContextView implements vscode.TreeDataProvider { private readonly emitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.emitter.event; @@ -467,156 +395,4 @@ class ContextView implements vscode.TreeDataProvider { ]; } } -} - -class ActionsNode extends ContextIem { - private readonly foundActions: ActionItem[] = []; - private revealIndex = -1; - - private readonly children; - - constructor(actions: Action[], localActions: Map) { - super(l10n.t("Actions"), { state: vscode.TreeItemCollapsibleState.Collapsed }); - this.contextValue = "actionsNode"; - this.children = [ - new ActionTypeNode(this, l10n.t("Member"), 'member', actions), - new ActionTypeNode(this, l10n.t("Object"), 'object', actions), - new ActionTypeNode(this, l10n.t("Streamfile"), 'streamfile', actions), - ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'file', localActions, workspace))) - ] - } - - getChildren() { - return this.children; - } - - getAllActionItems() { - return this.children.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(...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(true)); - this.refresh(); - this.goToNextSearchMatch(); - } - } - } - - goToNextSearchMatch() { - this.revealIndex += (this.revealIndex + 1) < this.foundActions.length ? 1 : -this.revealIndex; - const actionNode = this.foundActions[this.revealIndex]; - actionNode.reveal({ focus: true }); - } - - async clearSearch() { - this.getAllActionItems().forEach(node => node.setContext(false)); - this.revealIndex = -1; - this.foundActions.splice(0, this.foundActions.length); - await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasActionSearched`, false); - await this.refresh(); - } -} - -class ActionTypeNode extends ContextIem { - readonly actionItems: ActionItem[]; - constructor(parent: BrowserItem, label: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { - super(label, { parent, state: vscode.TreeItemCollapsibleState.Collapsed }); - this.contextValue = `actionTypeNode_${type}`; - this.description = workspace ? l10n.t("workspace actions") : undefined; - this.actionItems = actions.filter(action => action.type === type).map(action => new ActionItem(this, action, workspace)); - } - - getChildren() { - return this.actionItems; - } -} - -class ActionItem extends ContextIem { - static matchedColor = "charts.yellow"; - static contextValue = `actionItem`; - - constructor(parent: BrowserItem, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { - super(action.name, { parent }); - this.setContext(); - this.command = { - title: "Edit action", - command: "code-for-ibmi.context.action.edit", - arguments: [this] - } - } - - setContext(matched?: boolean) { - this.contextValue = `${ActionItem.contextValue}${this.workspace ? "Local" : "Remote"}${matched ? '_matched' : ''}`; - this.iconPath = new vscode.ThemeIcon("github-action", matched ? new vscode.ThemeColor(ActionItem.matchedColor) : undefined); - this.resourceUri = vscode.Uri.from({ scheme: ActionItem.contextValue, authority: this.action.name, query: matched ? "matched" : "" }); - this.description = matched ? l10n.t("search match") : undefined; - this.tooltip = this.action.command; - } -} - -class ProfilesNode extends ContextIem { - constructor() { - super(l10n.t("Profiles"), { 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)); - } -} - -class ProfileItem extends ContextIem { - static contextValue = `profileItem`; - static activeColor = "charts.green"; - - constructor(parent: BrowserItem, 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) - - if (!active) { - this.command = { - title: "Edit connection profile", - command: "code-for-ibmi.context.profile.edit", - arguments: [this.profile, this.parent] - } - } - } -} - -class CustomVariablesNode extends ContextIem { - constructor() { - super(l10n.t("Custom Variables"), { state: vscode.TreeItemCollapsibleState.Collapsed }); - this.contextValue = `customVariablesNode`; - } - - getChildren() { - return CustomVariables.getAll().map(customVariable => new CustomVariableItem(this, customVariable)); - } -} - -class CustomVariableItem extends ContextIem { - constructor(parent: BrowserItem, 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.context.variable.edit", - arguments: [this.customVariable] - } - } } \ No newline at end of file diff --git a/src/ui/views/context/customVariables.ts b/src/ui/views/context/customVariables.ts new file mode 100644 index 000000000..f82d2675a --- /dev/null +++ b/src/ui/views/context/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 { ContextItem } from "./contextItem"; + +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 ContextItem { + constructor() { + super(l10n.t("Custom Variables"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + this.contextValue = `customVariablesNode`; + } + + getChildren() { + return CustomVariables.getAll().map(customVariable => new CustomVariableItem(this, customVariable)); + } +} + +export class CustomVariableItem extends ContextItem { + constructor(parent: ContextItem, 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.context.variable.edit", + arguments: [this.customVariable] + } + } +} \ No newline at end of file From 46b31c59ccaf5d326bdc10768c72db149d7afe47 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 19:12:43 +0100 Subject: [PATCH 15/38] Put profile merger function in its own file Signed-off-by: Seb Julliand --- src/extension.ts | 30 ++---------------------------- src/mergeProfiles.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 src/mergeProfiles.ts diff --git a/src/extension.ts b/src/extension.ts index 3c66a45bc..6a7ef537b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ // The module 'vscode' contains the VS Code extensibility API -import { commands, ExtensionContext, l10n, languages, window, workspace } from "vscode"; +import { commands, ExtensionContext, languages, window, workspace } from "vscode"; // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -21,6 +21,7 @@ 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"; @@ -147,31 +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); -} - -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/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 From f3faead3ea08a7ded054b96790546c4070657783 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 27 Oct 2025 19:16:17 +0100 Subject: [PATCH 16/38] Fixed typings Signed-off-by: Seb Julliand --- src/ui/views/context/contextView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index d0bc6335d..8a18333d0 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -1,5 +1,5 @@ -import vscode, { l10n } from 'vscode'; +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'; @@ -63,7 +63,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.context.action.search.next", (node: ActionsNode) => node.goToNextSearchMatch()), vscode.commands.registerCommand("code-for-ibmi.context.action.search.clear", (node: ActionsNode) => node.clearSearch()), vscode.commands.registerCommand("code-for-ibmi.context.action.create", async (node: ActionsNode | ActionTypeNode, from?: ActionItem) => { - const typeNode = "type" in node ? node : (await vscode.window.showQuickPick(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; + const typeNode = "type" in node ? node : (await vscode.window.showQuickPick(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); From 3616a762a90328b809a4eee92f655fc9642d3bd2 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 1 Nov 2025 18:56:58 +0100 Subject: [PATCH 17/38] Allow prompting of library list command Signed-off-by: Seb Julliand --- src/editors/connectionProfileEditor.ts | 2 +- src/ui/views/context/contextView.ts | 47 +++++++++++++++----------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts index 154ef8cf9..7c8ae487f 100644 --- a/src/editors/connectionProfileEditor.ts +++ b/src/editors/connectionProfileEditor.ts @@ -16,7 +16,7 @@ export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: .addInput("homeDirectory", l10n.t("Home Directory"), '', { minlength: 1, default: profile.homeDirectory }) .addInput("currentLibrary", l10n.t("Current Library"), '', { minlength: 1, maxlength: 10, default: profile.currentLibrary }) .addInput("libraryList", l10n.t("Library List"), l10n.t("A comma-separated list of libraries."), { default: profile.libraryList.join(",") }) - .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."), { default: profile.setLibraryListCommand }) + .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")) diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index 8a18333d0..534e18526 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -239,8 +239,8 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.executeCommand("code-for-ibmi.context.profile.create", profilesNode, current); } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.edit", async (profile: ConnectionProfile, parentNode?: BrowserItem) => { - editConnectionProfile(profile, async () => contextView.refresh(parentNode)) + vscode.commands.registerCommand("code-for-ibmi.context.profile.edit", async (profile: ConnectionProfile) => { + editConnectionProfile(profile, async () => contextView.refresh(contextView.profilesNode)) }), vscode.commands.registerCommand("code-for-ibmi.context.profile.rename", async (item: ProfileItem) => { const currentName = item.profile.name; @@ -310,23 +310,29 @@ export function initializeContextView(context: vscode.ExtensionContext) { const profile = profileItem && ("profile" in profileItem ? profileItem?.profile : profileItem) || getConnectionProfile(config.get); if (profile?.setLibraryListCommand) { - 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, profile.setLibraryListCommand!); - - 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.`)); + 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)); } - } catch (e: any) { - vscode.window.showErrorMessage(l10n.t(`Failed to get library list from command: {0}`, e.message)); - } - }) + }); + } } } }), @@ -357,6 +363,7 @@ class ContextView 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); @@ -387,11 +394,11 @@ class ContextView implements vscode.TreeDataProvider { } this.actionsNode = new ActionsNode(actions, localActions); - + this.profilesNode = new ProfilesNode(); return [ this.actionsNode, new CustomVariablesNode(), - new ProfilesNode() + this.profilesNode ]; } } From ece6b6eb886674a5c67f1225884a756527f212f4 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sat, 1 Nov 2025 19:02:53 +0100 Subject: [PATCH 18/38] Allow active profile to be edited to change liblist command Signed-off-by: Seb Julliand --- src/editors/connectionProfileEditor.ts | 7 ++++--- src/ui/views/context/connectionProfiles.ts | 10 ++++------ src/ui/views/context/contextView.ts | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts index 7c8ae487f..e5244052e 100644 --- a/src/editors/connectionProfileEditor.ts +++ b/src/editors/connectionProfileEditor.ts @@ -12,10 +12,11 @@ type ConnectionProfileData = { } export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: () => Thenable) { + const activeProfile = instance.getConnection()?.getConfig().currentProfile === profile.name; new CustomEditor(`${profile.name}.profile`, data => save(profile, data).then(doAfterSave)) - .addInput("homeDirectory", l10n.t("Home Directory"), '', { minlength: 1, default: profile.homeDirectory }) - .addInput("currentLibrary", l10n.t("Current Library"), '', { minlength: 1, maxlength: 10, default: profile.currentLibrary }) - .addInput("libraryList", l10n.t("Library List"), l10n.t("A comma-separated list of libraries."), { default: profile.libraryList.join(",") }) + .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) diff --git a/src/ui/views/context/connectionProfiles.ts b/src/ui/views/context/connectionProfiles.ts index 2de113245..6ee91f39c 100644 --- a/src/ui/views/context/connectionProfiles.ts +++ b/src/ui/views/context/connectionProfiles.ts @@ -42,12 +42,10 @@ export class ProfileItem extends ContextItem { this.resourceUri = vscode.Uri.from({ scheme: this.contextValue, authority: profile.name, query: active ? "active" : "" }); this.tooltip = VscodeTools.profileToToolTip(profile) - if (!active) { - this.command = { - title: "Edit connection profile", - command: "code-for-ibmi.context.profile.edit", - arguments: [this.profile, this.parent] - } + this.command = { + title: "Edit connection profile", + command: "code-for-ibmi.context.profile.edit", + arguments: [this.profile] } } } \ No newline at end of file diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index 534e18526..d2a8489a7 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -219,7 +219,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { await updateConnectionProfile(profile); contextView.refresh(profilesNode); if (!from) { - vscode.commands.executeCommand("code-for-ibmi.context.profile.edit", profile, profilesNode); + vscode.commands.executeCommand("code-for-ibmi.context.profile.edit", profile); } else { vscode.window.showInformationMessage(l10n.t("Created connection Profile '{0}'.", profile.name), l10n.t("Activate profile {0}", profile.name)) From 41d39aae909557eb4c3938742f5a68499cce5441 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 5 Nov 2025 12:49:12 +0100 Subject: [PATCH 19/38] Fixed actions refresh Signed-off-by: Seb Julliand --- src/ui/views/context/actions.ts | 47 +++++++++++++++++++++-------- src/ui/views/context/contextView.ts | 37 ++++++++--------------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/ui/views/context/actions.ts b/src/ui/views/context/actions.ts index ed8a80304..bd03082aa 100644 --- a/src/ui/views/context/actions.ts +++ b/src/ui/views/context/actions.ts @@ -1,4 +1,5 @@ import vscode, { l10n } from "vscode"; +import { getActions } from "../../../api/actions"; import { Action, ActionType } from "../../../typings"; import { ContextItem } from "./contextItem"; @@ -17,32 +18,43 @@ export class ActionsNode extends ContextItem { private readonly foundActions: ActionItem[] = []; private revealIndex = -1; - private readonly children; + private readonly children: ActionTypeNode[] = []; - constructor(actions: Action[], localActions: Map) { + constructor() { super(l10n.t("Actions"), { state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = "actionsNode"; - this.children = [ - new ActionTypeNode(this, l10n.t("Member"), 'member', actions), - new ActionTypeNode(this, l10n.t("Object"), 'object', actions), - new ActionTypeNode(this, l10n.t("Streamfile"), 'streamfile', actions), - ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'file', localActions, workspace))) - ] } - getChildren() { + async getChildren() { + if (!this.children.length) { + 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"), 'member', actions), + new ActionTypeNode(this, l10n.t("Object"), 'object', actions), + new ActionTypeNode(this, l10n.t("Streamfile"), 'streamfile', actions), + ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'file', localActions, workspace))) + ); + } return this.children; } - getAllActionItems() { - return this.children.flatMap(child => child.actionItems); + 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(...this.getAllActionItems().filter(action => [action.action.name, action.action.command].some(text => text.toLocaleLowerCase().includes(nameOrCommand)))) > 0; + 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(true)); @@ -52,6 +64,11 @@ export class ActionsNode extends ContextItem { } } + 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]; @@ -59,7 +76,7 @@ export class ActionsNode extends ContextItem { } async clearSearch() { - this.getAllActionItems().forEach(node => node.setContext(false)); + (await this.getAllActionItems()).forEach(node => node.setContext(false)); this.revealIndex = -1; this.foundActions.splice(0, this.foundActions.length); await vscode.commands.executeCommand(`setContext`, `code-for-ibmi:hasActionSearched`, false); @@ -102,4 +119,8 @@ export class ActionItem extends ContextItem { this.description = matched ? l10n.t("search match") : undefined; this.tooltip = this.action.command; } +} + +function sortActions(a1: Action, a2: Action) { + return a1.name.localeCompare(a2.name); } \ No newline at end of file diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index d2a8489a7..cee0958b8 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -7,7 +7,7 @@ import IBMi from '../../../api/IBMi'; import { editAction } from '../../../editors/actionEditor'; import { editConnectionProfile } from '../../../editors/connectionProfileEditor'; import { instance } from '../../../instantiate'; -import { Action, ActionEnvironment, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../../typings'; +import { ActionEnvironment, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../../typings'; import { uriToActionTarget } from '../../actions'; import { ActionItem, Actions, ActionsNode, ActionTypeNode } from './actions'; import { ConnectionProfiles, ProfileItem, ProfilesNode } from './connectionProfiles'; @@ -63,7 +63,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.context.action.search.next", (node: ActionsNode) => node.goToNextSearchMatch()), vscode.commands.registerCommand("code-for-ibmi.context.action.search.clear", (node: ActionsNode) => node.clearSearch()), vscode.commands.registerCommand("code-for-ibmi.context.action.create", async (node: ActionsNode | ActionTypeNode, from?: ActionItem) => { - const typeNode = "type" in node ? node : (await vscode.window.showQuickPick(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; + 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); @@ -82,7 +82,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { command: '' }; await updateAction(action, typeNode.workspace); - contextView.refresh(typeNode.parent); + contextView.actionsNode?.forceRefresh(); vscode.commands.executeCommand("code-for-ibmi.context.action.edit", { action, workspace: typeNode.workspace }); } } @@ -100,11 +100,11 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (newName) { await updateAction(action, node.workspace, { newName }); - contextView.refresh(node.parent?.parent); + contextView.actionsNode?.forceRefresh(); } }), vscode.commands.registerCommand("code-for-ibmi.context.action.edit", (node: ActionItem) => { - editAction(node.action, async () => contextView.refresh(node.parent?.parent), node.workspace); + editAction(node.action, async () => contextView.actionsNode?.forceRefresh(), node.workspace); }), vscode.commands.registerCommand("code-for-ibmi.context.action.copy", async (node: ActionItem) => { vscode.commands.executeCommand('code-for-ibmi.context.action.create', node.parent, node); @@ -112,7 +112,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.context.action.delete", async (node: ActionItem) => { 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 }); - contextView.refresh(node.parent?.parent); + contextView.actionsNode?.forceRefresh(); } }), vscode.commands.registerCommand("code-for-ibmi.context.action.runOnEditor", (node: ActionItem) => { @@ -194,7 +194,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.create", async (profilesNode: ProfilesNode, from?: ConnectionProfile) => { + vscode.commands.registerCommand("code-for-ibmi.context.profile.create", async (from?: ConnectionProfile) => { const existingNames = getConnectionProfiles().map(profile => profile.name); const name = await vscode.window.showInputBox({ @@ -217,7 +217,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { objectFilters: [], }; await updateConnectionProfile(profile); - contextView.refresh(profilesNode); + contextView.refresh(contextView.profilesNode); if (!from) { vscode.commands.executeCommand("code-for-ibmi.context.profile.edit", profile); } @@ -236,7 +236,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (config) { const current = cloneProfile(config, ""); - vscode.commands.executeCommand("code-for-ibmi.context.profile.create", profilesNode, current); + vscode.commands.executeCommand("code-for-ibmi.context.profile.create", current); } }), vscode.commands.registerCommand("code-for-ibmi.context.profile.edit", async (profile: ConnectionProfile) => { @@ -259,16 +259,16 @@ export function initializeContextView(context: vscode.ExtensionContext) { await IBMi.connectionManager.update(config); updateUIContext(newName); } - contextView.refresh(item.parent); + contextView.refresh(contextView.profilesNode); } }), vscode.commands.registerCommand("code-for-ibmi.context.profile.copy", async (item: ProfileItem) => { - vscode.commands.executeCommand("code-for-ibmi.context.profile.create", item.parent, item.profile); + vscode.commands.executeCommand("code-for-ibmi.context.profile.create", item.profile); }), vscode.commands.registerCommand("code-for-ibmi.context.profile.delete", async (item: ProfileItem) => { 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 }); - contextView.refresh(item.parent); + contextView.refresh(contextView.profilesNode); } }), vscode.commands.registerCommand("code-for-ibmi.context.profile.activate", async (item: ProfileItem | ConnectionProfile) => { @@ -382,18 +382,7 @@ class ContextView implements vscode.TreeDataProvider { return item.getChildren?.(); } else { - const sortActions = (a1: Action, a2: Action) => a1.name.localeCompare(a2.name); - - 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.actionsNode = new ActionsNode(actions, localActions); + this.actionsNode = new ActionsNode(); this.profilesNode = new ProfilesNode(); return [ this.actionsNode, From d7e61941ffff5136fd45b0cd78a5a0166f43f0c4 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 5 Nov 2025 12:53:43 +0100 Subject: [PATCH 20/38] Fixed profile being empty on creation Signed-off-by: Seb Julliand --- src/ui/views/context/contextView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index cee0958b8..44a9bbab5 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -194,7 +194,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.create", async (from?: ConnectionProfile) => { + vscode.commands.registerCommand("code-for-ibmi.context.profile.create", async (node?: ProfilesNode, from?: ConnectionProfile) => { const existingNames = getConnectionProfiles().map(profile => profile.name); const name = await vscode.window.showInputBox({ @@ -236,7 +236,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (config) { const current = cloneProfile(config, ""); - vscode.commands.executeCommand("code-for-ibmi.context.profile.create", current); + vscode.commands.executeCommand("code-for-ibmi.context.profile.create", undefined, current); } }), vscode.commands.registerCommand("code-for-ibmi.context.profile.edit", async (profile: ConnectionProfile) => { @@ -263,7 +263,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), vscode.commands.registerCommand("code-for-ibmi.context.profile.copy", async (item: ProfileItem) => { - vscode.commands.executeCommand("code-for-ibmi.context.profile.create", item.profile); + vscode.commands.executeCommand("code-for-ibmi.context.profile.create", undefined, item.profile); }), vscode.commands.registerCommand("code-for-ibmi.context.profile.delete", async (item: ProfileItem) => { if (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete profile '{0}' ?", item.profile.name), { modal: true }, l10n.t("Yes"))) { From ec7bc690078efbb4f0293037db2748c399c9e7fa Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 5 Nov 2025 14:00:11 +0100 Subject: [PATCH 21/38] Display current profile in status bar (unless profile is default) Signed-off-by: Seb Julliand --- src/instantiate.ts | 2 +- src/ui/views/context/contextView.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/instantiate.ts b/src/instantiate.ts index 6ea967442..e21975c8f 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -110,7 +110,7 @@ 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.context.actions.focus)`; const debugRunning = await isDebugEngineRunning(); diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index 44a9bbab5..6f817060c 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -24,6 +24,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { const updateUIContext = async (profileName?: string) => { await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", profileName); contextTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); + vscode.commands.executeCommand("code-for-ibmi.updateConnectedBar"); }; context.subscriptions.push( From 00d6094cb1593cea4faf31839ee9d1ce078be84d Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 10:39:47 +0100 Subject: [PATCH 22/38] Renamed Context view to Environment Signed-off-by: Seb Julliand --- package.json | 180 ++++++++++----------- src/extension.ts | 6 +- src/instantiate.ts | 2 +- src/ui/views/context/actions.ts | 2 +- src/ui/views/context/connectionProfiles.ts | 2 +- src/ui/views/context/contextItem.ts | 4 +- src/ui/views/context/contextView.ts | 78 ++++----- src/ui/views/context/customVariables.ts | 2 +- 8 files changed, 138 insertions(+), 138 deletions(-) diff --git a/package.json b/package.json index 5182725db..f38e9e0f3 100644 --- a/package.json +++ b/package.json @@ -1616,143 +1616,143 @@ "enablement": "code-for-ibmi:connected && !code-for-ibmi:isReadonly" }, { - "command": "code-for-ibmi.context.refresh", + "command": "code-for-ibmi.environment.refresh", "enablement": "code-for-ibmi:connected", "title": "Refresh", "category": "IBM i", "icon": "$(refresh)" }, { - "command": "code-for-ibmi.context.action.search", + "command": "code-for-ibmi.environment.action.search", "enablement": "code-for-ibmi:connected", "title": "Search action", "category": "IBM i", "icon": "$(search)" }, { - "command": "code-for-ibmi.context.action.search.next", + "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.context.action.search.clear", + "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.context.action.create", + "command": "code-for-ibmi.environment.action.create", "enablement": "code-for-ibmi:connected", "title": "Create action", "category": "IBM i", "icon": "$(add)" }, { - "command": "code-for-ibmi.context.action.rename", + "command": "code-for-ibmi.environment.action.rename", "enablement": "code-for-ibmi:connected", "title": "Rename...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.action.copy", + "command": "code-for-ibmi.environment.action.copy", "enablement": "code-for-ibmi:connected", "title": "Copy...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.action.delete", + "command": "code-for-ibmi.environment.action.delete", "enablement": "code-for-ibmi:connected", "title": "Delete...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.action.runOnEditor", + "command": "code-for-ibmi.environment.action.runOnEditor", "title": "Run on active editor", "category": "IBM i", "icon": "$(debug-start)" }, { - "command": "code-for-ibmi.context.variable.declare", + "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.context.variable.edit", + "command": "code-for-ibmi.environment.variable.edit", "enablement": "code-for-ibmi:connected", "title": "Change value...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.variable.rename", + "command": "code-for-ibmi.environment.variable.rename", "enablement": "code-for-ibmi:connected", "title": "Rename...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.variable.copy", + "command": "code-for-ibmi.environment.variable.copy", "enablement": "code-for-ibmi:connected", "title": "Copy...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.variable.delete", + "command": "code-for-ibmi.environment.variable.delete", "enablement": "code-for-ibmi:connected", "title": "Delete...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.profile.create", + "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.context.profile.fromCurrent", + "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.context.profile.rename", + "command": "code-for-ibmi.environment.profile.rename", "enablement": "code-for-ibmi:connected", "title": "Rename...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.profile.copy", + "command": "code-for-ibmi.environment.profile.copy", "enablement": "code-for-ibmi:connected", "title": "Copy...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.profile.delete", + "command": "code-for-ibmi.environment.profile.delete", "enablement": "code-for-ibmi:connected", "title": "Delete...", "category": "IBM i" }, { - "command": "code-for-ibmi.context.profile.activate", + "command": "code-for-ibmi.environment.profile.activate", "enablement": "code-for-ibmi:connected", "category": "IBM i", "title": "Set as active profile", "icon": "$(arrow-circle-right)" }, { - "command": "code-for-ibmi.context.profile.runLiblistCommand", + "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.context.profile.unload", + "command": "code-for-ibmi.environment.profile.unload", "enablement": "code-for-ibmi:connected", "category": "IBM i", "title": "Unload active profile", @@ -1876,9 +1876,9 @@ "when": "!code-for-ibmi:connecting && !code-for-ibmi:connected && !code-for-ibmi:connectionBrowserDisabled" }, { - "id": "contextView", - "name": "Context", - "when": "code-for-ibmi:connected && !(code-for-ibmi:profilesViewDisabled || code-for-ibmi:contextViewDisabled)", + "id": "environmentView", + "name": "Environment", + "when": "code-for-ibmi:connected && !(code-for-ibmi:profilesViewDisabled || code-for-ibmi:environmentViewDisabled)", "visibility": "collapsed" }, { @@ -2421,91 +2421,91 @@ "when": "never" }, { - "command": "code-for-ibmi.context.refresh", + "command": "code-for-ibmi.environment.refresh", "when": "never" }, { - "command": "code-for-ibmi.context.action.search", + "command": "code-for-ibmi.environment.action.search", "when": "never" }, { - "command": "code-for-ibmi.context.action.search.next", + "command": "code-for-ibmi.environment.action.search.next", "when": "never" }, { - "command": "code-for-ibmi.context.action.search.clear", + "command": "code-for-ibmi.environment.action.search.clear", "when": "never" }, { - "command": "code-for-ibmi.context.action.create", + "command": "code-for-ibmi.environment.action.create", "when": "never" }, { - "command": "code-for-ibmi.context.action.rename", + "command": "code-for-ibmi.environment.action.rename", "when": "never" }, { - "command": "code-for-ibmi.context.action.copy", + "command": "code-for-ibmi.environment.action.copy", "when": "never" }, { - "command": "code-for-ibmi.context.action.delete", + "command": "code-for-ibmi.environment.action.delete", "when": "never" }, { - "command": "code-for-ibmi.context.action.runOnEditor", + "command": "code-for-ibmi.environment.action.runOnEditor", "when": "never" }, { - "command": "code-for-ibmi.context.variable.declare", + "command": "code-for-ibmi.environment.variable.declare", "when": "never" }, { - "command": "code-for-ibmi.context.variable.edit", + "command": "code-for-ibmi.environment.variable.edit", "when": "never" }, { - "command": "code-for-ibmi.context.variable.rename", + "command": "code-for-ibmi.environment.variable.rename", "when": "never" }, { - "command": "code-for-ibmi.context.variable.copy", + "command": "code-for-ibmi.environment.variable.copy", "when": "never" }, { - "command": "code-for-ibmi.context.variable.delete", + "command": "code-for-ibmi.environment.variable.delete", "when": "never" }, { - "command": "code-for-ibmi.context.profile.create", + "command": "code-for-ibmi.environment.profile.create", "when": "never" }, { - "command": "code-for-ibmi.context.profile.fromCurrent", + "command": "code-for-ibmi.environment.profile.fromCurrent", "when": "never" }, { - "command": "code-for-ibmi.context.profile.rename", + "command": "code-for-ibmi.environment.profile.rename", "when": "never" }, { - "command": "code-for-ibmi.context.profile.copy", + "command": "code-for-ibmi.environment.profile.copy", "when": "never" }, { - "command": "code-for-ibmi.context.profile.delete", + "command": "code-for-ibmi.environment.profile.delete", "when": "never" }, { - "command": "code-for-ibmi.context.profile.activate", + "command": "code-for-ibmi.environment.profile.activate", "when": "never" }, { - "command": "code-for-ibmi.context.profile.runLiblistCommand", + "command": "code-for-ibmi.environment.profile.runLiblistCommand", "when": "never" }, { - "command": "code-for-ibmi.context.profile.unload", + "command": "code-for-ibmi.environment.profile.unload", "when": "never" } ], @@ -2631,8 +2631,8 @@ "when": "code-for-ibmi:term5250Halted" }, { - "command": "code-for-ibmi.context.refresh", - "when": "view === contextView", + "command": "code-for-ibmi.environment.refresh", + "when": "view === environmentView", "group": "navigation@99" } ], @@ -2716,27 +2716,27 @@ }, { "command": "code-for-ibmi.launchActionsSetup", - "when": "view === contextView && viewItem =~ /^actionsNode/", + "when": "view === environmentView && viewItem =~ /^actionsNode/", "group": "inline@02" }, { - "command": "code-for-ibmi.context.action.search", - "when": "view === contextView && viewItem === actionsNode", + "command": "code-for-ibmi.environment.action.search", + "when": "view === environmentView && viewItem === actionsNode", "group": "inline@10" }, { - "command": "code-for-ibmi.context.action.search.next", - "when": "view === contextView && viewItem === actionsNode", + "command": "code-for-ibmi.environment.action.search.next", + "when": "view === environmentView && viewItem === actionsNode", "group": "inline@11" }, { - "command": "code-for-ibmi.context.action.search.clear", - "when": "view === contextView && viewItem === actionsNode", + "command": "code-for-ibmi.environment.action.search.clear", + "when": "view === environmentView && viewItem === actionsNode", "group": "inline@12" }, { - "command": "code-for-ibmi.context.action.create", - "when": "view === contextView && viewItem =~ /^(actionsNode|actionTypeNode)/", + "command": "code-for-ibmi.environment.action.create", + "when": "view === environmentView && viewItem =~ /^(actionsNode|actionTypeNode)/", "group": "inline@01" }, { @@ -3095,83 +3095,83 @@ "group": "1_objActions@6" }, { - "command": "code-for-ibmi.context.action.runOnEditor", - "when": "view === contextView && ((viewItem =~ /^actionItemRemote/ && code-for-ibmi:editorCanRunRemoteAction) || (viewItem =~ /^actionItemLocal/ && code-for-ibmi:editorCanRunLocalAction))", + "command": "code-for-ibmi.environment.action.runOnEditor", + "when": "view === environmentView && ((viewItem =~ /^actionItemRemote/ && code-for-ibmi:editorCanRunRemoteAction) || (viewItem =~ /^actionItemLocal/ && code-for-ibmi:editorCanRunLocalAction))", "group": "inline@00" }, { - "command": "code-for-ibmi.context.action.rename", - "when": "view === contextView && viewItem =~ /^actionItem/", + "command": "code-for-ibmi.environment.action.rename", + "when": "view === environmentView && viewItem =~ /^actionItem/", "group": "00_actionItemAction01" }, { - "command": "code-for-ibmi.context.action.copy", - "when": "view === contextView && viewItem =~ /^actionItem/", + "command": "code-for-ibmi.environment.action.copy", + "when": "view === environmentView && viewItem =~ /^actionItem/", "group": "10_actionItemAction01" }, { - "command": "code-for-ibmi.context.action.delete", - "when": "view === contextView && viewItem =~ /^actionItem/", + "command": "code-for-ibmi.environment.action.delete", + "when": "view === environmentView && viewItem =~ /^actionItem/", "group": "20_actionItemAction01" }, { - "command": "code-for-ibmi.context.variable.declare", - "when": "view === contextView && viewItem =~ /^customVariablesNode/", + "command": "code-for-ibmi.environment.variable.declare", + "when": "view === environmentView && viewItem =~ /^customVariablesNode/", "group": "inline@01" }, { - "command": "code-for-ibmi.context.variable.rename", - "when": "view === contextView && viewItem =~ /^customVariableItem/", + "command": "code-for-ibmi.environment.variable.rename", + "when": "view === environmentView && viewItem =~ /^customVariableItem/", "group": "00_customVariableItemAction01" }, { - "command": "code-for-ibmi.context.variable.copy", - "when": "view === contextView && viewItem =~ /^customVariableItem/", + "command": "code-for-ibmi.environment.variable.copy", + "when": "view === environmentView && viewItem =~ /^customVariableItem/", "group": "10_customVariableItemAction01" }, { - "command": "code-for-ibmi.context.variable.delete", - "when": "view === contextView && viewItem =~ /^customVariableItem/", + "command": "code-for-ibmi.environment.variable.delete", + "when": "view === environmentView && viewItem =~ /^customVariableItem/", "group": "20_customVariableItemAction01" }, { - "command": "code-for-ibmi.context.profile.create", - "when": "view === contextView && viewItem =~ /^profilesNode/", + "command": "code-for-ibmi.environment.profile.create", + "when": "view === environmentView && viewItem =~ /^profilesNode/", "group": "inline@01" }, { - "command": "code-for-ibmi.context.profile.fromCurrent", - "when": "view === contextView && viewItem =~ /^profilesNode/", + "command": "code-for-ibmi.environment.profile.fromCurrent", + "when": "view === environmentView && viewItem =~ /^profilesNode/", "group": "inline@02" }, { - "command": "code-for-ibmi.context.profile.unload", - "when": "view === contextView && viewItem =~ /^profilesNode/ && code-for-ibmi:activeProfile", + "command": "code-for-ibmi.environment.profile.unload", + "when": "view === environmentView && viewItem =~ /^profilesNode/ && code-for-ibmi:activeProfile", "group": "inline@03" }, { - "command": "code-for-ibmi.context.profile.activate", - "when": "view === contextView && viewItem =~ /^profileItem(?!_active)/", + "command": "code-for-ibmi.environment.profile.activate", + "when": "view === environmentView && viewItem =~ /^profileItem(?!_active)/", "group": "inline@01" }, { - "command": "code-for-ibmi.context.profile.runLiblistCommand", - "when": "view === contextView && viewItem =~ /^profileItem_active_command/", + "command": "code-for-ibmi.environment.profile.runLiblistCommand", + "when": "view === environmentView && viewItem =~ /^profileItem_active_command/", "group": "inline@01" }, { - "command": "code-for-ibmi.context.profile.rename", - "when": "view === contextView && viewItem =~ /^profileItem/", + "command": "code-for-ibmi.environment.profile.rename", + "when": "view === environmentView && viewItem =~ /^profileItem/", "group": "00_profileItemAction01" }, { - "command": "code-for-ibmi.context.profile.copy", - "when": "view === contextView && viewItem =~ /^profileItem/", + "command": "code-for-ibmi.environment.profile.copy", + "when": "view === environmentView && viewItem =~ /^profileItem/", "group": "10_profileItemAction01" }, { - "command": "code-for-ibmi.context.profile.delete", - "when": "view === contextView && viewItem =~ /^profileItem(?!_active)/", + "command": "code-for-ibmi.environment.profile.delete", + "when": "view === environmentView && viewItem =~ /^profileItem(?!_active)/", "group": "20_profileItemAction01" } ], diff --git a/src/extension.ts b/src/extension.ts index 6a7ef537b..29b3aec27 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,7 +28,7 @@ import { VscodeTools } from "./ui/Tools"; import { registerActionTools } from "./ui/actions"; import { initializeConnectionBrowser } from "./ui/views/ConnectionBrowser"; import { initializeLibraryListView } from "./ui/views/LibraryListView"; -import { initializeContextView } from "./ui/views/context/contextView"; +import { initializeContextView as initializeEnvironmentView } from "./ui/views/context/contextView"; import { initializeDebugBrowser } from "./ui/views/debugView"; import { HelpView } from "./ui/views/helpView"; import { initializeIFSBrowser } from "./ui/views/ifsBrowser"; @@ -63,7 +63,7 @@ export async function activate(context: ExtensionContext): Promise initializeDebugBrowser(context); initializeSearchView(context); initializeLibraryListView(context); - initializeContextView(context); + initializeEnvironmentView(context); context.subscriptions.push( window.registerTreeDataProvider( @@ -116,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.context.refresh"); + commands.executeCommand("code-for-ibmi.environment.refresh"); }); const customQsh = new CustomQSh(); diff --git a/src/instantiate.ts b/src/instantiate.ts index e21975c8f..37fe5d88c 100644 --- a/src/instantiate.ts +++ b/src/instantiate.ts @@ -112,7 +112,7 @@ async function updateConnectedBar() { const systemReadOnly = serverConfig?.codefori?.readOnlyMode || false; 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.context.actions.focus)`; + 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( diff --git a/src/ui/views/context/actions.ts b/src/ui/views/context/actions.ts index bd03082aa..14b45bad3 100644 --- a/src/ui/views/context/actions.ts +++ b/src/ui/views/context/actions.ts @@ -107,7 +107,7 @@ export class ActionItem extends ContextItem { this.setContext(); this.command = { title: "Edit action", - command: "code-for-ibmi.context.action.edit", + command: "code-for-ibmi.environment.action.edit", arguments: [this] } } diff --git a/src/ui/views/context/connectionProfiles.ts b/src/ui/views/context/connectionProfiles.ts index 6ee91f39c..a1ef774c3 100644 --- a/src/ui/views/context/connectionProfiles.ts +++ b/src/ui/views/context/connectionProfiles.ts @@ -44,7 +44,7 @@ export class ProfileItem extends ContextItem { this.command = { title: "Edit connection profile", - command: "code-for-ibmi.context.profile.edit", + command: "code-for-ibmi.environment.profile.edit", arguments: [this.profile] } } diff --git a/src/ui/views/context/contextItem.ts b/src/ui/views/context/contextItem.ts index 555c05395..99955add7 100644 --- a/src/ui/views/context/contextItem.ts +++ b/src/ui/views/context/contextItem.ts @@ -4,10 +4,10 @@ import { BrowserItem } from "../../types"; export class ContextItem extends BrowserItem { async refresh() { - await vscode.commands.executeCommand("code-for-ibmi.context.refresh.item", this); + await vscode.commands.executeCommand("code-for-ibmi.environment.refresh.item", this); } reveal(options?: FocusOptions) { - return vscode.commands.executeCommand(`code-for-ibmi.context.reveal`, this, options); + return vscode.commands.executeCommand(`code-for-ibmi.environment.reveal`, this, options); } } \ No newline at end of file diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/context/contextView.ts index 6f817060c..a8b1c066f 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/context/contextView.ts @@ -16,7 +16,7 @@ import { CustomVariableItem, CustomVariables, CustomVariablesNode } from './cust export function initializeContextView(context: vscode.ExtensionContext) { const contextView = new ContextView(); const contextTreeViewer = vscode.window.createTreeView( - `contextView`, { + `environmentView`, { treeDataProvider: contextView, showCollapseAll: true }); @@ -56,14 +56,14 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.refresh", () => contextView.refresh()), - vscode.commands.registerCommand("code-for-ibmi.context.refresh.item", (item: BrowserItem) => contextView.refresh(item)), - vscode.commands.registerCommand("code-for-ibmi.context.reveal", (item: BrowserItem, options?: FocusOptions) => contextTreeViewer.reveal(item, options)), + vscode.commands.registerCommand("code-for-ibmi.environment.refresh", () => contextView.refresh()), + vscode.commands.registerCommand("code-for-ibmi.environment.refresh.item", (item: BrowserItem) => contextView.refresh(item)), + vscode.commands.registerCommand("code-for-ibmi.environment.reveal", (item: BrowserItem, options?: FocusOptions) => contextTreeViewer.reveal(item, options)), - vscode.commands.registerCommand("code-for-ibmi.context.action.search", (node: ActionsNode) => node.searchActions()), - vscode.commands.registerCommand("code-for-ibmi.context.action.search.next", (node: ActionsNode) => node.goToNextSearchMatch()), - vscode.commands.registerCommand("code-for-ibmi.context.action.search.clear", (node: ActionsNode) => node.clearSearch()), - vscode.commands.registerCommand("code-for-ibmi.context.action.create", async (node: ActionsNode | ActionTypeNode, from?: ActionItem) => { + 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); @@ -84,11 +84,11 @@ export function initializeContextView(context: vscode.ExtensionContext) { }; await updateAction(action, typeNode.workspace); contextView.actionsNode?.forceRefresh(); - vscode.commands.executeCommand("code-for-ibmi.context.action.edit", { action, workspace: typeNode.workspace }); + vscode.commands.executeCommand("code-for-ibmi.environment.action.edit", { action, workspace: typeNode.workspace }); } } }), - vscode.commands.registerCommand("code-for-ibmi.context.action.rename", async (node: ActionItem) => { + vscode.commands.registerCommand("code-for-ibmi.environment.action.rename", async (node: ActionItem) => { const action = node.action; const existingNames = (await getActions(node.workspace)).filter(act => act.name === action.name).map(act => act.name); @@ -104,22 +104,22 @@ export function initializeContextView(context: vscode.ExtensionContext) { contextView.actionsNode?.forceRefresh(); } }), - vscode.commands.registerCommand("code-for-ibmi.context.action.edit", (node: ActionItem) => { + vscode.commands.registerCommand("code-for-ibmi.environment.action.edit", (node: ActionItem) => { editAction(node.action, async () => contextView.actionsNode?.forceRefresh(), node.workspace); }), - vscode.commands.registerCommand("code-for-ibmi.context.action.copy", async (node: ActionItem) => { - vscode.commands.executeCommand('code-for-ibmi.context.action.create', node.parent, node); + 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.context.action.delete", async (node: ActionItem) => { + vscode.commands.registerCommand("code-for-ibmi.environment.action.delete", async (node: ActionItem) => { 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 }); contextView.actionsNode?.forceRefresh(); } }), - vscode.commands.registerCommand("code-for-ibmi.context.action.runOnEditor", (node: ActionItem) => { + 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.context.action.edit", node); + 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) { @@ -142,9 +142,9 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.executeCommand(`code-for-ibmi.runAction`, uri, undefined, action, undefined, workspace); } }), - vscode.commands.registerCommand("code-for-ibmi.context.actions.focus", () => contextView.actionsNode?.reveal({ focus: true, expand: true })), + vscode.commands.registerCommand("code-for-ibmi.environment.actions.focus", () => contextView.actionsNode?.reveal({ focus: true, expand: true })), - vscode.commands.registerCommand("code-for-ibmi.context.variable.declare", async (variablesNode: CustomVariablesNode, from?: CustomVariable) => { + 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'), @@ -158,11 +158,11 @@ export function initializeContextView(context: vscode.ExtensionContext) { await CustomVariables.update(variable); contextView.refresh(variablesNode); if (!from) { - vscode.commands.executeCommand("code-for-ibmi.context.variable.edit", variable, variablesNode); + vscode.commands.executeCommand("code-for-ibmi.environment.variable.edit", variable, variablesNode); } } }), - vscode.commands.registerCommand("code-for-ibmi.context.variable.edit", async (variable: CustomVariable, variablesNode?: CustomVariablesNode) => { + 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; @@ -170,7 +170,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { contextView.refresh(variablesNode); } }), - vscode.commands.registerCommand("code-for-ibmi.context.variable.rename", async (variableItem: CustomVariableItem) => { + 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({ @@ -184,10 +184,10 @@ export function initializeContextView(context: vscode.ExtensionContext) { contextView.refresh(variableItem.parent); } }), - vscode.commands.registerCommand("code-for-ibmi.context.variable.copy", async (variableItem: CustomVariableItem) => { - vscode.commands.executeCommand("code-for-ibmi.context.variable.declare", variableItem.parent, variableItem.customVariable); + 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.context.variable.delete", async (variableItem: CustomVariableItem) => { + 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 }); @@ -195,7 +195,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.create", async (node?: ProfilesNode, from?: ConnectionProfile) => { + 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({ @@ -220,30 +220,30 @@ export function initializeContextView(context: vscode.ExtensionContext) { await updateConnectionProfile(profile); contextView.refresh(contextView.profilesNode); if (!from) { - vscode.commands.executeCommand("code-for-ibmi.context.profile.edit", profile); + 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.context.profile.activate", profile); + vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", profile); } }) } } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.fromCurrent", async (profilesNode: ProfilesNode) => { + 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.context.profile.create", undefined, current); + vscode.commands.executeCommand("code-for-ibmi.environment.profile.create", undefined, current); } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.edit", async (profile: ConnectionProfile) => { + vscode.commands.registerCommand("code-for-ibmi.environment.profile.edit", async (profile: ConnectionProfile) => { editConnectionProfile(profile, async () => contextView.refresh(contextView.profilesNode)) }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.rename", async (item: ProfileItem) => { + vscode.commands.registerCommand("code-for-ibmi.environment.profile.rename", async (item: ProfileItem) => { const currentName = item.profile.name; const existingNames = getConnectionProfiles().map(profile => profile.name).filter(name => name !== currentName); const newName = await vscode.window.showInputBox({ @@ -263,16 +263,16 @@ export function initializeContextView(context: vscode.ExtensionContext) { contextView.refresh(contextView.profilesNode); } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.copy", async (item: ProfileItem) => { - vscode.commands.executeCommand("code-for-ibmi.context.profile.create", undefined, item.profile); + 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.context.profile.delete", async (item: ProfileItem) => { + vscode.commands.registerCommand("code-for-ibmi.environment.profile.delete", async (item: ProfileItem) => { 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 }); contextView.refresh(contextView.profilesNode); } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.activate", async (item: ProfileItem | ConnectionProfile) => { + vscode.commands.registerCommand("code-for-ibmi.environment.profile.activate", async (item: ProfileItem | ConnectionProfile) => { const connection = instance.getConnection(); const storage = instance.getStorage(); if (connection && storage) { @@ -295,7 +295,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { contextView.refresh(); if (profile.name && profile.setLibraryListCommand) { - await vscode.commands.executeCommand("code-for-ibmi.context.profile.runLiblistCommand", profile); + await vscode.commands.executeCommand("code-for-ibmi.environment.profile.runLiblistCommand", profile); } await updateUIContext(profile.name); @@ -303,7 +303,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.runLiblistCommand", async (profileItem?: ProfileItem | ConnectionProfile) => { + vscode.commands.registerCommand("code-for-ibmi.environment.profile.runLiblistCommand", async (profileItem?: ProfileItem | ConnectionProfile) => { const connection = instance.getConnection(); const storage = instance.getStorage(); if (connection && storage) { @@ -337,8 +337,8 @@ export function initializeContextView(context: vscode.ExtensionContext) { } } }), - vscode.commands.registerCommand("code-for-ibmi.context.profile.unload", async () => { - vscode.commands.executeCommand("code-for-ibmi.context.profile.activate", getDefaultProfile()); + vscode.commands.registerCommand("code-for-ibmi.environment.profile.unload", async () => { + vscode.commands.executeCommand("code-for-ibmi.environment.profile.activate", getDefaultProfile()); }) ); diff --git a/src/ui/views/context/customVariables.ts b/src/ui/views/context/customVariables.ts index f82d2675a..6de26268f 100644 --- a/src/ui/views/context/customVariables.ts +++ b/src/ui/views/context/customVariables.ts @@ -65,7 +65,7 @@ export class CustomVariableItem extends ContextItem { this.command = { title: "Change value", - command: "code-for-ibmi.context.variable.edit", + command: "code-for-ibmi.environment.variable.edit", arguments: [this.customVariable] } } From 5b6d20acd70cc2380232d1ac9e2006b1f2994d4a Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 19:16:49 +0100 Subject: [PATCH 23/38] Changed profiles set/unload icons Signed-off-by: Seb Julliand --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f38e9e0f3..ea66d8515 100644 --- a/package.json +++ b/package.json @@ -1742,7 +1742,7 @@ "enablement": "code-for-ibmi:connected", "category": "IBM i", "title": "Set as active profile", - "icon": "$(arrow-circle-right)" + "icon": "$(arrow-swap)" }, { "command": "code-for-ibmi.environment.profile.runLiblistCommand", @@ -1756,7 +1756,7 @@ "enablement": "code-for-ibmi:connected", "category": "IBM i", "title": "Unload active profile", - "icon": "$(debug-restart)" + "icon": "$(sign-out)" } ], "customEditors": [ From 44490e1ad2ac8cc0d491eebbf6d885a228370af4 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 19:36:32 +0100 Subject: [PATCH 24/38] Fixed local actions setup + show warning when file exists Signed-off-by: Seb Julliand --- src/filesystems/local/deployTools.ts | 36 +++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/filesystems/local/deployTools.ts b/src/filesystems/local/deployTools.ts index e8867d126..68349a79e 100644 --- a/src/filesystems/local/deployTools.ts +++ b/src/filesystems/local/deployTools.ts @@ -1,18 +1,19 @@ +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 { 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 +26,19 @@ 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.`); + + if (!existsSync(localActionsUri.fsPath) || await vscode.window.showWarningMessage(l10n.t("Local actions are already defined for this workspace. Do you want to overwrite them?"), { modal: true }, l10n.t("Yes"))) { + 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.`); + } } } } From 941c394c94a54bdb2c9ea6058d59f8b125c8ec7c Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 19:40:12 +0100 Subject: [PATCH 25/38] Fully renamed context view to environment view Signed-off-by: Seb Julliand --- src/extension.ts | 2 +- .../views/{context => environment}/actions.ts | 12 ++--- .../connectionProfiles.ts | 8 ++-- .../customVariables.ts | 8 ++-- .../environmentItem.ts} | 2 +- .../environmentView.ts} | 48 +++++++++---------- 6 files changed, 40 insertions(+), 40 deletions(-) rename src/ui/views/{context => environment}/actions.ts (90%) rename src/ui/views/{context => environment}/connectionProfiles.ts (87%) rename src/ui/views/{context => environment}/customVariables.ts (89%) rename src/ui/views/{context/contextItem.ts => environment/environmentItem.ts} (88%) rename src/ui/views/{context/contextView.ts => environment/environmentView.ts} (92%) diff --git a/src/extension.ts b/src/extension.ts index 29b3aec27..44806b99a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,8 +28,8 @@ import { VscodeTools } from "./ui/Tools"; import { registerActionTools } from "./ui/actions"; import { initializeConnectionBrowser } from "./ui/views/ConnectionBrowser"; import { initializeLibraryListView } from "./ui/views/LibraryListView"; -import { initializeContextView as initializeEnvironmentView } from "./ui/views/context/contextView"; 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"; diff --git a/src/ui/views/context/actions.ts b/src/ui/views/environment/actions.ts similarity index 90% rename from src/ui/views/context/actions.ts rename to src/ui/views/environment/actions.ts index 14b45bad3..3b111b369 100644 --- a/src/ui/views/context/actions.ts +++ b/src/ui/views/environment/actions.ts @@ -1,7 +1,7 @@ import vscode, { l10n } from "vscode"; import { getActions } from "../../../api/actions"; import { Action, ActionType } from "../../../typings"; -import { ContextItem } from "./contextItem"; +import { EnvironmentItem } from "./environmentItem"; export namespace Actions { export function validateName(name: string, names: string[]) { @@ -14,7 +14,7 @@ export namespace Actions { } } -export class ActionsNode extends ContextItem { +export class ActionsNode extends EnvironmentItem { private readonly foundActions: ActionItem[] = []; private revealIndex = -1; @@ -84,9 +84,9 @@ export class ActionsNode extends ContextItem { } } -export class ActionTypeNode extends ContextItem { +export class ActionTypeNode extends EnvironmentItem { readonly actionItems: ActionItem[]; - constructor(parent: ContextItem, label: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { + constructor(parent: EnvironmentItem, label: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { super(label, { parent, state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = `actionTypeNode_${type}`; this.description = workspace ? l10n.t("workspace actions") : undefined; @@ -98,11 +98,11 @@ export class ActionTypeNode extends ContextItem { } } -export class ActionItem extends ContextItem { +export class ActionItem extends EnvironmentItem { static matchedColor = "charts.yellow"; static contextValue = `actionItem`; - constructor(parent: ContextItem, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { + constructor(parent: EnvironmentItem, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { super(action.name, { parent }); this.setContext(); this.command = { diff --git a/src/ui/views/context/connectionProfiles.ts b/src/ui/views/environment/connectionProfiles.ts similarity index 87% rename from src/ui/views/context/connectionProfiles.ts rename to src/ui/views/environment/connectionProfiles.ts index a1ef774c3..b5248f2cb 100644 --- a/src/ui/views/context/connectionProfiles.ts +++ b/src/ui/views/environment/connectionProfiles.ts @@ -3,7 +3,7 @@ import { getConnectionProfiles } from "../../../api/connectionProfiles"; import { instance } from "../../../instantiate"; import { ConnectionProfile } from "../../../typings"; import { VscodeTools } from "../../Tools"; -import { ContextItem } from "./contextItem"; +import { EnvironmentItem } from "./environmentItem"; export namespace ConnectionProfiles { export function validateName(name: string, names: string[]) { @@ -16,7 +16,7 @@ export namespace ConnectionProfiles { } } -export class ProfilesNode extends ContextItem { +export class ProfilesNode extends EnvironmentItem { constructor() { super(l10n.t("Profiles"), { state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = "profilesNode"; @@ -30,11 +30,11 @@ export class ProfilesNode extends ContextItem { } } -export class ProfileItem extends ContextItem { +export class ProfileItem extends EnvironmentItem { static contextValue = `profileItem`; static activeColor = "charts.green"; - constructor(parent: ContextItem, readonly profile: ConnectionProfile, active: boolean) { + 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' : ''}`; diff --git a/src/ui/views/context/customVariables.ts b/src/ui/views/environment/customVariables.ts similarity index 89% rename from src/ui/views/context/customVariables.ts rename to src/ui/views/environment/customVariables.ts index 6de26268f..506f99ff8 100644 --- a/src/ui/views/context/customVariables.ts +++ b/src/ui/views/environment/customVariables.ts @@ -2,7 +2,7 @@ import vscode, { l10n } from "vscode"; import IBMi from "../../../api/IBMi"; import { instance } from "../../../instantiate"; import { CustomVariable } from "../../../typings"; -import { ContextItem } from "./contextItem"; +import { EnvironmentItem } from "./environmentItem"; export namespace CustomVariables { export function getAll() { @@ -46,7 +46,7 @@ export namespace CustomVariables { } } -export class CustomVariablesNode extends ContextItem { +export class CustomVariablesNode extends EnvironmentItem { constructor() { super(l10n.t("Custom Variables"), { state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = `customVariablesNode`; @@ -57,8 +57,8 @@ export class CustomVariablesNode extends ContextItem { } } -export class CustomVariableItem extends ContextItem { - constructor(parent: ContextItem, readonly customVariable: 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; diff --git a/src/ui/views/context/contextItem.ts b/src/ui/views/environment/environmentItem.ts similarity index 88% rename from src/ui/views/context/contextItem.ts rename to src/ui/views/environment/environmentItem.ts index 99955add7..a8f432546 100644 --- a/src/ui/views/context/contextItem.ts +++ b/src/ui/views/environment/environmentItem.ts @@ -2,7 +2,7 @@ import vscode from "vscode"; import { FocusOptions } from "../../../typings"; import { BrowserItem } from "../../types"; -export class ContextItem extends BrowserItem { +export class EnvironmentItem extends BrowserItem { async refresh() { await vscode.commands.executeCommand("code-for-ibmi.environment.refresh.item", this); } diff --git a/src/ui/views/context/contextView.ts b/src/ui/views/environment/environmentView.ts similarity index 92% rename from src/ui/views/context/contextView.ts rename to src/ui/views/environment/environmentView.ts index a8b1c066f..f5c0c4058 100644 --- a/src/ui/views/context/contextView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -13,22 +13,22 @@ import { ActionItem, Actions, ActionsNode, ActionTypeNode } from './actions'; import { ConnectionProfiles, ProfileItem, ProfilesNode } from './connectionProfiles'; import { CustomVariableItem, CustomVariables, CustomVariablesNode } from './customVariables'; -export function initializeContextView(context: vscode.ExtensionContext) { - const contextView = new ContextView(); - const contextTreeViewer = vscode.window.createTreeView( +export function initializeEnvironmentView(context: vscode.ExtensionContext) { + const environmentView = new EnvironmentView(); + const environmentTreeViewer = vscode.window.createTreeView( `environmentView`, { - treeDataProvider: contextView, + treeDataProvider: environmentView, showCollapseAll: true }); const updateUIContext = async (profileName?: string) => { await vscode.commands.executeCommand(`setContext`, "code-for-ibmi:activeProfile", profileName); - contextTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); + environmentTreeViewer.description = profileName ? l10n.t("Current profile: {0}", profileName) : l10n.t("No active profile"); vscode.commands.executeCommand("code-for-ibmi.updateConnectedBar"); }; context.subscriptions.push( - contextTreeViewer, + environmentTreeViewer, vscode.window.onDidChangeActiveTextEditor(async editor => { let editorCanRunAction = false; let editorCanRunLocalAction = false; @@ -56,9 +56,9 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), - vscode.commands.registerCommand("code-for-ibmi.environment.refresh", () => contextView.refresh()), - vscode.commands.registerCommand("code-for-ibmi.environment.refresh.item", (item: BrowserItem) => contextView.refresh(item)), - vscode.commands.registerCommand("code-for-ibmi.environment.reveal", (item: BrowserItem, options?: FocusOptions) => contextTreeViewer.reveal(item, options)), + 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()), @@ -83,7 +83,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { command: '' }; await updateAction(action, typeNode.workspace); - contextView.actionsNode?.forceRefresh(); + environmentView.actionsNode?.forceRefresh(); vscode.commands.executeCommand("code-for-ibmi.environment.action.edit", { action, workspace: typeNode.workspace }); } } @@ -101,11 +101,11 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (newName) { await updateAction(action, node.workspace, { newName }); - contextView.actionsNode?.forceRefresh(); + environmentView.actionsNode?.forceRefresh(); } }), vscode.commands.registerCommand("code-for-ibmi.environment.action.edit", (node: ActionItem) => { - editAction(node.action, async () => contextView.actionsNode?.forceRefresh(), node.workspace); + 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); @@ -113,7 +113,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.environment.action.delete", async (node: ActionItem) => { 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 }); - contextView.actionsNode?.forceRefresh(); + environmentView.actionsNode?.forceRefresh(); } }), vscode.commands.registerCommand("code-for-ibmi.environment.action.runOnEditor", (node: ActionItem) => { @@ -142,7 +142,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.executeCommand(`code-for-ibmi.runAction`, uri, undefined, action, undefined, workspace); } }), - vscode.commands.registerCommand("code-for-ibmi.environment.actions.focus", () => contextView.actionsNode?.reveal({ focus: true, expand: true })), + 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); @@ -156,7 +156,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (name) { const variable = { name, value: from?.value } as CustomVariable; await CustomVariables.update(variable); - contextView.refresh(variablesNode); + environmentView.refresh(variablesNode); if (!from) { vscode.commands.executeCommand("code-for-ibmi.environment.variable.edit", variable, variablesNode); } @@ -167,7 +167,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (value !== undefined) { variable.value = value; await CustomVariables.update(variable); - contextView.refresh(variablesNode); + environmentView.refresh(variablesNode); } }), vscode.commands.registerCommand("code-for-ibmi.environment.variable.rename", async (variableItem: CustomVariableItem) => { @@ -181,7 +181,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { if (newName) { await CustomVariables.update(variable, { newName }); - contextView.refresh(variableItem.parent); + environmentView.refresh(variableItem.parent); } }), vscode.commands.registerCommand("code-for-ibmi.environment.variable.copy", async (variableItem: CustomVariableItem) => { @@ -191,7 +191,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { 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 }); - contextView.refresh(variableItem.parent); + environmentView.refresh(variableItem.parent); } }), @@ -218,7 +218,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { objectFilters: [], }; await updateConnectionProfile(profile); - contextView.refresh(contextView.profilesNode); + environmentView.refresh(environmentView.profilesNode); if (!from) { vscode.commands.executeCommand("code-for-ibmi.environment.profile.edit", profile); } @@ -241,7 +241,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { } }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.edit", async (profile: ConnectionProfile) => { - editConnectionProfile(profile, async () => contextView.refresh(contextView.profilesNode)) + editConnectionProfile(profile, async () => environmentView.refresh(environmentView.profilesNode)) }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.rename", async (item: ProfileItem) => { const currentName = item.profile.name; @@ -260,7 +260,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { await IBMi.connectionManager.update(config); updateUIContext(newName); } - contextView.refresh(contextView.profilesNode); + environmentView.refresh(environmentView.profilesNode); } }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.copy", async (item: ProfileItem) => { @@ -269,7 +269,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.registerCommand("code-for-ibmi.environment.profile.delete", async (item: ProfileItem) => { 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 }); - contextView.refresh(contextView.profilesNode); + environmentView.refresh(environmentView.profilesNode); } }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.activate", async (item: ProfileItem | ConnectionProfile) => { @@ -292,7 +292,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { vscode.commands.executeCommand(`code-for-ibmi.refreshIFSBrowser`), vscode.commands.executeCommand(`code-for-ibmi.refreshObjectBrowser`) ]); - contextView.refresh(); + environmentView.refresh(); if (profile.name && profile.setLibraryListCommand) { await vscode.commands.executeCommand("code-for-ibmi.environment.profile.runLiblistCommand", profile); @@ -360,7 +360,7 @@ export function initializeContextView(context: vscode.ExtensionContext) { }); } -class ContextView implements vscode.TreeDataProvider { +class EnvironmentView implements vscode.TreeDataProvider { private readonly emitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.emitter.event; actionsNode?: ActionsNode From 28aceb1427186a4530abc2939088887ff8f6af49 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 19:52:01 +0100 Subject: [PATCH 26/38] Ensure a document is dirty before saving it Signed-off-by: Seb Julliand --- src/editors/customEditorProvider.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/editors/customEditorProvider.ts b/src/editors/customEditorProvider.ts index c7a32a8bf..e12730673 100644 --- a/src/editors/customEditorProvider.ts +++ b/src/editors/customEditorProvider.ts @@ -6,6 +6,7 @@ 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) { super(); @@ -47,6 +48,7 @@ export class CustomEditor extends CustomHTML implements vscode.CustomDocument } onDataChange(data: T & { valid?: boolean }) { + this.dirty = true; this.valid = data.valid; delete data.valid; this.data = data; @@ -66,11 +68,14 @@ export class CustomEditorProvider implements vscode.CustomEditorProvider, cancellation: vscode.CancellationToken) { - if (document.valid) { - await document.save(); - } - else { - throw new Error("Can't save: some inputs are invalid"); + if (document.dirty) { + if (document.valid) { + await document.save(); + document.dirty = false; + } + else { + throw new Error("Can't save: some inputs are invalid"); + } } } From 5806194baee5e7f4bedb039bf98181dd038a8476 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 20:07:11 +0100 Subject: [PATCH 27/38] Fixed local action creation throwing an error Signed-off-by: Seb Julliand --- src/api/actions.ts | 2 +- src/ui/views/environment/environmentView.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/actions.ts b/src/api/actions.ts index 1d2f12353..710a3e618 100644 --- a/src/api/actions.ts +++ b/src/api/actions.ts @@ -60,7 +60,7 @@ async function getLocalActions(currentWorkspace: vscode.WorkspaceFolder) { typeof action.name === `string` && typeof action.command === `string` && [`ile`, `pase`, `qsh`].includes(action.environment) && - Array.isArray(action.extensions) + (!action.extensions || Array.isArray(action.extensions)) ) { actions.push({ ...action, diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index f5c0c4058..b72a310af 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -7,7 +7,7 @@ import IBMi from '../../../api/IBMi'; import { editAction } from '../../../editors/actionEditor'; import { editConnectionProfile } from '../../../editors/connectionProfileEditor'; import { instance } from '../../../instantiate'; -import { ActionEnvironment, BrowserItem, ConnectionProfile, CustomVariable, FocusOptions } from '../../../typings'; +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'; @@ -76,7 +76,7 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { }); if (name) { - const action = from ? { ...from.action, name } : { + const action : Action = from ? { ...from.action, name } : { name, type: typeNode.type, environment: "ile" as ActionEnvironment, From 6d4bf982e284d5518e41890c37c06a4bc8d7948d Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 20:46:04 +0100 Subject: [PATCH 28/38] Added onClosed callback for custom editors Signed-off-by: Seb Julliand --- src/editors/customEditorProvider.ts | 6 +++--- src/extension.ts | 2 +- src/typings.ts | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/editors/customEditorProvider.ts b/src/editors/customEditorProvider.ts index e12730673..f0e597d08 100644 --- a/src/editors/customEditorProvider.ts +++ b/src/editors/customEditorProvider.ts @@ -8,7 +8,7 @@ export class CustomEditor extends CustomHTML implements vscode.CustomDocument valid?: boolean; dirty = false; - constructor(target: string, private readonly onSave: (data: T) => Promise) { + constructor(target: string, private readonly onSave: (data: T) => Promise, private readonly onClosed?: () => void) { super(); this.uri = vscode.Uri.from({ scheme: "code4i", path: `/${target}` }); } @@ -58,8 +58,8 @@ export class CustomEditor extends CustomHTML implements vscode.CustomDocument await this.onSave(this.data); } - dispose(): void { - //nothing to dispose of + dispose() { + this.onClosed?.(); } } diff --git a/src/extension.ts b/src/extension.ts index 44806b99a..bf0b266f3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -137,7 +137,7 @@ export async function activate(context: ExtensionContext): Promise return { instance, customUI: () => new CustomUI(), - customEditor: (target, onSave) => new CustomEditor(target, onSave), + customEditor: (target, onSave, onClosed) => new CustomEditor(target, onSave, onClosed), deployTools: DeployTools, evfeventParser: parseErrors, tools: VscodeTools, diff --git a/src/typings.ts b/src/typings.ts index 7e963faa3..ab15e87f8 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -11,7 +11,7 @@ import { CustomUI } from "./webviews/CustomUI"; export interface CodeForIBMi { instance: Instance, customUI: () => CustomUI, - customEditor: (target: string, onSave: (data: T) => Promise) => CustomEditor, + customEditor: (target: string, onSave: (data: T) => Promise, onClosed?: () => void) => CustomEditor, deployTools: typeof DeployTools, evfeventParser: (lines: string[]) => Map, tools: typeof VscodeTools, @@ -27,3 +27,4 @@ export interface DeploymentParameters { export * from "./api/types"; export * from "./ui/types"; + From c9fc4762e0eb90a709e96747d1e4a2a5f3c1f272 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 20 Nov 2025 20:46:31 +0100 Subject: [PATCH 29/38] Prevent actions and profiles from being renamed/deleted when being edited Signed-off-by: Seb Julliand --- src/editors/actionEditor.ts | 10 ++- src/editors/connectionProfileEditor.ts | 10 ++- src/ui/views/environment/environmentView.ts | 76 +++++++++++++-------- 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/src/editors/actionEditor.ts b/src/editors/actionEditor.ts index 7fbb0ba85..4238446b0 100644 --- a/src/editors/actionEditor.ts +++ b/src/editors/actionEditor.ts @@ -29,9 +29,15 @@ type ActionData = { 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 = instance.getConnection()?.getConfig().customVariables.map(variable => `
  • &${variable.name}: ${variable.value}
  • `).join(``); - new CustomEditor(`${targetAction.name}.action`, (actionData) => save(targetAction, actionData, workspace).then(doAfterSave)) + 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`), @@ -125,6 +131,8 @@ export function editAction(targetAction: Action, doAfterSave?: () => Thenable&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) { diff --git a/src/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts index e5244052e..3aad1fd6c 100644 --- a/src/editors/connectionProfileEditor.ts +++ b/src/editors/connectionProfileEditor.ts @@ -11,9 +11,15 @@ type ConnectionProfileData = { 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 = instance.getConnection()?.getConfig().currentProfile === profile.name; - new CustomEditor(`${profile.name}.profile`, data => save(profile, data).then(doAfterSave)) + 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 }) @@ -28,6 +34,8 @@ export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: .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) { diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index b72a310af..3a6b4668d 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -4,8 +4,8 @@ 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 } from '../../../editors/actionEditor'; -import { editConnectionProfile } from '../../../editors/connectionProfileEditor'; +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'; @@ -76,7 +76,7 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { }); if (name) { - const action : Action = from ? { ...from.action, name } : { + const action: Action = from ? { ...from.action, name } : { name, type: typeNode.type, environment: "ile" as ActionEnvironment, @@ -90,18 +90,23 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { }), vscode.commands.registerCommand("code-for-ibmi.environment.action.rename", async (node: ActionItem) => { const action = node.action; - const existingNames = (await getActions(node.workspace)).filter(act => act.name === action.name).map(act => act.name); + 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) - }); + 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(); + if (newName) { + await updateAction(action, node.workspace, { newName }); + environmentView.actionsNode?.forceRefresh(); + } } }), vscode.commands.registerCommand("code-for-ibmi.environment.action.edit", (node: ActionItem) => { @@ -111,7 +116,10 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { 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 (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete action '{0}' ?", node.action.name), { modal: true }, l10n.t("Yes"))) { + 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(); } @@ -244,30 +252,38 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { editConnectionProfile(profile, async () => environmentView.refresh(environmentView.profilesNode)) }), vscode.commands.registerCommand("code-for-ibmi.environment.profile.rename", async (item: ProfileItem) => { - 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 (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); + 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); } - 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 (await vscode.window.showInformationMessage(l10n.t("Do you really want to delete profile '{0}' ?", item.profile.name), { modal: true }, l10n.t("Yes"))) { + 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); } From 4c4e6bdc04c3db255c83b622f349118216808bcc Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 30 Nov 2025 12:03:14 +0100 Subject: [PATCH 30/38] Use CustomVariables.getAll where applicable Signed-off-by: Seb Julliand --- src/editors/actionEditor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editors/actionEditor.ts b/src/editors/actionEditor.ts index 4238446b0..c889c03a8 100644 --- a/src/editors/actionEditor.ts +++ b/src/editors/actionEditor.ts @@ -1,8 +1,8 @@ import vscode from "vscode"; import { updateAction } from "../api/actions"; import { Tools } from "../api/Tools"; -import { instance } from "../instantiate"; import { Action, ActionEnvironment, ActionRefresh, ActionType } from "../typings"; +import { CustomVariables } from "../ui/views/environment/customVariables"; import { Tab } from "../webviews/CustomUI"; import { CustomEditor } from "./customEditorProvider"; @@ -36,7 +36,7 @@ export function isActionEdited(action: Action) { } export function editAction(targetAction: Action, doAfterSave?: () => Thenable, workspace?: vscode.WorkspaceFolder) { - const customVariables = instance.getConnection()?.getConfig().customVariables.map(variable => `
  • &${variable.name}: ${variable.value}
  • `).join(``); + 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`, From 55e677d8be5245958074cdebb2eccaf0169d4e52 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 30 Nov 2025 12:20:25 +0100 Subject: [PATCH 31/38] Fixed variables created with undefined values Signed-off-by: Seb Julliand --- src/ui/views/environment/environmentView.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index 3a6b4668d..ecc65ba69 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -163,9 +163,10 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { if (name) { const variable = { name, value: from?.value } as CustomVariable; - await CustomVariables.update(variable); - environmentView.refresh(variablesNode); - if (!from) { + if (from) { + await CustomVariables.update(variable); + environmentView.refresh(variablesNode); + } else { vscode.commands.executeCommand("code-for-ibmi.environment.variable.edit", variable, variablesNode); } } From 6d1e68953404ab8705d66723ace826098e5c8c54 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 30 Nov 2025 12:20:37 +0100 Subject: [PATCH 32/38] Avoid crash if variable value is undefined Signed-off-by: Seb Julliand --- src/api/variables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 || ''); } } } From 3e2152dec50c52d7c7d3c89e2e424926e8a6be7f Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 30 Nov 2025 17:20:11 +0100 Subject: [PATCH 33/38] Added icons on environment Nodes Signed-off-by: Seb Julliand --- src/ui/views/environment/actions.ts | 15 +++++++-------- src/ui/views/environment/connectionProfiles.ts | 2 +- src/ui/views/environment/customVariables.ts | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/ui/views/environment/actions.ts b/src/ui/views/environment/actions.ts index 3b111b369..d45c6816f 100644 --- a/src/ui/views/environment/actions.ts +++ b/src/ui/views/environment/actions.ts @@ -21,7 +21,7 @@ export class ActionsNode extends EnvironmentItem { private readonly children: ActionTypeNode[] = []; constructor() { - super(l10n.t("Actions"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + super(l10n.t("Actions"), { icon: "code-oss", state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = "actionsNode"; } @@ -37,10 +37,10 @@ export class ActionsNode extends EnvironmentItem { } this.children.push( - new ActionTypeNode(this, l10n.t("Member"), 'member', actions), - new ActionTypeNode(this, l10n.t("Object"), 'object', actions), - new ActionTypeNode(this, l10n.t("Streamfile"), 'streamfile', actions), - ...Array.from(localActions).map((([workspace, localActions]) => new ActionTypeNode(this, workspace.name, 'file', localActions, workspace))) + 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))) ); } return this.children; @@ -86,10 +86,9 @@ export class ActionsNode extends EnvironmentItem { export class ActionTypeNode extends EnvironmentItem { readonly actionItems: ActionItem[]; - constructor(parent: EnvironmentItem, label: string, readonly type: ActionType, actions: Action[], readonly workspace?: vscode.WorkspaceFolder) { - super(label, { parent, state: vscode.TreeItemCollapsibleState.Collapsed }); + 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.description = workspace ? l10n.t("workspace actions") : undefined; this.actionItems = actions.filter(action => action.type === type).map(action => new ActionItem(this, action, workspace)); } diff --git a/src/ui/views/environment/connectionProfiles.ts b/src/ui/views/environment/connectionProfiles.ts index b5248f2cb..e166625b0 100644 --- a/src/ui/views/environment/connectionProfiles.ts +++ b/src/ui/views/environment/connectionProfiles.ts @@ -18,7 +18,7 @@ export namespace ConnectionProfiles { export class ProfilesNode extends EnvironmentItem { constructor() { - super(l10n.t("Profiles"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + super(l10n.t("Profiles"), { icon: "account", state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = "profilesNode"; } diff --git a/src/ui/views/environment/customVariables.ts b/src/ui/views/environment/customVariables.ts index 506f99ff8..c4b38650c 100644 --- a/src/ui/views/environment/customVariables.ts +++ b/src/ui/views/environment/customVariables.ts @@ -48,7 +48,7 @@ export namespace CustomVariables { export class CustomVariablesNode extends EnvironmentItem { constructor() { - super(l10n.t("Custom Variables"), { state: vscode.TreeItemCollapsibleState.Collapsed }); + super(l10n.t("Custom Variables"), { icon: "variable-group", state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = `customVariablesNode`; } From 67e989e5eea121ae1751af871bf699bc4651280c Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 30 Nov 2025 23:35:02 +0100 Subject: [PATCH 34/38] Enable action run command based on active editor + highlight Signed-off-by: Seb Julliand --- package.json | 2 +- src/ui/views/environment/actions.ts | 70 ++++++++++++++++++--- src/ui/views/environment/environmentView.ts | 31 ++++----- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 714746589..bf7b2079a 100644 --- a/package.json +++ b/package.json @@ -3096,7 +3096,7 @@ }, { "command": "code-for-ibmi.environment.action.runOnEditor", - "when": "view === environmentView && ((viewItem =~ /^actionItemRemote/ && code-for-ibmi:editorCanRunRemoteAction) || (viewItem =~ /^actionItemLocal/ && code-for-ibmi:editorCanRunLocalAction))", + "when": "view === environmentView && viewItem =~ /^actionItem_canrun/", "group": "inline@00" }, { diff --git a/src/ui/views/environment/actions.ts b/src/ui/views/environment/actions.ts index d45c6816f..66cda652c 100644 --- a/src/ui/views/environment/actions.ts +++ b/src/ui/views/environment/actions.ts @@ -1,8 +1,17 @@ +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) { @@ -27,6 +36,7 @@ export class ActionsNode extends EnvironmentItem { 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 || []) { @@ -42,6 +52,10 @@ export class ActionsNode extends EnvironmentItem { 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; } @@ -57,13 +71,36 @@ export class ActionsNode extends EnvironmentItem { 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(true)); + 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(); @@ -76,7 +113,7 @@ export class ActionsNode extends EnvironmentItem { } async clearSearch() { - (await this.getAllActionItems()).forEach(node => node.setContext(false)); + (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); @@ -99,9 +136,13 @@ export class ActionTypeNode extends EnvironmentItem { export class ActionItem extends EnvironmentItem { static matchedColor = "charts.yellow"; - static contextValue = `actionItem`; + static canRunColor = "charts.blue"; + static matchedCanRunColor = "charts.green"; + static context = `actionItem`; + + private context: ActionContext = {} - constructor(parent: EnvironmentItem, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { + constructor(parent: ActionTypeNode, readonly action: Action, readonly workspace?: vscode.WorkspaceFolder) { super(action.name, { parent }); this.setContext(); this.command = { @@ -111,12 +152,23 @@ export class ActionItem extends EnvironmentItem { } } - setContext(matched?: boolean) { - this.contextValue = `${ActionItem.contextValue}${this.workspace ? "Local" : "Remote"}${matched ? '_matched' : ''}`; - this.iconPath = new vscode.ThemeIcon("github-action", matched ? new vscode.ThemeColor(ActionItem.matchedColor) : undefined); - this.resourceUri = vscode.Uri.from({ scheme: ActionItem.contextValue, authority: this.action.name, query: matched ? "matched" : "" }); - this.description = matched ? l10n.t("search match") : undefined; + 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' : ''}`; } } diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index ecc65ba69..67200b270 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -1,4 +1,5 @@ +import { parse as parseQuery } from "querystring"; import vscode, { l10n, QuickPickItem } from 'vscode'; import { getActions, updateAction } from '../../../api/actions'; import { GetNewLibl } from '../../../api/components/getNewLibl'; @@ -29,29 +30,23 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { context.subscriptions.push( environmentTreeViewer, - vscode.window.onDidChangeActiveTextEditor(async editor => { - let editorCanRunAction = false; - let editorCanRunLocalAction = false; - if (editor) { - const connection = instance.getConnection(); - if (connection) { - const uri = editor.document.uri; - if (uri) { - editorCanRunAction = ['streamfile', 'member', 'object'].includes(uri.scheme); - editorCanRunLocalAction = uri.scheme === 'file'; - } - } - } - vscode.commands.executeCommand(`setContext`, "code-for-ibmi:editorCanRunRemoteAction", editorCanRunAction); - vscode.commands.executeCommand(`setContext`, "code-for-ibmi:editorCanRunLocalAction", editorCanRunLocalAction); - }), + 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.contextValue && uri.query === "matched") { - return { color: new vscode.ThemeColor(ActionItem.matchedColor) }; + 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) }; + } } } }), From 4bff5c32ce553f60beea083663afef803ae785c5 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 1 Dec 2025 21:16:48 +0100 Subject: [PATCH 35/38] Save setLibraryList command when changed on active profile Signed-off-by: Seb Julliand --- src/api/connectionProfiles.ts | 9 +++++++++ src/editors/connectionProfileEditor.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/api/connectionProfiles.ts b/src/api/connectionProfiles.ts index dbad80b74..32cd15f4a 100644 --- a/src/api/connectionProfiles.ts +++ b/src/api/connectionProfiles.ts @@ -20,6 +20,11 @@ export async function updateConnectionProfile(profile: ConnectionProfile, option 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); } } @@ -79,4 +84,8 @@ export function assignProfile(fromProfile: ConnectionProfile, toProfile: Connect 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/editors/connectionProfileEditor.ts b/src/editors/connectionProfileEditor.ts index 3aad1fd6c..eaecb8372 100644 --- a/src/editors/connectionProfileEditor.ts +++ b/src/editors/connectionProfileEditor.ts @@ -1,5 +1,5 @@ import vscode, { l10n } from "vscode"; -import { updateConnectionProfile } from "../api/connectionProfiles"; +import { isActiveProfile, updateConnectionProfile } from "../api/connectionProfiles"; import { instance } from "../instantiate"; import { ConnectionProfile } from "../typings"; import { CustomEditor } from "./customEditorProvider"; @@ -11,16 +11,16 @@ type ConnectionProfileData = { setLibraryListCommand: string } -const editedProfiles : Set = new Set; +const editedProfiles: Set = new Set; -export function isProfileEdited(profile: ConnectionProfile){ +export function isProfileEdited(profile: ConnectionProfile) { return editedProfiles.has(profile.name); } export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: () => Thenable) { - const activeProfile = instance.getConnection()?.getConfig().currentProfile === profile.name; + 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("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 }) @@ -35,7 +35,7 @@ export function editConnectionProfile(profile: ConnectionProfile, doAfterSave?: .addParagraph(profile.customVariables.length ? `
      ${profile.customVariables.map(variable => `
    • &${variable.name}: ${variable.value}
    • `).join('')}
    ` : l10n.t("None")) .open(); - editedProfiles.add(profile.name); + editedProfiles.add(profile.name); } async function save(profile: ConnectionProfile, data: ConnectionProfileData) { From d930419bc6e392e3c53cc7357cc595f691fcc2fa Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 1 Dec 2025 21:17:09 +0100 Subject: [PATCH 36/38] Prevent profile from being loaded/unloaded when being edited Signed-off-by: Seb Julliand --- src/ui/views/environment/environmentView.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index 67200b270..897805e8f 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -290,8 +290,17 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { 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); } From 1f393a3d78b7defc90ca037a7dfd5617ebd8901a Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 1 Dec 2025 23:19:21 +0100 Subject: [PATCH 37/38] Added 'append' option when generating local actions Signed-off-by: Seb Julliand --- src/filesystems/local/deployTools.ts | 34 ++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/filesystems/local/deployTools.ts b/src/filesystems/local/deployTools.ts index 68349a79e..e5b8de72c 100644 --- a/src/filesystems/local/deployTools.ts +++ b/src/filesystems/local/deployTools.ts @@ -2,6 +2,7 @@ import { existsSync } from 'fs'; import createIgnore, { Ignore } from 'ignore'; import path, { basename } from 'path'; 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'; @@ -26,14 +27,39 @@ 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`)); - - if (!existsSync(localActionsUri.fsPath) || await vscode.window.showWarningMessage(l10n.t("Local actions are already defined for this workspace. Do you want to overwrite them?"), { modal: true }, l10n.t("Yes"))) { + 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(newActions, null, 2), `utf-8`) + Buffer.from(JSON.stringify(actions, null, 2), `utf-8`) ); - vscode.workspace.openTextDocument(localActionsUri).then(doc => vscode.window.showTextDocument(doc)); } catch (e) { console.log(e); From db2abeff4f75a6ace8d2310d3652a84329e0430b Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 1 Dec 2025 23:45:56 +0100 Subject: [PATCH 38/38] Refresh actions when location actions file change Signed-off-by: Seb Julliand --- src/ui/views/environment/environmentView.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ui/views/environment/environmentView.ts b/src/ui/views/environment/environmentView.ts index 897805e8f..5a05d8661 100644 --- a/src/ui/views/environment/environmentView.ts +++ b/src/ui/views/environment/environmentView.ts @@ -28,8 +28,14 @@ export function initializeEnvironmentView(context: vscode.ExtensionContext) { 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 {