Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Editor (LSP) Guide

Everything here comes from drut server, launched automatically by the VS Code/Open VSX extension — and, since it’s a standard Language Server Protocol implementation, usable from any LSP-capable editor, not only VS Code.

Diagnostics

Two kinds of diagnostics are published, at different severities.

Structural diagnostics (real problems, Error severity) — seven categories, covering unmatched blocks and a few other real structural defects:

DiagnosticFires on
UnmatchedIfAn IF with no matching ENDIF, or a dangling ENDIF/ELSEIF/ELSE.
UnmatchedLoopA LOOP with no matching ENDLOOP, or a dangling ENDLOOP.
UnclosedBlockCommentA block comment with no matching */ before end of file.
InvalidContinuationA continuation character with no valid following line.
UnmatchedRunA non-disabled RUN with no ENDRUN and no implicit closer (a following RUN or shell-escape statement), a disabled !RUN missing its required explicit ENDRUN, or a dangling ENDRUN.
UnmatchedProcessA PROCESS/PHASE= with no matching ENDPROCESS/ENDPHASE and no following PROCESS/PHASE= (the legitimate implicit-close pattern).
MisplacedBreakA BREAK with no enclosing block of any kind.

(An eighth category, InvalidEncoding, exists in voyager-core for raw-byte input but is unreachable through live editing — the LSP transport only ever delivers already-decoded text.)

Hint-level diagnostics (best-effort signals, not hard errors, Hint severity) — three of these, each its own distinct source so they’re visually and programmatically distinguishable from the structural set above:

DiagnosticSourceFires on
Unclosed ; FMT: OFFdrut-fmtA ; FMT: OFF marker with no matching ; FMT: ON before end of file — the rest of the file stays unformatted, and this tells you why.
Malformed drut.tomldrut-configAn unrecognized key or an out-of-range value in the resolved drut.toml — formatting still completes using the built-in default for just that field.
Undefined @token@drut-tokenAn @token@ reference with no assignment findable in the same file or a directly included one. Never a hard error — a resolver blind spot (a reference on a block-opener line, more than one level of READ FILE inclusion, or a token-built inclusion path) is never itself treated as evidence the token doesn’t exist; it may still be defined somewhere Drut can’t see.

Hover

Hovering a block keyword (IF, LOOP, RUN, …) shows its kind and where its matched counterpart is — correctly resolved even through RUN/PROCESS’s implicit-close quirk. Hovering an @token@ reference shows the value it currently resolves to and where that value was assigned (the most recent same-file assignment before the reference, or one found via a directly-included READ FILE).

Completion and spell-check

Autocomplete for control words and keyword=value pair names is scoped to the enclosing control word (e.g. completing inside RUN PGM=... only offers PGM-relevant pair keywords). A misspelled keyword gets a “did you mean” suggestion riding on the same hover mechanism.

Folding

Every block kind (IF/LOOP/RUN/PROCESS/JLOOP/LINKLOOP/ DISTRIBUTEMULTISTEP) and block comment can be collapsed/expanded like any other language.

Format-on-save and format-on-paste

Format-on-save is auto-enabled the first time the extension activates in a workspace (workspace-scoped, one-time — it won’t silently turn itself back on if you disable it afterward). Saving a .s/.block file reformats it automatically.

Format-on-paste stays off by default. Turn it on with:

{
  "[drut]": {
    "editor.formatOnPaste": true
  }
}

in your workspace’s .vscode/settings.json. Once enabled, pasting Cube Voyager script text into a .s/.block file reindents it to match its new surrounding structure immediately — correctly handling a paste that opens or closes a block.

Syntax highlighting

Static TextMate-grammar highlighting (works immediately, before the language server even attaches) recognizes these categories:

