Merge pull request #14 from levineuwirth/lsp-clangd-gopls

T M4.5: default C/C++ (clangd) + Go (gopls) language entries
This commit is contained in:
Levi Neuwirth 2026-05-19 01:21:38 +00:00 committed by GitHub
commit deb9935ee0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 0 deletions

View File

@ -47,6 +47,33 @@ pmacs.lsp.config.python = pmacs.lsp.config.python or {
},
}
-- C / C++ via clangd. One clangd binary serves both; `config.c` and
-- `config.cpp` are separate entries only so the `language_id` sent in
-- `didOpen` is accurate (clangd respects it). clangd takes its
-- project model from `compile_commands.json` / `compile_flags.txt`
-- at the project root, not `workspace/configuration`, so no
-- `settings` here; `--background-index` enables cross-file features.
-- Users override from init.lua before a C/C++ file opens.
pmacs.lsp.config.c = pmacs.lsp.config.c or {
command = "clangd",
args = { "--background-index" },
}
pmacs.lsp.config.cpp = pmacs.lsp.config.cpp or {
command = "clangd",
args = { "--background-index" },
}
-- Go via gopls. `gopls` with no args serves LSP over stdio. gopls
-- pulls its configuration via `workspace/configuration` (now
-- answered, #13) under the `gopls` section; an empty section means
-- "use defaults" — present, not null, which gopls prefers. Users
-- populate it (e.g. staticcheck, analyses) from init.lua.
pmacs.lsp.config.go = pmacs.lsp.config.go or {
command = "gopls",
args = {},
settings = { gopls = {} },
}
-- 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
@ -56,6 +83,16 @@ pmacs.lsp.config.python = pmacs.lsp.config.python or {
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"
-- C. `.h` is ambiguous C/C++; default it to C (clangd copes either
-- way, and users can remap `pmacs.lsp.filetypes.h = "cpp"`).
pmacs.lsp.filetypes.c = pmacs.lsp.filetypes.c or "c"
pmacs.lsp.filetypes.h = pmacs.lsp.filetypes.h or "c"
-- C++.
for _, ext in ipairs({ "cpp", "cc", "cxx", "hpp", "hh", "hxx", "ipp", "inl", "cppm" }) do
pmacs.lsp.filetypes[ext] = pmacs.lsp.filetypes[ext] or "cpp"
end
-- Go.
pmacs.lsp.filetypes.go = pmacs.lsp.filetypes.go or "go"
-- Per-buffer attachment record: { language, server, uri, version }.
-- Keyed by `tostring(BufferIdLua)` because BufferIdLua hands out fresh

View File

@ -1190,6 +1190,70 @@ fn m4_5_basedpyright_initializes_and_negotiates_encoding() {
let _ = mgr.borrow_mut().stop(sid);
}
/// Shared body for the PATH-gated real-server smoke tests: spawn,
/// reach `Initialized`, and assert the server negotiated a
/// `positionEncoding` pmacs can actually encode (absent ⇒ pmacs
/// defaults to UTF-16, also fine; a third encoding must never slip
/// through). Mirrors the basedpyright test for clangd / gopls —
/// strict-by-default servers that exercise the Option B path against
/// real implementations, not just the fake.
fn assert_lsp_initializes_and_negotiates(
label: &str,
language_id: &str,
command: &str,
args: &[&str],
) {
let (sup, mgr) = make_lsp_test_manager();
let mut spec = LspServerSpec::new(label, language_id, command);
spec.args = args.iter().map(|s| (*s).to_string()).collect();
spec.restart = LspRestartPolicy::Never;
let sid = mgr.borrow_mut().spawn(spec).expect("spawn server");
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");
let enc = caps.get("positionEncoding").and_then(|v| v.as_str());
assert!(
matches!(enc, None | Some("utf-8" | "utf-16")),
"{label} 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);
}
/// C/C++ via clangd (PATH-gated). clangd defaults UTF-16 and also
/// supports its own `offsetEncoding` extension; either way the
/// negotiated encoding must be one pmacs encodes.
#[test]
fn m4_5_clangd_initializes_and_negotiates_encoding() {
let Ok(_) = which_binary("clangd") else {
eprintln!("clangd not on PATH; skipping");
return;
};
assert_lsp_initializes_and_negotiates("clangd", "cpp", "clangd", &["--background-index"]);
}
/// Go via gopls (PATH-gated). gopls implements LSP 3.17
/// position-encoding, defaults UTF-16, and pulls config via
/// `workspace/configuration` — exercises the full stack end to end.
#[test]
fn m4_5_gopls_initializes_and_negotiates_encoding() {
let Ok(_) = which_binary("gopls") else {
eprintln!("gopls not on PATH; skipping");
return;
};
assert_lsp_initializes_and_negotiates("gopls", "go", "gopls", &[]);
}
/// 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> {