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

Editing, Running & Debugging in VSCode

This is a complete walkthrough of writing, running, and debugging an IEC 61131-3 Structured Text program in Visual Studio Code using the rust-plc toolchain.


Prerequisites

Before starting, make sure you have:

  • rust-plc repository cloned and built (cargo build -p st-cli)
  • VSCode extension installed (see VSCode Setup)
  • Or simply use the Devcontainer — everything is pre-configured

Fastest way to start: Open the repository in VSCode and click “Reopen in Container”. After the container builds, everything is ready.


Step 1: Create a New ST Program

Open VSCode with the playground/ folder (or any folder with .st files).

Create a new file called my_program.st:

File → New File → Save as my_program.st

Paste this code:

(*
 * My first ST program — a simple counter with threshold detection.
 *)

FUNCTION IsAboveThreshold : BOOL
VAR_INPUT
    value : INT;
    threshold : INT;
END_VAR
    IsAboveThreshold := value > threshold;
END_FUNCTION

PROGRAM Main
VAR
    counter   : INT := 0;
    limit     : INT := 50;
    exceeded  : BOOL := FALSE;
    message   : INT := 0;
END_VAR
    counter := counter + 1;

    exceeded := IsAboveThreshold(value := counter, threshold := limit);

    IF exceeded THEN
        message := 1;
    ELSE
        message := 0;
    END_IF;

    IF counter >= 100 THEN
        counter := 0;
    END_IF;
END_PROGRAM

What you should see immediately

As soon as you save the file:

  1. Syntax highlighting — Keywords (PROGRAM, IF, THEN, END_IF) appear in a distinct color. Types (INT, BOOL) are highlighted differently. Comments are dimmed. String and numeric literals have their own colors.

  2. No red squiggles — If the code is correct, no error underlines appear. The Problems panel (View → Problems) should show no errors.

  3. Status bar — The bottom-right of VSCode shows Structured Text as the language mode.


Step 2: Explore Editor Features

Hover for Type Information

Hold Ctrl (or Cmd on macOS) and hover over any variable or function name:

  • Hover over counter → shows: counter : INT with Var kind
  • Hover over IsAboveThreshold → shows the function signature: FUNCTION(value: INT, threshold: INT) : BOOL
  • Hover over exceeded → shows: exceeded : BOOL

Go to Definition

Ctrl+Click (or Cmd+Click) on any identifier to jump to its declaration:

  • Click on IsAboveThreshold in the exceeded := line → jumps to the FUNCTION IsAboveThreshold declaration at the top
  • Click on counter in the IF counter >= 100 line → jumps to the VAR block where counter is declared
  • Click on limit → jumps to its declaration

Code Completion

Start typing inside the program body. Completion suggestions appear automatically:

  • Type cou → completion list shows counter, count (if any), and keywords starting with “COU”
  • Type IF → completion offers the IF...END_IF snippet template
  • After a struct variable, type . → field names appear (e.g., myStruct. shows x, y, value)

Snippet completions insert full control structures:

TriggerExpands to
IFIF ${condition} THEN ... END_IF;
FORFOR ${i} := ${1} TO ${10} DO ... END_FOR;
WHILEWHILE ${condition} DO ... END_WHILE;
CASECASE ${expression} OF ... END_CASE;
FUNCTIONFull function template with VAR_INPUT
FUNCTION_BLOCKFull FB template
PROGRAMFull program template

Go to Type Definition

Ctrl+Shift+Click (or use the right-click context menu → “Go to Type Definition”) on any variable to jump to the declaration of its type. This is especially useful with user-defined types:

  • Click on a variable of type MyStruct → jumps to the TYPE MyStruct : STRUCT ... END_STRUCT; END_TYPE declaration
  • Click on a variable of type MyFB → jumps to the FUNCTION_BLOCK MyFB declaration

This differs from Go-to-definition (which jumps to where the variable is declared) by jumping to where the variable’s type is declared instead.

Signature Help