CategoryCoversScope
Control wordsIF, LOOP, RUN, ENDIF, …keyword.control.drut
Statement wordsPRINT, FILEI, FILEO, ARRAY, …support.function.statement.drut
Function callsA recognized Cube Voyager built-in function name immediately followed by (REPLACESTR(...), ROUND(...), and 136 others (see the Formatter Guide for the full list)support.function.builtin.drut
Pair-keyword namesA keyword=value pair’s keyword, e.g. PATHLOAD’s PATHvariable.parameter.drut
ValuesA pair’s bareword value, e.g. PGM=MATRIX’s MATRIXconstant.other.drut
Data referencesThe Matrix/Line/Node/Zone/Database family (MI, MW, DBA, ZONES, …), by name, regardless of positionvariable.language.data-reference.drut
User variablesAny other bareword identifier not covered by a category abovevariable.other.identifier.drut
@name@ substitutionVariable referencesvariable.other.readwrite.drut, plus a semantic-token variable override (below)
NumbersNumeric literalsconstant.numeric.drut
Operators=, +, -, <>, …keyword.operator.drut
Comments; ... and /* ... */comment.line.semicolon.drut / comment.block.drut
StringsQuoted string literalsstring.quoted.single.drut / string.quoted.double.drut

Function calls and statement words render in the same color by default (both use the generic “built-in procedure” convention most themes already style), but are independently recognized and independently colorable — see Highlight color customization below.

@name@ references also always get a real color, not just whatever a theme happens to assign — the extension auto-seeds a #4EC9B0 semantic-token override the first time it activates in a workspace, since some themes render that TextMate scope with no color at all. This seed is workspace-scoped and one-time only: deleting it from .vscode/settings.json by hand keeps it deleted, forever, for that workspace (the extension never fights that choice back) — unless you configure drut.highlight.namedVariables (below).

Editor client settings

All 13 [format] fields (see the Configuration Reference) are also available as personal VS Code settings, not only via a project’s committed drut.toml:

Settingdrut.toml field
drut.format.casingControlWordscasing_control_words
drut.format.casingPairKeywordscasing_pair_keywords
drut.format.casingDataReferencescasing_data_references
drut.format.casingFunctionCallscasing_function_calls
drut.format.indentTopLevelindent_top_level
drut.format.indentWidthindent_width
drut.format.operatorSpacingoperator_spacing
drut.format.blankLinesblank_lines
drut.format.blankLinesTopCapblank_lines_top_cap
drut.format.blankLinesNestedCapblank_lines_nested_cap
drut.format.lineWrapline_wrap
drut.format.lineWrapWidthline_wrap_width
drut.format.lineWrapStyleline_wrap_style

Set these through VS Code’s built-in Settings UI (search for “drut”), or directly in settings.json. A drut.toml value always wins over a conflicting client setting for the same field — a client setting is a personal fallback default, never a way to override a project’s own committed configuration. See the Configuration Reference’s Precedence section for the full four-tier chain. A changed setting takes effect on the very next format request against an already-open document — no reopen or editor restart needed.

Highlight color customization

Unlike the [format] fields above, drut.highlight.* settings are VS Code personal settings only — there is no drut.toml equivalent, no CLI flag, no MCP parameter. Color is a personal/accessibility preference (theme, colorblindness, monitor), not a shared file-content convention the way casing or indentation is, so there’s nothing to put in a committed project file.

Eleven settings, one per category from the Syntax highlighting table above (@name@ excepted — see below), each an optional CSS color (e.g. #RRGGBB):

SettingColors
drut.highlight.controlWordsControl words
drut.highlight.statementWordsStatement words
drut.highlight.functionCallsFunction calls
drut.highlight.pairKeywordsPair-keyword names
drut.highlight.valuesValues
drut.highlight.dataReferencesData references
drut.highlight.userVariablesUser variables
drut.highlight.numbersNumbers
drut.highlight.operatorsOperators
drut.highlight.commentsComments
drut.highlight.stringsStrings

Leaving any of these unset keeps your color theme’s own choice for that category — setting one takes effect immediately (no window reload), and clearing it afterward reverts to the theme’s color, not a stuck last value. None of these ever touch a rule they didn’t add themselves — another extension’s customizations, or your own hand-written ones, always survive untouched.

A bareword immediately before = always renders under pairKeywords, even if it’s also a userVariables-shaped identifier (LINKID in LINKID = _ANode) — this grammar has no real parse tree to tell a keyword-pair’s own name apart from an ordinary assignment’s target variable. The bareword immediately after = renders under pairValues only when it’s the entire right-hand side, with nothing else following (X = _ANode alone) — that shape is genuinely indistinguishable from a keyword-pair’s own value (PGM=MATRIX’s MATRIX) without a real parse tree. As soon as anything else follows on the same right-hand side — another operand, an operator, a string — the whole expression falls to userVariables instead, so LINKID = _ANode + '_' + _BNode’s _ANode and _BNode render identically (neither is a real keyword-pair value). dataReferences is the one exception to the adjacency rule entirely: a recognized data-reference name always wins that category even when it’s also pair-keyword-shaped (ZONES in RUN PGM=MATRIX ZONES=5 renders under dataReferences, not pairKeywords).

drut.highlight.namedVariables (@name@ substitution) works the same way from a user’s perspective, but is written into the current workspace’s settings (.vscode/settings.json), not your personal/global settings — VS Code resolves this particular setting per-scope rather than merging across scopes, and the auto-seeded default described above already lives at workspace scope, so a global-scope write would be silently invisible. Leaving it unset preserves the auto-seed behavior exactly (including “a manual deletion sticks forever”); setting it takes over live; clearing it afterward reverts to the #4EC9B0 default specifically, not to no color at all (a fully theme-driven state would reintroduce the invisibility problem this default exists to prevent).