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

Introduction

Drut is a linter, formatter, and editor tooling suite for Cube Voyager control-statement scripts (.s / .block files) — the script language transportation planners use to write travel-demand model logic.

Editing these scripts without Drut usually means no syntax highlighting, no error checking until the model actually runs, and no consistent formatting across a team. Drut catches structural mistakes as you type, keeps scripts formatted consistently, and adds real editor support — syntax highlighting, hover help, autocomplete — to VS Code and any other editor that speaks the Language Server Protocol.

What Drut does

  • Structural diagnostics — unmatched IF/LOOP/RUN/PROCESS blocks, unclosed comments, and other real mistakes, flagged before you run the model. See the Editor Guide for the full list.
  • Formatting — consistent indentation and, optionally, keyword casing, operator spacing, and blank-line normalization — on save, on paste, or from the CLI. See the Formatter Guide.
  • Hover, autocomplete, and “did you mean” spell-check for control words and keyword names.
  • Shared project configuration via a drut.toml file, so a team doesn’t need to agree on CLI flags or editor settings individually. See the Configuration Reference.
  • Three ways in: a drut CLI, a VS Code/Open VSX extension (built on the Language Server Protocol, so it works in any LSP-capable editor, not only VS Code), and a Model Context Protocol server for AI coding assistants — all built on one shared engine, so every surface agrees on what’s valid.

What Drut does not do

Drut is a structural and formatting tool, not a full semantic validator. It does not:

  • Validate program-box-specific keyword combinations (e.g. whether a particular PATHLOAD parameter combination is meaningful for the model you’re running).
  • Check cross-file/repo-wide semantics beyond the direct READ FILE inclusion hover already reaches.
  • Run or simulate your model in any way.

These are explicit, current scope boundaries — not oversights.

Who this is for

Anyone writing or reviewing Cube Voyager .s/.block scripts: transportation model developers, analysts maintaining a shared script library, and teams that want consistent formatting without hand-enforcing a style guide.

Where to go next

Install

Pick one or both — they’re independent, and the extension doesn’t require you to separately install the CLI.

Install Drut for Cube Voyager from the VS Code Marketplace, or from Open VSX on VS Code-compatible editors that use it instead (Cursor, VSCodium, and similar).

Nothing else to install. On first activation, the extension resolves a working drut language server binary automatically: it checks PATH first, then its own persistent extension storage from a prior activation, then — if neither is present — downloads the correct binary for your platform from the latest GitHub Release and verifies it against its published SHA-256 checksum before trusting it. If every option is unavailable (offline, an unsupported platform, a failed download), the extension degrades to syntax-highlighting-only rather than failing outright, and tells you why once.

Once installed this way, a throttled (at most once per 24 hours), non-blocking background check offers a dismissible notification when a newer release is available — it never silently replaces a running binary.

Just the CLI (for scripting or CI)

cargo install drut-cli

Or build from source:

cargo build --release -p drut-cli
# binary at target/release/drut(.exe) -- put it on PATH

Confirm it’s working:

drut --help

Continue to Getting Started to run it against a real script.

Getting Started

This walks through the CLI against a small sample script. If you’re only using the VS Code/Open VSX extension, the same check/format behavior happens automatically as you type and save — skip ahead to the Editor Guide.

1. A sample script

Save this as sample.s:

run pgm=matrix
  mati=base.mat,mo=out.mat
  if(i==1)
       ZONES = 100
  endif
endrun

It’s structurally valid Cube Voyager — but the indentation is inconsistent (2 spaces under RUN, then 7 under IF) and IF(i==1) has no space before its condition.

2. Check it

drut check sample.s

Expected output: nothing, and an exit code of 0. check only prints when it finds a real structural problem (an unmatched IF/LOOP/RUN/PROCESS, an unclosed comment, and similar) — a clean script produces no output at all, the same way a passing test suite often does.

3. See what formatting would change

drut format sample.s --diff --isolated

