undoStudio v0.1

undoStudio Released · v0.1

The IDE of the undoRT ecosystem. It edits what st2cpp compiles and what undoPLC runs: IEC 61131-3 Structured Text with the colouring and the completion lists driven by what the file actually declares, a project described in JSON, and Compile one button away from the transpiler.

Overview

undoStudio owns the application lifecycle, the window, the ImGui framework and the project on disk. It implements no domain functionality: everything else is an undoApp, an independent module built as a shared object and loaded from plugins/. The split is the architecture, not a convenience — an undoApp that has to be linked into the executable cannot be shipped on its own.

undoStudio: the Workspace tree on the left, the Editor panel in the middle with a Structured Text file open, the ST Output panel below and the ST Outline on the right
The shipped layout: Workspace, Editor, ST Output and ST Outline.
v0.1: the project configuration is JSON, every kind of file opens in one editor panel, a FUNCTION can be parsed, and the version is announced from one place. Three test suites — 40 tests — run on every build, all of them headless: no GPU and no display.

Installation

Download the archive

Grab the latest release from GitHub Releases:

tar -xzf undoStudio-<version>-linux-x86_64.tar.gz
cd undoStudio-<version>
./undoStudio
Start it from the directory it was unpacked into. Fonts, icons, themes and the shipped layout are opened as resources/… and the undoApps are loaded from plugins/, both relative to the working directory — from anywhere else the IDE comes up without them.

What is in the archive:

PathWhat
undoStudiothe IDE
libundoStudioCore.sothe core library it links
plugins/the undoApps, loaded at start-up
resources/fonts, icons, themes, default layout
st2cpp/the transpiler the Compile button invokes
docs/the feature documentation, a screenshot on every page

The X11 and OpenGL development libraries GLFW builds against, and glm, still have to be on the machine; they are in the README under “System packages”.

Build from source

Every dependency is a git submodule, and two of them are undoRT forks — the ImGuiColorTextEdit branch that carries the semantic tokens, and st2cpp itself:

git clone --recursive https://github.com/undoRT/undoStudio.git
cd undoStudio

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"

# the transpiler the Compile button shells out to
cmake -S third_party/st2cpp -B build/st2cpp -DCMAKE_BUILD_TYPE=Release
cmake --build build/st2cpp -j"$(nproc)"

The binary, the core library and the undoApps are written next to resources/, because that is the directory the IDE resolves them against. The intermediates go to build/ — -B decides where those go, not where the build puts anything it has to find again.

The Window

Four panels, all listed under View, all shown or hidden from there:

PanelWhat is in it
Workspacethe file tree and the project toolbar
Editorevery open file, above one tab bar
ST Outputbuild and validation messages, filterable
ST Outlinethe POU, its sections, methods, structs and enums, from the parsed file

View > Reset Layout puts them back where the shipped layout has them, and the terminal — an undoApp panel like any other — appears when its plugin is there.

The Editor Panel

undoStudio has one editor panel. A Structured Text file, a JSON file, a C++ file and a plain text file all open in it, each in its own tab, above the same tab bar. A document on screen is a document on screen whichever extension it has.

the Editor panel with three files open, one of them marked with an asterisk
Three files open in one panel; the asterisk is unsaved changes in that file.

The tab bar

What it does
A click keeps the tabthere is one preview slot, and browsing the tree through it replaces what was there. A file somebody asked to see is still there when they ask for another one.
A second click is not neededImGui only reports a double click when both clicks land within a few pixels of each other — a rule nobody can see.
An asterisk means unsaved changesit is the file's own text that is asked, not a flag set when the tab was left. Browsing past a file with edits pins it rather than throwing the typing away.
Ctrl+S savesthe button is there too, in the ST toolbar
Closing a tab unloads the fileclosing the project clears them all
Renaming follows the filea rename or a move in the tree rewrites the tab, the path a save goes to and the key its text is held under — all three, or Ctrl+S writes a copy where the original used to be

A file that is not on screen is not kept in memory as text on disk: its unsaved contents are stashed, and the stash is handed back when the tab is selected again — at the cursor and the pane division it was left at.

Which backend a file gets

ExtensionBackendNotes
.stStructured Textbelow
.jsonJSON tree, or textthe file's role, not its extension
.c .cpp .cc .cxx .h .hpp .hh .hxxC++syntax colouring from ImGuiColorTextEdit's own definition
anything elseTextno colouring

