Skip to content

vscode: the Activity Bar panel, the verbs, and tests against a real engine - #3

Merged
sajonaro merged 6 commits into
mainfrom
activity-bar-panel
Oct 3, 2026
Merged

sajonaro merged 6 commits into
mainfrom
activity-bar-panel

Conversation

@sajonaro

@sajonaro sajonaro commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

What

This brings activity-bar-panel to main, which has fallen behind:

  • Activity Bar panel and verbs. A panel plus the verbs check, show, derive and compare. The extension version tracks the engine's.
  • Engine reporting. The engine the editor is talking to is resolved and shown, with a verdict when its version doesn't match.
  • README. It moves to the Marketplace layout, and developer notes move to DEVELOPING.md.
  • Rename cleanup. Two commits clean up the install that the rename stranded, and find a sibling checkout. This is the content of Fix pre rename install #1, which this branch already contains.
  • Tests. scripts/test.sh and five plain-node test files, including test/engine.test.js from test: the extension against a real engine #2. That test drives the real writ-lsp and runs every command line the verbs build.

main currently has no scripts/ or test/ at all. Until this merges, writ's downstream CI prints a skip for the extension instead of testing it.

Merge order

Merge writ-lang/writ#13 first. It makes writ-lsp report serverInfo. writ's downstream job runs this repository's tests against writ main with WRIT_E2E_REQUIRED=1, and without #13 the two serverInfo checks fail. Every writ PR would go red until #13 lands.

Verified

  • Locally: scripts/test.sh gives manifest 45, resolve 21, status 30 and watcher 8 checks. The engine test skips without node_modules or a server.
  • Against writ #13: writ's downstream image, built from that branch with this branch as the vscode ref, succeeds for all six suites. The engine test there reports 23 of 23.

🤖 Generated with Claude Code

sajonaro and others added 6 commits August 13, 2026 20:20
…checkout

The extension appeared in the marketplace pane as `pol`, flagged "invalid
extensions detected", offering to Install Locally a thing that was not there.

An install made before "rename: pol becomes writ" is a symlink named
pol.pol-0.1.0 pointing into a directory the rename took away. VS Code scans its
extensions directory, cannot read a manifest through a dangling link, and
reports the extension as invalid — naming `pol`, which the user cannot remove
from the UI, because there is nothing behind it to uninstall. The entry in
extensions.json outlives the directory and is what keeps re-reporting it.

install.sh only ever removed writ.writ-0.1.0, so nothing cleaned this up: the
rename shipped without a migration path, and every pre-rename install stayed
broken and visible. It now purges legacy ids on INSTALL as well as uninstall —
the person hitting this is installing the new extension, not remembering an old
one — and tests `-e` OR `-L`, because `-e` is false for exactly the dangling
case being cleaned. The removal is of the link, never through it, which would
delete a live checkout.

Underneath that sat a second failure the first one hid. candidates() joined the
relative default onto workspace folders only, so the split into separate writ/
and writ-vscode/ repositories broke it: the natural thing to open is the
directory holding both, and the default then resolves against a folder with no
_build in it. The extension said "no language server" with a perfectly good one
a single directory down — the same symptom as never having built it, which
sends you off to check the wrong thing entirely. It now also tries checkouts one
level below each workspace folder, identified by dune-project rather than by
being named `writ`. One level, deliberately: this must not become a walk of the
workspace, which on a large tree would cost more than it saves.

install.sh had the same blind spot from the other side, looking only at
$here/../.. for the engine — which in the split layout is the directory
containing both repositories and never the engine itself. It now accepts
$WRIT_REPO, ../.., or ../writ. Its verify step no longer pipes into an empty
command name when there is no server to verify, which reported a shell error
about "" rather than the thing that was actually true.

TESTED, because all of this fails silently. test-resolve.js gains the split
layout, exercised against a real directory tree: that a checkout one level down
is found, that the workspace root still wins, that a sibling which is not a
checkout is not guessed at, that the nested build still beats PATH, and that a
workspace folder which does not exist is survivable rather than fatal at
activation. The legacy purge was checked by recreating the dangling symlink and
re-running the installer against it.

Also: package-lock.json still called the package politics-constructor, and the
run-path comments still named tooling/vscode/, which no longer exists.
…t of the .vsix

The grammar's comments said .afk-flow/check-grammar.py asserts the guard, effect
and rules-builtin alternations against the engine, and told the reader to keep
them as whole ( | ) alternatives so that it could. No such script is in this
repository, or in writ's.

