Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ structure — `extension.js` is the wiring, and one module per concern under
| | |
| --- | --- |
| `locate.js` | where the engine is: the candidate order, the checkout a server came from, and the `writ` belonging to it |
| `download.js` | fetching a released writ into the extension's storage: which tarball, the checksum, the unpack |
| `watcher.js` | restart when the server binary is replaced |
| `staleness.js` | is the built server older than the sources it was built from |
| `engine.js` | what engine this editor is talking to, and the verdict on it |
Expand All @@ -24,7 +25,7 @@ exercise, with nothing stubbed and nothing installed:
$ scripts/test.sh
```

Five files, and each one is there because its subject fails **silently**:
Six files, and each one is there because its subject fails **silently**:

| | |
| --- | --- |
Expand All @@ -33,6 +34,7 @@ Five files, and each one is there because its subject fails **silently**:
| `test/status.test.js` | the verdict is shown to users as a warning; getting it wrong means telling someone with a good install that it is broken |
| `test/manifest.test.js` | nothing typechecks `package.json` — a command contributed but not registered appears in the palette and does nothing |
| `test/engine.test.js` | the engine is a separate install — a capability `writ-lsp` stops advertising, or a flag the CLI renames, leaves the extension running and doing nothing. It drives the real server and runs each command line the verbs build; with no `writ-lsp` on PATH it skips, and `WRIT_E2E_REQUIRED=1` makes that a failure |
| `test/download.test.js` | the wrong tarball name is a 404 that reads like an outage, a download ranked ahead of PATH overrides a writ installed on purpose, and a download button shown to someone with `writ-lsp` on PATH fetches an engine that never runs. `WRIT_DOWNLOAD_LIVE=1` also downloads the real release and runs it |

## The version

Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,16 @@ to a `writ-lsp` you already have.

## Getting Started

**You need writ too.** The ordinary route needs `writ-lsp` on your `PATH`:
**You need writ too.** On Linux (x86_64 or arm64) the easy route is the
**Download writ** button: in the Writ panel, in the "no language server"
message, or as **Writ: Download writ** in the Command Palette. It fetches the
release matching this extension (or the latest one, if that version was never
released), verifies its checksum, unpacks it into the extension's own storage
and starts it. Nothing goes on your `PATH`, and uninstalling the extension
removes it.

A writ you install yourself always wins over a downloaded one. The usual route
puts `writ-lsp` on your `PATH`:

```console
$ opam pin add writ git+https://github.com/writ-lang/writ.git # or unpack a release tarball
Expand Down Expand Up @@ -111,9 +120,9 @@ want different fixes:

| It says | What happened | The button |
| ----------------------------------------- | ----------------------------------------------- | -------------------- |
| no language server | nothing was found at any candidate path | build, or update writ|
| no language server | nothing was found at any candidate path | download writ, build, or install with opam |
| older than `core/…` | the checkout was edited since it was built | build the engine |
| the engine is *x*, the extension is *y* | one of the two was installed on its own | build, or update writ|
| the engine is *x*, the extension is *y* | one of the two was installed on its own | build, update writ, or download a fresh one if the current one was a download |
| the server did not say which writ it is | a writ from before the server reported one | update writ |

The stale case is the one nothing else catches: a build from before this
Expand All @@ -127,7 +136,7 @@ was launched and says so when it swaps after a rebuild.

| Setting | Default | Description |
| ----------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `writ.serverPath` | `_build/default/tooling/lsp/bin/writ_lsp.exe` | A relative path is resolved against each open workspace folder, so the default finds a checkout's own build. If none has it, `writ-lsp` is looked up on `PATH`. An absolute path is used exactly as given. |
| `writ.serverPath` | `_build/default/tooling/lsp/bin/writ_lsp.exe` | A relative path is resolved against each open workspace folder, so the default finds a checkout's own build. If none has it, `writ-lsp` is looked up on `PATH`, and after that a writ downloaded by the extension. An absolute path is used exactly as given. |

If nothing exists at any of those, the extension says so and names what it
looked for.
Expand Down
41 changes: 29 additions & 12 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ const staleness = require("./src/staleness");
const watcher = require("./src/watcher");
const commands = require("./src/commands");
const panel = require("./src/panel");
const download = require("./src/download");

let watching;