Structured Text

The ST backend edits IEC 61131-3 Structured Text — POUs, variable sections, methods, structs and enums — with the file parsed, so the colouring, the outline and the completion lists all come from what the file declares rather than from a list of keywords.

a .st file open, Variables above Body, with the splitter between them
One POU in two panes: Variables above Body, with a splitter between.

The two panes

The upper pane holds the variable sections — VAR, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_EXTERNAL, VAR_GLOBAL, VAR_TEMP — and the lower one the cyclic code. Where the splitter sits is remembered per workspace.

Methods

A FUNCTION_BLOCK has method tabs beside the POU tab; PROGRAM and FUNCTION do not, because they have no methods in ST. Tabs can be dragged to reorder them.

Semantic colouring

Identifiers are coloured by what they resolve to — a declared variable, a function, a type, a keyword — using the symbol table st2cpp builds. A name that resolves to nothing is left uncoloured, which is how a typo shows up before it is compiled.

The toolbar

ButtonWhat it does
Savewrites the whole POU, every method tab included
Compilehands the project to st2cpp — below
Validateparses and analyses without generating
Closecloses the document
Tooltipsoff by default

Autocomplete & Signature Help

Three lists, plus signature help and declaration jumps, all driven by the parsed file — so what they offer is what the file actually declares.

member completion open after a dot, with the list drawn beside the cursor
Member completion after a .: the fields and methods of the thing before the dot.
signature help under a call being typed, with its parameters listed
Signature help inside a call: the parameters, with the one being filled in marked.
ListOffered whenOffers
Memberafter a .the fields and methods of the thing before the dot
Statementon a keyword fragmentthe ST statements and declarations that begin with what is typed — and the list knows which pane it is in
Signature helpinside the parentheses of a callthat function's parameters, one per line

Without a . the identifier being typed is the prefix, so a half-typed variable or method name is completable too; neither list opens inside a comment or a string literal, where a name cannot be completed anyway. Only one list is ever on screen: a statement list answers about the same word a member list would, and the more specific one wins.

Tab opens the parameter picker for the call being written, and the name and type of the parameter being filled in are shown in grey at the caret. ↑ ↓ move, PageUp PageDown move a page, Enter accepts, Esc dismisses.

Ctrl+Click on an identifier jumps to where it is declared, across files. The status line says so while the mouse is over it, and a click on something that is not declared is reported in the ST Output panel rather than doing nothing.

JSON: Text or Tree

A .json file gets one of two backends: text, which you can edit, or the tree viewer, which you can only look at. Which one it gets is the file's role, not its extension — answered from the project that is open.

project.json open as text in the Editor panel
project.json as text: it is the file the user is told to edit by hand.
the same file as a tree, with the node count and a Pretty Print button
The same file as a tree: nodes, the node count, Expand/Collapse, search and Pretty Print.

A project's own configuration would go to the viewer by extension, and the viewer draws a tree and has nothing to type into. Those are the files the user is told to edit by hand — exports.json holds the PROGRAM order, a task file its cycle time — so they open as text. Every other .json gets the tree, and outside a project there are no configuration files, so a .json there is a tree.

Right-click any .json in the Workspace tree for Open as Text and Open as Tree. Both replace the tab rather than adding a second one: one file is one row in the bar, and the row is whichever view it is in.

Keyboard

KeysWhereWhat
Ctrl+RglobalFile > Open Recent Projects
Ctrl+PglobalFile > Open Recent Files — what reaches for a file in VS Code, so the file list could not have had Ctrl+R
Ctrl+SEditor, ST documentsave — the ST document writes the whole POU
Ctrl+NST documentnew POU
Ctrl+WEditor, ST documentclose the document
Ctrl+ClickST documentgo to the declaration of the identifier under the cursor
↑ ↓ Enterrecent popupswalk the list and open the one chosen, without a mouse
xrecent popupsforget that one entry

Ctrl+R and Ctrl+P are held off while anything owns the keyboard — a code editor, the terminal — because in there the same two keys mean something else entirely, and stealing them would break the program the user is in.

The Workspace Tree

