Triton RPG ships a language server, rpg-lsp, alongside the rpgc compiler. It reports compiler diagnostics in your editor as you type, so you see the same errors rpgc would report without leaving the file and running a build.

The server uses the same compiler as the command line. A diagnostic shown in your editor is the diagnostic rpgc produces for that source, with the same TRNxxxx code and the same message - see the Diagnostic Reference.

Installing

rpg-lsp is installed alongside rpgc by both install paths - the Windows installer and the universal zip - so if you have the compiler you already have the language server. It needs Java 17 or later on the path, and nothing else: the compiler and runtime are bundled inside it.

triton-rpg/
  bin/
    rpgc          the compiler          (Linux, macOS)
    rpgc.cmd      the compiler          (Windows)
    rpg-lsp       the language server   (Linux, macOS)
    rpg-lsp.cmd   the language server   (Windows)
  lib/
    rpgc.jar        the compiler, as a jar
    rpg-lsp.jar     the language server, as a jar
    triton-rpg.jar  the runtime library

The launchers in bin find their jars at ../lib, so keep the tree intact. See Getting Started for the install steps.

Confirm it runs:

$ rpg-lsp < /dev/null

It exits immediately with status 1, which is what a language server does when its client disconnects without a shutdown request. That is the expected result of running it by hand - the server is meant to be started by an editor, not from a shell.

Connecting an editor

The server speaks the Language Server Protocol over standard input and output. Configure your editor to launch rpg-lsp as the language server for RPG source files. The exact configuration is editor-specific, but every client needs the same two things:

Setting Value
Command the absolute path to rpg-lsp
Transport stdio

Associate it with whichever file extensions your source uses - commonly .rpgle, .rpg, .sqlrpgle, and .rpgleinc.

Because stdout carries the protocol, the server writes nothing else to it. Anything it has to report goes to stderr, which editors normally surface as a server log - look there first if the connection does not come up.

The server also takes launch arguments, spelled as the matching rpgc flags: --license (see Licensing), and the four analysis settings covered under Include paths and externally described files. An argument the server does not recognise is ignored rather than refused, so a client that passes options of its own does not prevent it from starting.

The Triton RPG VS Code extension

Triton RPG includes a VS Code extension that connects the editor to rpg-lsp automatically, with syntax highlighting for both fixed-format and free-format RPG source.

Obtaining and installing

The extension is available from the client portal at portal.whitehorn.ltd, alongside the compiler. It is distributed as a .vsix file and is not published on the Visual Studio Marketplace - there is no marketplace listing and no auto-update. When you update the compiler, update the extension too: download the current .vsix from the portal and reinstall it.

Install from the command line:

$ code --install-extension triton-rpg-2026.9.3.vsix

Or open VS Code, press Ctrl/Cmd+Shift+P, type Extensions: Install from VSIX, and select the file. Both produce the same result.

The extension requires rpg-lsp to be installed separately (it is included in every Triton RPG install). It does not bundle the language server.

How the server is located

The extension looks for rpg-lsp in three places, in order:

  1. The tritonRpg.lsp.path setting - if set, this path is used. Point it at rpg-lsp (Linux/macOS) or rpg-lsp.jar (Windows, or when using the jar directly).
  2. The TRITON_RPG_HOME environment variable - if it names a directory containing bin/rpg-lsp, that binary is used.
  3. rpgc on PATH - if rpgc is on your system PATH, the extension looks for rpg-lsp beside it.

If none of these finds the server, the extension shows an error with a link to the setting.

Extension settings

Setting Scope Default Purpose
tritonRpg.lsp.path Machine (empty) Absolute path to rpg-lsp or rpg-lsp.jar. Leave empty for automatic discovery.
tritonRpg.licensePath Machine-overridable (empty) Absolute path to your license file. See Licensing.
tritonRpg.includePaths Resource [] Directories to search for /COPY and /INCLUDE members. Relative paths resolve against the workspace folder.
tritonRpg.ddsPath Resource (empty) Directory containing .dspf DDS files for display-file analysis. Relative paths resolve against the workspace folder.
tritonRpg.codepage Resource (empty) EBCDIC codepage for source files (e.g. IBM037, IBM500, IBM1047). Leave empty for the default.
tritonRpg.ebcdic Resource false Treat source files as EBCDIC-encoded rather than UTF-8.
tritonRpg.trace.server Window off Log the JSON-RPC traffic between VS Code and the server to the Triton RPG output channel. Values: off, messages, verbose. Useful when diagnosing a connection that does not come up.

Source format detection

The extension registers two language identifiers - rpgle-free for free-format and rpgle-fixed for fixed-format - each with its own syntax highlighting and column rulers.

VS Code assigns a language by file extension (.rpgle → free, .rpg → fixed), but many RPG source files use .rpgle for both formats. After opening a file, the language server analyses it and reports the format it detected - using the same rule as the compiler: if the first non-blank line is **FREE (case-insensitive), the file is free-format; otherwise it is fixed-format. The extension then swaps the language identifier to match, so the correct highlighting and column rulers apply automatically.