The word lists are still right — they match `guard_words` and `builtins` in
core/syntax/rules_parser.ml, checked by hand here — but a comment promising a
check that does not run is worse than no comment at all: it is exactly why
nobody notices when the grammar drifts from the engine. The formatting rule
stays, now with the reason that is actually true: so the sets can be diffed
against that list by eye, and so a word added to the engine is visibly missing
here.

Only the "comment" fields changed. Every match regex is untouched, so nothing
about the highlighting moves.

.vscodeignore additionally keeps the tests, install.sh, the lockfile, .mailmap
and .github/ out of a .vsix — shipping the installer inside the thing it
installs is a loop that can only confuse, and the rest is repository plumbing
that is invisible in the source tree and rides along unnoticed. docs/ stays in
deliberately, because README.md embeds docs/images/writ-mark-200.png and the
marketplace page loses its mark without it, as do LICENSE, NOTICE and AUTHORS,
which ship with the work.

VERIFIED with vsce rather than reasoned about, which is how .mailmap and
.github/ were found at all — nothing else lists them. `vsce package` now yields
340 files, of which exactly fourteen are this extension's: the manifest, the
client, the grammar, the language configuration, the icon, the README and its
images, and the notices. The other 326 are node_modules and vsce's own two
generated manifests.
… step

Three things this extension could not do, and one of them was the reason the
other two mattered.

THE PANEL. The extension bundles no engine and downloads none: it resolves a
`writ-lsp` at activation, from a checkout's `_build` or from PATH. Every way
that can go wrong produces the same symptom — an editor that highlights and
says nothing else — and the causes want different fixes. The extension said one
of them once, in a toast, at activation, and then never again. Now there is a
place to look: which server, which version, whether the two are in step, and
the button for whichever thing is wrong.

THE VERBS. writ is a command-line tool that answers questions; the extension
exposed none of them. check, show, derive and compare, from the panel, the
title bar, the context menus and the palette — each running the `writ` that
belongs to the SAME install as the language server, derived from the resolved
server rather than looked up on PATH. An editor whose "check this file"
disagreed with its own squiggles would be worse than having no command at all.

THE VERSION. It lived in three hand-kept copies here and had nothing to do with
writ's, which dune generates. install.sh now derives its extension id from the
manifest instead of holding one — installing under a stale id lands the new
extension beside the old one, both manifests valid, and VS Code loads whichever
it scans first. scripts/version.sh checks what is left, and `--from-writ` sets
it from a checkout's dune-project. This is worth enforcing now that the panel
shows a mismatch: a forgotten bump is a warning on somebody's working install.

Staleness is the part no version comparison can reach. writ's version moves
only when someone bumps it, so a server built before this morning's edit
reports exactly the same version as a current one — the two agree, and the
agreement is the misleading half. src/staleness.js compares times instead, and
the build is a Task rather than a terminal because only a task reports whether
it succeeded, and only a build that succeeded may clear the warning.

extension.js is now the wiring; the behaviour is six modules under src/, none
of which touches the vscode API outside its own register(). The two existing
tests move to test/ and lose their module-loader stubs — nothing needed them
once the workspace folders and the restart were arguments — and two new ones
join them: the verdict (shown to users as a warning, so being wrong means
telling someone their good install is broken) and the manifest (which nothing
typechecks: a command contributed but not registered appears in the palette and
does nothing).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The other test files check this repository's JavaScript, and none of them
would notice the engine changing underneath it. That coupling is the one
most likely to break silently: a capability writ-lsp stops advertising, or a
flag the CLI renames, leaves the extension running and doing nothing.

test/engine.test.js spawns the writ-lsp the extension would find and speaks
LSP to it over vscode-jsonrpc (already installed as the client's transport):
initialize and its serverInfo, full sync, hover, outline and completion
advertised, diagnostics on a good and a broken model, the outline, hover,
completion, and shutdown. It then runs every command-line shape
src/commands.js builds - check, check --claims, show --at, derive, derive
--why, compare - and asserts the CLI accepted each one (exit 0 or 1, never 2).

With no writ-lsp, or no node_modules, it prints a skip and passes, so
scripts/test.sh still needs nothing installed. WRIT_E2E_REQUIRED=1 turns a
skip into a failure; writ's downstream image sets it.

Against writ main the two serverInfo checks fail: the server does not send
it yet, though engine.js and the panel read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test: the extension against a real engine
@sajonaro
sajonaro merged commit 23a226e into main Oct 3, 2026
1 check passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant