M_B3: tree-sitter-cpp/c + dual-authority TUI styling

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) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-19 20:39:07 -04:00
parent 98923bf250
commit 0935efd454
5 changed files with 127 additions and 20 deletions

22
Cargo.lock generated
View File

@ -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"

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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";