fix(highlight): PR #116 round 2 — pin grammar across switch, attached env -S
Two follow-ups from review, both in builtin/runtime/syntax.lua. 1. [P2] Buffer switching bypassed the pinned grammar. after-edit already reparsed the pinned language, but the after-switch reattach path (attach_for_active_buffer) re-resolved from scratch — so open an extensionless `#!/bin/sh` (bash), edit its shebang to lua, switch away and back, and the grammar flipped to lua while the LSP side kept its bash attachment (lsp.lua's after-switch reuses the existing record). attach_for_active_buffer now reuses the language pinned at first attach whenever a parse view already exists; only a first-seen buffer resolves. A language change still needs a close/reopen, matching both the after-edit behavior and how extensions work. 2. [P2] Attached `env -S`/`--split-string` forms failed. The walk skipped the whole option token, but for split-string the interpreter rides inside it: `-Spython3`, `-vSpython3` (after no-operand short flags i/v/0), and `--split-string=python3` all resolved to nil (the last was also eaten by the earlier `=` branch). The env walk now extracts the interpreter from the attached value (`^-[iv0]*S(.+)$` / `^--split-string=(.+)$`); the separated forms (`-S python3`) still work by walking on to the next token. Tests (bite-verified against the round-1 syntax.lua — both fail there; scripts/bite HEAD builtin/runtime/syntax.lua): - m4_shebang_edit_keeps_pinned_grammar now adds a switch-away/back cycle (via pmacs.window.switch_buffer, which fires after-switch synchronously) and asserts the tree stays bash. - m4_shebang_resolver_maps_interpreters adds the attached split-string cases (`-Spython3`, `--split-string=python3`, `-vSpython3`). Gates: fmt; clippy -D warnings; m4_acceptance --skip basedpyright; GPU; git diff --check green. Only-known-flake caveat as round 1 (editor::composition_overhead_under_ten_percent perf microbenchmark, unrelated to this Lua change). Change is Lua-only plus the acceptance tests. 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
f300b77533
commit
558d00020f
|
|
@ -99,21 +99,34 @@ function pmacs.parse.language_from_shebang(buf)
|
|||
local seen_env = false
|
||||
local skip_next = false
|
||||
for tok in rest:gmatch("%S+") do
|
||||
-- `-S`/`--split-string` introduces a string whose FIRST word is the
|
||||
-- interpreter. GNU env accepts that value ATTACHED — `-Spython3`,
|
||||
-- `-vSpython3` (after no-operand short flags i/v/0), or
|
||||
-- `--split-string=python3` — where the interpreter rides inside the
|
||||
-- option token. Extract it; the separated forms (`-S python3`) are
|
||||
-- handled by simply walking on to the next token.
|
||||
local split_attached =
|
||||
tok:match("^%-[iv0]*S(.+)$") or tok:match("^%-%-split%-string=(.+)$")
|
||||
if not seen_env then
|
||||
seen_env = true -- the `env` path token itself
|
||||
elseif skip_next then
|
||||
skip_next = false -- the operand consumed by the previous option
|
||||
elseif split_attached then
|
||||
local word = split_attached:match("^(%S+)")
|
||||
base = word and (word:match("([^/]+)$") or word)
|
||||
break
|
||||
elseif tok == "-S" or tok == "--split-string" then
|
||||
-- Separated split-string: the next token starts the string, i.e.
|
||||
-- the interpreter — keep walking.
|
||||
elseif tok:find("=", 1, true) then
|
||||
-- `VAR=value` env assignment, or a `--long=value` option: both
|
||||
-- `VAR=value` env assignment, or another `--long=value` option:
|
||||
-- self-contained, skip.
|
||||
elseif tok:sub(1, 1) == "-" then
|
||||
-- An option. A few GNU-env short options and their long forms
|
||||
-- consume the *next* token as an operand (`-u NAME`, `-C DIR`,
|
||||
-- `-a NAME`); skip that operand too, or its value is mistaken for
|
||||
-- the interpreter. `-S`/`--split-string` is deliberately absent:
|
||||
-- the string it introduces contains the interpreter, which the
|
||||
-- walk then picks up. An option with an attached operand
|
||||
-- (`-uNAME`) is one self-contained token and needs no skip.
|
||||
-- the interpreter. An option with an attached operand (`-uNAME`)
|
||||
-- is one self-contained token and needs no skip.
|
||||
if tok == "-u" or tok == "-C" or tok == "-a"
|
||||
or tok == "--unset" or tok == "--chdir" or tok == "--argv0" then
|
||||
skip_next = true
|
||||
|
|
@ -159,11 +172,18 @@ end
|
|||
local function attach_for_active_buffer()
|
||||
local buf = pmacs.window.buffer()
|
||||
if not buf then return end
|
||||
-- Gate dispatch on `_has_language`: the chain above also resolves
|
||||
-- languages with no grammar (python, javascript), and dispatching one
|
||||
-- would raise "unknown language" (caught, but noise) — and an
|
||||
-- extensionless script must never get a wrong-grammar parse tree.
|
||||
local lang = resolve_active_language(buf)
|
||||
local key = tostring(buf)
|
||||
-- Reuse the language pinned at first attach if this buffer already has
|
||||
-- a parse view. A switch-away/back re-runs this hook (via after-switch);
|
||||
-- re-resolving there would re-sniff a shebang the user has since edited
|
||||
-- and silently swap the grammar — and diverge from the LSP side, which
|
||||
-- keeps its existing attachment across the switch. A first-seen buffer
|
||||
-- (no view yet) resolves normally. Gate dispatch on `_has_language`: the
|
||||
-- resolution chain also yields languages with no grammar (python,
|
||||
-- javascript), and dispatching one would raise "unknown language"
|
||||
-- (caught, but noise) and never gives a wrong-grammar tree.
|
||||
local lang = pmacs.parse._has_view(buf) and parse_lang_by_buffer[key]
|
||||
or resolve_active_language(buf)
|
||||
if not lang or not pmacs.parse._has_language(lang) then return end
|
||||
pmacs.parse._dispatch(buf, lang)
|
||||
-- T M4.3: install the syntax-highlight overlay for this buffer.
|
||||
|
|
@ -174,8 +194,8 @@ local function attach_for_active_buffer()
|
|||
-- table so we only push once per (buffer, after-load) cycle).
|
||||
-- `tostring(buf)` is stable per BufferId (the metamethod
|
||||
-- formats the wrapped id), so it's a safe table-key
|
||||
-- replacement for a `:id()` method we don't have to expose.
|
||||
local key = tostring(buf)
|
||||
-- replacement for a `:id()` method we don't have to expose (`key` is
|
||||
-- computed once at the top of this function).
|
||||
if not highlighted_buffers[key] then
|
||||
local ok = pmacs.parse._attach_highlight(buf, lang)
|
||||
if ok then highlighted_buffers[key] = true end
|
||||
|
|
|
|||
|
|
@ -5878,6 +5878,11 @@ fn m4_shebang_resolver_maps_interpreters() {
|
|||
("#!/usr/bin/env bash\n", "bash"),
|
||||
("#!/usr/bin/env python3\n", "python"),
|
||||
("#!/usr/bin/env -S python3 -u\n", "python"),
|
||||
// Attached split-string forms carry the interpreter inside the
|
||||
// option token.
|
||||
("#!/usr/bin/env -Spython3 -u\n", "python"),
|
||||
("#!/usr/bin/env --split-string=python3 -u\n", "python"),
|
||||
("#!/usr/bin/env -vSpython3 -u\n", "python"),
|
||||
// GNU-env options that consume an operand must not have the
|
||||
// operand mistaken for the interpreter.
|
||||
("#!/usr/bin/env -u FOO python3\n", "python"),
|
||||
|
|
@ -6081,6 +6086,33 @@ fn m4_shebang_edit_keeps_pinned_grammar() {
|
|||
Some("bash"),
|
||||
"editing the shebang must not re-switch the pinned grammar"
|
||||
);
|
||||
|
||||
// Switch away to another buffer and back: the after-switch reattach
|
||||
// must reuse the pinned bash grammar rather than re-sniff the (now
|
||||
// lua) shebang — otherwise grammar and LSP diverge, since the LSP side
|
||||
// keeps its bash attachment across the switch. `switch_buffer` fires
|
||||
// `buffer.after-switch` synchronously.
|
||||
let other = dir.path().join("other.txt");
|
||||
std::fs::write(&other, b"plain text\n").expect("write other");
|
||||
let other_disp = other.display();
|
||||
s.lua_host
|
||||
.lua()
|
||||
.load(format!(
|
||||
"local pinned = pmacs.window.buffer()
|
||||
pmacs.buffer.find_or_open('{other_disp}')
|
||||
pmacs.window.switch_buffer(pinned)"
|
||||
))
|
||||
.exec()
|
||||
.expect("switch away and back");
|
||||
for _ in 0..64 {
|
||||
s.tick_async();
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
}
|
||||
assert_eq!(
|
||||
current_tree_language(&s).as_deref(),
|
||||
Some("bash"),
|
||||
"switch-away/back must reuse the pinned grammar, not re-sniff the edited shebang"
|
||||
);
|
||||
let errs: i64 = s
|
||||
.lua_host
|
||||
.lua()
|
||||
|
|
|
|||
Loading…
Reference in New Issue