From 0935efd4546085abc26cdab74bd8927168d2c942 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 19 May 2026 20:39:07 -0400 Subject: [PATCH] M_B3: tree-sitter-cpp/c + dual-authority TUI styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the policy-A exclusivity that left grammar-backed languages without LSP semantic refinement. Adds tree-sitter-c (.c/.h) and tree-sitter-cpp (.cpp/.cc/.cxx/.hpp/...) to the bundle so the grid TUI gets lexical highlighting (keywords / strings / operators) on first open. The Lua attach in builtin/runtime/lsp.lua now pushes LspStyleView whenever an LSP server is up, regardless of grammar presence; with both views attached the cell-painter pipeline runs SyntaxHighlightView first (lexical) then LspStyleView (semantic) and their styles compose through crate::overlay::merge_styles. The result is the VSCode / Zed "TextMate + LSP semantic tokens" model on a terminal grid: keywords colored by tree-sitter, identifiers refined by clangd's semantic tokens. `.h` is ambiguous C / C++; the `c` BUILTIN_LANGUAGES entry claims it to match the LSP filetype map's default. Users who want `.h` parsed as C++ can override via Lua (extension → language map). Note the tree-sitter-c / -cpp crates expose `HIGHLIGHT_QUERY` (singular), matching tree-sitter-md's `HIGHLIGHT_QUERY_BLOCK` convention; tree-sitter-rust / -lua use `HIGHLIGHTS_QUERY` (plural). Same bundled highlights.scm either way. Regression guard: builtin_languages_include_c_and_cpp asserts the language entries exist and claim their canonical extensions. The LspStyleView module doc rewritten to reflect dual-authority composition; the existing headline test's comment updated (the test fixture still attaches only LspStyleView directly, so its asserted cells reflect the LSP authority alone — Lua-level attach_buffer is what exercises composition end-to-end). Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 22 +++++++++++++++ Cargo.toml | 8 ++++++ builtin/runtime/lsp.lua | 21 ++++++++------- src/highlight.rs | 37 +++++++++++++++++++------- src/syntax.rs | 59 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34e2b4a..9e307e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,6 +1264,8 @@ dependencies = [ "thiserror 2.0.18", "toml", "tree-sitter", + "tree-sitter-c", + "tree-sitter-cpp", "tree-sitter-lua", "tree-sitter-md", "tree-sitter-rust", @@ -2039,6 +2041,26 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-c" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2eb57a55fed6b00812912e730b7a275cf4fe98bfd6a5d76263d4438371728" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-cpp" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-language" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 6e39e54..02294ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,14 @@ tree-sitter = "0.26" # `crate::syntax::BUILTIN_LANGUAGES`. tree-sitter-rust = "0.24" tree-sitter-lua = "0.5" +# T M_B3 — C / C++. Bundled so opening a `.c`/`.cpp`/header file in +# the grid TUI gets lexical highlighting (keywords/strings/operators) +# via tree-sitter, with `LspStyleView` layered on top for semantic +# refinement (functions/types/macros via clangd's semantic tokens). +# `.h` is ambiguous C / C++; the `c` entry below claims it by default, +# matching the LSP filetype map in `builtin/runtime/lsp.lua`. +tree-sitter-c = "0.24" +tree-sitter-cpp = "0.23" # T M9.7: markdown grammar so prompt result buffers with # `_meta.format = "markdown"` get structured highlighting through the # same M4 path as rust/lua — no special-case painter in Lua. diff --git a/builtin/runtime/lsp.lua b/builtin/runtime/lsp.lua index b36f724..8161e7f 100644 --- a/builtin/runtime/lsp.lua +++ b/builtin/runtime/lsp.lua @@ -347,16 +347,17 @@ local function attach_buffer(buf) -- did_open is a notification; the manager queues it cleanly even -- while the server is in `starting` / `initializing`. pcall(pmacs.lsp.did_open, sid, uri, rec.version, active_buffer_text()) - -- M_B1: policy A — when this buffer has no bundled tree-sitter - -- grammar (C/C++, …), push the LSP-driven style overlay onto the - -- active window so the grid TUI gets cell coloring from LSP - -- semantic tokens. Grammar-backed languages already have - -- `SyntaxHighlightView`; never both authorities on one buffer. - -- `language_for_path` returning nil is the grammar-less signal - -- (the LSP filetype map is consulted independently above). - if not styled_buffers[key] - and not pmacs.parse.language_for_path(path) - then + -- M_B3: dual-authority styling. Always push the LSP style overlay + -- when an LSP server is up — whether or not the buffer has a + -- bundled tree-sitter grammar. When the grammar exists too, + -- `SyntaxHighlightView` paints first (lexical: keywords, strings, + -- operators) and `LspStyleView` paints after (semantic: function / + -- type / macro / namespace identifiers from clangd's tokens); their + -- styles compose via `crate::overlay::merge_styles`, so the final + -- cell carries both authorities' contributions. This replaces + -- M_B1's policy-A exclusivity, which left grammar-backed languages + -- (Rust, C, C++) without LSP semantic refinement. + if not styled_buffers[key] then local ok, attached = pcall(pmacs.lsp._attach_style, buf) if ok and attached then styled_buffers[key] = true end end diff --git a/src/highlight.rs b/src/highlight.rs index b925bd0..a995a45 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -464,13 +464,28 @@ pub fn is_default_style(style: Style) -> bool { // LspStyleView // --------------------------------------------------------------------------- -/// View that paints LSP semantic-token styling into cells for buffers -/// with **no bundled tree-sitter grammar** (C/C++, …). Sibling of -/// [`SyntaxHighlightView`] for the same policy-A "one styling -/// authority per language" rule that `SemanticRenderState` applies on -/// the semantic-frontend side — but feeding the grid TUI's existing -/// cell-painter pipeline instead. Closes the visible "no syntax -/// coloring for grammar-less languages in the TUI" gap. +/// View that paints LSP semantic-token styling into cells, layered +/// **alongside** [`SyntaxHighlightView`] when both exist for a +/// buffer. The grid TUI's `View::render` composition stack means +/// tree-sitter paints first (lexical: keywords, strings, operators, +/// the things a parser identifies from the source's *shape*) and +/// `LspStyleView` paints after (semantic: function vs variable, +/// macro vs constant, the things a language server identifies from +/// program *meaning*). Their styles compose through +/// [`crate::overlay::merge_styles`], so the final cell carries both +/// authorities' contributions — the `VSCode` / `Zed` "`TextMate` + +/// LSP semantic tokens" model, on a terminal grid. +/// +/// For languages with no bundled tree-sitter grammar (Python, Go, +/// languages added later via LSP only), `LspStyleView` is the *sole* +/// styling source and still works — the merge with an absent +/// tree-sitter overlay is identity. +/// +/// `M_B3` (this) dropped the earlier policy-A exclusivity gate in +/// `lsp.lua` that restricted attachment to grammar-less buffers. The +/// previous gate left grammar-backed languages (Rust, C, C++) without +/// LSP semantic refinement; dual-authority delivers richer coloring +/// without conflict. /// /// Holds shared handles only (`SharedLspManager` + `ThemeHandle`), so /// it survives buffer renames and LSP server restarts: every `render` @@ -763,9 +778,11 @@ mod tests { let state = EditorState::new(); let buffer_id = state.core.borrow().active_window().buffer_id; - // Seed the buffer: one line, `.cpp` path (no bundled grammar → - // SyntaxHighlightView never attaches, the LSP path owns the - // styling per policy A). + // Seed the buffer: one line, `.cpp` path. The test renders + // `LspStyleView` directly (no `SyntaxHighlightView` attached + // alongside in this fixture), so the asserted cells reflect + // the LSP authority alone — M_B3's dual-authority composition + // is exercised end-to-end by Lua's `attach_buffer`, not here. { let mut core = state.core.borrow_mut(); core.registry diff --git a/src/syntax.rs b/src/syntax.rs index 6ae7773..1f9264e 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -377,6 +377,33 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[ loader: || tree_sitter_md::LANGUAGE.into(), 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. + // `LspStyleView` layers on top with semantic refinement + // (functions / types / macros / namespaces) from clangd's + // semantic tokens — the two views' styles merge via + // `crate::overlay::merge_styles`. + // + // `.h` is ambiguous C / C++; the `c` entry below claims it + // (matches the LSP filetype map's default in `lsp.lua`). Users + // who want `.h` parsed as C++ can override via Lua. + // Note the const names: `tree-sitter-c` and `tree-sitter-cpp` + // expose `HIGHLIGHT_QUERY` (singular), matching the `tree-sitter-md` + // crate's `HIGHLIGHT_QUERY_BLOCK` style; `tree-sitter-rust` and + // `tree-sitter-lua` use `HIGHLIGHTS_QUERY` (plural). No semantic + // difference — same bundled `highlights.scm` either way. + LanguageEntry { + name: "c", + extensions: &["c", "h"], + loader: || tree_sitter_c::LANGUAGE.into(), + 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, + }, ]; /// Registry that the Lua surface ([`crate::lua_bindings::install_parse`]) @@ -712,6 +739,38 @@ mod tests { bundle } + #[test] + fn builtin_languages_include_c_and_cpp() { + // M_B3 regression guard: a future refactor must not silently + // drop C / C++ tree-sitter coverage — the dual-authority + // styling in the TUI depends on these entries existing and + // claiming their canonical extensions. + let c = BUILTIN_LANGUAGES + .iter() + .find(|l| l.name == "c") + .expect("`c` language entry must be present"); + assert!(c.extensions.contains(&"c"), "`c` claims `.c`"); + assert!( + c.extensions.contains(&"h"), + "`c` claims `.h` (matches the LSP filetype map's default)" + ); + assert!( + !c.highlights_query.is_empty(), + "`c` ships a non-empty highlights query" + ); + let cpp = BUILTIN_LANGUAGES + .iter() + .find(|l| l.name == "cpp") + .expect("`cpp` language entry must be present"); + for ext in ["cpp", "cc", "cxx", "hpp", "hh", "hxx"] { + assert!(cpp.extensions.contains(&ext), "`cpp` claims `.{ext}`"); + } + assert!( + !cpp.highlights_query.is_empty(), + "`cpp` ships a non-empty highlights query" + ); + } + #[test] fn byte_to_point_handles_first_line() { let src = b"hello world";