What is Vela Studio?
Vela Studio is a small editor made for Vela. You start it on a folder, and it opens in your browser: files on the left, the editor in the middle, and a panel below with problems, your program's output, and a terminal. F5 saves the file, compiles it, and runs it.
It understands Vela because it is Vela: the compiler's own
lexer, parser, and checker are compiled to WebAssembly and run inside
the page on every keystroke, and programs are built by the same code
as the vela command. Highlighting, errors, completion,
and the compiler can't disagree about the language. A file is analyzed
once for each change: hovering, completing, and going to a definition
ask the analysis it keeps, and the files it includes are sent to it,
and scanned, only when they change.
Install
Requirements
-
Rust, a recent stable release (tested with 1.97),
with the WebAssembly target:
rustup target add wasm32-unknown-unknown. - Node.js 22.12 or later, with npm, to build the page and run its tests.
-
A C compiler to run programs:
cc, or the one named byCC, exactly as for thevelacommand. Editing works without one. - A browser. Vela Studio is tested in Chromium; current Firefox and Safari are expected to work.
Linux is the tested platform. macOS and Windows are supported by the libraries Vela Studio uses (including Windows' ConPTY for terminals), but aren't tested yet.
Build
From the root of the Vela repository:
npm --prefix ide/web install npm --prefix ide/web run build # the WebAssembly module and the page cargo build -p vela-studio --release # the server, with the page inside it
The order matters: a release build embeds ide/web/dist,
so build the page first, and rebuild the server after changing it. The
result is a single program, target/release/vela-studio,
that needs nothing else from the repository. Copy it anywhere on your
PATH:
cp target/release/vela-studio ~/.local/bin/
Launch
vela-studio [WORKSPACE] [--port N] [--no-open]
| Argument | Meaning |
|---|---|
WORKSPACE |
The folder to open. Defaults to the current folder. |
--port N |
Listen on port N. Without it, a free port is
chosen.
|
--no-open |
Print the address without opening a browser. |
--help |
Print the usage. |
It prints the address, which carries a secret token, and opens it:
$ vela-studio examples Vela Studio is running for /home/you/vela/examples http://127.0.0.1:41377/#token=5f0c… Press Ctrl+C to stop.
Keep that terminal open while you work. Ctrl+C
stops Vela Studio, along with any program or shell it started. To work
on another folder at the same time, start another
vela-studio; each gets its own port and token.
Reloading the page keeps working. A new browser tab needs the full printed address, since the token is kept per tab. The exit status is 2 for wrong arguments and 1 when Vela Studio can't start (for example, the folder doesn't exist or the port is taken).
A Tour
┌──────────────────────────────────────────────────────────────────────────────┐ │ ≡ ◆ Vela Studio examples [Build] [▶ Run] [Open] [Theme] │ title bar ├─────────────┬────────────────────────────────────────────────────────────────┤ │ EXPLORER │ fib.vela ● kinds.vela hello.vela │ tabs │ demo.vela │ ────────────────────────────────────────────────────────────── │ │ fib.vela │ 1 task fib(n: int) -> int │ │ hello.vela │ 2 when n < 2 │ editor │ kinds.vela │ 3 <- n │ │ │ 4 fi │ │ OUTLINE │ 5 <- fib(n - 1) + fib(n - 2) │ │ ƒ fib ├────────────────────────────────────────────────────────────────┤ │ ƒ main │ PROBLEMS OUTPUT TERMINAL │ panel │ │ ▶ fib.vela │ │ │ 0 │ │ │ 1 │ │ │ … │ │ │ ── exited with code 0 · 3 ms ── │ ├─────────────┴────────────────────────────────────────────────────────────────┤ │ No problems fib.vela exited with code 0 · 3 ms Ln 3, Col 13 Vela │ status bar └──────────────────────────────────────────────────────────────────────────────┘
-
Title bar: toggle the sidebar, the folder's name,
Build (hammer), Run (which turns into a spinner while building and
Stop while the program runs), open a file by name, and the
light/dark theme. A No C compiler warning appears here when
ccor$CCcan't be started. - Sidebar: the Explorer (the folder's files) and the Outline (the open file's tasks, shapes, kinds, patterns, type aliases, and macros, with a shape's fields, a kind's variants, a pattern's fields and methods, the methods of shapes and kinds, and the types a task or shape takes under it; the one under the cursor is highlighted). Drag its edge to resize it; double-click the edge to reset.
- Tabs and editor: one tab per open file; a dot marks unsaved changes. With no file open, a welcome screen lists the folder's programs and the main shortcuts, with New File… and Open File… buttons.
- Panel: Problems (errors in your files), Output (the program you ran), and Terminal (a shell in the folder). Drag its top edge to resize it; the button at its right hides it, and Output has a button that clears it.
- Status bar: the problem count and the last run's result (click either to open its tab), the cursor position (with how many characters are selected, when some are), and the file's language.
Sizes, open tabs, expanded folders, the panel's tab, and the theme are remembered for next time.
Features
Writing Vela
Highlighting that knows names
Tasks, shapes and kinds, variants, parameters, variables, fields,
regions, and modules each get their own color, even in a
half-typed line. The types a task, shape or kind takes (<T: ordered>) are colored as types wherever they are written, as are the ones
a use gives (Box<int>), and the word asked of
one as a word of the language. A method is colored as a task where
it is declared and wherever it is called (c.step(2),
.cap()), even a kind's, whose members are written as
its variants are; alters and the this of
.this are words of the language,
@this is a region, and a method's parameters are
parameters. In text holding values,
"{name} is {age}" at r, the values are code: colored,
hovered, and followed to their declarations like any, with names
completed inside the braces, while the text around them stays
text.
Programs in several files
A file's include lines are followed into the files
they name, as you type: geometry.area is colored as a
task, hovers with its signature and module, and jumps into
geometry.vela with F12; completion after
geometry. and inside include … ( or
{ offers what the module declares, without its
priv names (on the include line, each by
its name alone); aliases (=>) are followed, so a
renamed name hovers, completes, and jumps under its new name, and
after Size. completion offers the kind's variants;
and an error in a module, even one not saved yet, is shown at the
include that leads to it, with the module's file and
line. An open file counts as edited; any other is read from the
workspace. Vela's library needs no file at
all: include std/files is read from the compiler,
completion right after include (or
include std/) offers its modules, and its names hover
and complete as a module's do, though F12 stays put,
since the library is no file to open.
Errors as you type
The compiler's error is underlined in place, marked in the gutter, explained on hover, and listed under Problems. F8 jumps to it. A warning, something the program may not mean though it builds, is marked the same way in the warning color.
Completion
Only what fits: types after :, ->,
^, maybe, new, a type's
[ and ~, each item of a tuple type
((int, text)), each comma of a header's results
(-> int, int), and each type a use gives a
declaration that takes them (first<int>(xs),
Box<int>, Box<int> { … }),
and nothing for an array type's count; the types a task, shape or
kind takes are offered as types inside it (T), and
after a type's : or & the words it
can be asked to allow (frozen, same,
shown, ordered); variables declared
above the cursor; the types after a kind's name before one of its
variants (Option<int>.None), colored as types,
and the variant after them; endless among the
statements and after a loop's name (outer =>),
with a snippet for it and one for a loop in three parts, and the
names a loop's start declares (loop flex i = 0, j = 9 || …, but not the values given them) inside that loop only, typed
even in a file that doesn't parse; break and
continue inside a loop, but not inside a lambda in
it, and after either the names of the loops around it (outer => each …, which hover as loops and are offered nowhere else, and the
variable a loop that gives a value gives,
fixed r = each … or
fixed r: int ~ LoopResult = each …, which hovers as
r = loop); fields and methods after . (a
method with its parameters to fill in, and a pointer's through
^.), and inside a method, after a
. starting a value, this and the fields
and methods of the value it is called on; a tuple's items by
position after its . (0, 1,
each with its type), and len after an array's,
slice's, or text's (and add and
del after a slice's), str after a
number's, a bool's, text's, an array's, a slice's, a tuple's, a
pointer's, or a maybe's, which fills in
str() at r, and copy after a value
holding text, a slice, or a pointer, and type after
every value's (which hovers with what
x.type reads);
int and float after a
scan's prompt, the two numbers that form reads; the
cases still missing after case and
case Kind., including {value},
none, and failure e (the variants of
ParseError
and
InputError
among them, which no file declares); the case forms that fit after
is (a tuple's names among them); the regions visible
after @, and the ones new,
scan, or copy can allocate in after
new@, scan(…)@, copy()@,
and a new's, scan's, or
copy's at, or as the argument for a
task's region parameter; and region as a
statement or a parameter's type; when after an
else, the one word that may follow it, which chains
the condition; and, where a value goes, match and
when with a skeleton for each, since either can be
the value. A call's result shows the caller's regions (push(nodes, list)
gives a ^Node@nodes), even while the file has an
error. After a finished value, only the words that can continue
it: is, in, at (after a
new's, copy()'s, or a line-reading
scan's value, never after
scan(prompt, int), which names no region),
and, or, and, after a written type, the
~ that gives it an error side. A task's call fills in
only the values it must give: pad(${t}) for
task pad(t: text, width: int = 8), whose header,
values and all, its hover shows, and a parameter hovers with its
type alone, even in a file that doesn't parse. A
new written without ( ),
new [text] or new Point, is a finished
value, so at follows it rather than ~,
and it hovers with what it makes even in a file that doesn't
parse, maps among them. Nothing is offered where a name is being
declared: the names after the commas of
fixed q, r = … and inside the parentheses of
fixed a, (b, c) = …, which each hover with the type
of the value in their place (a tuple's item, for a name that takes
one apart), and the names in the < > of
task first<T> and
shape Box<T>. After a : or an
& inside those < >, only the
five words a type can be asked to allow are offered, and inside
the < > of a call or of a shape given types,
the types are. A task's named results (-> total: int) are offered at the start of a line as
total <- , and hover as
result total: int. After match, only
case; new only where a region can be
allocated in; include only above a file's tasks,
shapes, and kinds; priv before those and a shape's
fields, with task, shape, and
kind after it; and priv,
alters, and shared at the start of a
shape's or a kind's member, with alters after its
priv; a shape's also offers read, with
flex, read, and new after
shared (a kind's, only new). After a
shape's name and a dot, Point., its
shared fields and
shared methods are
offered, and after a value's name its fields and methods, with the
default methods it has from the
patterns it follows; after a
type's : in < >, the five words
and the patterns; after a kind's name its variants (with those it
has from the kinds it
imitates) and shared
methods, never after a value's name or in a literal; a shared
field hovers as shared flex made: int, saying who may
change it, and a shared method as
shared new(x: int) -> Point, colored as a method
where it is declared and called. In a task's or a method's
parameters, where a name goes, flex and
flex&, which make the parameter the task's own
copy to change, or the caller's place, bind and
bind&, a copy or the caller's place whose
contents change but which is never assigned as a whole, and
fixed and fixed&, a copy or the
caller's place, only read; such a parameter hovers as
flex n: int, bind n: int or
flex& n: int, and after the & of
a call, where a place goes, the variables are offered. A
reference, flex& y = &x or
fixed& y = &x, hovers as
flex& y: int or fixed& y: int,
saying whether it changes its place. A method's body completes as
a task's does. bind is a variable word like
flex and fixed: it is colored, offered
where one goes, and hovered. hold is a reserved word
with no use yet: it is colored as a keyword and never offered, and
one written shows the compiler's error, which names
flex where a variable's word goes.
Snippets
task, shape, kind,
when, loop, each,
when … is, loop … is,
new … at, scan(…) at, and
match, which expands to one case per
variant (or {value} and none for a
maybe), with its bindings. Tab moves between the
fields.
Hover and go to definition
Hover a name to see its type or signature (a region says whether
the task can allocate in it, and a type with
@_ explains it), a type a task, shape or kind takes
(T), which says what the word after its
: allows, a tuple's item (t.1) to see
its type, or maybe, is,
fail, none, _,
region, new, and in for a
reminder of what they do. A kind that imitates others shows the
variants it has from them first, each saying which kind it is
from; a pattern shows its fields and methods, its defaults marked,
and a shape or kind following one shows the default methods it has
from it. A method shows its header, which shape or kind it belongs
to, and whether it changes the value it is called on;
.this shows that value's type, and
alters and @this what they mean.
F12 or Ctrl+click jumps to where it is
declared (a variant named alone in a case is the
matched value's kind's, even another module's);
Ctrl+Shift+O finds a declaration
by name.
Blocks that indent themselves
Enter indents after a block's first line (a method's header among
them), and after a line ending with a plan B's { (or
{ failure e); typing fi,
else, case, or } lines it
up with its block. An else when lines up as its
else does and indents its own block, since one
fi closes the whole chain. A match or a
when
used as a value opens its block
wherever it is written, so fixed area = match s takes
its cases at the fixed's own level. A line that
closes every block it opens, as a one-line plan B does, opens none
of them. Blocks, a method's among them, fold from the gutter.
The editor also has the usual tools: find and replace, multiple cursors, bracket matching and closing, comment toggling, and line moves (see Shortcuts). Other files open as plain text.
A shape given types reads as the program writes it: hover shows
Box<int> and Box<text>@r, never
the name the compiler makes for the shape it generates for those
types.
Files
-
Explorer: create files and folders (a name like
lib/util.velacreates the folder too), rename (a name with folders moves the entry), and delete, from the buttons, the right-click menu, or the keyboard. Hidden files and folders,target/,node_modules/, links to folders, and names holding:or\aren't listed. Refresh reads the folder again, and Collapse folders closes them all. The list stops at 10,000 entries and 16 folders deep, and then says Some files aren't shown. - Saving: Ctrl+S saves the file, Ctrl+Shift+S saves all. Closing a tab or leaving the page with unsaved changes asks first.
- Changes made elsewhere are noticed when you come back to the page: a file without unsaved changes reloads, keeping your cursor; a file with unsaved changes is marked, and saving it asks whether to overwrite the other version or reload it. A deleted file can be saved again.
- Files up to 1 MiB of UTF-8 text can be opened.
Running
-
F5 (or Ctrl+Enter) saves every file
with changes (a program may use any of them as a module), builds the
current
.velafile, and runs it in the Output tab. Output is a real terminal: colors work, and what you type goes to the program, soscanreads it a line at a time (Ctrl+D ends the input). -
The run ends with a summary line,
── exited with code 0 · 3 ms ──, and the status bar keeps the result. Shift+F5 or the Stop button ends a program; running again replaces the one that is running, even when the new build fails. -
Ctrl+Shift+B builds without
running, and says so when the build has no errors. Running or
building with no
.velafile open says to open one. -
When a build fails, a Vela error (or a missing
task main()) opens Problems, where clicking it selects the spot (an error in a module is listed under the module's file); a failure of the C compiler opens Output with its message. A build error stays listed until that file builds again. A program's modules are read from the workspace only, so anincludecan't read a file outside it. - Builds happen in a temporary folder that is removed afterwards. Running never writes into your folder.
Terminal
-
The Terminal tab starts your default shell in the
workspace folder the first time you open it
(Ctrl+`). Use it for the
velacommand,git, or anything else. - Its buttons restart the shell, stop it, and clear the screen. After the shell exits, any key starts a new one. Links in Output and the terminal open with Ctrl+click (⌘+click on a Mac).
Look and feel
- Dark and light themes (the dark one in these docs' colors); the first follows your system, until you choose one.
- Short animations for panels, tabs, completion, the Run button, and toasts, turned off when your system asks for reduced motion.
- Everything is keyboard-reachable: the explorer, tabs, panel tabs, and resize handles all work with arrow keys.
Keyboard Shortcuts
On a Mac, ⌘ replaces Ctrl except where the table says otherwise; completion stays ⌃+Space (or ⌥`) and the terminal ⌃+`. The running and panel shortcuts (run, build, show or hide the panel, show Problems) answer to either Ctrl or ⌘ on every system, so on a Mac ⌃J toggles the panel too rather than reaching the terminal. Browser shortcuts such as Ctrl+W still belong to the browser. In the Terminal and Output tabs, keys go to the program, except saving, opening a file, going to a declaration, and the running and panel shortcuts; so Ctrl+B, for example, reaches the shell.
| Action | Linux and Windows | macOS |
|---|---|---|
| Workbench | ||
| Open a file by name | Ctrl+P | ⌘P |
| Go to a declaration in the file | Ctrl+Shift+O | ⌘⇧O |
| Save | Ctrl+S | ⌘S |
| Save all | Ctrl+Shift+S | ⌘⇧S |
| Show or hide the sidebar | Ctrl+B | ⌘B |
| Show or hide the panel | Ctrl+J | ⌘J |
| Show Problems | Ctrl+Shift+M | ⌘⇧M |
| Terminal (hides it when it has focus) | Ctrl+` | ⌃` |
| Running | ||
| Save and run the file | F5 or Ctrl+Enter | F5 or ⌘↩ |
| Stop the program | Shift+F5 | ⇧F5 |
| Save and build the file | Ctrl+Shift+B | ⌘⇧B |
| Vela | ||
| Complete (also opens while typing) | Ctrl+Space | ⌃Space |
| Go to definition | F12 or Ctrl+click | F12 or ⌘+click |
| Next error | F8 | F8 |
| Toggle comment | Ctrl+/ | ⌘/ |
| Editing | ||
| Find / replace | Ctrl+F / Ctrl+H | ⌘F / ⌘⌥F |
| Next / previous match | F3 / Shift+F3, or Ctrl+G / Ctrl+Shift+G | ⌘G / ⌘⇧G |
| Go to line | Ctrl+Alt+G | ⌘⌥G |
| Select the next occurrence / all occurrences | Ctrl+D / Ctrl+Shift+L | ⌘D / ⌘⇧L |
| Add a cursor above / below | Ctrl+Alt+↑ / ↓ | ⌘⌥↑ / ↓ |
| Move the line up / down | Alt+↑ / ↓ | ⌥↑ / ↓ |
| Copy the line up / down | Shift+Alt+↑ / ↓ | ⇧⌥↑ / ↓ |
| Delete the line | Ctrl+Shift+K | ⌘⇧K |
| Indent / outdent | Tab / Shift+Tab | Tab / ⇧Tab |
| Fold / unfold the block | Ctrl+Shift+[ / ] | ⌘⌥[ / ] |
| Fold / unfold everything | Ctrl+Alt+[ / ] | ⌃⌥[ / ] |
| Undo / redo | Ctrl+Z / Ctrl+Y | ⌘Z / ⌘⇧Z |
| Indent / outdent the line | Ctrl+] / [ | ⌘] / [ |
| Indent the selection again | Ctrl+Alt+\ | ⌘⌥\ |
| Select the enclosing part | Ctrl+I | ⌘I |
| Go to the matching bracket | Ctrl+Shift+\ | ⌘⇧\ |
| Undo / redo a selection change | Ctrl+U / Alt+U | ⌘U / ⌘⇧U |
| Let Tab move focus out of the editor | Ctrl+M | ⇧⌥M |
| Explorer, tabs, and panels | ||
| Move in the explorer; open or close folders | ↑ ↓ Home End; → opens a folder, ← closes it or goes to its parent | |
| Open, rename, delete in the explorer | Enter, F2, Delete (fn+Delete on a Mac laptop; or right-click for the menu) | |
| Move between tabs; open or close one | ← → on the tabs, then Enter to open; Delete or a middle-click to close | |
| Resize the sidebar or panel | Arrow keys on its edge; double-click the edge to reset | |
Security
Vela Studio can read and write your files, run programs, and open a shell, so it is as powerful as a terminal. It is built so that only the page it opened for you can use it.
-
Only this computer. It listens on
127.0.0.1, never on a network address, and has no option to change that. -
A new secret for every launch. The address carries
a random 256-bit token after
#, a part that browsers never send to a server. The page moves it into the tab's session storage and removes it from the address bar. Every request needs it; when Vela Studio stops, the token stops working. - Nothing secret on the browser's command line. Other users of a computer can read the command lines of your programs, so Vela Studio doesn't pass the token, or even a launch link, to the browser. It writes a small page that only you can read (in a private temporary folder) and opens that file; the page sends the browser to Vela Studio with a one-time launch code, which it exchanges for the token. A code works once, for five minutes, and the file is removed when Vela Studio stops.
-
Other websites can't use it. A site open in another
tab doesn't have the token; besides, requests from another origin,
or addressed to any host but
127.0.0.1orlocalhoston its port (which stops DNS rebinding), are refused, and no cross-origin headers are ever sent. -
File operations stay in the folder. The editor's
reads, writes, renames, and deletes are confined to the workspace:
.., absolute paths, and symbolic links that lead outside are refused. - A locked-down page. A strict Content Security Policy allows only the page's own scripts and styles, and the page can't be framed. Fonts and the WebAssembly module are served by Vela Studio itself: nothing is loaded from the internet, and nothing is sent anywhere.
- Nothing left running. A program or shell whose page has been closed for 30 seconds is stopped along with the processes in its group, and stopping Vela Studio stops them all. When a terminal's shell exits, the background jobs it left get a hangup, as when a terminal window closes. (A job that ignores hangups can outlive it; the background-job hangup is on Linux only, and on Windows only the program itself is stopped.)
What it can't protect you from
- Programs run as you. The folder limit applies to the editor, not to programs you run or to the terminal, which have all of your account's permissions. Run code you trust, as you would in any terminal.
- The address is a key. Anyone with the full address can use Vela Studio until it stops. Don't paste it into chats or show it on a shared screen, and stop Vela Studio when you're done.
- It isn't meant for remote use. Forwarding its port to another machine hands a shell to whoever gets the address.
Troubleshooting
| You see | What to do |
|---|---|
| “The frontend hasn't been built yet” |
Build the page (npm --prefix ide/web run build),
then rebuild and restart vela-studio.
|
| “Open Vela Studio from its link” |
The page was opened without its token. Use the full address
that vela-studio printed, including
#token=….
|
| No C compiler in the title bar |
Install gcc or clang, or start vela-studio with
CC naming your compiler, as in
CC=clang vela-studio.
|
error: cannot listen on 127.0.0.1:N |
Another program uses that port. Pick another, or leave out
--port.
|
| “Couldn't open a browser”, or the browser shows an error for a local file | Open the printed address yourself. (Some sandboxed browsers, such as snap packages, can't open the private page Vela Studio writes for them.) |
| A file won't open | Only UTF-8 text files up to 1 MiB can be edited. Pipes and devices aren't listed at all. |
| “the file is read-only” when saving |
Vela Studio doesn't change read-only files; make the file
writable (chmod u+w) and save again.
|
| “This link no longer works” (“this link has already been used or has expired”) |
The browser's launch link works once. Open the address
vela-studio printed instead.
|
| A file changed by another program still shows the old text | Changes are checked when the page regains focus: switch away and back. |
Vela Studio marks every mistake the compiler finds in a file, each under the word it is about, and lists them all in Problems (see Current Limits). The line a mistake is on is the only one left without types, so hover, completion, and Go to Definition keep working around it. Ideas for what comes next are in Future Directions.
Working on Vela Studio
Vela Studio lives in ide/, separate from the compiler, as
members of the repository's Cargo workspace:
-
ide/analysis(vela-analysis): highlighting classes, diagnostics, outline, definitions, completion, and hover, built on the compiler's library. Pure Rust, no I/O: the page gives it the text of the modules a file uses, and it says which it still needs. -
ide/wasm(vela-analysis-wasm): that engine compiled to WebAssembly for the page, behind a small JSON interface. -
ide/server(vela-studio): the local server for files, builds, runs, and terminals (pseudo-terminals over WebSockets), which also serves the page. -
ide/web: the page, in TypeScript with CodeMirror 6 and xterm.js, built by Vite.
The compiler has no dependencies and doesn't depend on any of these:
plain cargo build and cargo test at the root
still cover only the compiler.
A debug server reads ide/web/dist from disk, so after
npm run build a reload shows the new page. For hot
reloading, run the server on a fixed port and Vite's dev server beside
it, then open http://localhost:5173/#token=… with the
token the server printed:
cargo run -p vela-studio -- examples --port 4000 --no-open VELA_STUDIO_PORT=4000 npm --prefix ide/web run dev
Tests:
cargo test --workspace # compiler, analysis, and server cargo clippy --workspace --all-targets npm --prefix ide/web test # unit tests npm --prefix ide/web run typecheck npm --prefix ide/web run e2e # Chromium against a real server; set CHROMIUM if it isn't /usr/bin/chromium