Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>Structured TextNew to Visual Studio Code? Get it now.
Structured Text

Structured Text

novaziun

|
2 installs
| (0) | Free
Language support for IEC 61131-3 Structured Text in .ST and .EXP files: highlighting, navigation, completion, formatting, renaming and workspace-wide warnings.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Structured Text

Language support for IEC 61131-3 Structured Text in .ST and .EXP files, with no project file, no compiler and no language server: the extension reads the files in the workspace folder and works out the rest.

One file holds one POU, and the filename is its name — that is the only project convention it assumes.

Authors

Developed by novaziun, an engineering company that writes software for mobile and stationary applications: from PLCs in vehicles to servers, mobile apps and desktop applications.

  • Design: Nico Bernold
  • Implementation: Claude Code

What it does

Syntax highlighting. A TextMate grammar for the dialect: nested (* ... *) comments, '...' strings with $ escapes, typed and time literals (T#7000ms, 16#FF), pragmas. On top of it, semantic highlighting colours each identifier by what it actually is — an input, an output, a constant, a global, a type, a function block instance, an enum value — resolved through the whole workspace, not guessed from the line.

Navigation. Ctrl-click a POU, variable, type, enum value or action to jump to its declaration. Hover shows the declaration as it is written, plus the comment next to it. Ctrl-Shift-O lists what is in the file; Ctrl-T searches every name in the workspace, library declarations included.

The outline covers the code as well as the declarations: every CASE statement, with one entry per branch label, nested as written. In a state machine, the breadcrumb bar shows the state the cursor is in, and sticky scroll keeps the current state's label pinned at the top while you scroll through it.

Completion. Members after ., inputs and outputs inside a call (IN := , Q => ), types in a type position, enum values where one is expected. Suggestions come from the project itself, so they follow your own code.

Find references and rename. Across every file, resolved rather than matched as text, so a rename that would collide with an existing name is refused instead of applied.

Formatting. Indentation, keyword case, and declarations aligned so types start in the same column throughout the project. Format on type uppercases keywords as you write them. Range formatting touches only the unit you selected.

Warnings, in the Problems pane, for the whole workspace rather than only for open files:

  • names that break the convention — i_ on inputs, io_ on in-outs, o_ on outputs, g_ on globals, none of them on anything else, uppercase only for constants

  • a call that leaves a function block input unassigned — not counting inputs that are themselves block instances, which callers set field by field if at all

  • a call argument naming an input (name :=) or output (name =>) that the function, program or function block does not have, and a member of a program or function block instance that does not exist — only where the callee is certain, never against one of two equally near declarations

  • an input never read, an output never assigned, a local never used

  • a program or function block assigning its own VAR_INPUT, or a member or element of one — inputs are for callers to set; calling a block-typed input or taking its address is not counted. Where a block assigns an input on purpose, say so in the declaration's comment, which CoDeSys keeps and ignores; the lightbulb on the warning adds the tag for you:

    i_reset: BOOL; (* @WRITABLE cleared once handled *)
    

Naming fixes. The lightbulb on a naming warning offers the rename that clears it, and a fix for every naming warning in the file at once; the latter is also under Source Action…. These are ordinary renames, so callers in other files follow and a name that would collide is left alone, with a note saying why. They are kept out of source.fixAll, so a fix-all on save never renames anything.

Unknown identifiers are never reported. The extension only ever knows part of what a project refers to, so silence is the only honest answer there.

Library stubs: .STUB files

A .lib is a binary the extension cannot read, so calls into one resolve to nothing. A .STUB file fills that gap: plain Structured Text declarations, written by hand next to the project, describing what a library offers. One .STUB file stands for one .lib.

Put them anywhere in the workspace. They are picked up automatically, highlighted and outlined like any other file, and their declarations show up in completion, hover, ctrl-click and symbol search — marked as belonging to a library, and never linted or renamed, since the binary would not follow.

A project declaration of the same name always wins, so a stub can never shadow your own code.

The format

A .STUB file is Structured Text. Everything in it is a declaration — no statement bodies, nothing is compiled. Two comment markers give it structure:

Marker Where Meaning
(* @LIBRARY <file> *) header, above the first declaration the .lib this file stands for; shown in hover and symbol search
(* @GROUP <label> *) between declarations starts a section; everything below it is nested under <label> in the outline until the next marker

Any other @TAG in the header is yours to use and is ignored.

A comment block directly above a declaration is its documentation and is shown on hover. A comment after a parameter's ; documents that parameter.

FUNCTION, FUNCTION_BLOCK, TYPE (enum, struct, alias) and VAR_GLOBAL blocks are all usable. A complete stub file, can.STUB:

(******************************************************************************
 * @LIBRARY can.lib
 * @TITLE   CAN bus driver
 ******************************************************************************)

(* @GROUP Types *)

(* Result of every call into the library. *)
TYPE can_result :
(
    can_ok,
    can_busy,
    can_bus_off
);
END_TYPE

(* @GROUP Messages *)

(* Transmits a CAN message. *)
FUNCTION can_write : can_result
VAR_INPUT
    channel:                                     BYTE;                   (* 0 or 1 *)
    id:                                          DWORD;                  (* 11 or 29 bit identifier *)
    data:                                        POINTER TO t_can_frame; (* the frame to send *)
END_VAR
END_FUNCTION

(* Receives messages on one identifier, one per cycle. *)
FUNCTION_BLOCK can_receiver
VAR_INPUT
    i_enable:                                    BOOL;
    i_id:                                        DWORD;
END_VAR
VAR_OUTPUT
    o_new:                                       BOOL;  (* TRUE for one cycle per message *)
    o_frame:                                     t_can_frame;
END_VAR
END_FUNCTION_BLOCK

(* @GROUP Status *)

VAR_GLOBAL
    can_error_count:                             UDINT; (* frames lost since start-up *)
END_VAR

Names a stub does not declare — t_can_frame above — simply resolve to nothing; they are not errors, so a library can be stubbed as far as it is worth stubbing and no further. The same goes for the IEC 61131-3 standard library (TON, CTU, SEL, the *_TO_* conversions, …): the extension knows nothing about it unless you write a stub for it.

Settings

Setting Default
structuredText.diagnostics.scope workspace openFiles restricts the Problems pane to files open in an editor
structuredText.format.tabSize 4 width of one indentation level
structuredText.format.insertSpaces true indent with spaces rather than tabs
structuredText.format.typeColumn 50 column where types start in VAR and STRUCT blocks
structuredText.format.normalizeIdentifierCase true rewrite identifiers to the spelling of their declaration

Files hidden by a .gitignore or .ignore inside the workspace folder are never indexed, the same files VS Code's search leaves out. Ignore your build output there: a second copy of a POU would make every ctrl-click ambiguous. Ignore files above the workspace folder are not read.

License

MIT with the Commons Clause, see the LICENSE file shipped with the extension. Free to use, including at work and in commercial projects; what is not allowed is selling the extension itself, or a product or service whose value comes mainly from it. Provided as is, with no warranty and no liability for any effects of using it.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft