Merge pull request #102 from levineuwirth/lsp-autopull-and-signature-trigger
This commit is contained in:
commit
2d157d8916
|
|
@ -261,6 +261,9 @@ local pending_did_change = {}
|
||||||
-- Forward declaration — defined below (needs helpers that follow);
|
-- Forward declaration — defined below (needs helpers that follow);
|
||||||
-- `flush_did_change` re-pulls inlay hints after each coalesced send.
|
-- `flush_did_change` re-pulls inlay hints after each coalesced send.
|
||||||
local pull_inlay_hints_quiet
|
local pull_inlay_hints_quiet
|
||||||
|
-- Same, for semantic tokens (Arc 1c). They are pull-model too, and
|
||||||
|
-- nothing was pulling them.
|
||||||
|
local pull_semantic_tokens_quiet
|
||||||
|
|
||||||
local function flush_did_change(key)
|
local function flush_did_change(key)
|
||||||
local pending = pending_did_change[key]
|
local pending = pending_did_change[key]
|
||||||
|
|
@ -282,6 +285,8 @@ local function flush_did_change(key)
|
||||||
-- supersede-keyed per (server, method, uri), so a burst of flushes
|
-- supersede-keyed per (server, method, uri), so a burst of flushes
|
||||||
-- cancels its own predecessors rather than piling up.
|
-- cancels its own predecessors rather than piling up.
|
||||||
pcall(pull_inlay_hints_quiet, rec)
|
pcall(pull_inlay_hints_quiet, rec)
|
||||||
|
-- Semantic tokens are pull-model on exactly the same terms (Arc 1c).
|
||||||
|
pcall(pull_semantic_tokens_quiet, rec)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function flush_did_change_for(rec)
|
local function flush_did_change_for(rec)
|
||||||
|
|
@ -447,6 +452,47 @@ function pull_inlay_hints_quiet(rec)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function server_supports_semantic_tokens(sid)
|
||||||
|
local ok, caps = pcall(pmacs.lsp.capabilities, sid)
|
||||||
|
if not ok or not caps then return false end
|
||||||
|
local p = caps.semanticTokensProvider
|
||||||
|
return p ~= nil and p ~= false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Arc 1c. Semantic tokens are pull-model, exactly like inlay hints: the
|
||||||
|
-- server never volunteers them, and the store only fills from a
|
||||||
|
-- `textDocument/semanticTokens/*` response. Until now the ONLY automatic
|
||||||
|
-- pull was in reply to a server-initiated `workspace/semanticTokens
|
||||||
|
-- /refresh` --- which most servers never send --- so semantic styling
|
||||||
|
-- silently never appeared unless the user ran `M-x lsp.semantic-tokens`
|
||||||
|
-- by hand. Attach and edit-flush now pull it, the same two points that
|
||||||
|
-- already pull inlay hints.
|
||||||
|
--
|
||||||
|
-- Assigns the forward-declared local above (a fresh `local function`
|
||||||
|
-- here would shadow it, leaving `flush_did_change`'s upvalue nil).
|
||||||
|
function pull_semantic_tokens_quiet(rec)
|
||||||
|
if not rec or not server_is_initialized(rec.server) then return end
|
||||||
|
if not server_supports_semantic_tokens(rec.server) then return end
|
||||||
|
-- The server must see the current text before computing token
|
||||||
|
-- positions against it. A no-op when called from `flush_did_change`
|
||||||
|
-- itself (the pending entry is removed before the send).
|
||||||
|
flush_did_change_for(rec)
|
||||||
|
-- Delta when we hold a `resultId` (the server only returns one when it
|
||||||
|
-- supports delta), full otherwise --- matching `pmacs.lsp
|
||||||
|
-- .semantic_tokens()`. Never clear the store first: a delta splices
|
||||||
|
-- against the retained raw stream.
|
||||||
|
local prev = pmacs.semantic_tokens.result_id(rec.server, rec.uri)
|
||||||
|
pmacs.async(function()
|
||||||
|
pcall(function()
|
||||||
|
if prev then
|
||||||
|
pmacs.lsp.request_semantic_tokens_delta(rec.server, rec.uri, prev):await()
|
||||||
|
else
|
||||||
|
pmacs.lsp.request_semantic_tokens(rec.server, rec.uri):await()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- M_B1: buffers that already had an `LspStyleView` overlay pushed,
|
-- M_B1: buffers that already had an `LspStyleView` overlay pushed,
|
||||||
-- so the after-load / on-demand attach paths don't stack duplicate
|
-- so the after-load / on-demand attach paths don't stack duplicate
|
||||||
-- overlays. Mirrors `highlighted_buffers` in `syntax.lua`; the entry
|
-- overlays. Mirrors `highlighted_buffers` in `syntax.lua`; the entry
|
||||||
|
|
@ -514,6 +560,9 @@ local function attach_buffer(buf)
|
||||||
if ok and attached then diag_viewed_buffers[key] = true end
|
if ok and attached then diag_viewed_buffers[key] = true end
|
||||||
end
|
end
|
||||||
pull_inlay_hints_quiet(rec)
|
pull_inlay_hints_quiet(rec)
|
||||||
|
-- Arc 1c: the LspStyleView was just attached above, but nothing ever
|
||||||
|
-- filled the semantic-token store it reads. Pull once on attach.
|
||||||
|
pull_semantic_tokens_quiet(rec)
|
||||||
return rec
|
return rec
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -589,6 +638,67 @@ pmacs.hook.add("buffer.after-switch", function()
|
||||||
if ok_d and attached_d then diag_viewed_buffers[key] = true end
|
if ok_d and attached_d then diag_viewed_buffers[key] = true end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- Arc 1d: signature-help auto-trigger ----------------------------------
|
||||||
|
--
|
||||||
|
-- `buffer.after-edit` carries no payload, so a *typed character* is
|
||||||
|
-- reconstructed from state exactly the way `completion.lua` does (Q#C9):
|
||||||
|
-- same buffer, cursor advanced by exactly one byte. Paste, undo, kill,
|
||||||
|
-- and remote CRDT edits produce any other delta and never auto-trigger.
|
||||||
|
-- (Trigger characters are ASCII, so a one-byte advance is sound.)
|
||||||
|
local last_typed = { key = nil, cursor = nil }
|
||||||
|
|
||||||
|
local function char_before(buf, cursor)
|
||||||
|
if cursor <= 0 then return nil end
|
||||||
|
local ok, s = pcall(function() return buf:slice(cursor - 1, cursor) end)
|
||||||
|
if not ok or type(s) ~= "string" or #s ~= 1 then return nil end
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The set of characters that should (re)open signature help, as the
|
||||||
|
-- server declares them. `retriggerCharacters` (usually `,`) refreshes an
|
||||||
|
-- open call's active parameter. A provider that declares neither still
|
||||||
|
-- gets the universal pair, which is what `(` auto-trigger means in
|
||||||
|
-- practice; no provider means no auto-trigger at all.
|
||||||
|
local function signature_trigger_chars(sid)
|
||||||
|
local ok, caps = pcall(pmacs.lsp.capabilities, sid)
|
||||||
|
if not ok or not caps then return nil end
|
||||||
|
local p = caps.signatureHelpProvider
|
||||||
|
if not p or p == false then return nil end
|
||||||
|
local chars = {}
|
||||||
|
for _, c in ipairs(p.triggerCharacters or {}) do chars[c] = true end
|
||||||
|
for _, c in ipairs(p.retriggerCharacters or {}) do chars[c] = true end
|
||||||
|
if next(chars) == nil then
|
||||||
|
chars["("] = true
|
||||||
|
chars[","] = true
|
||||||
|
end
|
||||||
|
return chars
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Like `pmacs.lsp.signature_help_at_cursor`, but silent: an auto-trigger
|
||||||
|
-- that announced "no signature help" on every `(` in a comment would be
|
||||||
|
-- unusable. Only a real signature reaches the status line.
|
||||||
|
local function signature_help_quiet(rec)
|
||||||
|
if not server_is_initialized(rec.server) then return end
|
||||||
|
-- The server must see the character we just typed before it can tell
|
||||||
|
-- us which parameter we are inside of.
|
||||||
|
flush_did_change_for(rec)
|
||||||
|
local line = pmacs.editor.cursor_line()
|
||||||
|
local col = pmacs.editor.cursor_col()
|
||||||
|
pmacs.signature.clear(rec.server, rec.uri)
|
||||||
|
pmacs.async(function()
|
||||||
|
local ok = pcall(function()
|
||||||
|
pmacs.lsp.request_signature_help(rec.server, rec.uri, line, col):await()
|
||||||
|
end)
|
||||||
|
if not ok then return end
|
||||||
|
local help = pmacs.signature.current(rec.server, rec.uri)
|
||||||
|
if not help or not help.signatures or #help.signatures == 0 then return end
|
||||||
|
local active = help.signatures[(help.active_signature or 0) + 1]
|
||||||
|
if active and active.label then
|
||||||
|
pmacs.editor.set_status("LSP: " .. active.label)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
pmacs.hook.add("buffer.after-edit", function()
|
pmacs.hook.add("buffer.after-edit", function()
|
||||||
local buf = pmacs.window.buffer()
|
local buf = pmacs.window.buffer()
|
||||||
if not buf then return end
|
if not buf then return end
|
||||||
|
|
@ -600,6 +710,13 @@ pmacs.hook.add("buffer.after-edit", function()
|
||||||
-- O(file) didChange send below is coalesced: render families
|
-- O(file) didChange send below is coalesced: render families
|
||||||
-- anchored to pre-edit positions are hidden from this edit on.
|
-- anchored to pre-edit positions are hidden from this edit on.
|
||||||
pcall(pmacs.lsp._mark_document_stale, rec.uri)
|
pcall(pmacs.lsp._mark_document_stale, rec.uri)
|
||||||
|
-- Arc 1d: did the user just type a signature trigger character?
|
||||||
|
-- Recorded before the early-outs below so the snapshot stays accurate
|
||||||
|
-- for the *next* edit even when this one doesn't trigger.
|
||||||
|
local cursor = pmacs.editor.cursor()
|
||||||
|
local prev_key, prev_cursor = last_typed.key, last_typed.cursor
|
||||||
|
last_typed.key, last_typed.cursor = key, cursor
|
||||||
|
local typed_one = key == prev_key and prev_cursor and cursor - prev_cursor == 1
|
||||||
local now = pmacs.editor.monotonic_ms()
|
local now = pmacs.editor.monotonic_ms()
|
||||||
local pending = pending_did_change[key]
|
local pending = pending_did_change[key]
|
||||||
if pending and pending.rec == rec then
|
if pending and pending.rec == rec then
|
||||||
|
|
@ -607,6 +724,14 @@ pmacs.hook.add("buffer.after-edit", function()
|
||||||
else
|
else
|
||||||
pending_did_change[key] = { rec = rec, first_ms = now, last_ms = now }
|
pending_did_change[key] = { rec = rec, first_ms = now, last_ms = now }
|
||||||
end
|
end
|
||||||
|
-- Fire *after* queuing the pending didChange: `signature_help_quiet`
|
||||||
|
-- flushes it, so the server sees the character we are asking about.
|
||||||
|
if not typed_one then return end
|
||||||
|
local ch = char_before(buf, cursor)
|
||||||
|
if not ch then return end
|
||||||
|
local triggers = signature_trigger_chars(rec.server)
|
||||||
|
if not (triggers and triggers[ch]) then return end
|
||||||
|
pcall(signature_help_quiet, rec)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Async request surface (T M4.5 async bridge). The Rust manager
|
-- Async request surface (T M4.5 async bridge). The Rust manager
|
||||||
|
|
@ -1148,8 +1273,15 @@ local function handle_server_requests()
|
||||||
pcall(unregister_file_watchers, sid,
|
pcall(unregister_file_watchers, sid,
|
||||||
ev.params and ev.params.unregisterations)
|
ev.params and ev.params.unregisterations)
|
||||||
elseif ev.kind == "initialized" then
|
elseif ev.kind == "initialized" then
|
||||||
|
-- Buffers attach before the server finishes initializing, so
|
||||||
|
-- the pulls in `attach_buffer` are no-ops for the FIRST file
|
||||||
|
-- (their `server_is_initialized` guard is false). This is the
|
||||||
|
-- site that actually lands them. Inlay hints were pulled here;
|
||||||
|
-- semantic tokens were not, which is why semantic styling never
|
||||||
|
-- appeared on the file that started the server (Arc 1c).
|
||||||
repull_for_attachments(sid, function(_, _, rec)
|
repull_for_attachments(sid, function(_, _, rec)
|
||||||
pull_inlay_hints_quiet(rec)
|
pull_inlay_hints_quiet(rec)
|
||||||
|
pull_semantic_tokens_quiet(rec)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@
|
||||||
//! `rootUri` received in `initialize` to the file named by
|
//! `rootUri` received in `initialize` to the file named by
|
||||||
//! `PMACS_FAKE_LSP_ROOT_SINK`, so a test can assert the
|
//! `PMACS_FAKE_LSP_ROOT_SINK`, so a test can assert the
|
||||||
//! auto-attach path derives the project root from the opened file.
|
//! auto-attach path derives the project root from the opened file.
|
||||||
|
//! * If launched with `PMACS_FAKE_LSP_MODE=sighelp`: additionally
|
||||||
|
//! advertises `signatureHelpProvider` with `(` / `,` triggers, so a
|
||||||
|
//! test can drive the Arc 1d auto-trigger. Every other mode omits the
|
||||||
|
//! capability and therefore never auto-triggers.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
|
@ -154,6 +158,16 @@ fn main() {
|
||||||
resp["result"]["capabilities"]["renameProvider"] =
|
resp["result"]["capabilities"]["renameProvider"] =
|
||||||
serde_json::json!({ "prepareProvider": true });
|
serde_json::json!({ "prepareProvider": true });
|
||||||
}
|
}
|
||||||
|
// Arc 1d: advertise signature help only in `sighelp`, so
|
||||||
|
// every other mode keeps the no-auto-trigger path (the
|
||||||
|
// `textDocument/signatureHelp` arm below still answers
|
||||||
|
// the manual `M-x lsp.signature-help` in any mode).
|
||||||
|
if mode == "sighelp" {
|
||||||
|
resp["result"]["capabilities"]["signatureHelpProvider"] = serde_json::json!({
|
||||||
|
"triggerCharacters": ["("],
|
||||||
|
"retriggerCharacters": [","]
|
||||||
|
});
|
||||||
|
}
|
||||||
// T M4.5 hardening `rooturi`: record the `rootUri` the
|
// T M4.5 hardening `rooturi`: record the `rootUri` the
|
||||||
// client sent in `initialize` to a side-channel file
|
// client sent in `initialize` to a side-channel file
|
||||||
// (env `PMACS_FAKE_LSP_ROOT_SINK`). Lets a test prove
|
// (env `PMACS_FAKE_LSP_ROOT_SINK`). Lets a test prove
|
||||||
|
|
|
||||||
|
|
@ -4297,6 +4297,240 @@ fn m4_19_semantic_tokens_refresh_repulls_via_server_request() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arc 1c — semantic tokens auto-pull **on attach**.
|
||||||
|
///
|
||||||
|
/// Regression for a shipped bug: semantic tokens are pull-model, but the
|
||||||
|
/// only automatic pull was in reply to a server-initiated
|
||||||
|
/// `workspace/semanticTokens/refresh`. Most servers never send one, so
|
||||||
|
/// semantic styling silently never appeared unless the user ran
|
||||||
|
/// `M-x lsp.semantic-tokens` by hand — while inlay hints, on the very
|
||||||
|
/// same pull model, were pulled on attach and on edit-flush.
|
||||||
|
///
|
||||||
|
/// The **default** fake advertises `semanticTokensProvider` and never
|
||||||
|
/// sends a refresh, which is exactly the broken case.
|
||||||
|
#[test]
|
||||||
|
fn arc1c_semantic_tokens_auto_pull_on_attach() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let a_path = dir.path().join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.lsp.config.rust = {{ command = '{fake}' }}"))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
|
||||||
|
let flag = format!(
|
||||||
|
"(function() \
|
||||||
|
local sid \
|
||||||
|
for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then sid=r.id end \
|
||||||
|
end \
|
||||||
|
if not sid then return false end \
|
||||||
|
local t = pmacs.semantic_tokens.tokens(sid, 'file://{a_disp}') \
|
||||||
|
return t ~= nil and #t > 0 \
|
||||||
|
end)()"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(&mut state, &flag, 5),
|
||||||
|
"attach never auto-pulled semantic tokens (no manual call, no server refresh)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arc 1c — semantic tokens re-pull **on edit-flush**, the second point
|
||||||
|
/// inlay hints already pulled from. Clears the store, types a character,
|
||||||
|
/// and waits for the debounced `didChange` flush to refill it.
|
||||||
|
#[test]
|
||||||
|
fn arc1c_semantic_tokens_repull_after_edit_flush() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let a_path = dir.path().join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"fn a() {}\n\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.lsp.config.rust = {{ command = '{fake}' }}"))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
|
||||||
|
let has_tokens = format!(
|
||||||
|
"(function() \
|
||||||
|
local sid \
|
||||||
|
for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then sid=r.id end \
|
||||||
|
end \
|
||||||
|
if not sid then return false end \
|
||||||
|
local t = pmacs.semantic_tokens.tokens(sid, 'file://{a_disp}') \
|
||||||
|
return t ~= nil and #t > 0 \
|
||||||
|
end)()"
|
||||||
|
);
|
||||||
|
assert!(pump_lua_flag(&mut state, &has_tokens, 5), "attach pull");
|
||||||
|
|
||||||
|
// Empty the store, then type — the flush must refill it.
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!(
|
||||||
|
"for _,r in ipairs(pmacs.lsp.list()) do \
|
||||||
|
if r.state and r.state.kind=='initialized' then \
|
||||||
|
pmacs.semantic_tokens.clear(r.id, 'file://{a_disp}') \
|
||||||
|
end \
|
||||||
|
end"
|
||||||
|
))
|
||||||
|
.exec()
|
||||||
|
.expect("clear the token store");
|
||||||
|
state.dispatch_key(
|
||||||
|
pmacs::protocol::FrontendId::LOCAL,
|
||||||
|
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pump_lua_flag(&mut state, &has_tokens, 5),
|
||||||
|
"edit-flush never re-pulled semantic tokens"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arc 1d — signature help auto-triggers on a server-declared trigger
|
||||||
|
/// character. Typing `(` (a one-byte cursor advance, the same typed-char
|
||||||
|
/// signature `completion.lua` uses) surfaces the active signature.
|
||||||
|
#[test]
|
||||||
|
fn arc1d_signature_help_auto_triggers_on_trigger_char() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let a_path = dir.path().join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!(
|
||||||
|
"pmacs.lsp.config.rust = {{
|
||||||
|
command = '{fake}',
|
||||||
|
env = {{ PMACS_FAKE_LSP_MODE = 'sighelp' }},
|
||||||
|
}}"
|
||||||
|
))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
let initialized = "(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)()";
|
||||||
|
assert!(pump_lua_flag(&mut state, initialized, 5), "server init");
|
||||||
|
|
||||||
|
// The first keystroke only seeds the typed-char snapshot; the second
|
||||||
|
// is the trigger. (A trigger char cannot fire off the very first edit
|
||||||
|
// in a buffer, which is correct: there is no prior cursor to compare.)
|
||||||
|
for c in ['f', '('] {
|
||||||
|
state.dispatch_key(
|
||||||
|
pmacs::protocol::FrontendId::LOCAL,
|
||||||
|
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
let mut saw = false;
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
state.tick_processes();
|
||||||
|
state.tick_lsp();
|
||||||
|
state.tick_async();
|
||||||
|
if state.core.borrow().status.contains("fn echo(") {
|
||||||
|
saw = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(saw, "typing `(` did not auto-trigger signature help");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arc 1d — an ordinary character does **not** auto-trigger, and neither
|
||||||
|
/// does a multi-byte edit (paste/undo/remote): only the one-byte typed
|
||||||
|
/// signature does. Guards against a signature request on every keystroke.
|
||||||
|
#[test]
|
||||||
|
fn arc1d_signature_help_does_not_trigger_on_ordinary_typing() {
|
||||||
|
use pmacs::editor::EditorState;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let a_path = dir.path().join("a.rs");
|
||||||
|
std::fs::write(&a_path, b"\n").expect("write a");
|
||||||
|
let a_disp = a_path.display().to_string();
|
||||||
|
|
||||||
|
let mut state = EditorState::new();
|
||||||
|
let fake = fake_lsp_path();
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!(
|
||||||
|
"pmacs.lsp.config.rust = {{
|
||||||
|
command = '{fake}',
|
||||||
|
env = {{ PMACS_FAKE_LSP_MODE = 'sighelp' }},
|
||||||
|
}}"
|
||||||
|
))
|
||||||
|
.exec()
|
||||||
|
.expect("override rust config");
|
||||||
|
state
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(format!("pmacs.buffer.find_or_open('{a_disp}')"))
|
||||||
|
.exec()
|
||||||
|
.expect("open a.rs");
|
||||||
|
let initialized = "(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)()";
|
||||||
|
assert!(pump_lua_flag(&mut state, initialized, 5), "server init");
|
||||||
|
|
||||||
|
for c in ['f', 'o', 'o'] {
|
||||||
|
state.dispatch_key(
|
||||||
|
pmacs::protocol::FrontendId::LOCAL,
|
||||||
|
KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
state.tick_processes();
|
||||||
|
state.tick_lsp();
|
||||||
|
state.tick_async();
|
||||||
|
assert!(
|
||||||
|
!state.core.borrow().status.contains("fn echo("),
|
||||||
|
"ordinary typing must not request signature help"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// T M4.5 — `textDocument/semanticTokens/range` through the Lua
|
/// T M4.5 — `textDocument/semanticTokens/range` through the Lua
|
||||||
/// surface. Same decode path as `/full`, scoped to a range; the
|
/// surface. Same decode path as `/full`, scoped to a range; the
|
||||||
/// fake returns one token.
|
/// fake returns one token.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue