T M4.5: Tier 1 language-server configs (ts/js, lua, bash, toml, zig)

Ship single-binary LSP servers pre-wired in the default bundle so a
user who installs the server gets attachment with no init.lua:

- typescript-language-server (--stdio) for the typescript /
  typescriptreact / javascript / javascriptreact language ids
- lua-language-server (settings.Lua present-not-null for the
  workspace/configuration pull)
- bash-language-server (start subcommand)
- taplo (lsp stdio; settings.taplo present-not-null)
- zls (no args)

Plus the pmacs.lsp.filetypes extension->language map entries
(ts/mts/cts, tsx, js/mjs/cjs, jsx, sh, bash, toml, zig, zon, lua),
keeping the same idempotent `or` guard so init.lua overrides win.

m4_25 asserts every config table and the filetype map resolve to
the documented values (binary-independent, spawns nothing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-19 15:15:41 -04:00
parent 3f9edb2e57
commit e0e176fe4d
2 changed files with 167 additions and 0 deletions

View File

@ -84,6 +84,65 @@ pmacs.lsp.config.go = pmacs.lsp.config.go or {
settings = { gopls = {} },
}
-- TypeScript / JavaScript via typescript-language-server (the
-- tsserver wrapper). One binary serves the whole family; like
-- `c`/`cpp` these are separate config entries purely so the
-- `language_id` sent in `didOpen` is accurate — tsserver keys
-- diagnostics and some code actions off it (typescript /
-- typescriptreact / javascript / javascriptreact). `--stdio` is the
-- LSP transport. The project model comes from `tsconfig.json` /
-- `jsconfig.json` (no `workspace/configuration` pull). Users
-- override from init.lua before a TS/JS file opens; swapping to
-- `vtsls` is just `command = "vtsls"`.
for _, lid in ipairs({ "typescript", "typescriptreact", "javascript", "javascriptreact" }) do
pmacs.lsp.config[lid] = pmacs.lsp.config[lid] or {
command = "typescript-language-server",
args = { "--stdio" },
}
end
-- Lua via lua-language-server (sumneko). Speaks LSP over stdio with
-- no transport flag. It pulls configuration via
-- `workspace/configuration` under the `Lua` section; an empty table
-- is "use defaults" — present, not null (same rationale as gopls).
-- `.lua` is tree-sitter-grammar-backed, so `pmacs.parse` already
-- resolves the `lua` language and this is the config that attaches.
-- Users populate `settings.Lua` (runtime.version, workspace.library,
-- diagnostics.globals = { "pmacs" }, …) from init.lua.
pmacs.lsp.config.lua = pmacs.lsp.config.lua or {
command = "lua-language-server",
args = {},
settings = { Lua = {} },
}
-- Bash / shell via bash-language-server. `start` is its
-- LSP-over-stdio subcommand. No project config; it shells out to
-- `shellcheck` (diagnostics) and `shfmt` (formatting) when those are
-- on PATH. Users override from init.lua before a shell file opens.
pmacs.lsp.config.bash = pmacs.lsp.config.bash or {
command = "bash-language-server",
args = { "start" },
}
-- TOML via taplo. `taplo lsp stdio` serves LSP over stdio. taplo
-- pulls configuration via `workspace/configuration` under the
-- `taplo` section (empty ⇒ defaults, present not null); a project
-- `.taplo.toml` still wins. Users populate it from init.lua.
pmacs.lsp.config.toml = pmacs.lsp.config.toml or {
command = "taplo",
args = { "lsp", "stdio" },
settings = { taplo = {} },
}
-- Zig via zls. `zls` with no args serves LSP over stdio; it reads
-- `zls.json` / `build.zig` for the project model (no
-- `workspace/configuration` pull). Users override from init.lua
-- before a `.zig` / `.zon` file opens.
pmacs.lsp.config.zig = pmacs.lsp.config.zig or {
command = "zls",
args = {},
}
-- 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
@ -103,6 +162,28 @@ for _, ext in ipairs({ "cpp", "cc", "cxx", "hpp", "hh", "hxx", "ipp", "inl", "cp
end
-- Go.
pmacs.lsp.filetypes.go = pmacs.lsp.filetypes.go or "go"
-- Tier 1 single-binary servers. TypeScript / JavaScript distinguish
-- the JSX variants so the server enables the JSX parser.
for _, ext in ipairs({ "ts", "mts", "cts" }) do
pmacs.lsp.filetypes[ext] = pmacs.lsp.filetypes[ext] or "typescript"
end
pmacs.lsp.filetypes.tsx = pmacs.lsp.filetypes.tsx or "typescriptreact"
for _, ext in ipairs({ "js", "mjs", "cjs" }) do
pmacs.lsp.filetypes[ext] = pmacs.lsp.filetypes[ext] or "javascript"
end
pmacs.lsp.filetypes.jsx = pmacs.lsp.filetypes.jsx or "javascriptreact"
-- Lua (lua-language-server). pmacs bundles a Lua grammar, so
-- `language_for_path` resolves `.lua` first; this entry is the LSP
-- fallback and keeps the language id stable if that ever changes.
pmacs.lsp.filetypes.lua = pmacs.lsp.filetypes.lua or "lua"
-- Bash / shell (bash-language-server).
pmacs.lsp.filetypes.sh = pmacs.lsp.filetypes.sh or "bash"
pmacs.lsp.filetypes.bash = pmacs.lsp.filetypes.bash or "bash"
-- TOML (taplo).
pmacs.lsp.filetypes.toml = pmacs.lsp.filetypes.toml or "toml"
-- Zig (zls). `.zon` is Zig Object Notation, handled by the same server.
pmacs.lsp.filetypes.zig = pmacs.lsp.filetypes.zig or "zig"
pmacs.lsp.filetypes.zon = pmacs.lsp.filetypes.zon or "zig"
-- Per-buffer attachment record: { language, server, uri, version }.
-- Keyed by `tostring(BufferIdLua)` because BufferIdLua hands out fresh

View File

@ -4364,6 +4364,92 @@ fn m4_24_workspace_did_change_watched_files() {
);
}
/// Tier 1 single-binary language servers ship pre-configured in the
/// default bundle. Binary-independent: we don't spawn anything, just
/// assert the `pmacs.lsp.config` tables and the `pmacs.lsp.filetypes`
/// extension→language map resolve to the documented values, so a user
/// who installs `typescript-language-server` / `lua-language-server` /
/// `bash-language-server` / `taplo` / `zls` gets attachment with no
/// init.lua.
#[test]
fn m4_25_tier1_language_server_configs_and_filetypes() {
use pmacs::editor::EditorState;
let s = EditorState::new();
let probe: mlua::Table = s
.lua_host
.lua()
.load(
r#"
local out = {}
local c = pmacs.lsp.config
local ft = pmacs.lsp.filetypes
-- TypeScript / JavaScript family: one binary, four language
-- ids so didOpen reports the right one. --stdio transport.
local tsjs_ok = true
for _, lid in ipairs({ "typescript", "typescriptreact",
"javascript", "javascriptreact" }) do
local e = c[lid]
if not (e and e.command == "typescript-language-server"
and e.args and e.args[1] == "--stdio") then
tsjs_ok = false
end
end
out.tsjs = tsjs_ok
-- Lua: lua-language-server, no transport flag, settings.Lua
-- present-not-null for the workspace/configuration pull.
out.lua = c.lua ~= nil
and c.lua.command == "lua-language-server"
and type(c.lua.settings) == "table"
and type(c.lua.settings.Lua) == "table"
-- Bash: bash-language-server start.
out.bash = c.bash ~= nil
and c.bash.command == "bash-language-server"
and c.bash.args and c.bash.args[1] == "start"
-- TOML: taplo lsp stdio, settings.taplo present-not-null.
out.toml = c.toml ~= nil
and c.toml.command == "taplo"
and c.toml.args and c.toml.args[1] == "lsp"
and c.toml.args[2] == "stdio"
and type(c.toml.settings) == "table"
and type(c.toml.settings.taplo) == "table"
-- Zig: zls, no args.
out.zig = c.zig ~= nil and c.zig.command == "zls"
-- Extension language map.
out.ft = ft.ts == "typescript"
and ft.mts == "typescript"
and ft.cts == "typescript"
and ft.tsx == "typescriptreact"
and ft.js == "javascript"
and ft.mjs == "javascript"
and ft.cjs == "javascript"
and ft.jsx == "javascriptreact"
and ft.sh == "bash"
and ft.bash == "bash"
and ft.toml == "toml"
and ft.zig == "zig"
and ft.zon == "zig"
and ft.lua == "lua"
return out
"#,
)
.eval()
.expect("probe tier1 config + filetypes");
assert!(probe.get::<bool>("tsjs").unwrap(), "ts/js family config");
assert!(probe.get::<bool>("lua").unwrap(), "lua config");
assert!(probe.get::<bool>("bash").unwrap(), "bash config");
assert!(probe.get::<bool>("toml").unwrap(), "toml config");
assert!(probe.get::<bool>("zig").unwrap(), "zig config");
assert!(probe.get::<bool>("ft").unwrap(), "filetype map");
}
/// Default LSP bundle (`builtin/runtime/lsp.lua`) is wired in: the
/// hooks are defined, the namespace tables exist, the user-facing
/// commands are registered with the command registry, and the default