(--isolated skips drut.toml discovery for this one run, so the output below is reproducible regardless of any config file elsewhere on your machine — see the Configuration Reference for what --isolated skips.)

Expected output:

--- sample.s
+++ sample.s
@@ -1,6 +1,6 @@
 run pgm=matrix
-  mati=base.mat,mo=out.mat
-  if(i==1)
-       ZONES = 100
-  endif
+    mati=base.mat,mo=out.mat
+    if(i==1)
+        ZONES = 100
+    endif
 endrun

Every nested line is now indented to a consistent 4 spaces per level (the built-in default — see indent_width to change it). Nothing about the script’s meaning changed — no line was reordered, no keyword was invented or removed.

4. Write the change

drut format sample.s --write --isolated

Reformats sample.s in place. --write, --check (report which files would change, write nothing), and --diff (shown above) are mutually exclusive — see the CLI Reference for the full flag list, including --casing-control-words, --operator-spacing, and every other formatting axis.

Next steps

CLI Reference

drut has four subcommands: check, format, server, and mcp.

check

drut check <PATH>

Reports every structural diagnostic found for each .s/.block file under <PATH> (a single file or a directory, scanned recursively). Prints nothing and exits 0 for a clean run.

FlagValuesDefaultEffect
--formattext, sariftextOutput format. sarif emits SARIF 2.1.0, for CI/tooling integration.

format

drut format <PATH> [flags]

Normalizes whitespace (and, opt-in, keyword casing/operator spacing/blank-line runs) for each .s/.block file under <PATH>. With none of --write, --check, or --diff, defaults to printing the reformatted result to stdout.

Disposition flags (mutually exclusive):

FlagEffect
--writeOverwrite each matched file in place.
--checkReport which files would change; write nothing.
--diffPrint a unified diff per changed file; write nothing.

Formatting-axis flags — every one of these mirrors a drut.toml [format] field one-to-one; see the Configuration Reference for accepted values, defaults, and what each actually does:

Flagdrut.toml field
--casing-control-wordscasing_control_words
--casing-pair-keywordscasing_pair_keywords
--casing-data-referencescasing_data_references
--casing-function-callscasing_function_calls
--indent-top-levelindent_top_level
--indent-widthindent_width
--operator-spacingoperator_spacing
--blank-linesblank_lines
--blank-lines-top-capblank_lines_top_cap
--blank-lines-nested-capblank_lines_nested_cap
--line-wrapline_wrap
--line-wrap-widthline_wrap_width
--line-wrap-styleline_wrap_style

An explicit flag here always wins over drut.toml and any editor setting for that one run — see the Configuration Reference’s Precedence section.

Other flags:

FlagEffect
--isolatedSkip drut.toml discovery entirely for this run — built-in defaults plus whatever other flags you passed. Useful for CI reproducibility or a one-off sanity check.

server

drut server

Speaks the Language Server Protocol over stdio. No flags — launched by an LSP client (like the VS Code extension), not run interactively. See the Editor Guide.

mcp

drut mcp

Speaks the Model Context Protocol over stdio. No flags — launched by an MCP-capable client (an AI coding assistant), not run interactively. Exposes four read-only tools, entirely independent of server above (no shared state). See the MCP Guide.

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).

MCP Guide

drut mcp speaks the Model Context Protocol over stdio — a way for an AI coding assistant to query Drut’s understanding of a Cube Voyager script directly, instead of guessing from raw text. All four tools are read-only: none of them write to disk.

Launch it the same way any MCP client launches a stdio server: point your client’s MCP configuration at the drut binary with the mcp argument.

diagnose

Reports every structural diagnostic voyager-core can find for a script (given as inline text or a file path). Returns an empty list for a structurally valid script. Same diagnostic categories as the Editor Guide’s structural set — the Hint-level streams (unclosed ; FMT: OFF, malformed drut.toml, undefined @token@) are LSP-only and never appear here.

Use it when: an assistant needs to check whether a script (or a change it’s about to propose) is structurally valid before suggesting it.

format

