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:
-
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. -
No red squiggles — If the code is correct, no error underlines appear. The Problems panel (View → Problems) should show no errors.
-
Status bar — The bottom-right of VSCode shows
Structured Textas 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 : INTwithVarkind - 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
IsAboveThresholdin theexceeded :=line → jumps to theFUNCTION IsAboveThresholddeclaration at the top - Click on
counterin theIF counter >= 100line → jumps to theVARblock wherecounteris declared - Click on
limit→ jumps to its declaration
Code Completion
Start typing inside the program body. Completion suggestions appear automatically:
- Type
cou→ completion list showscounter,count(if any), and keywords starting with “COU” - Type
IF→ completion offers theIF...END_IFsnippet template - After a struct variable, type
.→ field names appear (e.g.,myStruct.showsx,y,value)
Snippet completions insert full control structures:
| Trigger | Expands to |
|---|---|
IF | IF ${condition} THEN ... END_IF; |
FOR | FOR ${i} := ${1} TO ${10} DO ... END_FOR; |
WHILE | WHILE ${condition} DO ... END_WHILE; |
CASE | CASE ${expression} OF ... END_CASE; |
FUNCTION | Full function template with VAR_INPUT |
FUNCTION_BLOCK | Full FB template |
PROGRAM | Full 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 theTYPE MyStruct : STRUCT ... END_STRUCT; END_TYPEdeclaration - Click on a variable of type
MyFB→ jumps to theFUNCTION_BLOCK MyFBdeclaration
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) : BOOLwith 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 wherecounteris 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 → typecycle_count→ all occurrences ofcounterare renamed tocycle_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.stfiles - 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 tocounterin 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.
Document Links
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 declaringnew_var, the diagnostics underline it. Press Ctrl+. and select the quick fix to automatically addnew_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:
| Error | Example |
|---|---|
| Undeclared variable | x := unknown_var; |
| Type mismatch | int_var := TRUE; |
| Wrong condition type | IF int_var THEN (needs BOOL) |
| Missing parameters | MyFunc() when params are required |
| Unused variables | Variable declared but never read |
| EXIT outside loop | EXIT; in program body |
| Duplicate declarations | Two 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
- Open
my_program.stin the editor - Set a breakpoint — click in the gutter (left margin) next to line
counter := counter + 1;. A red dot appears. - Press F5 or click Run → Start Debugging
- If prompted, select “Debug Current ST File”
What Happens
The debugger:
- Compiles
my_program.stto bytecode - Starts the VM paused on the first instruction
- 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
| Button | Keyboard | Action |
|---|---|---|
| ▶ Continue | F5 | Run until next breakpoint (across scan cycles, up to 100,000) |
| ⏭ Step Over | F10 | Execute one statement, skip into function calls |
| ⏬ Step Into | F11 | Execute one statement, enter function calls |
| ⏫ Step Out | Shift+F11 | Run until current function returns |
| ⏹ Stop | Shift+F5 | End debug session |
PLC-Specific Debug Toolbar Buttons
The VSCode extension adds 4 PLC-specific buttons to the debug toolbar:
| Button | Action |
|---|---|
| Force | Force a variable to a specific value (overrides program logic) |
| Unforce | Remove the force override from a variable |
| List Forced | Show all currently forced variables and their values |
| Cycle Info | Display 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
- Press F10 (Step Over) — the highlighted line advances to the next statement
- After stepping past
counter := counter + 1;, check the Variables panel:counternow shows1
- Press F10 again — steps to the
exceeded := IsAboveThreshold(...)line - Press F11 (Step Into) — enters the
IsAboveThresholdfunction body - The Call Stack panel shows:
▼ PLC Scan Cycle
IsAboveThreshold line 10
Main line 24
- Press Shift+F11 (Step Out) — returns to
Main - 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→ showsTRUEorFALSE
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:
- The program stops on entry
- Press F5 to continue — it hits your breakpoint
- Check the Variables panel to see all local variables and their current values
- Step through the state machine logic
- 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
- Open the Command Palette:
Ctrl+Shift+P(orCmd+Shift+Pon macOS) - Type and select: “ST: Open PLC Monitor”
- 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
| Feature | Description |
|---|---|
| Live variables | All global and program-local variables update in real time |
| Force variable | Right-click a variable to override its value (useful for testing) |
| Unforce variable | Remove the override and let the program control the value again |
| Trend recording | Watch 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.serverPathpoints 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
VARdeclaration orEND_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
PROGRAMPOU (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
| Action | Shortcut / How |
|---|---|
| CLI | |
| Check file | st-cli check file.st |
| Run program | st-cli run file.st -n 100 |
| LSP Features | |
| Hover for type info | Ctrl+hover on identifier (Cmd+hover on macOS) |
| Go to definition | Ctrl+Click on identifier (Cmd+Click on macOS) |
| Go to type definition | Right-click → Go to Type Definition |
| Code completion | Start typing, or Ctrl+Space (Cmd+Space on macOS) |
| Signature help | Type ( or , inside a function call |
| Find all references | Shift+F12 |
| Rename symbol | F2 |
| Document symbols (outline) | Ctrl+Shift+O (Cmd+Shift+O on macOS) |
| Workspace symbols | Ctrl+T (Cmd+T on macOS) |
| Document highlight | Place cursor on identifier (automatic) |
| Fold block | Ctrl+Shift+[ (Cmd+Option+[ on macOS) |
| Unfold block | Ctrl+Shift+] (Cmd+Option+] on macOS) |
| Document links | Ctrl+Click on file path in comment |
| Format document | Shift+Alt+F (Shift+Option+F on macOS) |
| Code action (quick fix) | Ctrl+. (Cmd+. on macOS) |
| Problems panel | View → Problems |
| Debugging | |
| Start debugging | Open .st file → F5 |
| Set breakpoint | Click gutter or F9 |
| Step over | F10 |
| Step into | F11 |
| Step out | Shift+F11 |
| Continue | F5 |
| Stop debugging | Shift+F5 |
| Force variable | Debug toolbar button or force x = 42 in Debug Console |
| Unforce variable | Debug toolbar button or unforce x in Debug Console |
| List forced variables | Debug toolbar button or listForced in Debug Console |
| Scan cycle info | Debug toolbar button or scanCycleInfo in Debug Console |
| Monitor | |
| Open PLC Monitor | Ctrl+Shift+P → “ST: Open PLC Monitor” |
| Force variable (Monitor) | Right-click variable in Monitor panel |