fix(lsp): PR #114 round 1 — .cuh AST via fallbackFlags, real C/C++ highlights
Two functional gaps from review:
1. Standalone .cuh files got no clangd AST. clangd selects the
compiler language from the file extension, not the LSP languageId:
it knows .cu (-> -x cuda) but not .cuh, so a header with no compile
command fails with fe_expected_compiler_job. config.cuda now sets
init_options.fallbackFlags = { "-xcuda" }, which supplies -x cuda
for any file this server opens that lacks a compile_commands.json
entry (a real compile command still wins). This CUDA server only
ever serves .cu/.cuh, so the fallback cannot mis-flag C/C++.
2. The CUDA highlights query was only a delta. tree-sitter-cuda's
HIGHLIGHTS_QUERY opens with `; inherits: cpp` and defines only the
CUDA-specific captures (launch brackets, __global__/__device__) —
two capture classes. pmacs does not resolve `inherits:`, so ordinary
C/C++ syntax went unhighlighted. LanguageEntry.highlights_query is
now &[&str] (fragments joined base-first); the cuda entry carries
[c, cpp, cuda], compiling to ~16 capture classes. Fragments are
newline-joined, never bare-concatenated — a fragment can end mid
`; comment`, and abutting the next fragment's first token would
corrupt the query. Existing single-query grammars become one-element
slices (byte-identical effective query; no behavior change).
Tests:
- cuda_highlights_resolve_c_and_cpp_captures — asserts the COMPILED
cuda query carries the C base `@variable` capture and >= 8 capture
classes, not merely a non-empty query (the CUDA delta alone has 2 and
no `variable`, so this fails without the base prepend).
- builtin_languages_include_cuda — now asserts the entry composes the
c + cpp + cuda fragments.
- m4_12_default_bundle_wires_cuda — now asserts
config.cuda.init_options.fallbackFlags[1] == "-xcuda".
Gates green: fmt; clippy -D warnings; test --lib (1515); --features crdt
(1689); m4_acceptance --skip basedpyright (101); GPU (59); full
workspace sweep; git diff --check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ9FQ832QwftJXCD9LeFan
This commit is contained in:
parent
11075914f3
commit
ea3641bba2
|
|
@ -83,18 +83,26 @@ pmacs.lsp.config.cpp = pmacs.lsp.config.cpp or {
|
|||
}
|
||||
|
||||
-- CUDA (`.cu`/`.cuh`) via clangd — the same binary serves it; `config.cuda`
|
||||
-- is a separate entry only so the `language_id` sent in `didOpen` is
|
||||
-- `cuda` (clangd keys its CUDA parse mode off both the id and the `.cu`
|
||||
-- extension). Like C/C++, clangd takes the project model from
|
||||
-- `compile_commands.json` / `compile_flags.txt`, so no `settings` here.
|
||||
-- For real analysis clangd must also locate a CUDA toolkit: it probes
|
||||
-- common install roots (e.g. `/usr/local/cuda`), and a project can pin
|
||||
-- `--cuda-path=` / the GPU arch through its compile flags; absent those,
|
||||
-- navigation and hover still work but diagnostics may be noisy. Users
|
||||
-- override from init.lua before a CUDA file opens.
|
||||
-- is a separate entry so the `language_id` sent in `didOpen` is `cuda`.
|
||||
-- clangd, however, picks the compiler *language* from the file extension,
|
||||
-- NOT that `language_id`: it recognizes `.cu` (→ `-x cuda`) but not `.cuh`,
|
||||
-- so a standalone header with no compile command otherwise fails to build
|
||||
-- an AST (`fe_expected_compiler_job`). `initializationOptions.fallbackFlags
|
||||
-- = { "-xcuda" }` supplies `-x cuda` for any file this server opens that
|
||||
-- lacks a `compile_commands.json` entry, which fixes `.cuh` (and bare `.cu`)
|
||||
-- headers; a real compile command still wins where present. This server
|
||||
-- only ever serves `.cu`/`.cuh`, so the fallback can't mis-flag C/C++.
|
||||
-- Like C/C++, clangd takes the project model from `compile_commands.json`
|
||||
-- / `compile_flags.txt`, so no `settings` here. For real analysis clangd
|
||||
-- must also locate a CUDA toolkit: it probes common install roots (e.g.
|
||||
-- `/usr/local/cuda`), and a project can pin `--cuda-path=` / the GPU arch
|
||||
-- through its compile flags; absent those, navigation and hover still work
|
||||
-- but diagnostics may be noisy. Users override from init.lua before a CUDA
|
||||
-- file opens.
|
||||
pmacs.lsp.config.cuda = pmacs.lsp.config.cuda or {
|
||||
command = "clangd",
|
||||
args = { "--background-index" },
|
||||
init_options = { fallbackFlags = { "-xcuda" } },
|
||||
}
|
||||
|
||||
-- Go via gopls. `gopls` with no args serves LSP over stdio. gopls
|
||||
|
|
|
|||
113
src/syntax.rs
113
src/syntax.rs
|
|
@ -312,9 +312,9 @@ impl ParseViewHandle {
|
|||
/// [`tree_sitter::Language`] so the C-side grammar object isn't
|
||||
/// touched until the first buffer of that language is opened ---
|
||||
/// "load grammar lazily" per the M4.2 acceptance criterion. The
|
||||
/// [`Self::highlights_query`] string is `include_str!`'d at compile
|
||||
/// time so it ships in the binary; T M4.3 compiles it into a
|
||||
/// [`tree_sitter::Query`] on first highlight attach.
|
||||
/// [`Self::highlights_query`] fragments ship as `&'static str`
|
||||
/// constants in the binary; T M4.3 concatenates and compiles them
|
||||
/// into a [`tree_sitter::Query`] on first highlight attach.
|
||||
pub struct LanguageEntry {
|
||||
/// Canonical language name. Used by [`SyntaxRegistry::language`]
|
||||
/// lookups, surfaced through Lua as the grammar label.
|
||||
|
|
@ -328,10 +328,17 @@ pub struct LanguageEntry {
|
|||
/// once per registry lifetime --- the result is cached under
|
||||
/// `name` after the first invocation.
|
||||
pub loader: fn() -> tree_sitter::Language,
|
||||
/// Source of the bundled `highlights.scm` query (T M4.3). Empty
|
||||
/// string means the grammar has no highlight query (rare; in
|
||||
/// that case the highlight view runs but emits nothing).
|
||||
pub highlights_query: &'static str,
|
||||
/// Bundled `highlights.scm` query fragments (T M4.3), concatenated
|
||||
/// in order (base grammar first) to form the effective query. Most
|
||||
/// grammars ship one self-contained fragment. A grammar whose
|
||||
/// bundled query is a tree-sitter `; inherits: <lang>` delta lists
|
||||
/// the inherited base queries ahead of its own, because pmacs does
|
||||
/// not resolve `inherits:` directives — CUDA, for instance, ships a
|
||||
/// two-capture delta over C++ and must carry the C and C++ queries
|
||||
/// explicitly or ordinary C/C++ syntax goes unhighlighted. An empty
|
||||
/// slice (or all-empty fragments) means no highlights: the view
|
||||
/// runs but emits nothing.
|
||||
pub highlights_query: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// Bundled grammars (T M4.2 + M4.3). The order is significant only
|
||||
|
|
@ -340,8 +347,10 @@ pub struct LanguageEntry {
|
|||
///
|
||||
/// Adding a grammar:
|
||||
/// 1. Add `tree-sitter-foo = "X.Y"` to `Cargo.toml`.
|
||||
/// 2. Add one [`LanguageEntry`] here, including
|
||||
/// `tree_sitter_foo::HIGHLIGHTS_QUERY`.
|
||||
/// 2. Add one [`LanguageEntry`] here, with
|
||||
/// `highlights_query: &[tree_sitter_foo::HIGHLIGHTS_QUERY]` (or the
|
||||
/// inherited base queries ahead of it, if `foo`'s bundled query is
|
||||
/// a `; inherits:` delta — see the `cuda` entry).
|
||||
/// 3. (Done.) The Lua side picks up the new grammar through the
|
||||
/// `buffer.after-load` hook automatically and the highlight
|
||||
/// overlay attaches in the same step.
|
||||
|
|
@ -350,13 +359,13 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
|||
name: "rust",
|
||||
extensions: &["rs"],
|
||||
loader: || tree_sitter_rust::LANGUAGE.into(),
|
||||
highlights_query: tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
highlights_query: &[tree_sitter_rust::HIGHLIGHTS_QUERY],
|
||||
},
|
||||
LanguageEntry {
|
||||
name: "lua",
|
||||
extensions: &["lua"],
|
||||
loader: || tree_sitter_lua::LANGUAGE.into(),
|
||||
highlights_query: tree_sitter_lua::HIGHLIGHTS_QUERY,
|
||||
highlights_query: &[tree_sitter_lua::HIGHLIGHTS_QUERY],
|
||||
},
|
||||
// T M9.7: markdown grammar for prompt-result buffers with
|
||||
// `_meta.format = "markdown"`. Uses only the block grammar
|
||||
|
|
@ -375,7 +384,7 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
|||
name: "markdown",
|
||||
extensions: &["md", "markdown"],
|
||||
loader: || tree_sitter_md::LANGUAGE.into(),
|
||||
highlights_query: tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
|
||||
highlights_query: &[tree_sitter_md::HIGHLIGHT_QUERY_BLOCK],
|
||||
},
|
||||
// T M_B3 — C / C++. Lexical highlighting (keywords / strings /
|
||||
// operators) so the grid TUI shows code-shaped C++ on first open.
|
||||
|
|
@ -396,13 +405,13 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
|||
name: "c",
|
||||
extensions: &["c", "h"],
|
||||
loader: || tree_sitter_c::LANGUAGE.into(),
|
||||
highlights_query: tree_sitter_c::HIGHLIGHT_QUERY,
|
||||
highlights_query: &[tree_sitter_c::HIGHLIGHT_QUERY],
|
||||
},
|
||||
LanguageEntry {
|
||||
name: "cpp",
|
||||
extensions: &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "ipp", "inl", "cppm"],
|
||||
loader: || tree_sitter_cpp::LANGUAGE.into(),
|
||||
highlights_query: tree_sitter_cpp::HIGHLIGHT_QUERY,
|
||||
highlights_query: &[tree_sitter_cpp::HIGHLIGHT_QUERY],
|
||||
},
|
||||
// CUDA (`.cu` source, `.cuh` header). A dedicated grammar rather
|
||||
// than reusing `cpp`: CUDA extends C++ with `__global__`/`__device__`
|
||||
|
|
@ -414,11 +423,25 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
|||
// Note the const name: `tree-sitter-cuda` exposes `HIGHLIGHTS_QUERY`
|
||||
// (plural, the `tree-sitter-rust`/`tree-sitter-lua` idiom), NOT the
|
||||
// singular `HIGHLIGHT_QUERY` that `tree-sitter-c`/`-cpp`/`-md` use.
|
||||
//
|
||||
// The CUDA `highlights.scm` opens with `; inherits: cpp` and defines
|
||||
// only the CUDA-specific captures (`<<<...>>>` launch brackets, the
|
||||
// `__global__`/`__device__` modifiers) — two capture classes on its
|
||||
// own. pmacs does not resolve `inherits:`, so the C and C++ base
|
||||
// queries are prepended explicitly; the three compile together into
|
||||
// ~16 capture classes against the CUDA grammar (which is a superset
|
||||
// of C++). Order is base-first (C, then C++, then CUDA) so later
|
||||
// fragments refine earlier ones. Without this, ordinary C/C++ syntax
|
||||
// in a `.cu` file would go almost entirely unhighlighted.
|
||||
LanguageEntry {
|
||||
name: "cuda",
|
||||
extensions: &["cu", "cuh"],
|
||||
loader: || tree_sitter_cuda::LANGUAGE.into(),
|
||||
highlights_query: tree_sitter_cuda::HIGHLIGHTS_QUERY,
|
||||
highlights_query: &[
|
||||
tree_sitter_c::HIGHLIGHT_QUERY,
|
||||
tree_sitter_cpp::HIGHLIGHT_QUERY,
|
||||
tree_sitter_cuda::HIGHLIGHTS_QUERY,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -617,14 +640,19 @@ impl SyntaxRegistry {
|
|||
}
|
||||
let language = self.language(lang_name)?;
|
||||
let entry = BUILTIN_LANGUAGES.iter().find(|e| e.name == lang_name);
|
||||
let source = entry.map_or("", |e| e.highlights_query);
|
||||
if source.is_empty() {
|
||||
// Fragments are joined with a newline, never bare-concatenated: a
|
||||
// fragment can end mid-`; comment` or without a trailing newline,
|
||||
// and abutting it against the next fragment's first token would
|
||||
// corrupt the query (e.g. `@variable; Functions` swallows the
|
||||
// next line into a comment).
|
||||
let source = entry.map_or_else(String::new, |e| e.highlights_query.join("\n"));
|
||||
if source.trim().is_empty() {
|
||||
self.queries
|
||||
.borrow_mut()
|
||||
.insert(lang_name.to_owned(), Err("no highlights query".to_owned()));
|
||||
return None;
|
||||
}
|
||||
let compiled = tree_sitter::Query::new(&language, source)
|
||||
let compiled = tree_sitter::Query::new(&language, &source)
|
||||
.map(Arc::new)
|
||||
.map_err(|e| format!("compile {lang_name} highlights: {e:?}"));
|
||||
let result = compiled.as_ref().ok().cloned();
|
||||
|
|
@ -818,10 +846,10 @@ mod tests {
|
|||
#[test]
|
||||
fn builtin_languages_include_cuda() {
|
||||
// Regression guard mirroring `builtin_languages_include_c_and_cpp`:
|
||||
// the CUDA entry must keep claiming its canonical extensions and
|
||||
// shipping a highlights query, so `.cu`/`.cuh` get lexical
|
||||
// styling (with clangd's semantic tokens layered on top) instead
|
||||
// of falling back to no grammar at all.
|
||||
// the CUDA entry must keep claiming its canonical extensions and,
|
||||
// because its bundled query is a `; inherits: cpp` delta, prepend
|
||||
// the C and C++ base queries explicitly (see the entry comment and
|
||||
// `cuda_highlights_resolve_c_and_cpp_captures`).
|
||||
let cuda = BUILTIN_LANGUAGES
|
||||
.iter()
|
||||
.find(|l| l.name == "cuda")
|
||||
|
|
@ -829,8 +857,45 @@ mod tests {
|
|||
assert!(cuda.extensions.contains(&"cu"), "`cuda` claims `.cu`");
|
||||
assert!(cuda.extensions.contains(&"cuh"), "`cuda` claims `.cuh`");
|
||||
assert!(
|
||||
!cuda.highlights_query.is_empty(),
|
||||
"`cuda` ships a non-empty highlights query"
|
||||
cuda.highlights_query
|
||||
.contains(&tree_sitter_c::HIGHLIGHT_QUERY),
|
||||
"`cuda` prepends the C base highlights (it does not resolve `inherits:`)"
|
||||
);
|
||||
assert!(
|
||||
cuda.highlights_query
|
||||
.contains(&tree_sitter_cpp::HIGHLIGHT_QUERY),
|
||||
"`cuda` prepends the C++ base highlights"
|
||||
);
|
||||
assert!(
|
||||
cuda.highlights_query
|
||||
.contains(&tree_sitter_cuda::HIGHLIGHTS_QUERY),
|
||||
"`cuda` carries its own CUDA-specific highlights delta"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cuda_highlights_resolve_c_and_cpp_captures() {
|
||||
// Finding-2 regression: the bundled CUDA `highlights.scm` is only
|
||||
// a two-capture `; inherits: cpp` delta (launch brackets + CUDA
|
||||
// modifiers). pmacs does not resolve `inherits:`, so the entry
|
||||
// prepends the C and C++ base queries; assert the COMPILED query
|
||||
// actually carries ordinary C/C++ captures (the C base's
|
||||
// `@variable`) and far more than the delta's two capture classes —
|
||||
// not merely that some query is non-empty.
|
||||
let reg = SyntaxRegistry::new();
|
||||
let query = reg
|
||||
.highlights_query("cuda")
|
||||
.expect("cuda highlights compile");
|
||||
let names = query.capture_names();
|
||||
assert!(
|
||||
names.contains(&"variable"),
|
||||
"combined query carries the C base `@variable` capture; got {names:?}"
|
||||
);
|
||||
assert!(
|
||||
names.len() >= 8,
|
||||
"combined C+C+++CUDA query resolves many capture classes, not the \
|
||||
CUDA delta's two; got {} ({names:?})",
|
||||
names.len()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5760,7 +5760,15 @@ fn m4_12_default_bundle_wires_cuda() {
|
|||
.load(
|
||||
r"
|
||||
local out = {}
|
||||
out.cfg_cmd = pmacs.lsp.config.cuda and pmacs.lsp.config.cuda.command
|
||||
local cfg = pmacs.lsp.config.cuda
|
||||
out.cfg_cmd = cfg and cfg.command
|
||||
-- `.cuh` (and bare `.cu`) headers are not recognized as CUDA
|
||||
-- by clangd's extension-based language selection, so the
|
||||
-- server must pass `-x cuda` via fallbackFlags for files with
|
||||
-- no compile command.
|
||||
out.fallback = cfg and cfg.init_options
|
||||
and cfg.init_options.fallbackFlags
|
||||
and cfg.init_options.fallbackFlags[1]
|
||||
out.ft_cu = pmacs.lsp.filetypes.cu
|
||||
out.ft_cuh = pmacs.lsp.filetypes.cuh
|
||||
-- Grammar-backed detection (bundled CUDA grammar) wins first.
|
||||
|
|
@ -5776,6 +5784,11 @@ fn m4_12_default_bundle_wires_cuda() {
|
|||
"clangd",
|
||||
"config.cuda targets clangd"
|
||||
);
|
||||
assert_eq!(
|
||||
probe.get::<String>("fallback").unwrap(),
|
||||
"-xcuda",
|
||||
"config.cuda forces `-x cuda` so standalone `.cuh`/`.cu` headers get an AST"
|
||||
);
|
||||
assert_eq!(probe.get::<String>("ft_cu").unwrap(), "cuda");
|
||||
assert_eq!(probe.get::<String>("ft_cuh").unwrap(), "cuda");
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Reference in New Issue