// Say what is missing, once, and offer the two things that fix it.
// Say what is missing, once, and offer what fixes it — first of all a download,
// where there is a released writ for this platform.
//
// Silence here would look like a server that starts and answers nothing, which
// is the same symptom as a broken server and much harder to chase. The PATH
Expand All @@ -45,12 +47,16 @@ function reportMissing(tried) {
"install writ (`opam pin add writ git+https://github.com/writ-lang/writ.git`, " +
"or a release tarball) so " +
`\`${locate.INSTALLED}\` is on PATH — or set \`${engine.SETTING}\` to an ` +
"absolute path to the server you want.",
"absolute path to the server you want." +
(download.platformOf() ? " Or download a released writ in one click." : ""),
...(download.platformOf() ? ["Download writ"] : []),
"Open the Writ panel",
"Open settings"
)
.then((choice) => {
if (choice === "Open the Writ panel")
if (choice === "Download writ")
vscode.commands.executeCommand("writ.downloadEngine");
else if (choice === "Open the Writ panel")
vscode.commands.executeCommand("writ.openOverview");
else if (choice === "Open settings")
vscode.commands.executeCommand("writ.openSettings");
Expand Down Expand Up @@ -111,22 +117,33 @@ async function start(context) {
panel.refresh();
}

// Stop whatever is running and resolve the server again from scratch.
//
// The server may have moved, so everything derived from it is suspect: which
// binary, which version, whether it is stale, and whether it is running at
// all. A changed setting needs this, and so does a fresh download.
async function reload(context) {
if (watching) watching.dispose();
watching = undefined;
const client = engine.current().client;
if (client) await client.stop().catch(() => {});
engine.attach(null);
await start(context);
}

function activate(context) {
// Downloaded engines live in the extension's own storage, which VS Code
// keeps across updates of the extension and removes when it is uninstalled.
engine.setStorage(context.globalStorageUri.fsPath);
panel.register(context);
commands.register(context);
commands.register(context, { reload: () => reload(context) });

context.subscriptions.push(
// The server may have moved, so everything derived from it is suspect: which
// binary, which version, whether it is stale, and whether it is running at
// all. Reloading the window used to be the only way to pick up a corrected
// Reloading the window used to be the only way to pick up a corrected
// setting.
vscode.workspace.onDidChangeConfiguration(async (event) => {
if (!event.affectsConfiguration("writ")) return;
if (watching) watching.dispose();
const client = engine.current().client;
if (client) await client.stop().catch(() => {});
engine.attach(null);
await start(context);
await reload(context);
})
);

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
"command": "writ.update",
"title": "Writ: Update writ…"
},
{
"command": "writ.downloadEngine",
"title": "Writ: Download writ",
"icon": "$(cloud-download)"
},
{
"command": "writ.openOverview",
"title": "Writ: Open the Writ Panel"
Expand Down
66 changes: 65 additions & 1 deletion src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
const fs = require("fs");
const path = require("path");
const engine = require("./engine");
const download = require("./download");
const staleness = require("./staleness");
const panel = require("./panel");

Expand Down Expand Up @@ -320,7 +321,67 @@ function update() {
t.sendText(PIN, false); // NOT executed: an install command is the user's to press enter on
}

function register(context) {
// Download a released writ into the extension's storage, then start it.
//
// The one-click route for someone with no opam and no checkout. What it fetches
// and how it checks it is src/download.js; this is the progress, the result,
// and the restart. `reload` is extension.js's, because only it can start the
// client again.
async function downloadEngine(context, reload) {
const vscode = require("vscode");
const storage = engine.current().storage;
const wanted =
(context.extension &&
context.extension.packageJSON &&
context.extension.packageJSON.version) ||
"0.0.0";
if (!download.platformOf()) {
const choice = await vscode.window.showWarningMessage(
`Writ: there is no released writ for ${process.platform}-${process.arch} ` +
"to download. Install it with opam instead.",
"Install with opam…"
);
if (choice) update();
return;
}
try {
const got = await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Writ",
cancellable: false,
},
(progress) =>
download.download({
storage,
wanted,
say: (m) => progress.report({ message: m }),
})
);
await reload();
const s = engine.current();
if (s.serverPath !== got.server) {
// Downloaded, but something ahead of it in the search order still wins:
// a checkout's build, writ-lsp on PATH, or an absolute setting.
vscode.window.showInformationMessage(
`Writ: downloaded writ ${got.version}, but the server in use is still ` +
`${s.serverPath}, which comes first in the search order.`
);
} else {
vscode.window.showInformationMessage(
`Writ: writ ${got.version} downloaded and running.`
);
}
} catch (e) {
const choice = await vscode.window.showErrorMessage(
`Writ: could not download writ: ${e.message}`,
"Install with opam…"
);
if (choice) update();
}
}

function register(context, hooks = {}) {
const vscode = require("vscode");
const on = (name, fn) =>
context.subscriptions.push(vscode.commands.registerCommand(name, fn));
Expand All @@ -331,6 +392,9 @@ function register(context) {
on("writ.compare", compare);
on("writ.build", build);
on("writ.update", update);
on("writ.downloadEngine", () =>
downloadEngine(context, hooks.reload || (() => Promise.resolve()))
);

on("writ.restartServer", async () => {
await engine.restart();
Expand Down
Loading
Loading