Skip to content

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

IDEvent
connectedWhen Code for IBM i connects to a system
disconnectedWhen Code for IBM i is disconnected from a system
deployLocationWhen a deploy location changes
deployWhen 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 *SYSBAS
const doc = await vscode.workspace.openTextDocument(vscode.Uri.from({
scheme: `member`,
path: `/${library}/${file}/${name}.${extension}`
}));
//Member located on an iASP
const 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:

NameTypeDescription
readonlybooleanOpen 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.ts
let 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.ts
export 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.ts
export 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.ts
const 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 position
const 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.ts
return 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.ts
try {
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.ts
case `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.ts
instance.subscribe(context, `connected`, `db2i-connected`, () => {
DbCache.resetCache();
onCode4iConnect().then(async () => {
schemaBrowser.clearCacheAndRefresh();
exampleBrowser.refresh();
queryHistory.refresh();
});
});
// vscode-db2i, src/config.ts
getInstance().subscribe(context, `disconnected`, `db2i-disconnect`, async () => {
JobManagerView.setVisible(false);
await JobManager.endAll(true);
updateStatusBar();
});

Reacting to setting changes

// vscode-ibmi-fs, src/extension.ts
const 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.ts
export 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.ts
const autoRefreshInterval = getAutoRefreshInterval();
let autoRefreshTimer: NodeJS.Timeout | undefined;
// Guards against a tick starting while the previous query is still running
let 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.ts
const 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.ts
const 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.ts
generateHTML(): 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.ts
export 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:

  • view can be ifsBrowser or objectBrowser.
  • viewItem can be different depending on the view:
    • for ifsBrowser, it can be directory, streamfile or shortcut
    • for objectBrowser, it can be member (source member), object.<type> or object.<type>.<attribute> (any object, e.g. object.pgm.rpgle), SPF (source file) or filter.

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:

vscode-ibmi environment variable

We provide environment variables so Code for IBM i can be controlled by managed environments. See Variables.