If you manually set a file's language (via VS Code's language mode picker), the extension does not override your choice.

Version mismatch warning

Because the extension does not auto-update, it can drift out of step with the compiler. On startup, the extension compares its version against the server's. If they differ, it shows a one-time warning naming both versions and recommending an update from the portal.

What it provides

Diagnostics. Opening or editing an RPG file analyses it and reports errors and warnings. Both fixed-format and free-format source are supported, as they are on the command line.

Analysis runs over the buffer in your editor, not the file on disk. You see diagnostics for unsaved edits, and for a file you have created but never saved.

This extends to /COPY and /INCLUDE members. A copybook you have open is read from your editor too, so an unsaved change to it shows up immediately in every open program that includes it - add a field and the programs using it stop reporting it as undefined, without saving anything. A member you have created but never saved is found and included on the same terms. Close the member and the copy of it on disk takes over again; a member you do not have open is always read from disk.

The server never writes a .class file, so opening a file in your editor produces no build output.

Diagnostics refresh when you pause, not on every keystroke. The server waits for a short break in typing and then analyses once. Diagnostics you see always describe the text as it stands after the pause, never a half-typed intermediate state. Closing a file withdraws its diagnostics immediately rather than waiting.

Underline precision differs by source format. In free-format source a diagnostic underlines exactly the text the compiler faulted - the whole expression where the problem is an expression, and just the name where it is a name.

In fixed-format, a diagnostic on a declaration (D-spec) underlines the declared name. A diagnostic on a calculation (C-spec) still marks the statement on that line. Both point at the right line.

Outline. The editor's outline view - and its breadcrumb bar, where it has one - lists what the file declares, in source order: standalone fields, named constants, data structures, procedures, subroutines, display files, external programs, and enumerations.

Data-structure subfields appear beneath the structure they belong to rather than as separate top-level entries.

Only what you declared is listed. RPG predefines a great many names - every *INxx indicator among them - and those are left out; an outline of a short program would otherwise be over a hundred entries of which a handful were yours.

Hover. Resting the cursor on a name shows what it was declared as - the name with its RPG type, written the way you would declare it (PACKED(7:2), CHAR(30), IND), and what kind of thing it is: variable, named constant, parameter, procedure, subroutine, display file, external program, or enumeration.

A name the compiler could not resolve shows no hover. There is nothing to report about it, and an empty box would suggest it had been understood.

Hover works from either end: rest on a use of a field or on its declaration and you get the same answer.

Go to definition. Following a name - F12 in most editors, or Ctrl/Cmd-click - jumps to where it was declared, and selects the declared name so you can see what you landed on. It works for fields, named constants, data structures and their subfields, parameters, procedures, and subroutines, and from either end: following a declaration answers with itself rather than doing nothing.

Following a call goes to the called procedure, and following an EXSR goes to the matching BEGSR.

A qualified reference goes to the subfield, not to the structure containing it - Customer.Name follows to the line declaring Name.

A name the compiler could not resolve goes nowhere. There is no declaration to jump to, and guessing at one would move you somewhere wrong.

When the declaration is in a /COPY member, the jump opens that member.

Precision differs by source format, the same way diagnostic underlines do. In free-format source the name under the cursor is resolved exactly. In a fixed-format calculation a word that matches a declared name is treated as a reference, even where it is not one, so a jump or a listing may include more than you meant.

Find references. Asking for a name's references - Shift-F12 in most editors - lists every place it is used, in source order. It covers the same things go-to-definition does: fields, named constants, data structures and their subfields, parameters, procedures, and subroutines. Following a name and listing its uses always agree about which symbol you meant.

The whole workspace is searched, not only what you have open. The server indexes every RPG member in your workspace folders in the background when it starts, so a use in a file you have never opened is listed alongside the ones in front of you. See Workspace indexing below for what is covered and when it is ready.

The declaration is listed only when your editor asks for it, which most do under a separate "include declaration" setting. A name the compiler could not resolve lists nothing.

The same per-format precision note applies as for go-to-definition.

Rename. Renaming a name - F2 in most editors - rewrites its declaration and every use of it, across the whole workspace. A use in a member you have never opened is rewritten in the same operation, so a rename does not leave a library half-renamed. Your editor asks first what it is about to rename, so you see the current name in the input box, and positions that cannot be renamed are declined before the box appears.

Every spelling is rewritten. RPG names are case-insensitive, so a source that writes UnitPrice, unitprice and UNITPRICE has three spellings of one name, and all three change.

A name that merely contains the one being renamed is left alone - renaming Amt does not touch AmtTotal.

In fixed-format, column alignment is preserved. A declaration's name sits in a fixed column range, so a shorter or longer name would otherwise shift every column after it and the spec would no longer be valid. The name field is repadded instead, leaving the following columns where they were.

Rename is refused rather than half-applied when it cannot be done safely:

In each case your editor reports that the rename could not be performed and nothing is changed.

