📦 Turbo Core — a save that creates a file tells the server (workspace/didChangeWatchedFiles), so moon-lsp diagnoses a new .mbt from its first save
3561e52 parent: b91316e modified
.memory/handoffs/2026-09-19-canonical-paths.md +4 -0 | @@ -19,3 +19,7 @@ | ||
| 19 | 19 | ## Second pass |
| 20 | 20 | |
| 21 | 21 | The Mac found three more tests comparing paths as spelt; fixed (`windowFor` canonical, two `lsp` tests expect the canonical spelling, one new test). The whole suite is green here with `TMPDIR=/tmp/symroot/tmp TURBO_CORE_RELEASING=1 go test ./...` — run that before every release from now on, it is the closest Linux gets to a Mac's `/var/folders`. |
| 22 | + | |
| 23 | +## Third pass — v1.0.2: `FileCreated` | |
| 24 | + | |
| 25 | +turbo-moonbit's pinning test went red on the Mac for the opposite reason — moon-lsp there notices a new file by itself. The limit was ours (no `workspace/didChangeWatchedFiles`); fixed, four tests, docs. Release v1.0.2 with `./01-release.tag.sh`, then re-pin every editor: `go get rickub.com/turbo-editors/turbo-core@v1.0.2 && go mod tidy && GOWORK=off make check`. turbo-moonbit's suite fails until then. | |
| @@ -19,3 +19,7 @@ | |||
| 19 | ## Second pass | 19 | ## Second pass |
| 20 | 20 | ||
| 21 | The Mac found three more tests comparing paths as spelt; fixed (`windowFor` canonical, two `lsp` tests expect the canonical spelling, one new test). The whole suite is green here with `TMPDIR=/tmp/symroot/tmp TURBO_CORE_RELEASING=1 go test ./...` — run that before every release from now on, it is the closest Linux gets to a Mac's `/var/folders`. | 21 | The Mac found three more tests comparing paths as spelt; fixed (`windowFor` canonical, two `lsp` tests expect the canonical spelling, one new test). The whole suite is green here with `TMPDIR=/tmp/symroot/tmp TURBO_CORE_RELEASING=1 go test ./...` — run that before every release from now on, it is the closest Linux gets to a Mac's `/var/folders`. |
| 22 | + | ||
| 23 | +## Third pass — v1.0.2: `FileCreated` | ||
| 24 | + | ||
| 25 | +turbo-moonbit's pinning test went red on the Mac for the opposite reason — moon-lsp there notices a new file by itself. The limit was ours (no `workspace/didChangeWatchedFiles`); fixed, four tests, docs. Release v1.0.2 with `./01-release.tag.sh`, then re-pin every editor: `go get rickub.com/turbo-editors/turbo-core@v1.0.2 && go mod tidy && GOWORK=off make check`. turbo-moonbit's suite fails until then. | ||
modified
.memory/history.md +9 -0 | @@ -278,3 +278,12 @@ | ||
| 278 | 278 | - **Changes**: `app/actions_file.go` — `windowFor` compares `lsp.CanonicalPath` of both sides, not `filepath.Abs`; this is the real defect the first test caught — a location the server sends back could not find the window it was about, so the references list quoted the disk and a jump would have opened the file twice. `lsp/lsp_test.go` — the two tests now expect `CanonicalPath(path)`, with the reason in a comment. New `TestWindowForFindsAFileOpenedThroughALink` (app), **falsified** against the old `windowFor`. |
| 279 | 279 | - **Verified**: `make check` green; whole suite green with `TMPDIR` under a symlink (`TURBO_CORE_RELEASING=1` to keep the release scripts out of it). |
| 280 | 280 | - **Not done**: still not committed or tagged; `release.env` still says v1.0.1. Run `./01-release.tag.sh` again. |
| 281 | + | |
| 282 | +## 2026-09-19 (later) — a save that creates a file tells the server: moon-lsp never diagnosed a new .mbt | |
| 283 | + | |
| 284 | +- **Origin**: turbo-core v1.0.1 published, turbo-moonbit re-pinned; its `make check` on the Mac then failed `TestAFileCreatedAfterTheServerStartedIsNotDiagnosed` — the test pinning "moon-lsp never diagnoses a file created after it started" — because on macOS moon-lsp *does*. Here (Linux, moon 0.1.20260915) the test still passed, so the platforms disagree; the documentation it protected had in fact already been overtaken (turbo-moonbit's `enable-completion.md` has promised error marks from the first save since 2026-09-18), and `languages.md` no longer mentions it. | |
| 285 | +- **Diagnosed** with an LSP probe against `moon-lsp --stdio`: a new file announced by `didOpen` (and `didSave`) is never diagnosed on Linux; the moment `workspace/didChangeWatchedFiles` names it — in any order relative to the document notifications, type 1 or 2 — its diagnostics arrive. The server advertises no watcher registration, so nothing ever asked the editor to send this. turbo-moonbit's own summary had already written the fix down as turbo-core's to make. | |
| 286 | +- **Changes**: `lsp/protocol.go` — `FileChangeType` (`FileCreated`/`FileChanged`/`FileDeleted`), `FileEvent`, `DidChangeWatchedFilesParams`. `lsp/client.go` — `FileCreated(path)`. `app/language.go` — `FileCreated`. `app/actions_file.go` — `save` asks `fileExists` before writing; `afterSave` and `announceSaved` take `created`; the file event goes after the document is announced or saved. `app/autosave.go` — `writeQuietly` the same. `app/fakelsp_test.go` — records the last file event and the order of methods (`lastFileEvents`, `methodsSeen`). Tests: `TestSavingANewFileTellsTheServerTheFileExists`, `TestSavingAnExistingFileDoesNotClaimItWasCreated`, `TestAutosaveOfANewlyNamedFileTellsTheServerTheFileExists`; the lsp notification-order test includes the new one. The `firstIndexOf` helper: `actions_view.go` owns `indexOf`. | |
| 287 | +- **Verified**: `make check` green; whole suite green with `TMPDIR` under a symlink. In turbo-moonbit (through `go.work`), the rewritten `TestAFileCreatedInTheEditorIsDiagnosedFromItsFirstSaveWithRealMoonLSP` passes (plain and symlinked `TMPDIR`) and **fails against the published v1.0.1** — the falsification. A first version of that test failed for a reason of its own: its `typeText` dropped `\n`, so the four-line fixture became one `///|` doc-comment line and compiled; traced with a tee wrapper around `moon-lsp`. The helper now sends Enter for a newline. | |
| 288 | +- **Docs**: `app/README.md`, `docs/{en,fr}/reference/app.md`, `docs/{en,fr}/how-to/talk-to-a-language-server.md`, `lsp/README.md`. | |
| 289 | +- **Not done**: not committed, not tagged; `release.env` says v1.0.2. Whether moon-lsp on macOS watches the directory itself is inferred from the user's run, not measured. | |
| @@ -278,3 +278,12 @@ | |||
| 278 | - **Changes**: `app/actions_file.go` — `windowFor` compares `lsp.CanonicalPath` of both sides, not `filepath.Abs`; this is the real defect the first test caught — a location the server sends back could not find the window it was about, so the references list quoted the disk and a jump would have opened the file twice. `lsp/lsp_test.go` — the two tests now expect `CanonicalPath(path)`, with the reason in a comment. New `TestWindowForFindsAFileOpenedThroughALink` (app), **falsified** against the old `windowFor`. | 278 | - **Changes**: `app/actions_file.go` — `windowFor` compares `lsp.CanonicalPath` of both sides, not `filepath.Abs`; this is the real defect the first test caught — a location the server sends back could not find the window it was about, so the references list quoted the disk and a jump would have opened the file twice. `lsp/lsp_test.go` — the two tests now expect `CanonicalPath(path)`, with the reason in a comment. New `TestWindowForFindsAFileOpenedThroughALink` (app), **falsified** against the old `windowFor`. |
| 279 | - **Verified**: `make check` green; whole suite green with `TMPDIR` under a symlink (`TURBO_CORE_RELEASING=1` to keep the release scripts out of it). | 279 | - **Verified**: `make check` green; whole suite green with `TMPDIR` under a symlink (`TURBO_CORE_RELEASING=1` to keep the release scripts out of it). |
| 280 | - **Not done**: still not committed or tagged; `release.env` still says v1.0.1. Run `./01-release.tag.sh` again. | 280 | - **Not done**: still not committed or tagged; `release.env` still says v1.0.1. Run `./01-release.tag.sh` again. |
| 281 | + | ||
| 282 | +## 2026-09-19 (later) — a save that creates a file tells the server: moon-lsp never diagnosed a new .mbt | ||
| 283 | + | ||
| 284 | +- **Origin**: turbo-core v1.0.1 published, turbo-moonbit re-pinned; its `make check` on the Mac then failed `TestAFileCreatedAfterTheServerStartedIsNotDiagnosed` — the test pinning "moon-lsp never diagnoses a file created after it started" — because on macOS moon-lsp *does*. Here (Linux, moon 0.1.20260915) the test still passed, so the platforms disagree; the documentation it protected had in fact already been overtaken (turbo-moonbit's `enable-completion.md` has promised error marks from the first save since 2026-09-18), and `languages.md` no longer mentions it. | ||
| 285 | +- **Diagnosed** with an LSP probe against `moon-lsp --stdio`: a new file announced by `didOpen` (and `didSave`) is never diagnosed on Linux; the moment `workspace/didChangeWatchedFiles` names it — in any order relative to the document notifications, type 1 or 2 — its diagnostics arrive. The server advertises no watcher registration, so nothing ever asked the editor to send this. turbo-moonbit's own summary had already written the fix down as turbo-core's to make. | ||
| 286 | +- **Changes**: `lsp/protocol.go` — `FileChangeType` (`FileCreated`/`FileChanged`/`FileDeleted`), `FileEvent`, `DidChangeWatchedFilesParams`. `lsp/client.go` — `FileCreated(path)`. `app/language.go` — `FileCreated`. `app/actions_file.go` — `save` asks `fileExists` before writing; `afterSave` and `announceSaved` take `created`; the file event goes after the document is announced or saved. `app/autosave.go` — `writeQuietly` the same. `app/fakelsp_test.go` — records the last file event and the order of methods (`lastFileEvents`, `methodsSeen`). Tests: `TestSavingANewFileTellsTheServerTheFileExists`, `TestSavingAnExistingFileDoesNotClaimItWasCreated`, `TestAutosaveOfANewlyNamedFileTellsTheServerTheFileExists`; the lsp notification-order test includes the new one. The `firstIndexOf` helper: `actions_view.go` owns `indexOf`. | ||
| 287 | +- **Verified**: `make check` green; whole suite green with `TMPDIR` under a symlink. In turbo-moonbit (through `go.work`), the rewritten `TestAFileCreatedInTheEditorIsDiagnosedFromItsFirstSaveWithRealMoonLSP` passes (plain and symlinked `TMPDIR`) and **fails against the published v1.0.1** — the falsification. A first version of that test failed for a reason of its own: its `typeText` dropped `\n`, so the four-line fixture became one `///|` doc-comment line and compiled; traced with a tee wrapper around `moon-lsp`. The helper now sends Enter for a newline. | ||
| 288 | +- **Docs**: `app/README.md`, `docs/{en,fr}/reference/app.md`, `docs/{en,fr}/how-to/talk-to-a-language-server.md`, `lsp/README.md`. | ||
| 289 | +- **Not done**: not committed, not tagged; `release.env` says v1.0.2. Whether moon-lsp on macOS watches the directory itself is inferred from the user's run, not measured. | ||
modified
.memory/summary.md +4 -1 | @@ -127,6 +127,8 @@ The decisions inherited from turbo-go about the *editor's behaviour* — the eve | ||
| 127 | 127 | |
| 128 | 128 | - **Paths are canonical, not merely absolute (2026-09-19, v1.0.1).** `lsp.CanonicalPath` — `filepath.Abs` then `filepath.EvalSymlinks`, falling back to the deepest existing directory for a file not yet on disk — is what `lsp.PathToURI` sends the server, what `app.pathKey` keys documents and diagnostics by, and what `app.windowFor` compares when a location the server sent back has to land on a window already open. Reason: moon-lsp canonicalises a package's files, so a document announced as `/var/folders/…/main.mbt` (macOS: `/var` → `/private/var`) belonged to no package — no completion about the buffer's types, diagnostics published under `/private/var/…` that matched no open buffer. Turbo MoonBit's suite, green on Linux, failed both ways on its first run on a Mac; reproduced here with `TMPDIR` under a symlink, and with an LSP probe. Falsified: `TestDiagnosticsPublishedUnderTheRealPathReachAFileOpenedThroughALink` fails on the old `pathKey`. Rejected: fixing it in the editor's test by resolving `t.TempDir()` — that would hide the defect from every user whose files sit under a link. |
| 129 | 129 | |
| 130 | +- **A save that creates a file says so to the server (2026-09-19, v1.0.2).** `lsp.Client.FileCreated` sends `workspace/didChangeWatchedFiles` (type 1); `app.announceSaved` sends it after the document's `didOpen`/`didSave` whenever the save created the file (`fileExists` asked before the write, in both `save` and `writeQuietly`). Reason: moon-lsp lists a package's files from the directory, so a `.mbt` saved for the first time — `turbo-moonbit new.mbt`, type, save — was known as a document and never diagnosed. Verified at the protocol level (every order of didOpen/didSave/didChangeWatchedFiles gets the file diagnosed; nothing does without it) and by Turbo MoonBit's `TestAFileCreatedInTheEditorIsDiagnosedFromItsFirstSaveWithRealMoonLSP`, which fails against v1.0.1 and passes against this. Sent unasked (moon-lsp registers no watcher); harmless to the fake and, by the spec, to any server. Only `Created` is ever sent: Save As leaves the old file on disk, so no `Deleted`. | |
| 131 | + | |
| 130 | 132 | ## Build, test, run |
| 131 | 133 | |
| 132 | 134 | ```bash |
| @@ -161,7 +163,8 @@ python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | ||
| 161 | 163 | ## State as of 2026-09-19 |
| 162 | 164 | |
| 163 | 165 | - **v1.0.0 is released** under the new module path `rickub.com/turbo-editors/turbo-core`, by `01-release.tag.sh` and the Release workflow; turbo-go v1.0.0 is published against it and the five other editors are re-pinned to it, waiting for their first Rickub release. |
| 164 | -- **v1.0.1 is ready to tag** (`release.env` says so): the canonical-path fix above, `lsp.CanonicalPath` exported, three new tests (`lsp` ×2, `app` ×1), `app/fakelsp_test.go` records the last `didOpen`. Every editor should re-pin to it before releasing; Turbo MoonBit *must*, or its suite fails on macOS. | |
| 166 | +- **v1.0.1 is released**: the canonical-path fix (`lsp.CanonicalPath`, `pathKey`, `windowFor`). turbo-moonbit is re-pinned to it. | |
| 167 | +- **v1.0.2 is ready to tag** (`release.env` says so): `FileCreated` above; four new tests (`lsp` ×1 in the notification-order test, `app` ×3 in `save_test.go`), the fake server records the last `didOpen` and the last file event and the order of every method. Turbo MoonBit's suite needs it (its new first-save test fails on v1.0.1); every editor should re-pin before releasing. | |
| 165 | 168 | |
| 166 | 169 | ## Not yet established |
| 167 | 170 | |
| @@ -127,6 +127,8 @@ The decisions inherited from turbo-go about the *editor's behaviour* — the eve | |||
| 127 | 127 | ||
| 128 | - **Paths are canonical, not merely absolute (2026-09-19, v1.0.1).** `lsp.CanonicalPath` — `filepath.Abs` then `filepath.EvalSymlinks`, falling back to the deepest existing directory for a file not yet on disk — is what `lsp.PathToURI` sends the server, what `app.pathKey` keys documents and diagnostics by, and what `app.windowFor` compares when a location the server sent back has to land on a window already open. Reason: moon-lsp canonicalises a package's files, so a document announced as `/var/folders/…/main.mbt` (macOS: `/var` → `/private/var`) belonged to no package — no completion about the buffer's types, diagnostics published under `/private/var/…` that matched no open buffer. Turbo MoonBit's suite, green on Linux, failed both ways on its first run on a Mac; reproduced here with `TMPDIR` under a symlink, and with an LSP probe. Falsified: `TestDiagnosticsPublishedUnderTheRealPathReachAFileOpenedThroughALink` fails on the old `pathKey`. Rejected: fixing it in the editor's test by resolving `t.TempDir()` — that would hide the defect from every user whose files sit under a link. | 128 | - **Paths are canonical, not merely absolute (2026-09-19, v1.0.1).** `lsp.CanonicalPath` — `filepath.Abs` then `filepath.EvalSymlinks`, falling back to the deepest existing directory for a file not yet on disk — is what `lsp.PathToURI` sends the server, what `app.pathKey` keys documents and diagnostics by, and what `app.windowFor` compares when a location the server sent back has to land on a window already open. Reason: moon-lsp canonicalises a package's files, so a document announced as `/var/folders/…/main.mbt` (macOS: `/var` → `/private/var`) belonged to no package — no completion about the buffer's types, diagnostics published under `/private/var/…` that matched no open buffer. Turbo MoonBit's suite, green on Linux, failed both ways on its first run on a Mac; reproduced here with `TMPDIR` under a symlink, and with an LSP probe. Falsified: `TestDiagnosticsPublishedUnderTheRealPathReachAFileOpenedThroughALink` fails on the old `pathKey`. Rejected: fixing it in the editor's test by resolving `t.TempDir()` — that would hide the defect from every user whose files sit under a link. |
| 129 | 129 | ||
| 130 | +- **A save that creates a file says so to the server (2026-09-19, v1.0.2).** `lsp.Client.FileCreated` sends `workspace/didChangeWatchedFiles` (type 1); `app.announceSaved` sends it after the document's `didOpen`/`didSave` whenever the save created the file (`fileExists` asked before the write, in both `save` and `writeQuietly`). Reason: moon-lsp lists a package's files from the directory, so a `.mbt` saved for the first time — `turbo-moonbit new.mbt`, type, save — was known as a document and never diagnosed. Verified at the protocol level (every order of didOpen/didSave/didChangeWatchedFiles gets the file diagnosed; nothing does without it) and by Turbo MoonBit's `TestAFileCreatedInTheEditorIsDiagnosedFromItsFirstSaveWithRealMoonLSP`, which fails against v1.0.1 and passes against this. Sent unasked (moon-lsp registers no watcher); harmless to the fake and, by the spec, to any server. Only `Created` is ever sent: Save As leaves the old file on disk, so no `Deleted`. | ||
| 131 | + | ||
| 130 | ## Build, test, run | 132 | ## Build, test, run |
| 131 | 133 | ||
| 132 | ```bash | 134 | ```bash |
| @@ -161,7 +163,8 @@ python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | |||
| 161 | ## State as of 2026-09-19 | 163 | ## State as of 2026-09-19 |
| 162 | 164 | ||
| 163 | - **v1.0.0 is released** under the new module path `rickub.com/turbo-editors/turbo-core`, by `01-release.tag.sh` and the Release workflow; turbo-go v1.0.0 is published against it and the five other editors are re-pinned to it, waiting for their first Rickub release. | 165 | - **v1.0.0 is released** under the new module path `rickub.com/turbo-editors/turbo-core`, by `01-release.tag.sh` and the Release workflow; turbo-go v1.0.0 is published against it and the five other editors are re-pinned to it, waiting for their first Rickub release. |
| 164 | -- **v1.0.1 is ready to tag** (`release.env` says so): the canonical-path fix above, `lsp.CanonicalPath` exported, three new tests (`lsp` ×2, `app` ×1), `app/fakelsp_test.go` records the last `didOpen`. Every editor should re-pin to it before releasing; Turbo MoonBit *must*, or its suite fails on macOS. | 166 | +- **v1.0.1 is released**: the canonical-path fix (`lsp.CanonicalPath`, `pathKey`, `windowFor`). turbo-moonbit is re-pinned to it. |
| 167 | +- **v1.0.2 is ready to tag** (`release.env` says so): `FileCreated` above; four new tests (`lsp` ×1 in the notification-order test, `app` ×3 in `save_test.go`), the fake server records the last `didOpen` and the last file event and the order of every method. Turbo MoonBit's suite needs it (its new first-save test fails on v1.0.1); every editor should re-pin before releasing. | ||
| 165 | 168 | ||
| 166 | ## Not yet established | 169 | ## Not yet established |
| 167 | 170 | ||
modified
app/README.md +1 -1 | @@ -191,4 +191,4 @@ Absolute alone was not enough. moon-lsp canonicalises the files of a package, so | ||
| 191 | 191 | |
| 192 | 192 | A window that starts Untitled is skipped by every `didOpen`: it has no path to announce. The moment Save As gave it one used to send only `didSave` — and a server ignores `didChange` and `didSave` for a document it was never told is open, so that window had no completion, hover or diagnostics until the editor was restarted and the file reopened under its name. Found by a user whose first launch "had no LSP" until they saved, quit and relaunched. |
| 193 | 193 | |
| 194 | -`announceSaved` is the fix, in `afterSave` so both save paths get it: a save of a path the server `Knows` reports the write, and a save of one it does not announces the document. And because Save As can also **rename**, `save` remembers the buffer's previous path before `SaveAs` rewrites it, and `renamed` — compared through `pathKey`, so a change of spelling is not a rename — closes the old document on the server. Without that close a ghost stays open there for as long as the editor runs, keeping its diagnostics and shadowing the file if it is ever reopened. | |
| 194 | +`announceSaved` is the fix, in `afterSave` so both save paths get it: a save of a path the server `Knows` reports the write, and a save of one it does not announces the document. A save that **created** the file — `save` and `writeQuietly` both ask `fileExists` before writing — also sends `workspace/didChangeWatchedFiles` with the file as created, after the document has been announced. To moon-lsp a document being open and a file existing are two facts, and it works a package's files out from the directory: a `.mbt` saved for the first time got no diagnostics however loudly the document had been announced, until told the file is there. Found on macOS, the first time Turbo MoonBit's suite ran there (where moon-lsp evidently notices new files by itself, and a test pinning the old limit went red); confirmed at the protocol level on Linux, where the notification is the only thing that makes the difference. And because Save As can also **rename**, `save` remembers the buffer's previous path before `SaveAs` rewrites it, and `renamed` — compared through `pathKey`, so a change of spelling is not a rename — closes the old document on the server. Without that close a ghost stays open there for as long as the editor runs, keeping its diagnostics and shadowing the file if it is ever reopened. | |
| @@ -191,4 +191,4 @@ Absolute alone was not enough. moon-lsp canonicalises the files of a package, so | |||
| 191 | 191 | ||
| 192 | A window that starts Untitled is skipped by every `didOpen`: it has no path to announce. The moment Save As gave it one used to send only `didSave` — and a server ignores `didChange` and `didSave` for a document it was never told is open, so that window had no completion, hover or diagnostics until the editor was restarted and the file reopened under its name. Found by a user whose first launch "had no LSP" until they saved, quit and relaunched. | 192 | A window that starts Untitled is skipped by every `didOpen`: it has no path to announce. The moment Save As gave it one used to send only `didSave` — and a server ignores `didChange` and `didSave` for a document it was never told is open, so that window had no completion, hover or diagnostics until the editor was restarted and the file reopened under its name. Found by a user whose first launch "had no LSP" until they saved, quit and relaunched. |
| 193 | 193 | ||
| 194 | -`announceSaved` is the fix, in `afterSave` so both save paths get it: a save of a path the server `Knows` reports the write, and a save of one it does not announces the document. And because Save As can also **rename**, `save` remembers the buffer's previous path before `SaveAs` rewrites it, and `renamed` — compared through `pathKey`, so a change of spelling is not a rename — closes the old document on the server. Without that close a ghost stays open there for as long as the editor runs, keeping its diagnostics and shadowing the file if it is ever reopened. | 194 | +`announceSaved` is the fix, in `afterSave` so both save paths get it: a save of a path the server `Knows` reports the write, and a save of one it does not announces the document. A save that **created** the file — `save` and `writeQuietly` both ask `fileExists` before writing — also sends `workspace/didChangeWatchedFiles` with the file as created, after the document has been announced. To moon-lsp a document being open and a file existing are two facts, and it works a package's files out from the directory: a `.mbt` saved for the first time got no diagnostics however loudly the document had been announced, until told the file is there. Found on macOS, the first time Turbo MoonBit's suite ran there (where moon-lsp evidently notices new files by itself, and a test pinning the old limit went red); confirmed at the protocol level on Linux, where the notification is the only thing that makes the difference. And because Save As can also **rename**, `save` remembers the buffer's previous path before `SaveAs` rewrites it, and `renamed` — compared through `pathKey`, so a change of spelling is not a rename — closes the old document on the server. Without that close a ghost stays open there for as long as the editor runs, keeping its diagnostics and shadowing the file if it is ever reopened. |
modified
app/actions_file.go +26 -6 | @@ -4,6 +4,7 @@ package app | ||
| 4 | 4 | |
| 5 | 5 | import ( |
| 6 | 6 | "fmt" |
| 7 | + "os" | |
| 7 | 8 | "path/filepath" |
| 8 | 9 | |
| 9 | 10 | "rickub.com/turbo-editors/turbo-core/buffer" |
| @@ -169,6 +170,7 @@ func (a *App) save(view *editor.View, path string) { | ||
| 169 | 170 | // Remembered before the write, because SaveAs rewrites the buffer's path; |
| 170 | 171 | // it is the only record of which document the server had open until now. |
| 171 | 172 | previous := view.Buffer().Path() |
| 173 | + created := !fileExists(path) | |
| 172 | 174 | if err := view.Buffer().SaveAs(path); err != nil { |
| 173 | 175 | a.ShowMessage("Cannot save", err.Error()) |
| 174 | 176 | return |
| @@ -176,7 +178,14 @@ func (a *App) save(view *editor.View, path string) { | ||
| 176 | 178 | if renamed(previous, path) { |
| 177 | 179 | a.language.DidClose(previous) |
| 178 | 180 | } |
| 179 | - a.afterSave(view, path) | |
| 181 | + a.afterSave(view, path, created) | |
| 182 | +} | |
| 183 | + | |
| 184 | +// fileExists reports whether there is already a file at a path — asked before | |
| 185 | +// a write, so that afterwards the save knows whether it created the file. | |
| 186 | +func fileExists(path string) bool { | |
| 187 | + _, err := os.Stat(path) | |
| 188 | + return err == nil | |
| 180 | 189 | } |
| 181 | 190 | |
| 182 | 191 | // renamed reports whether a save gave the buffer a genuinely different file, |
| @@ -198,12 +207,12 @@ func renamed(previous, path string) bool { | ||
| 198 | 207 | // so that a step added to one cannot go missing from the other — re-reading the |
| 199 | 208 | // project's settings is exactly such a step, and would have been missing from |
| 200 | 209 | // autosave. |
| 201 | -func (a *App) afterSave(view *editor.View, path string) { | |
| 210 | +func (a *App) afterSave(view *editor.View, path string, created bool) { | |
| 202 | 211 | view.RefreshSyntax() |
| 203 | 212 | if window := a.windowOf(view); window != nil { |
| 204 | 213 | window.SetTitle(windowTitle(view.Buffer())) |
| 205 | 214 | } |
| 206 | - a.announceSaved(path, view.Buffer().Text()) | |
| 215 | + a.announceSaved(path, view.Buffer().Text(), created) | |
| 207 | 216 | a.refreshTree() |
| 208 | 217 | |
| 209 | 218 | // The message comes before the settings are re-read, so that re-reading can |
| @@ -223,12 +232,23 @@ func (a *App) afterSave(view *editor.View, path string) { | ||
| 223 | 232 | // window without completion, hover or diagnostics until the editor is |
| 224 | 233 | // restarted and the file is opened with its name — which is exactly how the |
| 225 | 234 | // defect was found. |
| 226 | -func (a *App) announceSaved(path, text string) { | |
| 235 | +// | |
| 236 | +// A save that created the file also says so, after the document has been | |
| 237 | +// announced: to a server that works a package's files out from the directory | |
| 238 | +// — moon-lsp — a document being open and a file existing are different facts, | |
| 239 | +// and it diagnoses a file saved for the first time only once told the file is | |
| 240 | +// there. Whether the document was already known does not enter into it: a | |
| 241 | +// buffer opened as `turbo-moonbit new.mbt` was announced long before anything | |
| 242 | +// was on disk. | |
| 243 | +func (a *App) announceSaved(path, text string, created bool) { | |
| 227 | 244 | if a.language.Knows(path) { |
| 228 | 245 | a.language.DidSave(path, text) |
| 229 | - return | |
| 246 | + } else { | |
| 247 | + a.language.DidOpen(path, text) | |
| 248 | + } | |
| 249 | + if created { | |
| 250 | + a.language.FileCreated(path) | |
| 230 | 251 | } |
| 231 | - a.language.DidOpen(path, text) | |
| 232 | 252 | } |
| 233 | 253 | |
| 234 | 254 | // windowOf returns the window holding a view. |
| @@ -4,6 +4,7 @@ package app | |||
| 4 | 4 | ||
| 5 | import ( | 5 | import ( |
| 6 | "fmt" | 6 | "fmt" |
| 7 | + "os" | ||
| 7 | "path/filepath" | 8 | "path/filepath" |
| 8 | 9 | ||
| 9 | "rickub.com/turbo-editors/turbo-core/buffer" | 10 | "rickub.com/turbo-editors/turbo-core/buffer" |
| @@ -169,6 +170,7 @@ func (a *App) save(view *editor.View, path string) { | |||
| 169 | // Remembered before the write, because SaveAs rewrites the buffer's path; | 170 | // Remembered before the write, because SaveAs rewrites the buffer's path; |
| 170 | // it is the only record of which document the server had open until now. | 171 | // it is the only record of which document the server had open until now. |
| 171 | previous := view.Buffer().Path() | 172 | previous := view.Buffer().Path() |
| 173 | + created := !fileExists(path) | ||
| 172 | if err := view.Buffer().SaveAs(path); err != nil { | 174 | if err := view.Buffer().SaveAs(path); err != nil { |
| 173 | a.ShowMessage("Cannot save", err.Error()) | 175 | a.ShowMessage("Cannot save", err.Error()) |
| 174 | return | 176 | return |
| @@ -176,7 +178,14 @@ func (a *App) save(view *editor.View, path string) { | |||
| 176 | if renamed(previous, path) { | 178 | if renamed(previous, path) { |
| 177 | a.language.DidClose(previous) | 179 | a.language.DidClose(previous) |
| 178 | } | 180 | } |
| 179 | - a.afterSave(view, path) | 181 | + a.afterSave(view, path, created) |
| 182 | +} | ||
| 183 | + | ||
| 184 | +// fileExists reports whether there is already a file at a path — asked before | ||
| 185 | +// a write, so that afterwards the save knows whether it created the file. | ||
| 186 | +func fileExists(path string) bool { | ||
| 187 | + _, err := os.Stat(path) | ||
| 188 | + return err == nil | ||
| 180 | } | 189 | } |
| 181 | 190 | ||
| 182 | // renamed reports whether a save gave the buffer a genuinely different file, | 191 | // renamed reports whether a save gave the buffer a genuinely different file, |
| @@ -198,12 +207,12 @@ func renamed(previous, path string) bool { | |||
| 198 | // so that a step added to one cannot go missing from the other — re-reading the | 207 | // so that a step added to one cannot go missing from the other — re-reading the |
| 199 | // project's settings is exactly such a step, and would have been missing from | 208 | // project's settings is exactly such a step, and would have been missing from |
| 200 | // autosave. | 209 | // autosave. |
| 201 | -func (a *App) afterSave(view *editor.View, path string) { | 210 | +func (a *App) afterSave(view *editor.View, path string, created bool) { |
| 202 | view.RefreshSyntax() | 211 | view.RefreshSyntax() |
| 203 | if window := a.windowOf(view); window != nil { | 212 | if window := a.windowOf(view); window != nil { |
| 204 | window.SetTitle(windowTitle(view.Buffer())) | 213 | window.SetTitle(windowTitle(view.Buffer())) |
| 205 | } | 214 | } |
| 206 | - a.announceSaved(path, view.Buffer().Text()) | 215 | + a.announceSaved(path, view.Buffer().Text(), created) |
| 207 | a.refreshTree() | 216 | a.refreshTree() |
| 208 | 217 | ||
| 209 | // The message comes before the settings are re-read, so that re-reading can | 218 | // The message comes before the settings are re-read, so that re-reading can |
| @@ -223,12 +232,23 @@ func (a *App) afterSave(view *editor.View, path string) { | |||
| 223 | // window without completion, hover or diagnostics until the editor is | 232 | // window without completion, hover or diagnostics until the editor is |
| 224 | // restarted and the file is opened with its name — which is exactly how the | 233 | // restarted and the file is opened with its name — which is exactly how the |
| 225 | // defect was found. | 234 | // defect was found. |
| 226 | -func (a *App) announceSaved(path, text string) { | 235 | +// |
| 236 | +// A save that created the file also says so, after the document has been | ||
| 237 | +// announced: to a server that works a package's files out from the directory | ||
| 238 | +// — moon-lsp — a document being open and a file existing are different facts, | ||
| 239 | +// and it diagnoses a file saved for the first time only once told the file is | ||
| 240 | +// there. Whether the document was already known does not enter into it: a | ||
| 241 | +// buffer opened as `turbo-moonbit new.mbt` was announced long before anything | ||
| 242 | +// was on disk. | ||
| 243 | +func (a *App) announceSaved(path, text string, created bool) { | ||
| 227 | if a.language.Knows(path) { | 244 | if a.language.Knows(path) { |
| 228 | a.language.DidSave(path, text) | 245 | a.language.DidSave(path, text) |
| 229 | - return | 246 | + } else { |
| 247 | + a.language.DidOpen(path, text) | ||
| 248 | + } | ||
| 249 | + if created { | ||
| 250 | + a.language.FileCreated(path) | ||
| 230 | } | 251 | } |
| 231 | - a.language.DidOpen(path, text) | ||
| 232 | } | 252 | } |
| 233 | 253 | ||
| 234 | // windowOf returns the window holding a view. | 254 | // windowOf returns the window holding a view. |
modified
app/autosave.go +2 -1 | @@ -120,12 +120,13 @@ func (a *App) autosaveOne(view *editor.View) { | ||
| 120 | 120 | // than the problem it describes. |
| 121 | 121 | func (a *App) writeQuietly(view *editor.View) bool { |
| 122 | 122 | path := view.Buffer().Path() |
| 123 | + created := !fileExists(path) | |
| 123 | 124 | if err := view.Buffer().SaveAs(path); err != nil { |
| 124 | 125 | a.Message("Cannot save " + filepath.Base(path) + ": " + err.Error()) |
| 125 | 126 | return false |
| 126 | 127 | } |
| 127 | 128 | |
| 128 | - a.afterSave(view, path) | |
| 129 | + a.afterSave(view, path, created) | |
| 129 | 130 | return true |
| 130 | 131 | } |
| 131 | 132 | |
| @@ -120,12 +120,13 @@ func (a *App) autosaveOne(view *editor.View) { | |||
| 120 | // than the problem it describes. | 120 | // than the problem it describes. |
| 121 | func (a *App) writeQuietly(view *editor.View) bool { | 121 | func (a *App) writeQuietly(view *editor.View) bool { |
| 122 | path := view.Buffer().Path() | 122 | path := view.Buffer().Path() |
| 123 | + created := !fileExists(path) | ||
| 123 | if err := view.Buffer().SaveAs(path); err != nil { | 124 | if err := view.Buffer().SaveAs(path); err != nil { |
| 124 | a.Message("Cannot save " + filepath.Base(path) + ": " + err.Error()) | 125 | a.Message("Cannot save " + filepath.Base(path) + ": " + err.Error()) |
| 125 | return false | 126 | return false |
| 126 | } | 127 | } |
| 127 | 128 | ||
| 128 | - a.afterSave(view, path) | 129 | + a.afterSave(view, path, created) |
| 129 | return true | 130 | return true |
| 130 | } | 131 | } |
| 131 | 132 | ||
modified
app/fakelsp_test.go +26 -0 | @@ -23,8 +23,10 @@ type fakeLSP struct { | ||
| 23 | 23 | |
| 24 | 24 | mu sync.Mutex |
| 25 | 25 | seen map[string]int |
| 26 | + order []string // every method, in the order it arrived | |
| 26 | 27 | answers map[string]json.RawMessage |
| 27 | 28 | lastOpened json.RawMessage // the params of the last textDocument/didOpen |
| 29 | + lastFiles json.RawMessage // the params of the last workspace/didChangeWatchedFiles | |
| 28 | 30 | } |
| 29 | 31 | |
| 30 | 32 | // frame is one JSON-RPC message, reduced to what this fake needs to look at. |
| @@ -89,6 +91,11 @@ func (s *fakeLSP) serve() { | ||
| 89 | 91 | s.lastOpened = msg.Params |
| 90 | 92 | s.mu.Unlock() |
| 91 | 93 | } |
| 94 | + if msg.Method == "workspace/didChangeWatchedFiles" { | |
| 95 | + s.mu.Lock() | |
| 96 | + s.lastFiles = msg.Params | |
| 97 | + s.mu.Unlock() | |
| 98 | + } | |
| 92 | 99 | |
| 93 | 100 | if len(msg.ID) == 0 || msg.Method == "" { |
| 94 | 101 | continue // a notification, or an answer to something we asked |
| @@ -136,6 +143,14 @@ func (s *fakeLSP) record(method string) { | ||
| 136 | 143 | s.mu.Lock() |
| 137 | 144 | defer s.mu.Unlock() |
| 138 | 145 | s.seen[method]++ |
| 146 | + s.order = append(s.order, method) | |
| 147 | +} | |
| 148 | + | |
| 149 | +// methodsSeen returns every method the client has sent, in order. | |
| 150 | +func (s *fakeLSP) methodsSeen() []string { | |
| 151 | + s.mu.Lock() | |
| 152 | + defer s.mu.Unlock() | |
| 153 | + return append([]string(nil), s.order...) | |
| 139 | 154 | } |
| 140 | 155 | |
| 141 | 156 | // lastOpenedURI returns the URI the client announced in its most recent |
| @@ -154,6 +169,17 @@ func (s *fakeLSP) lastOpenedURI() string { | ||
| 154 | 169 | return params.TextDocument.URI |
| 155 | 170 | } |
| 156 | 171 | |
| 172 | +// lastFileEvents returns the changes the client reported in its most recent | |
| 173 | +// workspace/didChangeWatchedFiles. | |
| 174 | +func (s *fakeLSP) lastFileEvents() []lsp.FileEvent { | |
| 175 | + waitForMethodQuietly(s, "workspace/didChangeWatchedFiles") | |
| 176 | + s.mu.Lock() | |
| 177 | + defer s.mu.Unlock() | |
| 178 | + var params lsp.DidChangeWatchedFilesParams | |
| 179 | + _ = json.Unmarshal(s.lastFiles, ¶ms) | |
| 180 | + return params.Changes | |
| 181 | +} | |
| 182 | + | |
| 157 | 183 | // waitForMethodQuietly is waitForMethod for a caller that reports its own |
| 158 | 184 | // failure: it gives up after two seconds and returns. |
| 159 | 185 | func waitForMethodQuietly(server *fakeLSP, method string) { |
| @@ -23,8 +23,10 @@ type fakeLSP struct { | |||
| 23 | 23 | ||
| 24 | mu sync.Mutex | 24 | mu sync.Mutex |
| 25 | seen map[string]int | 25 | seen map[string]int |
| 26 | + order []string // every method, in the order it arrived | ||
| 26 | answers map[string]json.RawMessage | 27 | answers map[string]json.RawMessage |
| 27 | lastOpened json.RawMessage // the params of the last textDocument/didOpen | 28 | lastOpened json.RawMessage // the params of the last textDocument/didOpen |
| 29 | + lastFiles json.RawMessage // the params of the last workspace/didChangeWatchedFiles | ||
| 28 | } | 30 | } |
| 29 | 31 | ||
| 30 | // frame is one JSON-RPC message, reduced to what this fake needs to look at. | 32 | // frame is one JSON-RPC message, reduced to what this fake needs to look at. |
| @@ -89,6 +91,11 @@ func (s *fakeLSP) serve() { | |||
| 89 | s.lastOpened = msg.Params | 91 | s.lastOpened = msg.Params |
| 90 | s.mu.Unlock() | 92 | s.mu.Unlock() |
| 91 | } | 93 | } |
| 94 | + if msg.Method == "workspace/didChangeWatchedFiles" { | ||
| 95 | + s.mu.Lock() | ||
| 96 | + s.lastFiles = msg.Params | ||
| 97 | + s.mu.Unlock() | ||
| 98 | + } | ||
| 92 | 99 | ||
| 93 | if len(msg.ID) == 0 || msg.Method == "" { | 100 | if len(msg.ID) == 0 || msg.Method == "" { |
| 94 | continue // a notification, or an answer to something we asked | 101 | continue // a notification, or an answer to something we asked |
| @@ -136,6 +143,14 @@ func (s *fakeLSP) record(method string) { | |||
| 136 | s.mu.Lock() | 143 | s.mu.Lock() |
| 137 | defer s.mu.Unlock() | 144 | defer s.mu.Unlock() |
| 138 | s.seen[method]++ | 145 | s.seen[method]++ |
| 146 | + s.order = append(s.order, method) | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +// methodsSeen returns every method the client has sent, in order. | ||
| 150 | +func (s *fakeLSP) methodsSeen() []string { | ||
| 151 | + s.mu.Lock() | ||
| 152 | + defer s.mu.Unlock() | ||
| 153 | + return append([]string(nil), s.order...) | ||
| 139 | } | 154 | } |
| 140 | 155 | ||
| 141 | // lastOpenedURI returns the URI the client announced in its most recent | 156 | // lastOpenedURI returns the URI the client announced in its most recent |
| @@ -154,6 +169,17 @@ func (s *fakeLSP) lastOpenedURI() string { | |||
| 154 | return params.TextDocument.URI | 169 | return params.TextDocument.URI |
| 155 | } | 170 | } |
| 156 | 171 | ||
| 172 | +// lastFileEvents returns the changes the client reported in its most recent | ||
| 173 | +// workspace/didChangeWatchedFiles. | ||
| 174 | +func (s *fakeLSP) lastFileEvents() []lsp.FileEvent { | ||
| 175 | + waitForMethodQuietly(s, "workspace/didChangeWatchedFiles") | ||
| 176 | + s.mu.Lock() | ||
| 177 | + defer s.mu.Unlock() | ||
| 178 | + var params lsp.DidChangeWatchedFilesParams | ||
| 179 | + _ = json.Unmarshal(s.lastFiles, ¶ms) | ||
| 180 | + return params.Changes | ||
| 181 | +} | ||
| 182 | + | ||
| 157 | // waitForMethodQuietly is waitForMethod for a caller that reports its own | 183 | // waitForMethodQuietly is waitForMethod for a caller that reports its own |
| 158 | // failure: it gives up after two seconds and returns. | 184 | // failure: it gives up after two seconds and returns. |
| 159 | func waitForMethodQuietly(server *fakeLSP, method string) { | 185 | func waitForMethodQuietly(server *fakeLSP, method string) { |
modified
app/language.go +9 -0 | @@ -213,6 +213,15 @@ func (l *Language) DidSave(path, text string) { | ||
| 213 | 213 | } |
| 214 | 214 | } |
| 215 | 215 | |
| 216 | +// FileCreated tells the server a file has appeared on disk — what a save under | |
| 217 | +// a new name does, and what a server that lists a package's files from the | |
| 218 | +// directory needs to hear before it will diagnose the document. | |
| 219 | +func (l *Language) FileCreated(path string) { | |
| 220 | + if client := l.connected(); client != nil && path != "" { | |
| 221 | + _ = client.FileCreated(path) | |
| 222 | + } | |
| 223 | +} | |
| 224 | + | |
| 216 | 225 | // DidClose tells the server a file is no longer open, and forgets its |
| 217 | 226 | // diagnostics. |
| 218 | 227 | func (l *Language) DidClose(path string) { |
| @@ -213,6 +213,15 @@ func (l *Language) DidSave(path, text string) { | |||
| 213 | } | 213 | } |
| 214 | } | 214 | } |
| 215 | 215 | ||
| 216 | +// FileCreated tells the server a file has appeared on disk — what a save under | ||
| 217 | +// a new name does, and what a server that lists a package's files from the | ||
| 218 | +// directory needs to hear before it will diagnose the document. | ||
| 219 | +func (l *Language) FileCreated(path string) { | ||
| 220 | + if client := l.connected(); client != nil && path != "" { | ||
| 221 | + _ = client.FileCreated(path) | ||
| 222 | + } | ||
| 223 | +} | ||
| 224 | + | ||
| 216 | // DidClose tells the server a file is no longer open, and forgets its | 225 | // DidClose tells the server a file is no longer open, and forgets its |
| 217 | // diagnostics. | 226 | // diagnostics. |
| 218 | func (l *Language) DidClose(path string) { | 227 | func (l *Language) DidClose(path string) { |
modified
app/save_test.go +99 -0 | @@ -3,6 +3,9 @@ package app | ||
| 3 | 3 | import ( |
| 4 | 4 | "path/filepath" |
| 5 | 5 | "testing" |
| 6 | + "time" | |
| 7 | + | |
| 8 | + "rickub.com/turbo-editors/turbo-core/lsp" | |
| 6 | 9 | ) |
| 7 | 10 | |
| 8 | 11 | // These tests cover what saving says to the language server. The defect they |
| @@ -95,3 +98,99 @@ func TestSavingAKnownDocumentReportsTheWrite(t *testing.T) { | ||
| 95 | 98 | t.Errorf("didOpen was sent %d times; saving an open document must not re-announce it", got) |
| 96 | 99 | } |
| 97 | 100 | } |
| 101 | + | |
| 102 | +func TestSavingANewFileTellsTheServerTheFileExists(t *testing.T) { | |
| 103 | + // To moon-lsp a document being open and a file existing are two facts, | |
| 104 | + // and it works a package's files out from the directory: a .mbt saved for | |
| 105 | + // the first time got no diagnostics, however loudly the document had been | |
| 106 | + // announced, until the server was told the file is there. Found on macOS | |
| 107 | + // the first time Turbo MoonBit's suite ran there; confirmed at the | |
| 108 | + // protocol level — the same file is diagnosed the moment | |
| 109 | + // workspace/didChangeWatchedFiles names it. | |
| 110 | + project := t.TempDir() | |
| 111 | + t.Chdir(project) | |
| 112 | + a, _ := newTestApp(t) | |
| 113 | + client, server := newFakeLanguage(t, a) | |
| 114 | + connectLanguage(a, client) | |
| 115 | + a.Tick() | |
| 116 | + | |
| 117 | + a.NewFile() | |
| 118 | + typeText(a, "package main") | |
| 119 | + path := filepath.Join(project, "main.go") | |
| 120 | + a.save(a.ActiveView(), path) | |
| 121 | + | |
| 122 | + waitForMethod(t, server, "workspace/didChangeWatchedFiles") | |
| 123 | + events := server.lastFileEvents() | |
| 124 | + if len(events) != 1 || events[0].Type != lsp.FileCreated { | |
| 125 | + t.Fatalf("the server was told %+v, want one created file", events) | |
| 126 | + } | |
| 127 | + if want := lsp.PathToURI(path); events[0].URI != want { | |
| 128 | + t.Errorf("the created file is %q, want %q", events[0].URI, want) | |
| 129 | + } | |
| 130 | + // The document first, then the file: the order the fix was verified in. | |
| 131 | + methods := server.methodsSeen() | |
| 132 | + if open, created := firstIndexOf(methods, "textDocument/didOpen"), firstIndexOf(methods, "workspace/didChangeWatchedFiles"); open < 0 || created < open { | |
| 133 | + t.Errorf("the file was reported before the document was announced: %v", methods) | |
| 134 | + } | |
| 135 | +} | |
| 136 | + | |
| 137 | +func TestSavingAnExistingFileDoesNotClaimItWasCreated(t *testing.T) { | |
| 138 | + // A file that was already on disk is not news to the server, and a save | |
| 139 | + // that claimed otherwise would have moon-lsp rescan the package on every | |
| 140 | + // Ctrl-S. | |
| 141 | + project := t.TempDir() | |
| 142 | + t.Chdir(project) | |
| 143 | + t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) | |
| 144 | + a, _ := newTestApp(t) | |
| 145 | + client, server := newFakeLanguage(t, a) | |
| 146 | + connectLanguage(a, client) | |
| 147 | + path := filepath.Join(project, "main.go") | |
| 148 | + writeTestFile(t, path, "package main\n") | |
| 149 | + a.Open(path) | |
| 150 | + a.Tick() | |
| 151 | + | |
| 152 | + a.save(a.ActiveView(), path) | |
| 153 | + | |
| 154 | + waitForMethod(t, server, "textDocument/didSave") | |
| 155 | + if got := server.methodCount("workspace/didChangeWatchedFiles"); got != 0 { | |
| 156 | + t.Errorf("saving a file that already existed reported it created %d time(s)", got) | |
| 157 | + } | |
| 158 | +} | |
| 159 | + | |
| 160 | +func TestAutosaveOfANewlyNamedFileTellsTheServerTheFileExists(t *testing.T) { | |
| 161 | + // The other way a file gets written. `turbo-moonbit new.mbt` opens a | |
| 162 | + // buffer for a file that is not on disk yet; the first automatic save | |
| 163 | + // creates it, and the server has to hear that from this path too. | |
| 164 | + project := t.TempDir() | |
| 165 | + t.Chdir(project) | |
| 166 | + t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) | |
| 167 | + a, _ := newTestApp(t) | |
| 168 | + client, server := newFakeLanguage(t, a) | |
| 169 | + connectLanguage(a, client) | |
| 170 | + path := filepath.Join(project, "new.go") | |
| 171 | + a.Open(path) // not on disk: an empty buffer with that name | |
| 172 | + a.Tick() | |
| 173 | + a.SetAutosave(true, time.Millisecond) | |
| 174 | + | |
| 175 | + typeText(a, "package main") | |
| 176 | + deadline := time.Now().Add(2 * time.Second) | |
| 177 | + for server.methodCount("workspace/didChangeWatchedFiles") == 0 && time.Now().Before(deadline) { | |
| 178 | + time.Sleep(2 * time.Millisecond) | |
| 179 | + a.Tick() | |
| 180 | + } | |
| 181 | + | |
| 182 | + events := server.lastFileEvents() | |
| 183 | + if len(events) != 1 || events[0].Type != lsp.FileCreated || events[0].URI != lsp.PathToURI(path) { | |
| 184 | + t.Errorf("autosave reported %+v, want %s created", events, path) | |
| 185 | + } | |
| 186 | +} | |
| 187 | + | |
| 188 | +// firstIndexOf returns where a string first appears in a list, or -1. | |
| 189 | +func firstIndexOf(list []string, want string) int { | |
| 190 | + for i, s := range list { | |
| 191 | + if s == want { | |
| 192 | + return i | |
| 193 | + } | |
| 194 | + } | |
| 195 | + return -1 | |
| 196 | +} | |
| @@ -3,6 +3,9 @@ package app | |||
| 3 | import ( | 3 | import ( |
| 4 | "path/filepath" | 4 | "path/filepath" |
| 5 | "testing" | 5 | "testing" |
| 6 | + "time" | ||
| 7 | + | ||
| 8 | + "rickub.com/turbo-editors/turbo-core/lsp" | ||
| 6 | ) | 9 | ) |
| 7 | 10 | ||
| 8 | // These tests cover what saving says to the language server. The defect they | 11 | // These tests cover what saving says to the language server. The defect they |
| @@ -95,3 +98,99 @@ func TestSavingAKnownDocumentReportsTheWrite(t *testing.T) { | |||
| 95 | t.Errorf("didOpen was sent %d times; saving an open document must not re-announce it", got) | 98 | t.Errorf("didOpen was sent %d times; saving an open document must not re-announce it", got) |
| 96 | } | 99 | } |
| 97 | } | 100 | } |
| 101 | + | ||
| 102 | +func TestSavingANewFileTellsTheServerTheFileExists(t *testing.T) { | ||
| 103 | + // To moon-lsp a document being open and a file existing are two facts, | ||
| 104 | + // and it works a package's files out from the directory: a .mbt saved for | ||
| 105 | + // the first time got no diagnostics, however loudly the document had been | ||
| 106 | + // announced, until the server was told the file is there. Found on macOS | ||
| 107 | + // the first time Turbo MoonBit's suite ran there; confirmed at the | ||
| 108 | + // protocol level — the same file is diagnosed the moment | ||
| 109 | + // workspace/didChangeWatchedFiles names it. | ||
| 110 | + project := t.TempDir() | ||
| 111 | + t.Chdir(project) | ||
| 112 | + a, _ := newTestApp(t) | ||
| 113 | + client, server := newFakeLanguage(t, a) | ||
| 114 | + connectLanguage(a, client) | ||
| 115 | + a.Tick() | ||
| 116 | + | ||
| 117 | + a.NewFile() | ||
| 118 | + typeText(a, "package main") | ||
| 119 | + path := filepath.Join(project, "main.go") | ||
| 120 | + a.save(a.ActiveView(), path) | ||
| 121 | + | ||
| 122 | + waitForMethod(t, server, "workspace/didChangeWatchedFiles") | ||
| 123 | + events := server.lastFileEvents() | ||
| 124 | + if len(events) != 1 || events[0].Type != lsp.FileCreated { | ||
| 125 | + t.Fatalf("the server was told %+v, want one created file", events) | ||
| 126 | + } | ||
| 127 | + if want := lsp.PathToURI(path); events[0].URI != want { | ||
| 128 | + t.Errorf("the created file is %q, want %q", events[0].URI, want) | ||
| 129 | + } | ||
| 130 | + // The document first, then the file: the order the fix was verified in. | ||
| 131 | + methods := server.methodsSeen() | ||
| 132 | + if open, created := firstIndexOf(methods, "textDocument/didOpen"), firstIndexOf(methods, "workspace/didChangeWatchedFiles"); open < 0 || created < open { | ||
| 133 | + t.Errorf("the file was reported before the document was announced: %v", methods) | ||
| 134 | + } | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +func TestSavingAnExistingFileDoesNotClaimItWasCreated(t *testing.T) { | ||
| 138 | + // A file that was already on disk is not news to the server, and a save | ||
| 139 | + // that claimed otherwise would have moon-lsp rescan the package on every | ||
| 140 | + // Ctrl-S. | ||
| 141 | + project := t.TempDir() | ||
| 142 | + t.Chdir(project) | ||
| 143 | + t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) | ||
| 144 | + a, _ := newTestApp(t) | ||
| 145 | + client, server := newFakeLanguage(t, a) | ||
| 146 | + connectLanguage(a, client) | ||
| 147 | + path := filepath.Join(project, "main.go") | ||
| 148 | + writeTestFile(t, path, "package main\n") | ||
| 149 | + a.Open(path) | ||
| 150 | + a.Tick() | ||
| 151 | + | ||
| 152 | + a.save(a.ActiveView(), path) | ||
| 153 | + | ||
| 154 | + waitForMethod(t, server, "textDocument/didSave") | ||
| 155 | + if got := server.methodCount("workspace/didChangeWatchedFiles"); got != 0 { | ||
| 156 | + t.Errorf("saving a file that already existed reported it created %d time(s)", got) | ||
| 157 | + } | ||
| 158 | +} | ||
| 159 | + | ||
| 160 | +func TestAutosaveOfANewlyNamedFileTellsTheServerTheFileExists(t *testing.T) { | ||
| 161 | + // The other way a file gets written. `turbo-moonbit new.mbt` opens a | ||
| 162 | + // buffer for a file that is not on disk yet; the first automatic save | ||
| 163 | + // creates it, and the server has to hear that from this path too. | ||
| 164 | + project := t.TempDir() | ||
| 165 | + t.Chdir(project) | ||
| 166 | + t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) | ||
| 167 | + a, _ := newTestApp(t) | ||
| 168 | + client, server := newFakeLanguage(t, a) | ||
| 169 | + connectLanguage(a, client) | ||
| 170 | + path := filepath.Join(project, "new.go") | ||
| 171 | + a.Open(path) // not on disk: an empty buffer with that name | ||
| 172 | + a.Tick() | ||
| 173 | + a.SetAutosave(true, time.Millisecond) | ||
| 174 | + | ||
| 175 | + typeText(a, "package main") | ||
| 176 | + deadline := time.Now().Add(2 * time.Second) | ||
| 177 | + for server.methodCount("workspace/didChangeWatchedFiles") == 0 && time.Now().Before(deadline) { | ||
| 178 | + time.Sleep(2 * time.Millisecond) | ||
| 179 | + a.Tick() | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + events := server.lastFileEvents() | ||
| 183 | + if len(events) != 1 || events[0].Type != lsp.FileCreated || events[0].URI != lsp.PathToURI(path) { | ||
| 184 | + t.Errorf("autosave reported %+v, want %s created", events, path) | ||
| 185 | + } | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +// firstIndexOf returns where a string first appears in a list, or -1. | ||
| 189 | +func firstIndexOf(list []string, want string) int { | ||
| 190 | + for i, s := range list { | ||
| 191 | + if s == want { | ||
| 192 | + return i | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | + return -1 | ||
| 196 | +} | ||
modified
docs/en/how-to/talk-to-a-language-server.md +1 -1 | @@ -45,7 +45,7 @@ Start it **after** opening the files, not before. The editor announces the docum | ||
| 45 | 45 | |
| 46 | 46 | ### A file is created inside the editor |
| 47 | 47 | |
| 48 | -Nothing to do. A window that begins Untitled has no path, so the server hears nothing about it — until the first save, which announces the document rather than merely reporting a write. Completion, hover and diagnostics work in that window from the moment it has a name, with no restart. A Save As under a genuinely new name also closes the old document on the server, so no ghost stays open there. | |
| 48 | +Nothing to do. A window that begins Untitled has no path, so the server hears nothing about it — until the first save, which announces the document rather than merely reporting a write. Completion, hover and diagnostics work in that window from the moment it has a name, with no restart. The save that creates the file also tells the server a file appeared (`workspace/didChangeWatchedFiles`): a server that lists a package's files from the directory — moon-lsp — would otherwise know the document and still never diagnose it. A Save As under a genuinely new name also closes the old document on the server, so no ghost stays open there. | |
| 49 | 49 | |
| 50 | 50 | ### The server needs configuration |
| 51 | 51 | |
| @@ -45,7 +45,7 @@ Start it **after** opening the files, not before. The editor announces the docum | |||
| 45 | 45 | ||
| 46 | ### A file is created inside the editor | 46 | ### A file is created inside the editor |
| 47 | 47 | ||
| 48 | -Nothing to do. A window that begins Untitled has no path, so the server hears nothing about it — until the first save, which announces the document rather than merely reporting a write. Completion, hover and diagnostics work in that window from the moment it has a name, with no restart. A Save As under a genuinely new name also closes the old document on the server, so no ghost stays open there. | 48 | +Nothing to do. A window that begins Untitled has no path, so the server hears nothing about it — until the first save, which announces the document rather than merely reporting a write. Completion, hover and diagnostics work in that window from the moment it has a name, with no restart. The save that creates the file also tells the server a file appeared (`workspace/didChangeWatchedFiles`): a server that lists a package's files from the directory — moon-lsp — would otherwise know the document and still never diagnose it. A Save As under a genuinely new name also closes the old document on the server, so no ghost stays open there. |
| 49 | 49 | ||
| 50 | ### The server needs configuration | 50 | ### The server needs configuration |
| 51 | 51 | ||
modified
docs/en/reference/app.md +1 -1 | @@ -79,7 +79,7 @@ The settings file is also re-read **after every save that writes it**, from eith | ||
| 79 | 79 | | `FirstError(path string) (lsp.Diagnostic, bool)` | The first error-level diagnostic. | |
| 80 | 80 | | `AllDiagnostics() []FileDiagnostic` | Every problem for every file, sorted by file then line. | |
| 81 | 81 | |
| 82 | -Saving keeps the server's set of open documents true. Writing a file the server knows sends `didSave`; writing one it does not know — a window that began Untitled, saved for the first time — sends `didOpen`, so the document works from that save on. Save As under a genuinely different name also closes the old document on the server first; a mere change of spelling of the same file does not count as a rename. | |
| 82 | +Saving keeps the server's set of open documents true. Writing a file the server knows sends `didSave`; writing one it does not know — a window that began Untitled, saved for the first time — sends `didOpen`, so the document works from that save on. A save that creates the file also reports it created (`workspace/didChangeWatchedFiles`), for servers that list a package's files from the directory and would otherwise never diagnose it — moon-lsp. Save As under a genuinely different name also closes the old document on the server first; a mere change of spelling of the same file does not count as a rename. | |
| 83 | 83 | |
| 84 | 84 | ## What is on screen |
| 85 | 85 | |
| @@ -79,7 +79,7 @@ The settings file is also re-read **after every save that writes it**, from eith | |||
| 79 | | `FirstError(path string) (lsp.Diagnostic, bool)` | The first error-level diagnostic. | | 79 | | `FirstError(path string) (lsp.Diagnostic, bool)` | The first error-level diagnostic. | |
| 80 | | `AllDiagnostics() []FileDiagnostic` | Every problem for every file, sorted by file then line. | | 80 | | `AllDiagnostics() []FileDiagnostic` | Every problem for every file, sorted by file then line. | |
| 81 | 81 | ||
| 82 | -Saving keeps the server's set of open documents true. Writing a file the server knows sends `didSave`; writing one it does not know — a window that began Untitled, saved for the first time — sends `didOpen`, so the document works from that save on. Save As under a genuinely different name also closes the old document on the server first; a mere change of spelling of the same file does not count as a rename. | 82 | +Saving keeps the server's set of open documents true. Writing a file the server knows sends `didSave`; writing one it does not know — a window that began Untitled, saved for the first time — sends `didOpen`, so the document works from that save on. A save that creates the file also reports it created (`workspace/didChangeWatchedFiles`), for servers that list a package's files from the directory and would otherwise never diagnose it — moon-lsp. Save As under a genuinely different name also closes the old document on the server first; a mere change of spelling of the same file does not count as a rename. |
| 83 | 83 | ||
| 84 | ## What is on screen | 84 | ## What is on screen |
| 85 | 85 | ||
modified
docs/fr/how-to/talk-to-a-language-server.md +1 -1 | @@ -45,7 +45,7 @@ Démarrez-le **après** avoir ouvert les fichiers, pas avant. L'éditeur annonce | ||
| 45 | 45 | |
| 46 | 46 | ### Un fichier est créé dans l'éditeur |
| 47 | 47 | |
| 48 | -Rien à faire. Une fenêtre née sans titre n'a pas de chemin, donc le serveur n'en entend pas parler — jusqu'à la première sauvegarde, qui annonce le document au lieu de simplement signaler une écriture. La complétion, le survol et les diagnostics fonctionnent dans cette fenêtre dès qu'elle a un nom, sans redémarrage. Un « Enregistrer sous » vers un nom réellement nouveau ferme aussi l'ancien document côté serveur, pour qu'aucun fantôme n'y reste ouvert. | |
| 48 | +Rien à faire. Une fenêtre née sans titre n'a pas de chemin, donc le serveur n'en entend pas parler — jusqu'à la première sauvegarde, qui annonce le document au lieu de simplement signaler une écriture. La complétion, le survol et les diagnostics fonctionnent dans cette fenêtre dès qu'elle a un nom, sans redémarrage. La sauvegarde qui crée le fichier dit aussi au serveur qu'un fichier est apparu (`workspace/didChangeWatchedFiles`) : un serveur qui établit la liste des fichiers d'un paquet depuis le répertoire — moon-lsp — connaîtrait sinon le document sans jamais le diagnostiquer. Un « Enregistrer sous » vers un nom réellement nouveau ferme aussi l'ancien document côté serveur, pour qu'aucun fantôme n'y reste ouvert. | |
| 49 | 49 | |
| 50 | 50 | ### Le serveur a besoin d'une configuration |
| 51 | 51 | |
| @@ -45,7 +45,7 @@ Démarrez-le **après** avoir ouvert les fichiers, pas avant. L'éditeur annonce | |||
| 45 | 45 | ||
| 46 | ### Un fichier est créé dans l'éditeur | 46 | ### Un fichier est créé dans l'éditeur |
| 47 | 47 | ||
| 48 | -Rien à faire. Une fenêtre née sans titre n'a pas de chemin, donc le serveur n'en entend pas parler — jusqu'à la première sauvegarde, qui annonce le document au lieu de simplement signaler une écriture. La complétion, le survol et les diagnostics fonctionnent dans cette fenêtre dès qu'elle a un nom, sans redémarrage. Un « Enregistrer sous » vers un nom réellement nouveau ferme aussi l'ancien document côté serveur, pour qu'aucun fantôme n'y reste ouvert. | 48 | +Rien à faire. Une fenêtre née sans titre n'a pas de chemin, donc le serveur n'en entend pas parler — jusqu'à la première sauvegarde, qui annonce le document au lieu de simplement signaler une écriture. La complétion, le survol et les diagnostics fonctionnent dans cette fenêtre dès qu'elle a un nom, sans redémarrage. La sauvegarde qui crée le fichier dit aussi au serveur qu'un fichier est apparu (`workspace/didChangeWatchedFiles`) : un serveur qui établit la liste des fichiers d'un paquet depuis le répertoire — moon-lsp — connaîtrait sinon le document sans jamais le diagnostiquer. Un « Enregistrer sous » vers un nom réellement nouveau ferme aussi l'ancien document côté serveur, pour qu'aucun fantôme n'y reste ouvert. |
| 49 | 49 | ||
| 50 | ### Le serveur a besoin d'une configuration | 50 | ### Le serveur a besoin d'une configuration |
| 51 | 51 | ||
modified
docs/fr/reference/app.md +1 -1 | @@ -79,7 +79,7 @@ Le fichier de réglages est aussi relu **après chaque enregistrement qui l'écr | ||
| 79 | 79 | | `FirstError(path string) (lsp.Diagnostic, bool)` | Le premier diagnostic de niveau erreur. | |
| 80 | 80 | | `AllDiagnostics() []FileDiagnostic` | Tous les problèmes de tous les fichiers, triés par fichier puis par ligne. | |
| 81 | 81 | |
| 82 | -Sauvegarder maintient exact l'ensemble des documents que le serveur croit ouverts. Écrire un fichier que le serveur connaît envoie `didSave` ; écrire un fichier qu'il ne connaît pas — une fenêtre née sans titre, sauvée pour la première fois — envoie `didOpen`, si bien que le document fonctionne dès cette sauvegarde. Un « Enregistrer sous » vers un nom réellement différent ferme d'abord l'ancien document côté serveur ; un simple changement d'orthographe du même fichier ne compte pas comme un renommage. | |
| 82 | +Sauvegarder maintient exact l'ensemble des documents que le serveur croit ouverts. Écrire un fichier que le serveur connaît envoie `didSave` ; écrire un fichier qu'il ne connaît pas — une fenêtre née sans titre, sauvée pour la première fois — envoie `didOpen`, si bien que le document fonctionne dès cette sauvegarde. Une sauvegarde qui crée le fichier le signale aussi comme créé (`workspace/didChangeWatchedFiles`), pour les serveurs qui établissent la liste des fichiers d'un paquet depuis le répertoire et ne le diagnostiqueraient jamais sinon — moon-lsp. Un « Enregistrer sous » vers un nom réellement différent ferme d'abord l'ancien document côté serveur ; un simple changement d'orthographe du même fichier ne compte pas comme un renommage. | |
| 83 | 83 | |
| 84 | 84 | ## Ce qui est à l'écran |
| 85 | 85 | |
| @@ -79,7 +79,7 @@ Le fichier de réglages est aussi relu **après chaque enregistrement qui l'écr | |||
| 79 | | `FirstError(path string) (lsp.Diagnostic, bool)` | Le premier diagnostic de niveau erreur. | | 79 | | `FirstError(path string) (lsp.Diagnostic, bool)` | Le premier diagnostic de niveau erreur. | |
| 80 | | `AllDiagnostics() []FileDiagnostic` | Tous les problèmes de tous les fichiers, triés par fichier puis par ligne. | | 80 | | `AllDiagnostics() []FileDiagnostic` | Tous les problèmes de tous les fichiers, triés par fichier puis par ligne. | |
| 81 | 81 | ||
| 82 | -Sauvegarder maintient exact l'ensemble des documents que le serveur croit ouverts. Écrire un fichier que le serveur connaît envoie `didSave` ; écrire un fichier qu'il ne connaît pas — une fenêtre née sans titre, sauvée pour la première fois — envoie `didOpen`, si bien que le document fonctionne dès cette sauvegarde. Un « Enregistrer sous » vers un nom réellement différent ferme d'abord l'ancien document côté serveur ; un simple changement d'orthographe du même fichier ne compte pas comme un renommage. | 82 | +Sauvegarder maintient exact l'ensemble des documents que le serveur croit ouverts. Écrire un fichier que le serveur connaît envoie `didSave` ; écrire un fichier qu'il ne connaît pas — une fenêtre née sans titre, sauvée pour la première fois — envoie `didOpen`, si bien que le document fonctionne dès cette sauvegarde. Une sauvegarde qui crée le fichier le signale aussi comme créé (`workspace/didChangeWatchedFiles`), pour les serveurs qui établissent la liste des fichiers d'un paquet depuis le répertoire et ne le diagnostiqueraient jamais sinon — moon-lsp. Un « Enregistrer sous » vers un nom réellement différent ferme d'abord l'ancien document côté serveur ; un simple changement d'orthographe du même fichier ne compte pas comme un renommage. |
| 83 | 83 | ||
| 84 | ## Ce qui est à l'écran | 84 | ## Ce qui est à l'écran |
| 85 | 85 | ||
modified
lsp/README.md +1 -0 | @@ -57,6 +57,7 @@ Every request is bounded: `RequestTimeout` (3 s) for the ordinary ones, `Initial | ||
| 57 | 57 | | `NewClient(stream, root) *Client` | A client over any stream — what the tests use | |
| 58 | 58 | | `(*Client) Initialize / Run / Ready` | Handshake and read loop | |
| 59 | 59 | | `(*Client) DidOpen / DidChange / DidSave / DidClose` | Document synchronisation, whole-document | |
| 60 | +| `(*Client) FileCreated` | `workspace/didChangeWatchedFiles` for a file that appeared on disk — a document being open and a file existing are two facts to moon-lsp, which diagnoses a new file only once told of the second | | |
| 60 | 61 | | `(*Client) Complete / Hover` | The two requests answered with something to read | |
| 61 | 62 | | `(*Client) Definition / TypeDefinition / Implementation / References` | The four answered with places in the code. One decoder serves all four, because the protocol lets a server answer a single location as a bare object rather than an array of one — and several do. | |
| 62 | 63 | | `(*Client) DocumentSymbols / WorkspaceSymbols` | What a file declares, and what a project holds matching a query | |
| @@ -57,6 +57,7 @@ Every request is bounded: `RequestTimeout` (3 s) for the ordinary ones, `Initial | |||
| 57 | | `NewClient(stream, root) *Client` | A client over any stream — what the tests use | | 57 | | `NewClient(stream, root) *Client` | A client over any stream — what the tests use | |
| 58 | | `(*Client) Initialize / Run / Ready` | Handshake and read loop | | 58 | | `(*Client) Initialize / Run / Ready` | Handshake and read loop | |
| 59 | | `(*Client) DidOpen / DidChange / DidSave / DidClose` | Document synchronisation, whole-document | | 59 | | `(*Client) DidOpen / DidChange / DidSave / DidClose` | Document synchronisation, whole-document | |
| 60 | +| `(*Client) FileCreated` | `workspace/didChangeWatchedFiles` for a file that appeared on disk — a document being open and a file existing are two facts to moon-lsp, which diagnoses a new file only once told of the second | | ||
| 60 | | `(*Client) Complete / Hover` | The two requests answered with something to read | | 61 | | `(*Client) Complete / Hover` | The two requests answered with something to read | |
| 61 | | `(*Client) Definition / TypeDefinition / Implementation / References` | The four answered with places in the code. One decoder serves all four, because the protocol lets a server answer a single location as a bare object rather than an array of one — and several do. | | 62 | | `(*Client) Definition / TypeDefinition / Implementation / References` | The four answered with places in the code. One decoder serves all four, because the protocol lets a server answer a single location as a bare object rather than an array of one — and several do. | |
| 62 | | `(*Client) DocumentSymbols / WorkspaceSymbols` | What a file declares, and what a project holds matching a query | | 63 | | `(*Client) DocumentSymbols / WorkspaceSymbols` | What a file declares, and what a project holds matching a query | |
modified
lsp/client.go +15 -0 | @@ -174,6 +174,21 @@ func (c *Client) DidSave(path, text string) error { | ||
| 174 | 174 | }) |
| 175 | 175 | } |
| 176 | 176 | |
| 177 | +// FileCreated tells the server a file has appeared on disk. | |
| 178 | +// | |
| 179 | +// A document being open (DidOpen) and a file existing are two different facts | |
| 180 | +// to a server that works out a package's files from the directory: moon-lsp | |
| 181 | +// diagnoses a .mbt file saved for the first time only once this has been sent, | |
| 182 | +// however loudly the document was announced. The spec has clients send these | |
| 183 | +// for files the server asked to watch; sending one unasked is harmless to the | |
| 184 | +// servers this library has been driven against, and is the only way to tell a | |
| 185 | +// server that asks for nothing. | |
| 186 | +func (c *Client) FileCreated(path string) error { | |
| 187 | + return c.conn.Notify("workspace/didChangeWatchedFiles", DidChangeWatchedFilesParams{ | |
| 188 | + Changes: []FileEvent{{URI: PathToURI(path), Type: FileCreated}}, | |
| 189 | + }) | |
| 190 | +} | |
| 191 | + | |
| 177 | 192 | // DidClose tells the server a file is no longer being edited. |
| 178 | 193 | func (c *Client) DidClose(path string) error { |
| 179 | 194 | c.mu.Lock() |
| @@ -174,6 +174,21 @@ func (c *Client) DidSave(path, text string) error { | |||
| 174 | }) | 174 | }) |
| 175 | } | 175 | } |
| 176 | 176 | ||
| 177 | +// FileCreated tells the server a file has appeared on disk. | ||
| 178 | +// | ||
| 179 | +// A document being open (DidOpen) and a file existing are two different facts | ||
| 180 | +// to a server that works out a package's files from the directory: moon-lsp | ||
| 181 | +// diagnoses a .mbt file saved for the first time only once this has been sent, | ||
| 182 | +// however loudly the document was announced. The spec has clients send these | ||
| 183 | +// for files the server asked to watch; sending one unasked is harmless to the | ||
| 184 | +// servers this library has been driven against, and is the only way to tell a | ||
| 185 | +// server that asks for nothing. | ||
| 186 | +func (c *Client) FileCreated(path string) error { | ||
| 187 | + return c.conn.Notify("workspace/didChangeWatchedFiles", DidChangeWatchedFilesParams{ | ||
| 188 | + Changes: []FileEvent{{URI: PathToURI(path), Type: FileCreated}}, | ||
| 189 | + }) | ||
| 190 | +} | ||
| 191 | + | ||
| 177 | // DidClose tells the server a file is no longer being edited. | 192 | // DidClose tells the server a file is no longer being edited. |
| 178 | func (c *Client) DidClose(path string) error { | 193 | func (c *Client) DidClose(path string) error { |
| 179 | c.mu.Lock() | 194 | c.mu.Lock() |
modified
lsp/lsp_test.go +4 -1 | @@ -395,6 +395,9 @@ func TestDocumentSynchronisationSendsTheRightNotifications(t *testing.T) { | ||
| 395 | 395 | if err := client.DidSave("main.go", "package main\n"); err != nil { |
| 396 | 396 | t.Fatalf("DidSave() error = %v", err) |
| 397 | 397 | } |
| 398 | + if err := client.FileCreated("main.go"); err != nil { | |
| 399 | + t.Fatalf("FileCreated() error = %v", err) | |
| 400 | + } | |
| 398 | 401 | if err := client.DidClose("main.go"); err != nil { |
| 399 | 402 | t.Fatalf("DidClose() error = %v", err) |
| 400 | 403 | } |
| @@ -402,7 +405,7 @@ func TestDocumentSynchronisationSendsTheRightNotifications(t *testing.T) { | ||
| 402 | 405 | waitForMethod(t, server, "textDocument/didClose") |
| 403 | 406 | want := []string{ |
| 404 | 407 | "textDocument/didOpen", "textDocument/didChange", |
| 405 | - "textDocument/didSave", "textDocument/didClose", | |
| 408 | + "textDocument/didSave", "workspace/didChangeWatchedFiles", "textDocument/didClose", | |
| 406 | 409 | } |
| 407 | 410 | got := server.methods()[2:] // after initialize and initialized |
| 408 | 411 | for i, method := range want { |
| @@ -395,6 +395,9 @@ func TestDocumentSynchronisationSendsTheRightNotifications(t *testing.T) { | |||
| 395 | if err := client.DidSave("main.go", "package main\n"); err != nil { | 395 | if err := client.DidSave("main.go", "package main\n"); err != nil { |
| 396 | t.Fatalf("DidSave() error = %v", err) | 396 | t.Fatalf("DidSave() error = %v", err) |
| 397 | } | 397 | } |
| 398 | + if err := client.FileCreated("main.go"); err != nil { | ||
| 399 | + t.Fatalf("FileCreated() error = %v", err) | ||
| 400 | + } | ||
| 398 | if err := client.DidClose("main.go"); err != nil { | 401 | if err := client.DidClose("main.go"); err != nil { |
| 399 | t.Fatalf("DidClose() error = %v", err) | 402 | t.Fatalf("DidClose() error = %v", err) |
| 400 | } | 403 | } |
| @@ -402,7 +405,7 @@ func TestDocumentSynchronisationSendsTheRightNotifications(t *testing.T) { | |||
| 402 | waitForMethod(t, server, "textDocument/didClose") | 405 | waitForMethod(t, server, "textDocument/didClose") |
| 403 | want := []string{ | 406 | want := []string{ |
| 404 | "textDocument/didOpen", "textDocument/didChange", | 407 | "textDocument/didOpen", "textDocument/didChange", |
| 405 | - "textDocument/didSave", "textDocument/didClose", | 408 | + "textDocument/didSave", "workspace/didChangeWatchedFiles", "textDocument/didClose", |
| 406 | } | 409 | } |
| 407 | got := server.methods()[2:] // after initialize and initialized | 410 | got := server.methods()[2:] // after initialize and initialized |
| 408 | for i, method := range want { | 411 | for i, method := range want { |
modified
lsp/protocol.go +24 -0 | @@ -86,6 +86,30 @@ type DidSaveParams struct { | ||
| 86 | 86 | Text string `json:"text,omitempty"` |
| 87 | 87 | } |
| 88 | 88 | |
| 89 | +// FileChangeType says what happened to a watched file. | |
| 90 | +type FileChangeType int | |
| 91 | + | |
| 92 | +// The three things that can happen to a file, as the protocol numbers them. | |
| 93 | +const ( | |
| 94 | + FileCreated FileChangeType = 1 | |
| 95 | + FileChanged FileChangeType = 2 | |
| 96 | + FileDeleted FileChangeType = 3 | |
| 97 | +) | |
| 98 | + | |
| 99 | +// FileEvent is one change to a file on disk. | |
| 100 | +type FileEvent struct { | |
| 101 | + URI string `json:"uri"` | |
| 102 | + Type FileChangeType `json:"type"` | |
| 103 | +} | |
| 104 | + | |
| 105 | +// DidChangeWatchedFilesParams reports changes to files on disk — as opposed | |
| 106 | +// to documents, which are what the editor has open. A server that works a | |
| 107 | +// project's file list out from disk learns of a new file from this and from | |
| 108 | +// nothing else: moon-lsp does not diagnose a file it was not told exists. | |
| 109 | +type DidChangeWatchedFilesParams struct { | |
| 110 | + Changes []FileEvent `json:"changes"` | |
| 111 | +} | |
| 112 | + | |
| 89 | 113 | // Severity is how serious a diagnostic is. |
| 90 | 114 | type Severity int |
| 91 | 115 | |
| @@ -86,6 +86,30 @@ type DidSaveParams struct { | |||
| 86 | Text string `json:"text,omitempty"` | 86 | Text string `json:"text,omitempty"` |
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | +// FileChangeType says what happened to a watched file. | ||
| 90 | +type FileChangeType int | ||
| 91 | + | ||
| 92 | +// The three things that can happen to a file, as the protocol numbers them. | ||
| 93 | +const ( | ||
| 94 | + FileCreated FileChangeType = 1 | ||
| 95 | + FileChanged FileChangeType = 2 | ||
| 96 | + FileDeleted FileChangeType = 3 | ||
| 97 | +) | ||
| 98 | + | ||
| 99 | +// FileEvent is one change to a file on disk. | ||
| 100 | +type FileEvent struct { | ||
| 101 | + URI string `json:"uri"` | ||
| 102 | + Type FileChangeType `json:"type"` | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +// DidChangeWatchedFilesParams reports changes to files on disk — as opposed | ||
| 106 | +// to documents, which are what the editor has open. A server that works a | ||
| 107 | +// project's file list out from disk learns of a new file from this and from | ||
| 108 | +// nothing else: moon-lsp does not diagnose a file it was not told exists. | ||
| 109 | +type DidChangeWatchedFilesParams struct { | ||
| 110 | + Changes []FileEvent `json:"changes"` | ||
| 111 | +} | ||
| 112 | + | ||
| 89 | // Severity is how serious a diagnostic is. | 113 | // Severity is how serious a diagnostic is. |
| 90 | type Severity int | 114 | type Severity int |
| 91 | 115 | ||