When calling a function or function block, the editor shows parameter hints automatically:

  • Type IsAboveThreshold( → a tooltip appears showing (value: INT, threshold: INT) : BOOL with the first parameter highlighted
  • Type a , after the first argument → the tooltip advances to highlight the next parameter

This works for all FUNCTION and FUNCTION_BLOCK calls, showing parameter names and types as you type each argument.

Find All References

Press Shift+F12 (or right-click → “Find All References”) on any identifier to find every usage in the file:

  • On counter → shows all lines where counter is read or assigned
  • On IsAboveThreshold → shows the declaration and all call sites

The search is case-insensitive and matches whole words only, consistent with IEC 61131-3 semantics.

Rename Symbol

Press F2 on any variable or POU name to rename it across all occurrences in the file:

  • Place the cursor on counter → press F2 → type cycle_count → all occurrences of counter are renamed to cycle_count

The rename is case-insensitive and applies to all references, declarations, and usages simultaneously.

Document Symbols (Outline)

Press Ctrl+Shift+O to open the document symbol picker, or open the Outline panel (View → Open View → Outline):

▼ Main (PROGRAM)
    counter : Var : INT
    limit : Var : INT
    exceeded : Var : BOOL
    message : Var : INT
▼ IsAboveThreshold (FUNCTION : BOOL)
    value : VarInput : INT
    threshold : VarInput : INT

This shows all POUs and their variables in a navigable tree. Type in the picker to filter by name.

Workspace Symbols

Press Ctrl+T to search for any POU or type across all open files in the workspace. This is useful when working with multi-file projects:

  • Type Main → shows all PROGRAM/FUNCTION/FUNCTION_BLOCK declarations named “Main” across all .st files
  • Type Temp → finds any POU or type whose name contains “Temp”

Document Highlight

Place your cursor on any identifier and all other occurrences of that symbol in the file are instantly highlighted with a background color. This happens automatically with no keyboard shortcut needed:

  • Click on counter → every reference to counter in the file lights up
  • Click on exceeded → all usages are highlighted

This makes it easy to visually trace how a variable flows through your program.

Folding Ranges

Click the fold icons in the gutter (the small triangles next to line numbers) to collapse code blocks:

  • PROGRAM / FUNCTION / FUNCTION_BLOCK — collapse entire POU bodies
  • VAR / VAR_INPUT / VAR_OUTPUT — collapse variable declaration blocks
  • IF / FOR / WHILE / CASE — collapse control flow blocks
  • Comment blocks (* ... *) — collapse multi-line comments

You can also use Ctrl+Shift+[ to fold and Ctrl+Shift+] to unfold the block at the cursor.

File paths mentioned in comments become clickable links:

(* See utils.st for helper functions *)
// Reference: alarm_logic.st

Ctrl+Click on these paths to open the referenced file directly in the editor.

Formatting

Press Shift+Alt+F to auto-format the entire document. The formatter normalizes indentation to produce consistently readable code. You can also right-click and select “Format Document” from the context menu.

Code Actions (Quick Fixes)

When the LSP reports an undeclared variable, press Ctrl+. (or click the lightbulb icon) to see available quick fixes:

  • If you type new_var := 42; without declaring new_var, the diagnostics underline it. Press Ctrl+. and select the quick fix to automatically add new_var : INT; to the nearest VAR block.

Diagnostics (Error Detection)

Try introducing an error — change counter := counter + 1; to:

counter := counter + TRUE;

Immediately you’ll see:

  • A red squiggly underline under TRUE
  • The Problems panel shows: left operand of '+' must be numeric, found 'BOOL'
  • A red circle appears on the file tab and in the Explorer

Fix the error to clear the diagnostic.

Common diagnostics the LSP catches:

ErrorExample
Undeclared variablex := unknown_var;
Type mismatchint_var := TRUE;
Wrong condition typeIF int_var THEN (needs BOOL)
Missing parametersMyFunc() when params are required
Unused variablesVariable declared but never read
EXIT outside loopEXIT; in program body
Duplicate declarationsTwo variables with the same name

Step 3: Run the Program

From the Terminal

Open the integrated terminal (Ctrl+`) and run:

# Check for errors (no execution)
st-cli check my_program.st

# Run a single scan cycle
st-cli run my_program.st

# Run 1000 scan cycles (like a real PLC)
st-cli run my_program.st -n 1000

Expected output for 1000 cycles:

Executed 1000 cycle(s) in 1.2ms (avg 1.2µs/cycle, 28 instructions)

This tells you:

  • 1000 cycles were executed (like a PLC running for 1000 scans)
  • 1.2µs per cycle — the average execution time
  • 28 instructions — bytecode instructions per cycle

Understanding Scan Cycles

In a real PLC, programs execute in a continuous loop called the scan cycle:

┌─────────────┐
│ Read Inputs │ ← from sensors, switches
├─────────────┤
│ Execute     │ ← your ST program runs here
│ Program     │
├─────────────┤
│ Write       │ ← to motors, valves, lights
│ Outputs     │
└─────┬───────┘
      │ repeat
      └───────→ back to top

The -n 1000 flag simulates 1000 iterations of this loop. Global variables (VAR_GLOBAL) persist across cycles, so a counter increments each time.


Step 4: Debug the Program

Start a Debug Session

  1. Open my_program.st in the editor
  2. Set a breakpoint — click in the gutter (left margin) next to line counter := counter + 1;. A red dot appears.
  3. Press F5 or click Run → Start Debugging
  4. If prompted, select “Debug Current ST File”

What Happens

The debugger:

  1. Compiles my_program.st to bytecode
  2. Starts the VM paused on the first instruction
  3. Shows the Debug toolbar at the top of the editor:
  ▶ Continue  ⏭ Step Over  ⏬ Step Into  ⏫ Step Out  🔄 Restart  ⏹ Stop

The editor highlights the current line (typically the first executable statement) with a yellow background.

Debug Controls

ButtonKeyboardAction
▶ ContinueF5Run until next breakpoint (across scan cycles, up to 100,000)
⏭ Step OverF10Execute one statement, skip into function calls
⏬ Step IntoF11Execute one statement, enter function calls
⏫ Step OutShift+F11Run until current function returns
⏹ StopShift+F5End debug session

PLC-Specific Debug Toolbar Buttons

The VSCode extension adds 4 PLC-specific buttons to the debug toolbar:

ButtonAction
ForceForce a variable to a specific value (overrides program logic)
UnforceRemove the force override from a variable
List ForcedShow all currently forced variables and their values
Cycle InfoDisplay scan cycle statistics (count, timing)

You can also use these via the Debug Console by typing evaluate expressions:

force counter = 42
unforce counter
listForced
scanCycleInfo

Inspect Variables

While paused, look at the Variables panel on the left (Debug sidebar):

▼ Locals
    counter    0    INT
    limit      50   INT
    exceeded   FALSE BOOL
    message    0    INT
▼ Globals
    (empty — no VAR_GLOBAL in this program)

The values update as you step through the code.

Step Through Code

  1. Press F10 (Step Over) — the highlighted line advances to the next statement
  2. After stepping past counter := counter + 1;, check the Variables panel:
    • counter now shows 1
  3. Press F10 again — steps to the exceeded := IsAboveThreshold(...) line
  4. Press F11 (Step Into) — enters the IsAboveThreshold function body
  5. The Call Stack panel shows:
▼ PLC Scan Cycle
    IsAboveThreshold    line 10
    Main                line 24
  1. Press Shift+F11 (Step Out) — returns to Main
  2. Press F5 (Continue) — runs until the breakpoint is hit again (next scan cycle)

Watch Expressions

In the Watch panel, click + and type a variable name:

  • Type counter → shows the current value
  • Type exceeded → shows TRUE or FALSE

The watch panel evaluates variable names against the current scope (locals first, then globals).

Breakpoint Features

  • Toggle breakpoint: Click the gutter or press F9 on a line
  • Remove all breakpoints: Run → Remove All Breakpoints
  • Conditional breakpoints are not yet supported (future feature)

Debug a Program with Global Variables

Create counter_demo.st:

VAR_GLOBAL
    total_cycles : INT;
END_VAR

PROGRAM Main
VAR
    x : INT := 0;
END_VAR
    total_cycles := total_cycles + 1;
    x := total_cycles * 2;
END_PROGRAM

Debug this file and use Continue (F5) multiple times. Watch total_cycles increment in the Globals scope each time the program completes a cycle and restarts.


Step 5: Debug a Multi-POU Program

The debugger supports stepping into function calls across POUs.

Open playground/06_full_demo.st and set a breakpoint inside the CASE state OF block. Press F5 to start debugging:

  1. The program stops on entry
  2. Press F5 to continue — it hits your breakpoint
  3. Check the Variables panel to see all local variables and their current values
  4. Step through the state machine logic
  5. Use Step Into (F11) when a function like Clamp(...) is called to enter it

Call Stack Navigation

When stopped inside a nested function call, the Call Stack panel shows all active frames:

▼ PLC Scan Cycle
    Clamp              line 32    ← current position
    BottleFiller       line 112   ← caller

Click on BottleFiller in the call stack to view the caller’s local variables and source position.


Step 6: PLC Monitor Panel

The Monitor Panel provides a live dashboard for observing and controlling PLC variables in real time while the program runs.

Open the Monitor Panel

  1. Open the Command Palette: Ctrl+Shift+P (or Cmd+Shift+P on macOS)
  2. Type and select: “ST: Open PLC Monitor”
  3. A webview panel opens showing all variables with live values

The monitor connects to the runtime via a WebSocket server that streams variable state after each scan cycle.

Monitor Features

FeatureDescription
Live variablesAll global and program-local variables update in real time
Force variableRight-click a variable to override its value (useful for testing)
Unforce variableRemove the override and let the program control the value again
Trend recordingWatch how variable values change over time

Forcing Variables

Forcing is essential during commissioning and testing. When a variable is forced:

  • The forced value is written at the start of each scan cycle, overriding program logic
  • A visual indicator shows which variables are currently forced
  • Use “Unforce” to release the variable back to normal program control

Online Change via the Monitor

The monitor server’s WebSocket API also supports online change (hot-reload). When you modify and recompile a source file, you can push the new module to the running engine without stopping it:

  • Compatible changes (e.g., modified logic, same variable layout) are applied instantly
  • Variable values are automatically migrated to the new module
  • Incompatible changes (e.g., added/removed variables, changed types) require a full restart

This enables an iterative development workflow where you can edit program logic and see the effects immediately in the monitor panel, without losing runtime state.


Troubleshooting

“Failed to start ST language server”

  • Build the CLI: cargo build -p st-cli
  • Check the setting structured-text.serverPath points to the built binary

Breakpoints appear as gray circles (unverified)

  • The line may not correspond to any executable bytecode instruction
  • Try setting the breakpoint on an assignment or function call line instead of a VAR declaration or END_IF

No syntax highlighting

  • Check the status bar shows “Structured Text” (not “Plain Text”)
  • If not, click the language mode and select “Structured Text”
  • Reload the window: Ctrl+Shift+P → “Developer: Reload Window”

Debug session ends immediately

  • Ensure the file has a PROGRAM POU (not just functions/FBs)
  • Check the terminal for compilation errors

Variables show <unknown>

  • The variable may be out of scope
  • Step into the function where the variable is declared

Quick Reference

ActionShortcut / How
CLI
Check filest-cli check file.st
Run programst-cli run file.st -n 100
LSP Features
Hover for type infoCtrl+hover on identifier (Cmd+hover on macOS)
Go to definitionCtrl+Click on identifier (Cmd+Click on macOS)
Go to type definitionRight-click → Go to Type Definition
Code completionStart typing, or Ctrl+Space (Cmd+Space on macOS)
Signature helpType ( or , inside a function call
Find all referencesShift+F12
Rename symbolF2
Document symbols (outline)Ctrl+Shift+O (Cmd+Shift+O on macOS)
Workspace symbolsCtrl+T (Cmd+T on macOS)
Document highlightPlace cursor on identifier (automatic)
Fold blockCtrl+Shift+[ (Cmd+Option+[ on macOS)
Unfold blockCtrl+Shift+] (Cmd+Option+] on macOS)
Document linksCtrl+Click on file path in comment
Format documentShift+Alt+F (Shift+Option+F on macOS)
Code action (quick fix)Ctrl+. (Cmd+. on macOS)
Problems panelView → Problems
Debugging
Start debuggingOpen .st file → F5
Set breakpointClick gutter or F9
Step overF10
Step intoF11
Step outShift+F11
ContinueF5
Stop debuggingShift+F5
Force variableDebug toolbar button or force x = 42 in Debug Console
Unforce variableDebug toolbar button or unforce x in Debug Console
List forced variablesDebug toolbar button or listForced in Debug Console
Scan cycle infoDebug toolbar button or scanCycleInfo in Debug Console
Monitor
Open PLC MonitorCtrl+Shift+P → “ST: Open PLC Monitor”
Force variable (Monitor)Right-click variable in Monitor panel