Vela Studio.

The editor for Vela: write, check, and run programs, with a terminal beside them.

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

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

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

Running

Terminal

Look and feel

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.

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:

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