a project tree with a folder expanded and a file selected
A project open: node names coloured by what they are — POUs in greens, .st in violet, configuration in amber.
the right-click menu on a .json showing Open as Text and Open as Tree
The context menu on a file. The menu knows which folder it is on, because inside a project each one means something different.

The two toolbars

Without a projectWith a project
Above the tree New Project, Open Project, Select Workspace, Refresh the project name in green, Save, Close, Select Workspace, Refresh
Below the tree + POU, + Folder, + File + PLC, + Task

What the context menu offers

FolderItems
POUsNew PROGRAM, New FUNCTION_BLOCK, New FUNCTION
GVLsNew GVL
DUTsNew STRUCT, New ENUM
LibsAdd Library File
undoLogicAdd PLC…
a PLCAdd Task…, Rename PLC…, Delete PLC
TasksAdd Task…
anything elseNew POU, New File, New Folder, Rename, Delete Folder

Delete is not on the keyboard: removing a file is not something a stray keypress should do. Dragging a file onto a folder moves it there, and the rename follows the tab, the save path and the recent files entry with it.

Projects on Disk

a project open, its name and the Save and Close buttons in the Workspace toolbar
A project open: the header names it in green, with Save and Close beside it.

A project is a directory with a .undoProject/ descriptor in it:

myProject/
|-- .undoProject/
|   |-- project.json          name, version, author, target, semantics
|   |-- plcs.json             the PLCs
|   `-- tasks.json            the task list
|-- undoCore/
|   |-- tasks/                one JSON file per task
|   `-- ...                   the core sources
|-- undoLogic/
|   `-- <plc>/
|       `-- exports.json      the PROGRAM order of that PLC
`-- ...
FileWhat it holds
.undoProject/project.jsonthe project's name, version, author and creation date; the target architecture, OS and kernel; and the semantics settings
.undoProject/plcs.jsonthe PLCs in the project
.undoProject/tasks.jsonthe task list
undoCore/tasks/<name>.jsonone task: its PLC, cycle, priority and CPU
undoLogic/<plc>/exports.jsonthe PROGRAM order of one PLC
JSON only. Nothing reads the old TOML format and nothing migrates it, so a project written against it has to be rewritten once. strictness under semantics is a boolean rather than a string, and a project written before that key existed reads as strict — which is what a new project is written with.

The tree is built from the roles in plcs.json and the shape of the directory rather than from names alone: undoLogic/<plc>/exports.json is the PLC's exports file because the PLC is in the list, not because of what it is called. A project with no plcs.json has no PLC, and its exports file is an ordinary .json.

A folder can also be opened as a workspace with no project in it — Select Workspace. The tree is flat rather than role-aware, the folder menu offers the generic items, and every .json in it is a tree.

Recent Projects & Files

the recent projects popup, with the path of the selected one under it
The recent projects popup: the full path of the entry under the cursor, because a row is identified by its path and not by its name.
What it does
Ctrl+Rthe recent projects — File > Open Recent Projects
Ctrl+Pthe recent files — File > Open Recent Files
Nothing is reopened on its owna project is not opened at startup because it happens to be on disk, and a file is not reopened because it was open last time
xforget that one entry
Rememberhow many to keep — the limit is editable in the popup it governs
(missing)an entry whose file or folder is gone is kept and marked, rather than dropped: an entry vanishing is never a mistake

A file is remembered however it was reached — clicked in the tree, opened from the menu, dropped onto the window — and opening it puts it back where it was, at the cursor and the pane it was left in. The list is ordered by when a file was last looked at, and an entry follows a rename or a move.

Both lists read and write the same max key in the same [recent] section: there is nothing useful to configure separately about how much of two lists of the same kind to remember. Changing it through either list trims the other on the next load.

State Between Runs

Four files are written next to the layout. They are yours, they are not tracked, and deleting any of them puts that part of the setup back to its default.

FileWhat it holds
undoStudio_layout.inithe dock layout. A first run adopts the one under resources/, which is tracked, because it is the layout a fresh install opens with
undoStudio.inithe window size, the recent projects, the recent files, and the one limit they share
undoApp.Editor.iniwhere the Variables and Body sections are divided
undoApp.Terminal.inithe size the terminal is drawn at

They sit in the working directory, which is why an installed copy has to be started from the directory holding resources/ and plugins/. They are the IDE's state and not the project's: nothing in a project reads them, so deleting them costs nothing but a layout.

Compiling & ST Output

the ST Output panel with its severity switches, the search field, and errors in the log
ST Output: a switch and a count per severity, a search field, and Copy for the lines currently shown.

Compile on the ST toolbar hands the project to st2cpp, which is a separate command-line binary built from third_party/st2cpp. The Editor looks for it on PATH first and then at build/st2cpp/st2cpp, and prints the path it used at the top of the panel. A missing transpiler is reported plainly and nothing else stops working.

Validate parses and analyses without generating.

What it does
Errors, Warnings, Success, Detailshow or hide each severity, with a count beside it
Generated STthe generated file in full, off by default
Search outputfilter the lines — and it belongs to the panel and to the workspace it was typed in, so a query left over from one project does not filter the next project's log to nothing
Copythe lines currently shown, not the whole log
Clearempty the panel
The generated file is not the file you edit. A .st file is regenerated from the panes being edited, and a source map maps each generated line back to the editor that owns it. A diagnostic is therefore reported against the generated file, and its line numbers mean nothing without that map — so one that falls outside every editor's own lines says so and names the generated line.

undoApps

A plugin is a shared object exporting two C symbols. createUndoApp is expected to have fully initialised the app — registering its panels and all — before it returns, and destroyUndoApp to have removed them again:

extern "C" {

void* createUndoApp()
{
    auto& app = MyApp::getInstance();
    app.initialize();   // registers the panels
    return &app;
}

void destroyUndoApp(void* app)
{
    static_cast<MyApp*>(app)->shutdown();   // removes them
}

} // extern "C"
undoAppWhat it is
undoApp.EditorStructured Text, JSON, text and C++ editing, the Workspace tree and the projects
undoApp.Terminalthe embedded shell
undoApp.Demothe smallest possible undoApp, there to show the shape of one. BUILD_DEMO_APP is off by default

Registering a panel by a name another undoApp already registered replaces that one's callback rather than adding a second panel, so the load order decides which body is drawn. The core cannot call into an undoApp: it leaves a request and the undoApp picks it up, which is what the add-METHOD dialog and both recent-files lists live by.

The ones that are meant to

Each is described by what it is for rather than what it does, because none of them exists yet.

undoAppFor
undoApp.PLCPLC configuration, tasks, variables, deployment, against undoPLC
undoApp.BUSEtherCAT and fieldbus configuration, PDO mapping, network diagnostics
undoApp.DiagnosticsPLC status, task execution, cycle time, jitter, CPU, runtime errors
undoApp.OPCUAOPC UA servers and clients, namespaces, node publishing
undoApp.Motionaxes, motion profiles, drives, kinematics
undoApp.Importmigrating TwinCAT, CODESYS and other IEC 61131 projects

Terminal

a shell session in its own tab, with the font size buttons beside it
One shell per tab: a build running in one tab does not stop you using another.

A libvterm terminal drawn with ImGui, each tab running a real shell on a pty.

What it does
+a new shell
x on a tabclose that shell
A− / A+smaller / larger text, with the size in pixels beside them — and Ctrl + wheel is the same. The buttons are there because a terminal sized by a shortcut nobody can find is a terminal people stop reading
Known limitation: a terminal tab cannot be dragged to reorder, unlike the file tabs beside it. ImGui would move the tab and the panel would keep drawing the shell it thinks is there, so it is not offered rather than offered and wrong.

Testing

Three suites, all headless: no GPU and no display. They create an ImGui context with a font atlas and drive real frames.

./src/core/tests/run_tests.sh                    # 12 tests
./undoApps/undoApp.Editor/tests/run_tests.sh     # 26 tests
./undoApps/undoApp.Terminal/tests/run_tests.sh   # 2 tests

# a subset, by name
./undoApps/undoApp.Editor/tests/run_tests.sh completion

Each check that could not fail would not be a check, so every one of them found a real bug: a fuzzy match stealing the member completion, 1841 cells in 1920 painted the wrong colour, a key written above its own section header, and a row in the recent-files popup that rendered in the right order, in the right place, with the right name on it, and could not be picked because its ID belonged to another row.

The Editor and Terminal suites link against the built core and need the project compiled first. They run on every release build, which is why the archive is only ever attached to a release somebody deliberately made.