Reformats a script’s whitespace/indentation (and, opt-in via the same parameters drut format/drut.toml accept — casing_control_words, operator_spacing, blank_lines, and the rest) and returns the result plus whether anything changed. Idempotent: formatting an already-formatted script reports changed=false.

Use it when: an assistant is about to write or hand back Cube Voyager script text and wants it consistently formatted first.

query_structure

Reports which of the seven block kinds (If/Loop/Run/Process/JLoop/ LinkLoop/DistributeMultistep), if any, encloses a given 1-based line/column position in a script, and where its matched counterpart is — correctly resolved even through Run/Process’s implicit-close quirk. Reports kind: null, not an error, when no block encloses the position.

Use it when: an assistant needs to understand the block structure around a specific position — e.g. “is this line inside an IF, and if so, where does that IF end?”

lookup_keyword

Looks up real, corpus-evidenced keyword=value pair-name candidates for a given enclosing control word (e.g. RUN), falling back to the general control-word list when none is given. Optionally also runs a “did you mean” spell-check against a supplied token.

Use it when: an assistant is generating or validating a keyword=value pair and wants to confirm the keyword name is real (or find the likely intended one for a typo).

Formatter Guide

What formatting guarantees

The formatter is idempotent — running it twice produces the same result as running it once (format(format(x)) == format(x)). It is strictly behavior-preserving: it never reorders statements, never changes which lines are continuations of a prior statement, and never alters program meaning. It only ever changes whitespace and, opt-in, keyword casing. If a script is structurally broken (see the Editor Guide’s diagnostic list), the formatter still does its best on the parts it understands rather than refusing outright — but a diagnosed/unmatched block’s own children are left with their original indentation rather than guessed at.

Every field below is documented in full in the Configuration Reference — this page focuses on what changes, with real examples.

Casing

The four independent casing fields — casing_control_words, casing_pair_keywords, casing_data_references, and casing_function_calls — only ever touch keyword names — never a value, and never a category they aren’t scoped to. This example makes several boundaries visible at once, with casing_control_words = "upper":

-run pgm=matrix
+RUN pgm=matrix
     mati=base.mat,mo=out.mat
-endrun
+ENDRUN

run/endrun (control words) are uppercased — but pgm (a pair-keyword name, casing_pair_keywords’s own scope, left unset here), matrix (a value, never touched), and mati/mo (data-reference tokens, casing_data_references’s own scope, also left unset here) all stay exactly as written. Each field only ever affects its own category — set the one(s) you want independently.

Function-call casing

