T M4.5: default Python LSP (basedpyright) + LSP-independent filetypes
First real language on the now-correct async + UTF-16 substrate. - pmacs.lsp.config.python → `basedpyright-langserver --stdio`. basedpyright (MIT fork of pyright) re-enables inlay hints / semantic tokens in the OSS server that upstream pyright withholds for Pylance — matches the deferred-feature roadmap. No init_options: strictness is project config (pyrightconfig.json / [tool.pyright]); pmacs does not yet advertise workspace/configuration, so an editor-side typeCheckingMode would not be honoured regardless (documented in-line, with the upstream-pyright one-field override). - LSP language detection separated from tree-sitter. pmacs.parse's extension registry is grammar-gated (rejects "python" — no bundled grammar). New user-extensible pmacs.lsp.filetypes map (py/pyi → python); active_buffer_language() tries grammar-backed parse first (rust/.rs etc. unchanged) then falls back to the map, so a language with a server but no grammar still auto-attaches. - PATH-gated acceptance test mirroring m4_5_rust_analyzer_initializes; unique assertion: a real basedpyright must negotiate a positionEncoding pmacs can encode — validates Option B against a real strict server, not just the fake. Skips when absent. Gate: fmt clean; clippy --all-targets -D warnings clean; lib 1229/0; m4_acceptance 61/0; m9_1 18/0; m8_1/m8_9/m8_10 green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f63876bb46
commit
9ab931d6dd
|
|
@ -28,6 +28,33 @@ pmacs.lsp.config.rust = pmacs.lsp.config.rust or {
|
|||
},
|
||||
}
|
||||
|
||||
-- Default Python config: basedpyright (an MIT fork of pyright that
|
||||
-- re-enables inlay hints / semantic tokens in the open-source server,
|
||||
-- which upstream pyright withholds for Pylance). `--stdio` is the
|
||||
-- LSP transport. No `init_options`: basedpyright/pyright take their
|
||||
-- strictness from project config (`pyrightconfig.json` /
|
||||
-- `[tool.pyright]` in `pyproject.toml`), and pmacs does not yet
|
||||
-- advertise `workspace/configuration` (a deferred capability), so an
|
||||
-- editor-side `typeCheckingMode` would not be honoured anyway. Until
|
||||
-- the project pins it, basedpyright's stricter defaults can make the
|
||||
-- diagnostics gutter noisier than upstream pyright — documented, not
|
||||
-- a bug. Users override any field from init.lua before a .py opens;
|
||||
-- swapping to upstream pyright is just `command = "pyright-langserver"`.
|
||||
pmacs.lsp.config.python = pmacs.lsp.config.python or {
|
||||
command = "basedpyright-langserver",
|
||||
args = { "--stdio" },
|
||||
}
|
||||
|
||||
-- LSP-side extension → language map, deliberately independent of the
|
||||
-- tree-sitter detection in `pmacs.parse` (which is grammar-gated:
|
||||
-- Python has an LSP server but no bundled grammar). Consulted only
|
||||
-- when `pmacs.parse.language_for_path` finds nothing, so grammar-
|
||||
-- backed languages keep their existing detection. Extensible from
|
||||
-- init.lua: `pmacs.lsp.filetypes.foo = "bar"`.
|
||||
pmacs.lsp.filetypes = pmacs.lsp.filetypes or {}
|
||||
pmacs.lsp.filetypes.py = pmacs.lsp.filetypes.py or "python"
|
||||
pmacs.lsp.filetypes.pyi = pmacs.lsp.filetypes.pyi or "python"
|
||||
|
||||
-- Per-buffer attachment record: { language, server, uri, version }.
|
||||
-- Keyed by `tostring(BufferIdLua)` because BufferIdLua hands out fresh
|
||||
-- userdata each call (so two handles to the same buffer wouldn't hash
|
||||
|
|
@ -69,7 +96,13 @@ end
|
|||
local function active_buffer_language()
|
||||
local path = active_buffer_path()
|
||||
if not path then return nil end
|
||||
return pmacs.parse.language_for_path(path)
|
||||
-- Grammar-backed detection first (keeps rust/.rs etc. exactly as
|
||||
-- before); fall back to the LSP-only filetype map so languages
|
||||
-- with a server but no tree-sitter grammar (Python) still attach.
|
||||
local lang = pmacs.parse.language_for_path(path)
|
||||
if lang then return lang end
|
||||
local ext = path:match("%.([%w_]+)$")
|
||||
return ext and pmacs.lsp.filetypes[ext] or nil
|
||||
end
|
||||
|
||||
local function ensure_server(language)
|
||||
|
|
|
|||
|
|
@ -1144,6 +1144,52 @@ fn m4_5_rust_analyzer_initializes() {
|
|||
let _ = mgr.borrow_mut().stop(sid);
|
||||
}
|
||||
|
||||
/// PATH-gated, mirrors `m4_5_rust_analyzer_initializes` for the
|
||||
/// default Python server (basedpyright). Validates the whole stack
|
||||
/// against a real, strict-by-default server: the registry command +
|
||||
/// `--stdio` launches, the LSP handshake completes, and the server
|
||||
/// negotiates a `positionEncoding` against the
|
||||
/// `general.positionEncodings: ["utf-8","utf-16"]` we advertise
|
||||
/// (Option B) — proving negotiation round-trips with a real server,
|
||||
/// not only the fake. Skips cleanly when basedpyright is absent.
|
||||
#[test]
|
||||
fn m4_5_basedpyright_initializes_and_negotiates_encoding() {
|
||||
let Ok(_) = which_binary("basedpyright-langserver") else {
|
||||
eprintln!("basedpyright-langserver not on PATH; skipping");
|
||||
return;
|
||||
};
|
||||
let (sup, mgr) = make_lsp_test_manager();
|
||||
let mut spec = LspServerSpec::new("basedpyright", "python", "basedpyright-langserver");
|
||||
spec.args = vec!["--stdio".into()];
|
||||
spec.restart = LspRestartPolicy::Never;
|
||||
let sid = mgr.borrow_mut().spawn(spec).expect("spawn basedpyright");
|
||||
let evs = drain_lsp_until(&sup, &mgr, sid, Duration::from_secs(20), |evs| {
|
||||
evs.iter()
|
||||
.any(|e| matches!(e.kind, LspEventKind::Initialized { .. }))
|
||||
});
|
||||
let caps = evs
|
||||
.iter()
|
||||
.find_map(|e| match &e.kind {
|
||||
LspEventKind::Initialized { capabilities } => Some(capabilities.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.expect("must observe Initialized event");
|
||||
assert!(caps.is_object(), "capabilities should be a JSON object");
|
||||
// If basedpyright implements LSP 3.17 position-encoding it echoes
|
||||
// its choice, which must be one we can actually encode. A server
|
||||
// predating 3.17 omits the field — correct too, since pmacs then
|
||||
// defaults to UTF-16 (the spec default). What must never happen:
|
||||
// a third encoding we don't handle.
|
||||
let enc = caps.get("positionEncoding").and_then(|v| v.as_str());
|
||||
assert!(
|
||||
matches!(enc, None | Some("utf-8" | "utf-16")),
|
||||
"basedpyright negotiated an encoding pmacs cannot handle: {enc:?}"
|
||||
);
|
||||
let state = mgr.borrow().state(sid).cloned();
|
||||
assert!(matches!(state, Some(LspClientState::Initialized { .. })));
|
||||
let _ = mgr.borrow_mut().stop(sid);
|
||||
}
|
||||
|
||||
/// Helper: scan PATH for a binary by name. Returns the absolute
|
||||
/// path if found.
|
||||
fn which_binary(name: &str) -> std::io::Result<std::path::PathBuf> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue