API Examples
Event listener
The Code for IBM i API provides an event listener. This allows your extension to react when something happens in Code for IBM i.
const instance = getInstance();
// Each context/name couple must be unique.instance.subscribe(context, `connected`, `Log the connection`, () => { console.log(`It connected!`);});Available events
| ID | Event |
|---|---|
connected | When Code for IBM i connects to a system |
disconnected | When Code for IBM i is disconnected from a system |
deployLocation | When a deploy location changes |
deploy | When a deploy successfully finishes |
Running commands with the user library list
IBMi#runCommand can be used by an extension to execute a remote command on the IBM i.
It has a parameter which is an object with some properties. When executing a command in the ile or qsh environment, it will use the library list from the current connection.
interface RemoteCommand { command: string; /** describes what environment the command will be executed. Is optional and defaults to `ile` */ environment?: `pase`|`ile`|`qsh`; /** set this as the working directory for the command when it is executed. Is optional and defaults to the users working directory in Code for IBM i. */ cwd?: string; /** the variables made available to the command; also accepts `&LIBL` and `&CURLIB` in the `ile` environment */ env?: Record<string, string>; /** when `true`, the command is run without the user library list */ noLibList?: boolean; /** when `true`, the spooled files produced by the command are returned in the result */ getSpooledFiles?: boolean;}The command returns an object:
interface CommandResult { stdout: string; stderr: string; code: number;}const result = await instance.getConnection().runCommand({ command: `WRKACTJOB`, environment: `ile`});You can also provide a custom library list and current library when executing a command in the ile environment:
const detail: RemoteCommand = { environment: `ile`, command: `CRTBNDRPG...`, env: { // Space delimited library list '&LIBL': 'LIBA LIBB LIBC', '&CURLIB': 'LIBD' }}Running SQL queries
Code for IBM i has an API that lets you run SQL statements and get a result back.
const instance = getInstance();
const rows = await instance.getConnection().runSQL(`select * from schema.yourtable`);Get members and streamfiles
It is possible for extensions to utilize the file systems provided by Code for IBM i.
openTextDocument returns a TextDocument .
Getting a member
//Member located on *SYSBASconst doc = await vscode.workspace.openTextDocument(vscode.Uri.from({ scheme: `member`, path: `/${library}/${file}/${name}.${extension}`}));
//Member located on an iASPconst doc = await vscode.workspace.openTextDocument(vscode.Uri.from({ scheme: `member`, path: `/${iasp}/${library}/${file}/${name}.${extension}`, query: 'readonly=true' //Optional: open in read-only mode}));Getting a streamfile
const doc = await vscode.workspace.openTextDocument(vscode.Uri.from({ scheme: `streamfile`, path: streamfilePath, query: 'readonly=true' //Optional: open in read-only mode}));File system options
Both member and streamfile file systems support the following query parameters:
| Name | Type | Description |
|---|---|---|
readonly | boolean | Open the text editor in read only mode. Defaults to false. |
Connect to a system
It is possible for your API to automate connecting to an IBM i instead of the user using the connection view.
const connectionData: ConnectionData = {...};
const connected: boolean = await vscode.commands.executeCommand(`code-for-ibmi.connectDirect`, connectionData);
if (connected) { // do a thing.} else { // something went wrong.}Real world examples
The snippets below are taken from two extensions that are built on top of Code for IBM i and use most of the API surface:
Activating the base extension and registering components
Db2 for IBM i activates Code for IBM i itself (instead of assuming it is already active) and immediately registers its own components, so they get installed and checked on every connection.
// vscode-db2i, src/base.tslet baseExtension: CodeForIBMi;
export async function loadBase(context: ExtensionContext) { const code4iExtension = extensions.getExtension<CodeForIBMi>(`halcyontechltd.code-for-ibmi`); if (code4iExtension) { baseExtension = code4iExtension.isActive ? code4iExtension.exports : await code4iExtension.activate();
const componentRegistry = baseExtension.componentRegistry; componentRegistry.registerComponent(context, new ValidateStatementComponent()); componentRegistry.registerComponent(context, new CheckStatementComponent()); } else { // This cannot happen since the dependency is in package.json throw new Error(`${context.extension.id} requires halcyontechltd.code-for-ibmi extension`); }}
export function getBase(): CodeForIBMi { return baseExtension;}
export function getInstance(): Instance { return baseExtension.instance;}A component describes how it is detected on the system and how it is installed. Code for IBM i calls getRemoteState on connect and update when the component is missing or out of date.
// vscode-db2i, src/connection/components/checkStatement.tsexport class CheckStatementComponent implements IBMiComponent { static ID = "CheckStatementComponent"; private static readonly VERSION = 1;
getIdentification() { return { name: CheckStatementComponent.ID, version: CheckStatementComponent.VERSION, signature: `QSYS/QSQCHKS` }; }
async getRemoteState(connection: IBMi, installDirectory: string): Promise<SecureComponentState> { const remoteSignature = await connection.getContent().getSQLRoutineSignature(library, functionName, `PROCEDURE`); return { status: remoteSignature ? "Installed" : "NotInstalled", remoteSignature }; }
async update(connection: IBMi, installDirectory: string): Promise<SecureComponentState> { return connection.withTempDirectory(async tempDir => { const srcPath = getVSCodeTools().ensureFullPath(posix.join(tempDir, `sqlchecker.sql`), connection.getConfig().homeDirectory); await connection.getContent().writeStreamfileRaw(srcPath, this.getSource(library, functionName, version));
const result = await connection.runCommand({ command: `QSYS/RUNSQLSTM SRCSTMF('${srcPath}') COMMIT(*NONE) NAMING(*SYS) DFTRDBCOL(${library})`, cwd: `/`, noLibList: true, getSpooledFiles: true });
if (result.code !== 0) { throw Error(result.stderr || result.stdout); }
return this.getRemoteState(connection, installDirectory); }); }}Once connected, the component is retrieved from the connection with connection.getComponent<CheckStatementComponent>(CheckStatementComponent.ID).
Running remote commands
Both extensions use IBMi#runCommand for everything that has no SQL equivalent. The pattern is always the same: check there is a connection, build the RemoteCommand, then branch on CommandResult.code — 0 means the command completed. In the ile environment stderr carries the job log messages produced by the command, which is what these extensions show to the user when something fails.
// vscode-ibmi-fs, src/commonOperations.tsexport const holdJob = async (jobId: JobIdentifier, showConfirmation: boolean = true): Promise<boolean> => { const connection = getInstance()?.getConnection();
if (!connection) { vscode.window.showErrorMessage(vscode.l10n.t("Not connected to IBM i")); return false; }
if (showConfirmation) { const confirmed = await vscode.window.showWarningMessage( vscode.l10n.t("Are you sure you want to hold job {0}?", jobId.job), { modal: true }, vscode.l10n.t("Hold job") ); if (!confirmed) { return false; } }
try { const cmdrun: CommandResult = await connection.runCommand({ command: `QSYS/HLDJOB JOB(${jobId.job})`, environment: `ile` });
if (cmdrun.code === 0) { vscode.window.showInformationMessage(vscode.l10n.t("Job held.")); return true; } else { vscode.window.showErrorMessage(vscode.l10n.t("Unable to hold selected job:\n{0}", String(cmdrun.stderr))); return false; } } catch (error) { vscode.window.showErrorMessage(vscode.l10n.t("Error holding job: {0}", String(error))); return false; }};Reading the output of a command
A CL command that only writes a spooled file produces no stdout on its own: set getSpooledFiles: true and the spooled files are returned in stdout. IBM i File System uses this to get the library list of a job, since DSPJOB has no OUTFILE for OPTION(*LIBL).
// vscode-ibmi-fs, src/views/wrkjob.tsconst liblspl = await connection.runCommand({ command: `QSYS/DSPJOB JOB(${jobName}) OPTION(*LIBL)`, environment: `ile`, getSpooledFiles: true,});
if (!liblspl || !liblspl.stdout) { return [];}
// Parse library list from spool output// The output has fixed-width columns, so we parse by positionconst lines = liblspl.stdout.split('\n');const libraries: LibraryEntry[] = [];
for (const line of lines) { const library = line.substring(3, 13).trim(); const type = line.substring(15, 18).trim(); const asp = line.substring(26, 36).trim(); const description = line.substring(38, 90).trim();
// Skip separator lines if (!library || library.startsWith('*')) { continue; }
if (type === 'SYS' || type === 'CUR' || type === 'USR') { libraries.push({ library, type, asp, description }); }}Chaining commands and using a temporary directory
Several CL commands separated by a newline are executed in the same ile call, which keeps QTEMP and the job for the whole sequence. Combined with withTempDirectory — which removes the directory when the callback resolves — this copies a spooled file to the IFS and opens it with the streamfile file system.
// vscode-ibmi-fs, src/commonOperations.tsreturn connection.withTempDirectory(async (tempDir): Promise<boolean> => { const tempSourcePath = posix.join(tempDir, `${spoolId.job.replaceAll('/', '-')}_${spoolId.spoolname}_${spoolId.nbr}.txt`);
const result = await connection.runCommand({ command: `QSYS/CPYSPLF FILE(${spoolId.spoolname}) JOB(${spoolId.job}) SPLNBR(${spoolId.nbr}) TOFILE(*TOSTMF) TOSTMF('${tempSourcePath}') QSYS/CPY OBJ('${tempSourcePath}') TOOBJ('${tempSourcePath}') TOCCSID(1208) DTAFMT(*TEXT) REPLACE(*YES)`, environment: 'ile' });
if (result.code === 0) { const uri = vscode.Uri.parse(tempSourcePath).with({ scheme: `streamfile` }); await vscode.commands.executeCommand(`vscode.open`, uri, { preview: false, preserveFocus: false }); return true; } else { vscode.window.showErrorMessage(vscode.l10n.t("Error opening spool: {0}", String(result.stderr))); return false; }});The pase environment
environment: 'pase' runs the command in the shell, which is the simplest way to do file operations on the IFS. Here it is used in a finally so the temporary file is removed even when the download fails.
// vscode-ibmi-fs, src/commonOperations.tstry { await connection.client.getFile(saveLocation.fsPath, tempRemotePath);} catch (error) { result.successful = false; result.error = String(error);} finally { // Clean up temporary file on IBM i await connection.runCommand({ command: `rm -f ${tempRemotePath}`, environment: `pase` });}Db2 for IBM i uses the two environments side by side to execute the cells of a notebook: the cl cells go to ile with the spooled files, the shellscript cells to pase.
// vscode-db2i, src/notebooks/Controller.tscase `cl`: try { const command = await connection.runCommand({ command: cell.document.getText(), environment: `ile`, getSpooledFiles: true });
if (command.stdout) { items.push(vscode.NotebookCellOutputItem.text([`\`\`\``, command.stdout, `\`\`\``].join(`\n`), `text/markdown`)); }
if (command.stderr) { items.push(vscode.NotebookCellOutputItem.text([`\`\`\``, command.stderr, `\`\`\``].join(`\n`), `text/markdown`)); } } catch (e) { items.push( vscode.NotebookCellOutputItem.stderr(`Failed to run command. Are you connected?`), vscode.NotebookCellOutputItem.stderr(e instanceof Error ? e.message : String(e)) ); } break;
case `shellscript`: const command = await connection.runCommand({ command: cell.document.getText(), environment: `pase` });Reacting to connection events
Each context/name couple must be unique, so a view can subscribe once and be cleaned up with the extension context.
// vscode-db2i, src/extension.tsinstance.subscribe(context, `connected`, `db2i-connected`, () => { DbCache.resetCache(); onCode4iConnect().then(async () => { schemaBrowser.clearCacheAndRefresh(); exampleBrowser.refresh(); queryHistory.refresh(); });});
// vscode-db2i, src/config.tsgetInstance().subscribe(context, `disconnected`, `db2i-disconnect`, async () => { JobManagerView.setVisible(false); await JobManager.endAll(true); updateStatusBar();});Reacting to setting changes
// vscode-ibmi-fs, src/extension.tsconst base = loadBase();if (base) { context.subscriptions.push( base.onCodeForIBMiConfigurationChange("connectionSettings", () => updateFsActionsStatusBar()) );}Reading the shared view settings
IBM i File System reads the page size and the auto refresh interval through the base extension instead of reading the settings itself, so defaults and bounds stay in one place.
// vscode-ibmi-fs, src/config.tsexport function getItemsPerPage(): number { return loadBase()!.viewSettings.getItemsPerPage();}
export function getAutoRefreshInterval(): number { return loadBase()!.viewSettings.getAutoRefreshInterval();}The interval is returned in milliseconds and is 0 when auto refresh is disabled:
// vscode-ibmi-fs, src/views/wrkactjob.tsconst autoRefreshInterval = getAutoRefreshInterval();let autoRefreshTimer: NodeJS.Timeout | undefined;// Guards against a tick starting while the previous query is still runninglet refreshing = false;
if (autoRefreshInterval > 0) { autoRefreshTimer = setInterval(() => refresh(true), autoRefreshInterval);}
panel.onDidDispose(() => { if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = undefined; }});Rendering a searchable table
generateFastTable builds the whole page, generateFastTableUpdate builds the message that replaces the rows of a table already on screen. Both need the same tableId.
// vscode-ibmi-fs, src/views/wrkactjob.tsconst ACTJOB_TABLE_ID = 'wrkactjob-jobs';
const jobColumns: FastTableColumn<Entry>[] = [ { title: vscode.l10n.t("Subsystem"), width: "1fr", getValue: e => e.subsystem }, { title: vscode.l10n.t("Job"), width: "1.5fr", getValue: e => e.job }, { title: vscode.l10n.t("User"), width: "0.7fr", getValue: e => e.user }, { title: vscode.l10n.t("Status"), width: "0.5fr", getValue: e => e.status }, { title: vscode.l10n.t("Actions"), width: "2fr", getValue: e => { const arg = encodeURIComponent(JSON.stringify(e)); return `<vscode-button appearance="primary" href="action:wrkJob?entry=${arg}">${vscode.l10n.t("Details")}</vscode-button>`; } }];
panel.webview.html = generatePage(generateFastTable({ title: vscode.l10n.t("Work with Active Jobs"), subtitle: vscode.l10n.t("Total Active Jobs: {0}", String(activeJobs.length)), columns: jobColumns, data: activeJobs, stickyHeader: true, emptyMessage: vscode.l10n.t("No active jobs found."), enableSearch: true, searchPlaceholder: vscode.l10n.t("Search jobs..."), searchTerm: searchTerm, tableId: ACTJOB_TABLE_ID}));The search is server-side: the webview posts a search message and waits for an answer. Note that a failed query must be answered too, otherwise the table keeps spinning until its own safety timeout.
// vscode-ibmi-fs, src/views/wrkactjob.tsconst postTableUpdate = async () => { await panel.webview.postMessage(generateFastTableUpdate({ columns: jobColumns, data: activeJobs, totalItems: activeJobs.length, currentPage: 1, subtitle: vscode.l10n.t("Total Active Jobs: {0}", String(activeJobs.length)), tableId: ACTJOB_TABLE_ID }));};
panel.webview.onDidReceiveMessage(async message => { if (message.command === 'search') { if (message.searchTerm !== undefined) { searchTerm = message.searchTerm; }
try { const newJobs = await fetchActiveJobs(searchTerm); if (newJobs) { activeJobs = newJobs; } await postTableUpdate(); } catch (error) { // The webview spins its busy indicator until an answer arrives vscode.window.showErrorMessage(vscode.l10n.t("Failed to load active jobs: {0}", String(error))); await panel.webview.postMessage({ command: 'updateTableFailed', tableId: ACTJOB_TABLE_ID }); } return; }
// ...handle the `action:` hrefs posted by the buttons of the Actions column});The rows themselves come from a plain runSQL on an IBM i service:
const connection = getInstance()?.getConnection();const rows = await connection.runSQL(` SELECT JOB_NAME, AUTHORIZATION_NAME, JOB_STATUS, SUBSYSTEM FROM TABLE(QSYS2.ACTIVE_JOB_INFO(DETAILED_INFO => 'NONE', RESET_STATISTICS => 'NO')) x ORDER BY x.SUBSYSTEM ASC`);Rendering a detail page
Object views use generateDetailTable for the key/value pages. codeColumns renders a value in a monospaced block.
// vscode-ibmi-fs, src/types/dataArea.tsgenerateHTML(): string { return generateDetailTable({ title: vscode.l10n.t("Data Area: {0}/{1}", this.library, this.name), subtitle: vscode.l10n.t("Data Area Information"), columns: this.columns, data: this.dta, codeColumns: ['DATA_AREA_VALUE'] });}Building a form with customUI
Db2 for IBM i builds the Copy to file prompt with the same builder Code for IBM i uses for its own pages.
// vscode-db2i, src/views/schemaBrowser/copyUI.tsexport function getCopyUi() { return getBase()!.customUI() .addInput('toFile', 'To File', 'Name', { minlength: 1, maxlength: 10 }) .addInput('toLib', 'Library', 'Name', { default: '*LIBL', minlength: 1, maxlength: 10 }) .addInput('fromMbr', 'From member', 'Name, generic*, *FIRST, *ALL', { default: '*FIRST' }) .addSelect('mbrOpt', 'Replace or add records', [ { text: '*NONE', description: '*NONE', value: '*NONE' }, { text: '*ADD', description: '*ADD', value: '*ADD' }, { text: '*REPLACE', description: '*REPLACE', value: '*REPLACE' }, { text: '*UPDADD', description: '*UPDADD', value: '*UPDADD' }, ]) .addButtons( { id: 'copy', label: 'Copy', requiresValidation: true }, { id: 'cancel', label: 'Cancel' } );}VS Code integration
Right click options
It is possible for your extension to add right click options to:
- objects in the Object Browser
- members in the Object Browser
- directories in the IFS Browser
- streamfiles in the IFS Browser
- much more
You would register a command as you’d normally expect, but expect a parameter for the chosen node from the tree view. Here is the sample for deleting a streamfile in the IFS Browser.
context.subscriptions.push( // `node` is the object passed in directly from the IFS Browser. vscode.commands.registerCommand(`your-extension.deleteIFS`, async (node) => { if (node) { //Running from right click let result = await vscode.window.showWarningMessage(`Are you sure you want to delete ${node.path}?`, `Yes`, `Cancel`);
if (result === `Yes`) { const connection = instance.getConnection();
try { // Run a pase command with the connection API await connection.runCommand({ command: `rm -rf "${node.path}"`, environment: `pase`, });
vscode.window.showInformationMessage(`Deleted ${node.path}.`);
this.refresh(); } catch (e) { vscode.window.showErrorMessage(`Error deleting streamfile! ${e}`); } } } else { // If it's reached this point, it usually // indicates that it's running from the command palette } }));Following that, we need to register the command so it has a label. We do this in package.json
{ "command": "your-extension.deleteIFS", "title": "Delete object", "category": "Your extension"}Finally, we add it to a context menu:
"menus": { "view/item/context": [ { "command": "your-extension.deleteIFS", "when": "view == ifsBrowser", "group": "yourext@1" } ]}When adding your command to a menu context, there are lots of possible values for your when clause:
viewcan beifsBrowserorobjectBrowser.viewItemcan be different depending on the view:- for
ifsBrowser, it can bedirectory,streamfileorshortcut - for
objectBrowser, it can bemember(source member),object.<type>orobject.<type>.<attribute>(any object, e.g.object.pgm.rpgle),SPF(source file) orfilter.
- for
Items that are read only or protected get a _readonly or _protected suffix (e.g. member_readonly, directory_protected), so use a regular expression when you want to match an item whatever its state:
{ "command": "your-extension.deleteIFS", "when": "view == ifsBrowser && viewItem =~ /^streamfile/"}This allows your extension to provide commands for specific types of objects or specific items in the treeview.
Read more about the when clause on the VS Code docs website.
Views
Code for IBM i provides a context so you can control when a command, view, etc, can work. code-for-ibmi:connected can and should be used if your view depends on a connection. For example
This will show a welcome view when there is no connection:
"viewsWelcome": [{ "view": "git-client-ibmi.commits", "contents": "No connection found. Please connect to an IBM i.", "when": "code-for-ibmi:connected !== true" }],This will show a view when there is a connection:
"views": { "scm": [{ "id": "git-client-ibmi.commits", "name": "Commits", "contextualTitle": "IBM i", "when": "code-for-ibmi:connected == true" }] }FAQs
Is there a connection?
You can use instance.getConnection() to determine if there is a connection:
async getChildren(element) { const connection = instance.getConnection();
let items: TreeItem[] = [];
if (connection) { //Do work here...
} else { items = [new vscode.TreeItem(`Please connect to an IBM i and refresh.`)]; }
return items;}connected context
If you refer to the Views section, you can make it so the view is only shown when connected. This means by the time the view is used, there should be a connection.
"views": { "explorer": [{ "id": "yourIbmiView", "name": "My custom View", "contextualTitle": "Extension name", "when": "code-for-ibmi:connected == true" }]}"activationEvents": [ "onView:yourIbmiView"]Examples
See the following code bases for large examples of extensions that use Code for IBM i:
- Db2 for IBM i
- IBM i File System
- VS Code extension to manage IBM i IWS services
- Git for IBM i extension
vscode-ibmi environment variable
We provide environment variables so Code for IBM i can be controlled by managed environments. See Variables.