casing_function_calls normalizes a recognized Cube Voyager built-in function name’s casing, but only where it’s immediately followed by ( with no intervening whitespace — the unambiguous call position, since Voyager has no user-definable functions:

-RouteName = replacestr(RouteName,'-','',0)
+RouteName = REPLACESTR(RouteName,'-','',0)

The recognized list (138 names) spans the general Control Language core (ABS, TRIM, REPLACESTR, ROUND, …), Highway/Matrix-program functions (ROWSUM, PATHTRACE, …), Public Transport skim functions (TIMEA, BRDINGS, GCOST, …), the CONVERGE-phase iteration-statistics family (GAPCHANGE, RGAPMIN, …), and CUBE Cluster utility functions — the same list the VS Code extension’s syntax highlighting uses (see the Editor Guide).

Two real names exist as more than one thing in Cube Voyager: FORMAT is also a FILEO pair-keyword, and LOG is also a control word. Each occurrence’s own position decides which field governs it — never both, never neither:

 [format]
 casing_pair_keywords = "upper"
 casing_function_calls = "lower"
-FILEO format=csv
+FILEO FORMAT=csv
-X = FORMAT(volume,8,2,',')
+X = format(volume,8,2,',')

format on the first line is a pair-keyword name (followed by =), governed by casing_pair_keywords alone. FORMAT on the second line is a function call (followed by (), governed by casing_function_calls alone.

Indentation

indent_top_level controls depth-0 statements; indent_width controls spacing per nesting level inside a block, relative to the block’s own opening-statement column. See Getting Started for a full before/after example.

Operator spacing

preserve (the default) leaves spacing exactly as written. fixed normalizes every operator to exactly one space on each side and removes interior padding inside brackets/parens:

-IF(ZONES==1)
-    ZONES=1
-    CNT=2
-    ITER=333
+IF(ZONES == 1)
+    ZONES = 1
+    CNT = 2
+    ITER = 333
 ENDIF

auto does everything fixed does, plus vertically aligns the = of consecutive Assignment statements at the same nesting depth to the longest left-hand side in the run:

-IF(ZONES==1)
-    ZONES=1
-    CNT=2
-    ITER=333
+IF(ZONES == 1)
+    ZONES = 1
+    CNT   = 2
+    ITER  = 333
 ENDIF

A run resets at a blank line, a comment-only line, a nesting-depth change, or a non-Assignment statement — so alignment never reaches across unrelated sections of a script.

Blank-line normalization

preserve (the default) leaves every run of consecutive blank lines exactly as written, however long. auto contracts a run down to the applicable cap (blank_lines_top_cap between top-level statements/blocks, default 2; blank_lines_nested_cap inside any block’s body, default 1) — only when a run exceeds the cap, never padding a shorter run up:

 RUN PGM=MATRIX
-
 
     MATI=a.mat
 
-
     MATO=b.mat
-
-
 
 ENDRUN

Line wrapping

preserve (the default) leaves every line exactly as written, however long. auto wraps an over-width Control statement’s keyword=value pair list across multiple physical lines once it exceeds line_wrap_width (default 120) — using Cube Voyager’s own existing line-continuation syntax, the same trailing comma that already makes the next physical line a continuation of the same statement. Only Control statements are eligible; an Assignment statement’s arithmetic/string expression is never touched by this feature.

line_wrap_style decides how pairs are distributed across the new continuation lines. fill (the default) packs as many pairs as fit per line:

-RUN PGM=MATRIX, ZONES=5, PRINT=1, COMBINE=T
+RUN PGM=MATRIX, ZONES=5, PRINT=1,
+    COMBINE=T

one_per_line places exactly one pair per continuation line instead, however much width is left over on any given line:

-RUN PGM=MATRIX, ZONES=5, PRINT=1, COMBINE=T
+RUN PGM=MATRIX,
+    ZONES=5,
+    PRINT=1,
+    COMBINE=T

(Both examples above use a narrowed line_wrap_width = 40 so the wrap is visible at doc-page width — the real default is 120.)

A statement that already contains a continuation character anywhere — i.e. you already hand-wrapped it — is left completely untouched, regardless of width:

RUN PGM=MATRIX,
    ZONES=5, PRINT=1, COMBINE=T

stays exactly as written under line_wrap = "auto", even though the combined statement is well over 40 characters. This is deliberate, not a missed case: it’s the safest boundary for an output-modifying transform (no fighting hand-formatted content) and it’s also what makes the feature idempotent by construction — once auto wraps a statement, the wrapped result itself contains a continuation character, so a second format pass sees “already continued” and leaves it alone. format(format(x)) == format(x) holds without needing to re-derive it from scratch.

; FMT: OFF / ; FMT: ON regions

Wrap a range in ; FMT: OFF / ; FMT: ON to exclude it from formatting entirely — useful for a block whose hand-tuned spacing carries meaning to a reviewer that automatic formatting would otherwise flatten:

-RUN PGM=MATRIX
+RUN PGM = MATRIX
 ; FMT: OFF
     ZONES=1
 ; FMT: ON
-    MATI=a.mat
+    MATI = a.mat
 ENDRUN

Everything between the markers (ZONES=1 above) is untouched, while the lines outside them still get operator_spacing = "fixed" applied. An unclosed ; FMT: OFF (no matching ; FMT: ON before end of file) protects through the rest of the file and is always surfaced — never silently unbounded with no indication — as a Hint diagnostic (source drut-fmt, see the Editor Guide), a CLI stderr notice, or an MCP format response field, depending on which surface you’re using.

Configuration Reference

Every [format] field Drut currently understands, in one place. All of them are set the same way, in a drut.toml file at (or above) the file you’re formatting:

[format]
casing_control_words = "lower"
indent_width = 2

Drut discovers the nearest drut.toml by walking up from the file being processed, stopping at the first drut.toml found, a .git boundary, or the filesystem root — whichever comes first. A project with no drut.toml anywhere behaves exactly like a project with an empty one: every field uses its built-in default. Every field is optional; omitting a key is identical to writing its default value explicitly.

Starter drut.toml

Every field, commented out at its built-in default. Copy this into a drut.toml at your project root and uncomment (then change) only the fields you want to override — a commented-out line changes nothing, so you never need to remember the full field list or its defaults from scratch:

[format]
# casing_control_words = "preserve"    # preserve | upper | lower
# casing_pair_keywords = "preserve"    # preserve | upper | lower
# casing_data_references = "preserve"  # preserve | upper | lower
# casing_function_calls = "preserve"   # preserve | upper | lower
# indent_top_level = "preserve"        # preserve | auto
# indent_width = 4                     # 1-16
# operator_spacing = "preserve"        # preserve | fixed | auto
# blank_lines = "preserve"             # preserve | auto
# blank_lines_top_cap = 2              # 1-50, only used when blank_lines = "auto"
# blank_lines_nested_cap = 1           # 1-50, only used when blank_lines = "auto"
# line_wrap = "preserve"               # preserve | auto
# line_wrap_width = 120                # 20-500, only used when line_wrap = "auto"
# line_wrap_style = "fill"             # fill | one_per_line, only used when line_wrap = "auto"

See Fields below for what each one actually does, and Precedence for how a set value interacts with CLI flags, MCP parameters, and editor settings.

A malformed value never blocks formatting. An unrecognized key or an out-of-range value only affects that one field — it warns (CLI stderr, an LSP Hint diagnostic, or the MCP format tool’s config_warnings field) and falls back to that field’s built-in default. Every other valid setting in the same file still applies.

Precedence

Every field below resolves the same four-tier way, checked in this order — the first tier that sets a value wins:

  1. An explicit CLI flag or MCP tool parameter, passed for one specific invocation — always wins when given.
  2. drut.toml — the nearest one found by the discovery walk above.
  3. An editor client setting — for VS Code, one of the drut.format.* settings under Settings; delivered to drut-lsp via the standard LSP workspace/configuration mechanism. A personal editor preference never overrides a project’s own committed drut.toml — it only fills in a field drut.toml leaves unset.
  4. The built-in default — used only if none of the above set the field.

Each tier only fills in a field the tier(s) before it left unset; a field is never assembled from pieces at different tiers. Every field below resolves this same plain four-tier chain — no field has any extra fallback wrinkle.

A flat casing field once existed, covering control_words+pair_keywords together — removed once the three granular fields below fully superseded it. A drut.toml/CLI/MCP/editor-setting still using casing no longer does anything; it degrades exactly like any other unrecognized key (a warning, falling back to each field’s own built-in default), never a hard failure.

Fields

casing_control_words

Casing convention for the control_words category (things like IF, ENDIF, LOOP, ENDLOOP).

Values: preserve ← default, upper, lower.

Default: preserve.

Also known as: CLI flag --casing-control-words; MCP format tool parameter casing_control_words.

Example:

[format]
casing_control_words = "upper"

Precedence: follows the four-tier chain above.

casing_pair_keywords

Casing convention for the pair_keywords category (keyword names inside a Control statement’s keyword=value pairs, e.g. PATHLOAD, MATI), same shape as casing_control_words above.

Values: preserve ← default, upper, lower.

Default: preserve.

Also known as: CLI flag --casing-pair-keywords; MCP format tool parameter casing_pair_keywords.

Example:

[format]
casing_pair_keywords = "lower"

Precedence: follows the four-tier chain above.

casing_data_references

Casing for the data-reference category: Matrix/Line/Node/Zone/Database abbreviations (MI/MO/MW, LI/LW, NI/NW, ZI/ZONES/Z, DBI/DBA), RO, the link-endpoint fields A/B, and the reserved loop-index identifiers I/J.

Values: preserve ← default, upper, lower.

Default: preserve.

Also known as: CLI flag --casing-data-references; MCP format tool parameter casing_data_references.

Example:

[format]
casing_data_references = "lower"

Precedence: follows the four-tier chain above.

casing_function_calls

Casing for recognized Cube Voyager built-in function names (e.g. REPLACESTR, ROUND, TRIM) — only a name immediately followed by ( with no intervening whitespace counts as a call; the same name elsewhere (a keyword=value pair name, a plain identifier) is untouched by this field. Covers 138 names spanning the general Control Language core (Numeric/Trig/Character-String functions), Highway/Matrix-program functions, Public Transport skim functions, the CONVERGE-phase iteration-statistics family, and CUBE Cluster utility functions — see the Formatter Guide for the full list and how two real names (FORMAT, LOG) that also exist as a pair-keyword/ control word respectively are disambiguated by position.

Values: preserve ← default, upper, lower.

Default: preserve.

Also known as: CLI flag --casing-function-calls; MCP format tool parameter casing_function_calls.

Example:

[format]
casing_function_calls = "upper"

Precedence: follows the four-tier chain above.

indent_top_level

Whether top-level (depth-0, not inside any block) statement indentation is left exactly as written, or normalized to column 0.

Values:

  • preserve ← default — leave top-level indentation exactly as written.
  • auto — force every top-level line to column 0.

Default: preserve.

Also known as: CLI flag --indent-top-level; MCP format tool parameter indent_top_level.

Example:

[format]
indent_top_level = "auto"

Precedence: follows the four-tier chain above.

indent_width

Spaces per nesting level of block indentation, relative to the enclosing block’s own opening-statement column.

Values: any integer from 1 to 16default 4.

Default: 4.

Also known as: CLI flag --indent-width; MCP format tool parameter indent_width.

Example:

[format]
indent_width = 2

Precedence: follows the four-tier chain above. An out-of-range value (0, 500, …) at any tier is treated as unset for that tier — resolution falls through to the next tier exactly as if the field had been omitted there.

operator_spacing

Whitespace normalization around =, comparison operators (==, <>, >=, <=, <, >), binary arithmetic (+, -, *, /), comma spacing between multiple keyword=value pairs, and interior padding inside [...]/(...).

Values:

  • preserve ← default — leave existing spacing exactly as written.
  • fixed — normalize every occurrence to exactly one space on each side (and zero interior padding inside brackets/parens), independent of neighboring lines.
  • auto — everything fixed does, plus vertically aligns the = of consecutive Assignment statements at the same nesting depth to the column of the longest left-hand side in the run. A run resets at a blank line, a comment-only line, a nesting-depth change, or a non-Assignment statement.

Default: preserve.

Also known as: CLI flag --operator-spacing; MCP format tool parameter operator_spacing.

Example:

[format]
operator_spacing = "auto"

See the Formatter Guide for full before/after examples of fixed vs. auto.

Precedence: follows the four-tier chain above.

blank_lines

Whether runs of consecutive blank lines (including whitespace-only lines) are left as written or contracted down to a configured cap.

Values:

  • preserve ← default — leave every blank-line run exactly as written, however long.
  • auto — contract a run down to the applicable cap (blank_lines_top_cap or blank_lines_nested_cap) only when the run exceeds that cap — never pads a shorter run up.

Default: preserve.

Also known as: CLI flag --blank-lines; MCP format tool parameter blank_lines.

Example:

[format]
blank_lines = "auto"

Precedence: follows the four-tier chain above.

blank_lines_top_cap

The maximum number of consecutive blank lines blank_lines = "auto" allows between top-level statements/blocks before contracting the run. Only meaningful when blank_lines is "auto".

Values: any integer from 1 to 50default 2.

Default: 2.

Also known as: CLI flag --blank-lines-top-cap; MCP format tool parameter blank_lines_top_cap.

Example:

[format]
blank_lines = "auto"
blank_lines_top_cap = 1

Precedence: follows the four-tier chain above. An out-of-range value at any tier is treated as unset for that tier, same as indent_width.

blank_lines_nested_cap

The maximum number of consecutive blank lines blank_lines = "auto" allows inside any block’s own body, uniformly regardless of nesting depth, before contracting the run. Only meaningful when blank_lines is "auto".

Values: any integer from 1 to 50default 1.

Default: 1.

Also known as: CLI flag --blank-lines-nested-cap; MCP format tool parameter blank_lines_nested_cap.

Example:

[format]
blank_lines = "auto"
blank_lines_nested_cap = 2

Precedence: follows the four-tier chain above. Same out-of-range handling as blank_lines_top_cap.

line_wrap

Whether an over-width Control statement’s keyword=value pair list is wrapped across multiple physical lines, using Cube Voyager’s own existing line-continuation syntax (a trailing comma already tells the parser the next physical line continues the same statement — no new syntax is introduced). Only Control statements are eligible — an Assignment statement’s arithmetic/string expression is out of scope. A statement that already contains a continuation character anywhere is left completely untouched, however long, regardless of width — never re-flowed, which is also what makes this field idempotent by construction: once wrapped, a statement is “already continued” on the next pass.

Values:

  • preserve ← default — leave every line exactly as written, however long.
  • auto — wrap once a statement’s line length exceeds line_wrap_width, using line_wrap_style to decide how pairs are distributed across the new continuation lines.

Default: preserve.

Also known as: CLI flag --line-wrap; MCP format tool parameter line_wrap.

Example (width lowered to 40 here only to keep the before/after readable on this page — 120 is the real default):

[format]
line_wrap = "auto"
line_wrap_width = 40

Before:

RUN PGM=MATRIX, ZONES=5, PRINT=1, COMBINE=T

After, with line_wrap_style = "fill" (the default) — packs as many pairs as fit per continuation line:

RUN PGM=MATRIX, ZONES=5, PRINT=1,
    COMBINE=T

After, with line_wrap_style = "one_per_line" instead — exactly one pair per continuation line:

RUN PGM=MATRIX,
    ZONES=5,
    PRINT=1,
    COMBINE=T

See the Formatter Guide for this same example plus the already-hand-wrapped case.

Precedence: follows the four-tier chain above.

line_wrap_width

The maximum line width line_wrap = "auto" wraps toward — once a Control statement’s line exceeds this many characters, it becomes a wrap candidate. Only meaningful when line_wrap is "auto".

Values: any integer from 20 to 500default 120.

Default: 120.

Also known as: CLI flag --line-wrap-width; MCP format tool parameter line_wrap_width.

Example:

[format]
line_wrap = "auto"
line_wrap_width = 100

Precedence: follows the four-tier chain above. An out-of-range value at any tier is treated as unset for that tier, same as indent_width.

line_wrap_style

How a wrapped statement’s keyword=value pairs are distributed across the new continuation lines once line_wrap wraps it. Only meaningful when line_wrap is "auto".

Values:

  • fill ← default — greedily packs as many pairs as fit within line_wrap_width onto each continuation line before breaking.
  • one_per_line — places exactly one pair per continuation line, regardless of how much width is left over on any given line.

Default: fill — chosen over one_per_line deliberately: a statement’s wrap style, once applied, is never undone by a later format pass (see line_wrap above), so fill is the cheaper direction to manually diverge from afterward if you don’t like it for one specific statement, versus starting from one_per_line and wanting to hand-compact it back down.

Also known as: CLI flag --line-wrap-style; MCP format tool parameter line_wrap_style.

Example:

[format]
line_wrap = "auto"
line_wrap_style = "one_per_line"

See line_wrap above for the before/after example showing both styles side by side.

Precedence: follows the four-tier chain above.