fix(lsp): read the glob form from the pattern, and stop a cancelled scan emitting
Review of the #233 implementation found two correctness defects. Both are cases where the FIRST fix for #233 was itself wrong, which is worth naming: this lane repaired absolute globs and, in the same change, broke a case that had worked since May. P1 --- `resolve_watcher` returned "absolute" for EVERY string, so the form was carried but derived from the union arm rather than from the pattern. A bare `*.txt` is a valid relative pattern (LSP 3.17 defines `Pattern` relative to a base path; VS Code treats string watchers as applying across workspace folders), and classifying it absolute matched it against `<base>/foo.txt`, which `^[^/]*%.txt$` can never match. A leading `/` is what makes a pattern absolute. The `filewatchflat` test could not catch this: it sends the RelativePattern OBJECT form, so it constrains the object arm, never the string arm the regression lived in --- the framing's own F1 finding, repeating inside the lane that named it. P2 --- `scan_tree` awaits `read_dir` once per directory, so the watcher coroutine sits suspended for most of a tick with `_sleep` already cleared. A cancel arriving there sets `cancelled` and has no sleep to interrupt, and the resumed scan ran on to `did_change_watched_files`: one stale batch under the superseded pattern, which is a wrong-pattern notification the server acts on. Cancellation and liveness are rechecked after the scan. Both tests are mutation-checked and each bite fails only its own defect. Reverting P1 fails the bare-string test alone --- the absolute test still passes, so the two readings are independent. Deleting P2's recheck reproduces the defect verbatim: `.received = "1 file:///…/foo.txt"`, a batch from a watcher that was already cancelled. P2's witness needs a seam. The race is a cancel landing during one of the scan's suspensions, which no arrangement of real timing produces on demand --- the same situation, and the same device, as `git.lua`'s `_deliver_status`. `pmacs.lsp._after_scan_for_tests` is nil in production and is handed the SCAN RESULT deliberately: a test that cancels on any other scan passes with the recheck deleted, because the loop would break at the post-sleep check and emit nothing anyway. Gates: 9/9 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
0723017754
commit
2a16e0eed5
|
|
@ -2081,6 +2081,29 @@ local function start_file_watcher(sid, base, glob, kind_mask, record)
|
||||||
if record.cancelled or not server_is_live(sid) then break end
|
if record.cancelled or not server_is_live(sid) then break end
|
||||||
|
|
||||||
local cur = scan_tree(base, matches)
|
local cur = scan_tree(base, matches)
|
||||||
|
-- The seam that makes the recheck below WITNESSABLE. `scan_tree`
|
||||||
|
-- suspends on `read_dir` once per directory, and the race is a
|
||||||
|
-- cancel arriving during one of those suspensions --- which no
|
||||||
|
-- arrangement of real timing can be made to happen on demand.
|
||||||
|
-- Same reason `git.lua` exposes `_deliver_status`: the contract is
|
||||||
|
-- about an interleaving the caller does not choose. Unset in
|
||||||
|
-- production, so this costs one nil test per tick.
|
||||||
|
-- `cur` is handed over so a test can cancel on THE SCAN THAT
|
||||||
|
-- OBSERVED a given change. Cancelling on any other scan is not a
|
||||||
|
-- witness: the loop would break at the post-sleep check on the
|
||||||
|
-- next iteration and emit nothing anyway, so the assertion would
|
||||||
|
-- pass with the recheck below deleted.
|
||||||
|
if pmacs.lsp._after_scan_for_tests then
|
||||||
|
pcall(pmacs.lsp._after_scan_for_tests, record, cur)
|
||||||
|
end
|
||||||
|
-- RECHECKED AFTER THE SCAN, not only after the sleep (review P2).
|
||||||
|
-- The coroutine is suspended for most of a tick with `_sleep`
|
||||||
|
-- already cleared, so a cancel landing there sets `cancelled` and
|
||||||
|
-- has no sleep to interrupt. Without this line the resumed scan
|
||||||
|
-- runs on to `did_change_watched_files` below and a SUPERSEDED
|
||||||
|
-- watcher emits one last batch under its OLD pattern. One batch is
|
||||||
|
-- enough: it is a wrong-pattern notification the server acts on.
|
||||||
|
if record.cancelled or not server_is_live(sid) then break end
|
||||||
local changes = {}
|
local changes = {}
|
||||||
for rel, sig in pairs(cur) do
|
for rel, sig in pairs(cur) do
|
||||||
local was = prev[rel]
|
local was = prev[rel]
|
||||||
|
|
@ -2110,11 +2133,20 @@ end
|
||||||
|
|
||||||
-- Resolve a GlobPattern (string | { baseUri, pattern }) to
|
-- Resolve a GlobPattern (string | { baseUri, pattern }) to
|
||||||
-- (base_dir, pattern, form). The form must travel with the pair: a
|
-- (base_dir, pattern, form). The form must travel with the pair: a
|
||||||
-- RelativePattern's pattern is relative to its baseUri, but a bare
|
-- RelativePattern's pattern is relative to its baseUri, and dropping
|
||||||
-- string matches the file's ABSOLUTE path (real servers send absolute
|
-- that distinction is what made absolute server globs unable to match
|
||||||
-- globs), and dropping the distinction here is what made those globs
|
-- anything. A bare string with no base falls back to the directory of
|
||||||
-- unable to match anything. A bare string with no base falls back to
|
-- an attached file on `sid` (best effort).
|
||||||
-- the directory of an attached file on `sid` (best effort).
|
--
|
||||||
|
-- THE FORM COMES FROM THE PATTERN, NOT FROM THE UNION ARM (review P1).
|
||||||
|
-- The first fix for #233 returned `"absolute"` for every string, which
|
||||||
|
-- is a different bug wearing the same shape: LSP 3.17 defines `Pattern`
|
||||||
|
-- relative to a base path, and VS Code treats a string watcher as
|
||||||
|
-- applying across workspace folders, so a bare `*.txt` is a VALID
|
||||||
|
-- relative pattern. Classifying it absolute matched it against
|
||||||
|
-- `<base>/foo.txt`, which `^[^/]*%.txt$` can never match --- so that
|
||||||
|
-- fix silently broke a case that worked before it. A leading `/` is
|
||||||
|
-- what makes a pattern absolute; the arm it arrived in is not.
|
||||||
local function resolve_watcher(sid, gp)
|
local function resolve_watcher(sid, gp)
|
||||||
if type(gp) == "table" and gp.baseUri then
|
if type(gp) == "table" and gp.baseUri then
|
||||||
return pmacs.lsp.path_for_uri(gp.baseUri), gp.pattern or "**", "relative"
|
return pmacs.lsp.path_for_uri(gp.baseUri), gp.pattern or "**", "relative"
|
||||||
|
|
@ -2124,7 +2156,9 @@ local function resolve_watcher(sid, gp)
|
||||||
if rec.server == sid and rec.uri then
|
if rec.server == sid and rec.uri then
|
||||||
local p = pmacs.lsp.path_for_uri(rec.uri)
|
local p = pmacs.lsp.path_for_uri(rec.uri)
|
||||||
local dir = p and p:match("^(.*)/[^/]*$")
|
local dir = p and p:match("^(.*)/[^/]*$")
|
||||||
if dir then return dir, gp, "absolute" end
|
if dir then
|
||||||
|
return dir, gp, (gp:sub(1, 1) == "/") and "absolute" or "relative"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -222,16 +222,38 @@ is resolved**, even though #227 is green and cleared.
|
||||||
**`githubsucks/lsp-file-watcher` is the authoritative tip** — the ref,
|
**`githubsucks/lsp-file-watcher` is the authoritative tip** — the ref,
|
||||||
not a SHA. Recover with
|
not a SHA. Recover with
|
||||||
`git fetch githubsucks && git checkout lsp-file-watcher`.
|
`git fetch githubsucks && git checkout lsp-file-watcher`.
|
||||||
- **Framing `docs/lsp-file-watcher-framing.md`, revision 1, APPROVED
|
- **Framing `docs/lsp-file-watcher-framing.md`, revision 2** — design
|
||||||
2026-08-10.** The ruling that blocked implementation is ANSWERED (see
|
approved 2026-08-10; revision 2 records a review round **against the
|
||||||
below); D1 and D2 are implemented on the branch atop the framing
|
implementation** that found two correctness defects, both now fixed
|
||||||
commit. Committed at the branch's first commit so it is portable
|
(see below). Committed at the branch's first commit so it is portable
|
||||||
during review — the standing lesson from the GUI arc framing, which
|
during review — the standing lesson from the GUI arc framing, which
|
||||||
spent two review rounds as an untracked file in one worktree.
|
spent two review rounds as an untracked file in one worktree.
|
||||||
|
- **Review round 1 on the code found two defects, and both are cases
|
||||||
|
where the first fix was itself wrong:**
|
||||||
|
1. **P1 — a bare-string `*.txt` stopped working.**
|
||||||
|
`resolve_watcher` classified **every** string as absolute, so a
|
||||||
|
valid relative pattern was matched against `<base>/foo.txt` and
|
||||||
|
could never fire. That path worked **before** this lane, so the
|
||||||
|
repair broke a live case while fixing another. The form now comes
|
||||||
|
from the pattern (a leading `/`), not from the union arm. The
|
||||||
|
flat-pattern test did not catch it because it exercises the
|
||||||
|
RelativePattern **object** arm — F1's tested-vs-exercised split,
|
||||||
|
repeating inside the lane that named it.
|
||||||
|
2. **P2 — a scan finishing after cancellation still emitted one
|
||||||
|
batch.** `scan_tree` suspends on `read_dir` per directory with
|
||||||
|
`_sleep` already cleared, so a cancel landing there had nothing to
|
||||||
|
interrupt and the resumed scan ran on to
|
||||||
|
`did_change_watched_files` under the superseded pattern.
|
||||||
|
Cancellation and liveness are rechecked after the scan.
|
||||||
|
- **A test seam was added**: `pmacs.lsp._after_scan_for_tests`, nil in
|
||||||
|
production, handed the scan result. P2's race cannot be produced by
|
||||||
|
timing; the seam is the same device as `git.lua`'s `_deliver_status`.
|
||||||
|
It takes the scan result because a test cancelling on any *other*
|
||||||
|
scan would pass with the fix deleted.
|
||||||
- **What the implementation is**: `resolve_watcher` returns
|
- **What the implementation is**: `resolve_watcher` returns
|
||||||
`(base, pattern, form)` and the watch record carries the form —
|
`(base, pattern, form)` and the watch record carries the form —
|
||||||
`"absolute"` (plain string) matches `base .. "/" .. rel`,
|
`"absolute"` (pattern begins `/`) matches `base .. "/" .. rel`,
|
||||||
`"relative"` (RelativePattern) matches `rel`; and
|
`"relative"` matches `rel`; and
|
||||||
`register_file_watchers` cancels the outgoing record list through the
|
`register_file_watchers` cancels the outgoing record list through the
|
||||||
new `cancel_watch_records` (shared with `unregister_file_watchers`)
|
new `cancel_watch_records` (shared with `unregister_file_watchers`)
|
||||||
before the table write drops the only reference to it.
|
before the table write drops the only reference to it.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,47 @@
|
||||||
# LSP file watcher — framing
|
# LSP file watcher — framing
|
||||||
|
|
||||||
**Status: revision 1 — APPROVED 2026-08-10.** The user ruled that D1
|
**Status: revision 2 — approved design (2026-08-10), plus two
|
||||||
and D2 proceed with the walking explicitly surviving this lane; D3 gets
|
correctness findings from review OF THE IMPLEMENTATION.** The user ruled
|
||||||
its own framing. The acceptance bar for this lane is correctness and
|
that D1 and D2 proceed with the walking explicitly surviving this lane;
|
||||||
the leak, not "the flipping stops".
|
D3 gets its own framing. The acceptance bar for this lane is correctness
|
||||||
|
and the leak, not "the flipping stops".
|
||||||
|
|
||||||
|
**Revision 2 records a review round against the code, not the design.
|
||||||
|
Both findings are cases where the first fix was itself wrong**, and both
|
||||||
|
were confirmed against the tree before being acted on:
|
||||||
|
|
||||||
|
- **P1 — the form must be read from the PATTERN, not from the union
|
||||||
|
arm.** `resolve_watcher` returned `"absolute"` for *every* string, so
|
||||||
|
a bare `*.txt` — a valid relative pattern under LSP 3.17, and how VS
|
||||||
|
Code treats string watchers across workspace folders — was matched
|
||||||
|
against `<base>/foo.txt` and could never fire. **That case worked
|
||||||
|
before this lane touched it**, so the repair for #233 silently broke a
|
||||||
|
live path while fixing another. A leading `/` is what makes a pattern
|
||||||
|
absolute; classification now reads the string.
|
||||||
|
- **P2 — a scan completing after cancellation still emitted.**
|
||||||
|
`scan_tree` awaits `read_dir` once per directory, so the coroutine
|
||||||
|
sits suspended for most of a tick with `_sleep` already cleared. A
|
||||||
|
cancel arriving there sets `cancelled` and has no sleep to interrupt,
|
||||||
|
and the resumed scan ran on to `did_change_watched_files` — one stale
|
||||||
|
batch under the superseded pattern, which is a wrong-pattern
|
||||||
|
notification the server acts on. Cancellation and liveness are now
|
||||||
|
rechecked after the scan.
|
||||||
|
|
||||||
|
**F1's lesson repeated itself inside this lane.** The flat-pattern test
|
||||||
|
constrains the RelativePattern **object** arm, so it said nothing about
|
||||||
|
the **string** arm P1's regression lived in — the same
|
||||||
|
tested-path/exercised-path split this framing opened by naming. Both
|
||||||
|
findings now have tests, and both tests were mutation-checked: each
|
||||||
|
fails only its own defect.
|
||||||
|
|
||||||
|
**A test seam was added, and is recorded here rather than buried.**
|
||||||
|
`pmacs.lsp._after_scan_for_tests` is a production hook, nil in normal
|
||||||
|
operation, that P2's witness requires: the race is a cancel landing
|
||||||
|
during one of the scan's suspensions, which no arrangement of real
|
||||||
|
timing produces on demand. Same device and justification as `git.lua`'s
|
||||||
|
`_deliver_status`. It is handed the scan result deliberately — a test
|
||||||
|
that cancels on any *other* scan passes with the fix deleted, because
|
||||||
|
the loop would break at the post-sleep check and emit nothing anyway.
|
||||||
|
|
||||||
Answers issue #233. **Scope is D1 and D2 only** — the two bug-shaped
|
Answers issue #233. **Scope is D1 and D2 only** — the two bug-shaped
|
||||||
defects. D3 (the polling cost) is named here, deferred with reasons, and
|
defects. D3 (the polling cost) is named here, deferred with reasons, and
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,32 @@ fn main() {
|
||||||
});
|
});
|
||||||
write_frame(&mut stdout, &req);
|
write_frame(&mut stdout, &req);
|
||||||
}
|
}
|
||||||
|
// Issue #233 review P1 guard: `filewatchbare` registers a
|
||||||
|
// BARE STRING with no base and no leading `/` — `*.txt`.
|
||||||
|
// The string arm and the `filewatchflat` arm below carry the
|
||||||
|
// same pattern deliberately: `flat` proves a
|
||||||
|
// RelativePattern stays relative, and this proves the
|
||||||
|
// classification is read from THE PATTERN rather than from
|
||||||
|
// the union arm it arrived in. The first fix for #233
|
||||||
|
// called every string absolute, which matched this against
|
||||||
|
// `<base>/foo.txt` and broke a case that had worked since
|
||||||
|
// May. Without this mode that regression is invisible.
|
||||||
|
("initialized", _) if mode == "filewatchbare" => {
|
||||||
|
let req = serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 9304,
|
||||||
|
"method": "client/registerCapability",
|
||||||
|
"params": { "registrations": [{
|
||||||
|
"id": "watch-bare",
|
||||||
|
"method": "workspace/didChangeWatchedFiles",
|
||||||
|
"registerOptions": { "watchers": [{
|
||||||
|
"globPattern": "*.txt",
|
||||||
|
"kind": 7
|
||||||
|
}] }
|
||||||
|
}] }
|
||||||
|
});
|
||||||
|
write_frame(&mut stdout, &req);
|
||||||
|
}
|
||||||
// Issue #233 F2 guard: `filewatchflat` registers a
|
// Issue #233 F2 guard: `filewatchflat` registers a
|
||||||
// RelativePattern whose pattern has no leading `**/`
|
// RelativePattern whose pattern has no leading `**/`
|
||||||
// (`*.txt` at the base). It matches base-level files
|
// (`*.txt` at the base). It matches base-level files
|
||||||
|
|
|
||||||
|
|
@ -5506,6 +5506,174 @@ fn m4_24_relative_pattern_without_globstar_stays_relative() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Issue #233 review P2 — a scan that completes AFTER cancellation
|
||||||
|
/// must not emit.
|
||||||
|
///
|
||||||
|
/// `scan_tree` awaits `read_dir` once per directory, so the watcher
|
||||||
|
/// coroutine spends most of a tick suspended with `_sleep` already
|
||||||
|
/// cleared. A cancel arriving there — re-registration or unregistration
|
||||||
|
/// — sets `cancelled` and has no sleep to interrupt, so before the fix
|
||||||
|
/// the resumed scan ran on and emitted one last batch under the
|
||||||
|
/// superseded pattern.
|
||||||
|
///
|
||||||
|
/// No arrangement of real timing produces that interleaving on demand,
|
||||||
|
/// so it is driven through `pmacs.lsp._after_scan_for_tests`, the same
|
||||||
|
/// device `git.lua` uses for out-of-order completions. The hook is
|
||||||
|
/// handed the scan result and cancels **only on the scan that observed
|
||||||
|
/// `foo.txt`** — cancelling on any other scan would pass with the fix
|
||||||
|
/// deleted, because the loop would break at the post-sleep check and
|
||||||
|
/// emit nothing regardless.
|
||||||
|
#[test]
|
||||||
|
fn m4_24_a_scan_finishing_after_cancellation_emits_nothing() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let base = dir.path().to_path_buf();
|
||||||
|
let base_disp = base.display().to_string();
|
||||||
|
let a_path = base.join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"fn a() {}\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
let received = base.join(".received");
|
||||||
|
|
||||||
|
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!(
|
||||||
|
"pmacs.lsp.config.rust = {{ command = '{fake}',
|
||||||
|
env = {{ PMACS_FAKE_LSP_MODE = 'filewatch',
|
||||||
|
PMACS_FAKE_LSP_WATCH_BASE = '{base_disp}' }} }}"
|
||||||
|
))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(
|
||||||
|
&mut state,
|
||||||
|
"(function() for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then return true end \
|
||||||
|
end return false end)()",
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
"fake never initialized"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Armed BEFORE the file exists, so the cancel cannot land early:
|
||||||
|
// the hook fires on every scan and only cancels once the scan it is
|
||||||
|
// inspecting actually contains foo.txt.
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(
|
||||||
|
"pmacs.lsp._after_scan_for_tests = function(record, cur)
|
||||||
|
if cur and cur['foo.txt'] then record.cancelled = true end
|
||||||
|
end",
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.expect("install scan hook");
|
||||||
|
|
||||||
|
let warm = Instant::now() + Duration::from_millis(900);
|
||||||
|
while Instant::now() < warm {
|
||||||
|
state.tick_processes();
|
||||||
|
state.tick_lsp();
|
||||||
|
state.tick_async();
|
||||||
|
std::thread::sleep(Duration::from_millis(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(base.join("foo.txt"), b"one\n").expect("write foo.txt");
|
||||||
|
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(4);
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
state.tick_processes();
|
||||||
|
state.tick_lsp();
|
||||||
|
state.tick_async();
|
||||||
|
std::thread::sleep(Duration::from_millis(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
let got = std::fs::read_to_string(&received).unwrap_or_default();
|
||||||
|
assert!(
|
||||||
|
!got.contains("foo.txt"),
|
||||||
|
"a watcher cancelled during its scan emitted a stale batch \
|
||||||
|
anyway; .received = {got:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issue #233 review P1 — a BARE-STRING glob with no leading `/` is a
|
||||||
|
/// relative pattern and must stay one.
|
||||||
|
///
|
||||||
|
/// The first fix for #233 classified every string-arm pattern as
|
||||||
|
/// absolute, so `*.txt` was matched against `<base>/foo.txt` and could
|
||||||
|
/// never fire — silently breaking a case that had worked since May
|
||||||
|
/// while fixing the absolute one. `m4_24_relative_pattern_without_globstar_stays_relative`
|
||||||
|
/// does not cover it: that mode sends the `RelativePattern` OBJECT form,
|
||||||
|
/// so it constrains the object arm only. This sends the same pattern
|
||||||
|
/// through the STRING arm, which is the arm the regression lived in.
|
||||||
|
#[test]
|
||||||
|
fn m4_24_bare_string_glob_stays_relative() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let base = dir.path().to_path_buf();
|
||||||
|
let base_disp = base.display().to_string();
|
||||||
|
let a_path = base.join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"fn a() {}\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
let received = base.join(".received");
|
||||||
|
let foo_uri = format!("file://{}", base.join("foo.txt").display());
|
||||||
|
|
||||||
|
let mut state = EditorState::new_with_roots(&crate::iso::roots());
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!(
|
||||||
|
"pmacs.lsp.config.rust = {{ command = '{fake}',
|
||||||
|
env = {{ PMACS_FAKE_LSP_MODE = 'filewatchbare',
|
||||||
|
PMACS_FAKE_LSP_WATCH_BASE = '{base_disp}' }} }}"
|
||||||
|
))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(
|
||||||
|
&mut state,
|
||||||
|
"(function() for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then return true end \
|
||||||
|
end return false end)()",
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
"fake never initialized"
|
||||||
|
);
|
||||||
|
|
||||||
|
let warm = Instant::now() + Duration::from_millis(900);
|
||||||
|
while Instant::now() < warm {
|
||||||
|
state.tick_processes();
|
||||||
|
state.tick_lsp();
|
||||||
|
state.tick_async();
|
||||||
|
std::thread::sleep(Duration::from_millis(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(base.join("foo.txt"), b"one\n").expect("write foo.txt");
|
||||||
|
assert!(
|
||||||
|
pump_until_file_contains(&mut state, &received, &format!("1 {foo_uri}"), 6),
|
||||||
|
"CREATED for foo.txt never reported under a bare-string `*.txt` \
|
||||||
|
glob — the string arm is being classified absolute again; \
|
||||||
|
.received = {:?}",
|
||||||
|
std::fs::read_to_string(&received).unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Issue #233 D2 — re-registering a live id supersedes it. The
|
/// Issue #233 D2 — re-registering a live id supersedes it. The
|
||||||
/// `filewatchrereg` fake registers `watch-re` TWICE with no
|
/// `filewatchrereg` fake registers `watch-re` TWICE with no
|
||||||
/// unregister between — `**/*.old`, then `**/*.new` — exactly the
|
/// unregister between — `**/*.old`, then `**/*.new` — exactly the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue