The editor
Vilan ships a language server, vilan-lsp, beside the vilan binary in
every release — same repository, same version number, installed by the same
script, updated by the same vilan upgrade (which replaces the server
first, so the pair is never newer-CLI/older-server). It speaks LSP over
stdio, so any LSP client can drive it; the one the project configures and
tests is the VS Code extension.
Getting it
The toolchain install puts
vilan-lsp next to vilan in ~/.vilan/bin, and — when VS Code’s code
is on your PATH — installs Vilan (vilan-lang.vilan) from the VS
Code Marketplace or Open VSX too; vilan upgrade brings the extension to
the toolchain’s version each time it runs. Otherwise install it from
either gallery yourself. The
extension activates on a .vl file or a workspace containing a
vilan.toml, and finds the server by looking, in order, at
vilan.server.path, a target/release/vilan-lsp or target/debug/
build in the workspace (so a checkout of the compiler outranks the
installed copy — deliberate, for people working on vilan itself),
~/.cargo/bin, ~/.vilan/bin, and finally vilan-lsp on your PATH.
If it finds none it says so, naming the command it tried and offering a
button into settings.
No other editor is configured in the repository today. The server needs no
arguments, so a generic LSP client pointed at vilan-lsp for the vilan
language works; nothing here is VS Code-specific except the packaging.
The server resolves std itself — $VILAN_STD, then an in-checkout
vilan/std, then a copy embedded in its own binary — so go-to-definition
lands inside the standard library on a machine with nothing else installed.
What it gives you
Diagnostics, published as you type on a 150 ms debounce, the same
messages vilan check prints (notes included, as related information).
A save re-checks the saved file and every open file that imports it, so
breaking a shared module lights up its consumers rather than waiting for
you to visit them.
Hover — inferred types on locals and parameters, loan conventions
included; a field’s name: type and a method’s full signature behind a
.; your own doc comments; and documentation on the language’s own
keywords, deep-linked into this book.
A /// above a struct FIELD reaches every position that field appears in:
its declaration, a read of it, the field name inside a Point { x = … }
literal, and both completion lists that offer it (where the popup shows
the first paragraph, as it does for a function). Hovering a field answers
about the field; the struct’s whole declaration block is what its own
name answers.
Hovering a call to a GENERIC function shows two signatures in one block:
the declaration as it is written, then the same signature under the
bindings this call solved — fun get_or_insert(self, key: K, make: || V): V
above fun get_or_insert(self, key: UserId, make: || SignalCell<Option<User>>): SignalCell<Option<User>>. A call that substitutes nothing, and a hover on
the declaration itself, show the one line they always did.
Every function’s hover opens with WHERE it lives — mod pkg::lib::overlay,
mod std::list — and every function states a return type, : void where
none is written. A module segment of a path (overlay in import pkg::lib::overlay::{ .. }, model in model::global()) hovers as that
module: its mod line, the /// that opens its file, what it exports and
where it lives. A field names the struct it belongs to (struct Ring<T> above
items: List<T>); a match leg names what it matches over and the bindings it
introduces; a trait named in a parameter’s type lists what can be passed
there; a [derive(..)] shows the impls it writes for the item.
A METHOD’s hover names the block it is declared in, on its own line above
the fun line and in real syntax: impl Memo<type K: Hashable, type V>
for an inherent method, impl MemoCell<type T> with Source<T> for a
trait’s implementation, impl type S: Source<type T> for a blanket’s, and
trait Flow<T> for a trait’s own requirement or default. A completion
item’s detail line carries the same header. Every signature prints each
parameter’s convention — own self, &mut self, own item: T,
xs: &List<i32> — because it is part of what a caller and an implementor
write against.
self hovers like any other binding: the block that introduces it, then
self: User in the receiver’s convention (own self: User), or
self: Self inside a trait and self: S inside a blanket, whose header
line spells the bound. A variable, a parameter, a field and self all show
their type’s DEFINITION in a second block under the name: Type line —
a struct’s fields or an enum’s variants, one level deep and with the
instantiation’s arguments substituted (let pair: Pair<i32, str> shows
left: i32 and right: str), and for a trait-typed or dyn value the
trait’s required members. A long definition lists twelve members and then
…; a primitive or an opaque host type shows none.
Inlay hints — the inferred type of a binding you left unannotated
(let/mut, a for binder, a comprehension binder). A parameter is not
hinted: its type is written in the signature already. While you type, a
hint stays where it is and follows the text like any other position —
typing before it on its line moves it right, a line inserted above moves
it down, typing after it or below it leaves it alone — and keeps its last
analysed type until the next analysis replaces the set; deleting the name
it follows removes it.
A pipeline node’s type spells its whole upstream, so a type whose
declaration carries [hint(Trait<..>)] is hinted by the trait it is
used as: let selected = combine((names, index)).derive(..) shows
: ~Pipe<Option<str>>, not : Derive<Combine<(List<str>, usize)>, …>. The
~ marks the abbreviation — it lexes nowhere, so it cannot be mistaken
for a type you could write — and a node nested in another type
abbreviates in place ((~Pipe<Option<str>>, i32)). It is printed only
when the value’s own instantiation is admitted by an impl of that trait,
so it never promises what the value lacks. The hint’s tooltip carries the
full type, and hover shows both: the full type in the declaration, and
Shown as ~Pipe<Option<str>> beneath it. std’s pipe stages
(~Pipe<T>) and collection stages (~CollPipe<T>, sealed as
~CollSource<T>), its sealed MemoCell and RemoteSource (~Source<..>),
.transient()’s seal (~TransientSource<T, E>) and
its iterator adapters (~Iterator<T>) carry the attribute, and a package’s
own types can too. vilan.inlayHints.abbreviate turns it off.
A chain written one stage per line is hinted per stage, in the
spelling an ascription is written in: each line that ends a stage shows
as List<usize> — or as ~Pipe<str> where the abbreviation applies —
and so does the chain’s head when the first link breaks onto a new line.
vilan.inlayHints.chains = "changes" hints a stage only where its type, as
the hint prints it, differs from the stage before it (the first stage
always); Ascribe every stage of this chain still writes every stage. An
element’s tag and its head’s items are not a chain you wrote, and carry no
stage hints. Hovering a hint shows what its binding’s name hovers, and a
click on its type’s name goes to that type.
A stage you have already ascribed shows nothing, and neither does the
last stage of a chain that lands in an annotated let. On a hinted line
the refactor Ascribe this stage writes it into the file — the full
type, or the bare trait for an abbreviated hint (as Pipe<str>, which is
checked against the trait and keeps the concrete type, so nothing after
it changes) — and Ascribe every stage of this chain writes them all.
A type is written the shortest way the file can name it, through a module
the file imports where needed; a type the file cannot name without a new
import offers no action, because the action never adds one.
Semantic highlighting from the analyzer, over the TextMate grammar,
which also highlights vilan fences inside Markdown.
Go to definition (including into std), find references,
rename, and a document outline. References and rename reach in
both directions across your open files: asked in the file that defines
a symbol, they also find the open files that import it.
Formatting — the same vilan_core formatter vilan fmt runs, so the
editor and the CLI cannot disagree. Whole-document only; there is no range
formatting. When the printer declines a file — it does not
parse, or it carries a construct the printer has no rule for — the server
says so in a message naming the line and the construct, rather than
leaving a save that did nothing looking like a save that had nothing to
do. Once per file per cause, so format-on-save does not repeat it. It reads
your package’s [fmt] wrap_comments from the nearest vilan.toml above the
file, the same climb vilan fmt walks, so a save and a command-line format
produce the same bytes.
Linked editing for markup tag pairs: rename <div> and </div>
follows.
Which platform, and why, before any error. The platform a file is
analyzed under decides which std twin its types come from, so the status
bar’s vilan 0.43.0 item says it for the vilan file in front of you, in its
tooltip — This file is analyzed under browser: the client entry reaches it.
A file can say it itself: [platform("browser")] mod self; as its first line
— and at the top of a file, completion offers that line.
The status bar menu. Click the vilan item for a menu: the platform the
file is analyzed under and why, the entry whose world it is analyzed in, the
server’s version and commit, and the last analysis’ size as counts (files,
entities, impls — work, not milliseconds); a switch for each feature the
server reads live (inlay hints, the ~Trait abbreviation, semantic
highlighting, the generic < pairing, Organize Imports on save), which flips
the workspace setting where one is set and the user setting otherwise; and
restart, stop or start, the status page and the output channel. A stopped
server reads vilan (stopped).
A file is analyzed as its entry sees it, once two of them are open. A
file open on its own keeps its own quick analysis for its squiggles; as soon
as a second file of the same entry’s world is open, the editor analyzes the
entry — client.vl, say — once per edit, and every open file it reaches is
answered from that one analysis. Find References and rename always answer
from the entry’s whole world, every file it reaches open or not: for a file
open on its own, the first such request after an edit analyzes the entry,
and the next one reuses it. Edit model.vl with
client.vl, views.vl and channel.vl open and the four are re-analyzed
together, not one after another; open a fifth file of the same world and
it is answered at once, from the analysis already in hand. A module’s
diagnostics are the ones its entry’s build would report (an error a caller
provokes shows in the module), and hover, go-to-definition and completion
read the entry’s program. The entry does not need to be open. A module
both a browser and a node entry reach is answered from the browser
entry’s world and reports the node entry’s diagnostics too; an entry, a
module no entry reaches, and a file outside any package are each analyzed
on their own, as before.
A file holding platform-fenced twins (a function or a trait impl
written once under [platform("browser")] and once under
[platform("@process")]) is analyzed under each twin’s platform, and the
editor keeps every one of those analyses: inside the twin the file’s own
platform excludes, hover, completion, inlay hints and colour come from the
analysis that admits it, as they do everywhere else. Go-to-definition on a
call to a twin offers both twins, the one the file’s platform compiles
first. Such a file is served from the entries’ worlds like any other once
two files of its world are open: the browser entry’s world answers it, and
the twin that world excludes is answered from the world of the entry whose
platform admits it. Only a twin no entry’s platform admits (a deno twin in
a package whose entries are a browser and a node one) keeps the file on its
own analyses.
A generic < closes itself. List<, HashMap<, fun pair< and a
generic call’s own argument list each get their > as you type the <.
The editor’s static bracket pairs cannot do this — < is also the
comparison operator, and a < b must not grow a > — so the server
decides, on what the name before the < means: a struct, an enum, a
trait or a generic function, or a declaration’s own name right after
fun, struct, enum, trait, impl or type. Everything else is
left alone. In VS Code the placed > is also typed over when you reach
it, so List<i32> never becomes List<i32>> — which takes over the
editor’s type command, and only one extension can own that: turn
vilan.autoClosing.generics off to coexist with Vim emulation, and the
server places the > without the type-over. Selecting text and typing
< wraps it either way, and so does a backtick, which pairs everywhere
except inside a string.
Dead code, faded. Code nothing uses is dimmed rather than warned
about: it does not enter the Problems count, does not badge the file, and
gates nothing. Five things fade — an unused import, a let inside a
function body that nothing reads, the statements after one that cannot
return, a top-level item no entry of your package reaches, and every
top-level item of a module no entry loads at all.
The last two are the whole-package ones, and they are worth knowing precisely, because a fade means “you may delete this.”
- What can fade: a top-level
funand a module-levellet. Astruct, anenum, atraitand animplblock never fade, used or unused — they compile to nothing either way, so “does the build keep it” is not a question that has an answer for them. - What “unused” means: no entry of the package reaches it. A package
with several entries is a union — an item only your
probeentry calls is used. An item behind a[platform]fence is used if the entry for that platform reaches it. - A
[library]never fades a top-level item. A library has no entries, so there is nothing to compute the answer from, and every top-level item is surface a consumer may import — which is what keeps you from having to fork a library that forgot to export something. This holds for a library inside a workspace too, even when the only consumer today is a sibling package. To keep a name out of consumers’ completion without forbidding it, do notexportit: a private item is offered by neither completion nor the add-import fix, and an importer who needs it anyway writes the reach (import pkg::a::{ #helper };).[doc(hidden)]used to be the spelling for this and is retired — it meant exactly what a private item means, and no tool ever read it. - A declared
generatedroot never fades.[package] generated = "src/icons"already tellsvilan fmtto leave a machine-written directory alone; it tells the fade the same thing. _-led names never fade, the way an unused local does not. It is the “I know” marker.- Trait impl members never fade in this version. Whether a trait method is reached depends on which types your program constructs, so the answer moves in blocks as you edit; inherent impl members do fade.
- The fade goes off while you type and comes back a moment after you stop. The fact that would prove an item used lives in another file, so a fade held across an edit could say “dead” about code you have just started calling — and that is the one mistake a fade must not make. Being late is fine; being wrong is not. A syntax error anywhere in the package holds the whole package’s fades off for the same reason.
Completion
Where the cursor is decides what is offered.
- A bare position offers everything in scope, every keyword, and four
construct snippets (
fun … ( ) { },struct … { },for … in { },match … { }), which sort last. - After
.offers the receiver’s fields and methods; after?.offers the lifted element’s, so anOption<Profile>offersProfile’s members. What counts as “after.” ignores the whitespace and comments around it, so a chain written down the page (items, then.filter(f)on the next line) completes at each link; and what follows the cursor never changes the answer, so completing between two links works too. The methods include the ones the receiver’s type inherits as trait defaults —xs.iter().offers the wholeIteratorsurface, not the one methodListIterator’s ownimplblock writes out, and a type that implementsOrdoffersmin/max/clampwith the comparisons its supertraits provide. - Inside a struct initializer —
Point {— offers that struct’s fields, and nothing that is merely in scope: the one thing you are writing there is a field name. The fields you have already written drop out of the list, each candidate shows its declared type and its own doc, and accepting one writesname =— or justname, the shorthand, when a binding of that name is in scope andPoint { x }already meansPoint { x = x }. Past the=you are writing an expression again, so the ordinary answers come back. - Inside a string or a comment nothing is offered. A caption is text, not
code, and a
.in one is not a member access. - Visibility filters the list, once the module has an opinion. A module
that carries
exportanywhere is curated, and completion offers only what it exports — the same filter the add-import fix and the “import it first” steer use. A module with no marker anywhere is uncurated and offers everything, so a package that has never thought about visibility is unchanged. The filter is presentation only: nothing here blocks a name, and an importer who wants a private item writes the reach (import pkg::a::{ #helper };) and gets it. std is curated, which is whystd::rpc::offers its surface rather than its dispatcher internals. - Inside a
cssblock the vocabulary is CSS, and nothing in scope is offered at all. An undotted item completes property names — every slot aStylemethod writes, so the list is std’s own surface rather than an invented one — and a dotted item completes the condition combinators (md,hover,within, …). A value offers nothing: it is source text, not an expression, so a name in scope would be the wrong answer. A{…}hole is an ordinary expression and completes as one, which is how a typed value gets in. All four answers hold in a block you are still writing — one whose closing}you have not typed yet — because that is when completion is most use; a nested rule left open the same way is a body too. - After
::offers an enum’s variants and statics, a struct’s statics, or a module’s members — and only when the name on the left is actually in scope. It used to match that name against every type the compiler happened to have loaded, and the derive prelude keeps a handful of std modules loaded permanently, soOrdering::offeredLess/Equal/Greaterin a file that had never heard ofstd::compare. It resolves through scope now, exactly as the receiver of a.always has. - Inside an
importpath the whole path completes, at every level: the head offers the origins (std,pkg, and each dependency by the name you import it under), an origin offers its modules, and a module offers its members — loaded on demand, soimport std::random::completes in a file that has never mentionedstd::random. A brace set completes at the same level as its module, soimport std::json::{ Json,keeps going. (Imports are read as single-line items; a braced group’s later lines are not recognized.) Inside an impl SELECTOR the answer is not a name the module offers but a block it writes: afterimplthe module’s impl subjects (import std::web::style::{ (imploffersLength,Color, …), and after)::that block’s members ((impl Length)::offersrem). Both come from the module’s parsed text, like every other answer here. - A name you have not imported is offered too, labeled with the module
it comes from and carrying the
importas part of accepting it — one action writes both. Your own package’s names rank ahead ofstd’s, so a std-heavy file’s loaded surface cannot crowd them out of the 20-candidate cap.
Completion shapes a call for you by default; vilan.completion.functionCall
turns that down to parentheses only, or off.
vilan.toml gets its own completion — manifest keys and their closed sets
of values — from the server rather than from a JSON schema, so every editor
sees it.
Quick fixes
Twenty-five, each attached to the diagnostic that earns it:
| Action | Offered on |
|---|---|
Import `X` from std::json | cannot find 'X' where X is importable — in an expression, a type, a struct literal’s head, a pattern’s path head or an impl’s with trait. One action per module when more than one exports the name — never a guess between them. Also on a trait’s method called where the file does not import the trait: …`to_string` is `Display`'s, and this file does not import `Display`… (the call would resolve because another loaded module imports it; refused since v0.45.0) and i32 has no method 'to_string'; import std::display::Display to use it … (nothing loaded it) — the trait the message names, from the module it names when that module declares it |
Remove the duplicate import (Organize Imports) | Json is already imported on line 1 — the warning a repeated import leaf carries; the fix is Organize Imports’ own edit for that run, which merges the repeat away |
Change to `entries` | a did you mean …? note on a misspelled struct-initializer field |
Analyze this file under its platform: add `[platform("browser")] mod self;` | a member the OTHER std twin declares (struct 'Region' has no field 'anchor' in a file analyzed under node, whose note names the browser twin that has it). The edit is the file’s first line — the one place a file’s platform may be written — and the attribute is the one the note spells |
Insert `;` | expected `;` to end this statement, at the gap the diagnostic points at |
Remove `;` | the `;` discards this body's last value — it finds the right ; from the diagnostic’s own bookkeeping, and declines rather than guess when a comment sits in the gap |
Import as `#hidden` | a plain import of an item its module does not export. A zero-width insertion of the reach marker at the leaf — the whole edit — which says the reach was deliberate and silences the warning |
Delete the `#` | a reach marker on an item that IS exported. The marker states a belief about the module that is not true, and the fix removes the one character |
Use `.md { … }` | @media (min-width: …) in a css block. The breakpoint is chosen by the query’s own min-width, and an arbitrary one becomes .media("900px") rather than no fix. The other at-rules have no combinator spelling, so they get the explanation alone |
Declare the inferred contexts | …'s body reads context `b`, which this `context` clause does not declare — a fun’s declared context clause narrower than what its body reads. The refusal spells the clause the body needs and the fix writes exactly that, over the clause’s own name list |
Declare it `const let` | either half of G24’s const steer — a `const` expression reads only compile-time-known bindings at a read, or a `const` result must be plain data; this evaluates to a closure at a let x = const ... One edit either way: the declaration keyword. At a read it is a zero-width const in front of the binding the message names, ahead of the read and only where this file’s own text still opens it with let; at the closure result the const MOVES to the head of the declaration, which is entirely the text in front of the diagnostic’s span |
Write it as a call: `padding(px(4));` | a `css` declaration is a CALL — the property: value; spelling A101 retired, which is what every CSS author types and what every migrating program hits. The rewrite is the codemod’s: one {expr} hole becomes the expression, a text value becomes the typed constructor where a unit names one (4px → px(4)) and a string literal otherwise, and a whitespace-separated value becomes several arguments, which the block joins with one space exactly as CSS’s own value lists do. A GLUED value (calc({w} + 2px), {150}ms) and one carrying !important are declined rather than guessed: the first wants an i-string with a piece(..) per hole, which is a decision about the value, and the second has its own fix |
Write all 2 `css` declarations as calls | the same refusal, when the file carries more than one rewritable declaration — A101 reports per declaration, so a migrating block offers one fix per row and this one action for all of them. A declaration the rewrite declines travels through the edit exactly as you wrote it |
Remove `!important` | !important in a css block — a Style merges by record update, so a later declaration on the same property already wins. Takes the space before the marker with it |
Parenthesize the closure type | a fun whose RETURN type is an un-parenthesized closure type carrying a context clause (B343) — the type grammar’s own context suffix is greedy, so the clause lands on the closure’s own return type, which cannot carry one. The edit covers the written return type and adds the parentheses that give the clause to the FUNCTION; the refusal also names the other reading (the clause on the closure that is RETURNED), which is a second pair of parentheses and yours to choose |
Rewrite as `child(each(…))` | one of the six View methods A99 retired — when, swap, swap_split, bind_each, bind_each_values, bind_each_by. The edit covers the call alone, so the receiver, the chain around it and the arguments are untouched text; the {each(…)} hole the diagnostic also names is left to you, since whether a hole is right is a question about the markup around the call |
Convert with `.as_u53()` | Expected u53, but got i32 instead. There are no implicit numeric conversions; convert with `.as_u53()` — a value of one numeric width where another is declared (an argument, a let annotation, a reassignment, a return, a field, a subscript’s index). The edit writes the conversion the message names after the value, parenthesizing it first unless it is already a name, a field path or a call chain, so xs.len() + 1 becomes (xs.len() + 1).as_u53() — the whole value, not its last operand. Where the message names an index (Expected usize (an index: a position, a length or a count), but got i32 instead. …) it is the same edit, and it is also offered on an operator between a usize and another integer width (`<` compares two values of the same type, but the operands are `i32` and `usize`), where nothing is declared: the non-index operand converts to usize |
Declare `at` a `usize` | the same index mismatch, when the value is a counter bound by a bare literal earlier in the same function (mut at = 0;). The edit writes : usize after the counter’s name, because the counter IS an index — converting at each of its uses would leave .as_usize() on a value that should never have been anything else. Offered ahead of the conversion |
Convert all 2 indexes in this file | any index mismatch, when the file carries more than one — a file migrating to usize meets one per index, and this action takes each one’s first fix (the declaration where there is one, else the conversion) in ONE edit. It is the edit vilan check --fix makes, file by file, until nothing is left to fix |
Write all 2 declaration heads in the order | a declaration head written out of THE order — a declaration's attributes are written in one order — … or a declaration's markers are written in one order — … (a keyword ahead of an attribute, or two keywords inverted); both are refused — when the file carries more than one. Each head also carries its own fix, titled with the head in the order (Write `[deprecated(..)] [must_use] fun` ); this action takes every head in the file in ONE edit. Each head’s attributes and keywords move exactly as written, and what stood between them — the blanks, a line break, a comment — stays where it stood |
Import all 2 traits this file calls | either trait-method diagnostic above, when the file needs more than one trait — every one of them imported in ONE edit, a trait called twice imported once. A trait more than one module declares is left to its own fix |
Take the parameter as `&str` | this closure takes `str` by value where its type takes a view `&str` … — a closure LITERAL whose parameter is written in the other mode from the closure type it meets (a value closure and a view closure are different types, and no adapter is inserted). The edit rewrites that parameter’s type in the type’s mode — &, &mut or by value — and nothing else; a body that read the value may then want a * |
Adapt it: `|c| g(*c)` | the same refusal on a NAMED one-parameter closure (apply(g)), or on a named one-parameter FUNCTION (the function `count` takes …), which a call cannot rewrite: the adapter the refusal names, written around it — |c| g(*c) copies a view’s value out for a value closure, |c| g(&c) lends a view closure the value. Only around a plain name or path, since around any other expression the adapter would evaluate it on every call; a writable view meeting a value closure has none |
Write `auto str` | a stale auto (stale `auto`: `load` now returns `str`, not the written `auto i32` …) or a bare one’s warning (unfilled `auto`: …): the annotation the message ends with, written over it — the edit vilan check --fix makes |
Add `auto` type (`auto i32`) | not a diagnostic: the name of any unannotated function return or let binding whose type the file can name — : auto T after its parameter list or its name, never on a void return |
and three source actions:
| Action | Does |
|---|---|
| Organize Imports | sorts each top-level import run into canonical order (the same key vilan fmt uses), prunes unused leaves (shrinking a brace set rather than deleting it), and strips imports the prelude already covers. A statement whose every leaf is unused is rewritten rather than deleted when the module’s file is where an impl the code calls a method from lives — impls travel with any import that reaches the module, so deleting the statement would break the build. The rewrite is the narrowest statement that keeps it: an impl SELECTOR (import pkg::a::b; becomes import pkg::a::{ (impl Style) };) when everything the file uses from that module is one subject’s blocks, and the bare module import (import pkg::a;) when it is not. A selector is a leaf like any other: one whose implementation the file never calls a method from fades and prunes, one it does use stays. Visibility spellings are preserved verbatim, never rewritten: a reach marker (#helper) stays on its leaf, a trailing only stays on its statement, and a selector keeps the type it was written with. The module-level export *; is not an import and does not move — a NEW leading import is inserted above it, so the marker keeps the slot vilan fmt gives it, just below the file’s import run. Offered only when it would change something |
| Add All Missing Imports | applies every unambiguous import quickfix in the file at once, skipping the ambiguous ones |
Keep auto types current | rewrites every stale auto in the file and fills every bare one (source.fixAll.vilan.auto, what vilan.autoTypes.onSave runs) — nothing while the file has any other error |
An import the prelude covers is stripped for the same reason an unused
one is: import std::io::print; in a package whose prelude binds print
is redundant, and removing it changes nothing about what the file means.
The match is on the definition, not the name — an import of a
different print survives.
Organizing prunes only against a clean, current analysis: a file with
diagnostics, or one you are mid-edit in, is sorted and never pruned —
because “unused” is an answer you can only trust from a program that
type-checked. A module imported and used only through ::
(import std::math; referenced as math::min(1, 2)) counts as used and
is kept.
vilan.organizeImports.onSave (off by default) runs the same action on
save. It is the extension’s own hook rather than a line in your
editor.codeActionsOnSave, and organizing is a fixed point, so turning
both on is harmless.
auto types (auto annotations).
vilan.autoTypes.onSave (off by default) runs the server’s Keep auto
types current source action (source.fixAll.vilan.auto) on save: every
stale auto T in the file is rewritten to what its item now infers and
every bare auto is filled — the edits vilan check --fix makes, from the
analysis the editor already holds. It writes nothing while the file has
any other error, since a type filled from a broken program is noise. While
you type, a stale auto shows the type it would become after it
(: auto i32 ⟶ str), and its diagnostic carries the rewrite as a quick
fix. On any unannotated return or let binding, Add auto type writes
: auto T after its parameter list or its name — never on a void return,
and never for a type the file cannot name without a new import.
Refactors
A refactor is offered on the construct your cursor is in, rather than on a diagnostic or on the file:
| Action | Does |
|---|---|
Convert to a style() chain | rewrites the css { … } block the cursor is in as the builder chain it lowers to — a declaration becomes a .raw link, a nested rule becomes a combinator link carrying the inner chain |
| Reflow this comment | re-prints the file with comment wrapping on, whatever [fmt] wrap_comments says — an explicit action is consent where a format-on-save is not. Offered with the cursor in a comment, and only where the wrap changes something: a run already filled, one no line of which is over the width, a trailing comment after code, and each of the ten never-reflow classes get no action. The edit is the whole buffer, because the fill is the formatter’s own and asking it is what keeps this and format-on-save in agreement |
Convert to a css block | the inverse, on a style() chain — seeded by any path ending in style(), so style::style() (what the web prelude publishes) reads as one. A typed property method is converted by inlining its std body: .padding_x(space(4)) is with_length("padding-left", value).with_length("padding-right", value), so it writes both declarations with the argument in each. A link with no block spelling — a user extension, a method whose body is not a chain (.border(…)), .class_list() — splits the chain instead of refusing it: everything before it becomes the block and the rest is written as a postfix chain on it (css { … }.select_off()). Not offered when no link converts |
Both directions decline rather than guess. A comment inside the
construct stops the conversion, because its attachment is not recoverable
across the reshape — the same refusal vilan fmt makes when it declines to
reorder a commented block. So does a declaration with several
arguments (margin(px(4), px(8))): its chain twin needs
std::web::style::piece, which is ambient inside a block and nowhere else, so
the chain written out would name something the file does not import. The
two directions are inverses on everything they accept.
What it does while you type
Half-typed code is what a compiler is asked about most of the time. Two behaviors follow from that, and both are worth knowing because they are the difference between an editor that helps mid-edit and one that goes blank.
A syntax error no longer blanks out the file. The parser recovers at
statement and item boundaries: a statement it cannot read is reported and
skipped to the next ;, }, or declaration keyword, and everything around
it — the statements beside it, the functions below it, the whole file tail
— still reaches the analyzer. So the diagnostics a file already had stay on
screen instead of vanishing for the two keystrokes it takes to type
print(1);, and a function body with one broken statement in it loses that
statement rather than being discarded whole. vilan check answers the same
way; vilan build still stops, because a recovered file is not something
to emit from.
Answers stick to their text between a keystroke and the analysis it schedules. Every answer the server holds is the last analysis’s, and every edit since is carried through it: a squiggle, a “see here” note, an inlay hint, a reference, a definition, an outline entry and a quick fix all move with the code they are about — a line typed above shifts them down, typing inside a flagged span stretches it, typing after it leaves it alone — and the squiggles are republished at once, without waiting for the analysis. An edit across one end of a span takes that span away until the analysis says again, and a caret inside text typed since the analysis gets no hover or definition: the analysis saw nothing there. Semantic highlighting keeps the colours of every unchanged line and paints the edited ones from syntax, and carries the unchanged tail of the file across an analysis, so the colours below your edit do not flash off and back.
Two requests decline rather than answer wrong while the buffer is ahead of the analysis: rename (“still analyzing this file; retry in a moment”), because text typed since may hold a reference it would miss, and Organize Imports, which declines silently because editors ask for it on save. A quick fix is offered from the last analysis only where you have not typed over the text it would rewrite.
What it does not have
Named so you do not go looking: there is no signature-help popup (a signature reaches you through hover and a completion item’s detail line), no folding ranges, no workspace-wide symbol search (the outline is per-file), no document-highlight-on-cursor, no code lens, no call or type hierarchy, no go-to-type-definition / implementation / declaration beyond plain go-to-definition, and no pull diagnostics — diagnostics are pushed.
Settings
| Setting | Default | |
|---|---|---|
vilan.server.path | — | an explicit vilan-lsp binary; changing it restarts the client |
vilan.stdPath | — | an explicit std root, overriding discovery |
vilan.inlayHints.enabled | true | |
vilan.inlayHints.abbreviate | true | show a [hint]ed type by its trait, ~Source<T>; off shows every full type |
vilan.inlayHints.chains | every | changes: hint a chain’s stage only where its type changes |
vilan.semanticTokens.enabled | true | off falls back to the TextMate grammar |
vilan.completion.functionCall | full | parensOnly, or none |
vilan.autoClosing.generics | true | pair a generic < and type over its >; off for Vim emulation |
vilan.organizeImports.onSave | false | |
vilan.autoTypes.onSave | false | rewrite stale and fill bare auto types on save |
Everything but the two paths applies live. Vilan: Restart Language Server is in the command palette when you want the blunt instrument, and the Vilan Language Server output channel carries the server’s own log. Vilan: Stop Language Server silences a server that is misbehaving or a file it cannot get through: its squiggles are cleared and nothing starts it again — not a settings change, not a crash policy — until Vilan: Start Language Server or a window reload.
Vilan: Show Language Server Status writes this session’s profile to that
same channel: the extension’s request tally first — session age, server
starts, per-request count / mean / max, slowest total first — and then the
server’s own page, which adds its retained-state cardinalities (open
documents, how many still hold a program, caches, pending analyses), the
analysis counts, its own per-request profile, and its memory reading:
resident size with the heap split into in-use and retained-free. The server
writes that page by itself every 500 requests; the command asks for it now,
which is when a session has started feeling slow. A figure the host declines
to report prints ? rather than a zero.