Completion. Asking for completions offers the names that are in scope where you are asking - the globals, plus the enclosing procedure's parameters and locals. Each entry carries its kind, so a named constant does not read as a variable, and its RPG type as written (PACKED(7:2), CHAR(30), IND).

A procedure's locals are offered only inside that procedure. Offering a name that is not in scope would have you accept a suggestion the compiler then rejects.

Names are offered as you declared them, and a partially-typed word narrows the list without regard to case - unitp finds UnitPrice, because RPG names are case-insensitive.

Only what you declared is offered. RPG predefines a great many names - every *INxx indicator among them - and a list mostly made of those would bury the handful that are yours.

Workspace indexing

To answer "where else is this used?" across a source library rather than across your open tabs, the server reads every RPG member in your workspace folders once, in the background, as soon as it starts. Your editor shows the progress; find-references and rename are complete once it finishes.

Until it does, both still work - they simply see the files indexed so far, plus whatever you have open. If a search comes back emptier than you expected, check whether indexing is still running before concluding a name is unused.

Only RPG source is read: .rpgle, .rpg, .sqlrpgle, .rpgleinc and .sqlrpgleinc. A workspace holding Java, Markdown, JSON or build output alongside your RPG is the normal case, and everything else is passed over silently rather than reported as a problem.

What is skipped:

Nothing is written to disk, so there is no cache to clear or to go stale. It is kept current as you work: editing a file updates it without a save, and files that change outside the editor - a branch switch, a pull - are re-read when your editor reports them. Changing a /COPY member re-checks the programs that include it.

Nothing is indexed when the server has no workspace folder - a single file opened on its own, for instance. References and rename then cover that file alone.

What it does not provide

Completion covers declared names only - opcode and built-in-function keywords are not offered. Nothing beyond the features listed above is provided: there is no formatting, no code actions, no inlay hints, and no build or run integration.

Include paths and externally described files

The directory containing the file being edited is always searched for /COPY and /INCLUDE members, so a copybook sitting beside your source resolves with no configuration.

Tell the server where else to look, and it resolves those too - and reports what it finds wrong. The same four settings rpgc takes on the command line can be given to the server, either as launch arguments or through your editor's settings:

Launch argument Editor setting Purpose
--include-path <dir> (repeatable) includePaths Where /COPY and /INCLUDE members live
--dds-path <dir> ddsPath Where DDS files live
--codepage <cp> codepage Source code page
--ebcdic ebcdic Source is EBCDIC-encoded

Your editor's settings win over the launch arguments, and changing them re-analyses your open files straight away - no restart. A setting your editor does not mention keeps whatever the launch supplied.

What the server does not know, it does not complain about. Given no include path, it will not report a /COPY member as missing, and it will not report the fields that member would have declared as undefined - it has no way to tell a genuinely missing member from one it was simply never pointed at. Given no DDS path, it will not report an externally described file as unresolvable.

This is deliberate: an editor that underlines code which compiles clean teaches you to ignore its underlines, and then the real problems go unread too.

It is not silence about real errors. Anything provable from the source in front of it is reported in full whatever you have configured - a duplicate declaration, a name that resolves to nothing with no unresolved copybook to explain it, an unclosed /IF, an EXTNAME with an empty file name. Configuring the paths adds the diagnostics that need them; it never changes the ones that don't.

An error inside a /COPY member is still reported, but is shown at the top of the file you have open with the member's own file, line, and column stated in the message - your editor has no other place to attach it while that member is not itself open.

Verify mode is off in the editor. The server never connects to a database, so it does not resolve externally described file layouts from a catalog and reports them as unresolved. Use rpgc with its verify options when you need those checked - see the Command-Line Reference.

Licensing

The language server requires the same license as rpgc, and looks for it in the same three places, in the same order:

  1. an explicit path - --license <path> as a launch argument, or the tritonRpg.licensePath setting in VS Code, which the extension forwards as that argument;
  2. the RPGC_LICENSE environment variable;
  3. ~/.rpgc/license.lic.

One license covers both. If you already run rpgc, the server finds the same file and there is nothing further to do.

Set tritonRpg.licensePath, or use ~/.rpgc/license.lic. An editor started from the Dock, the Start menu, or a desktop launcher does not inherit your shell's environment, so an RPGC_LICENSE exported from ~/.zshrc or ~/.profile is usually invisible to it - even though the same variable works perfectly for rpgc in a terminal. The other two locations do not have that problem.

Without a license

The server starts, connects, and then tells you why it is doing nothing:

Triton RPG is not analysing your RPG files - No valid license found. Machine Key:
TRP-XXXX-XXXX-XXXX To obtain a license, provide this Machine Key to your account
representative or visit https://portal.whitehorn.ltd

Your editor shows that as an error notification, with the full text in the server's log. It carries the machine key you need in order to obtain a license - including when the file is present but expired, corrupt, or issued for a different machine, which is exactly when you need a fresh key to send us. See Getting Started for the rest of that process.

Until a license is in place, no file is analysed: no diagnostics, no outline, no hover, no go-to-definition, no find-references, no rename, no completion. Nothing is reported as an error in your source, because nothing was examined.