Skip to content

fix(ui): resolve fence language from the filetype, not the extension - #539

Open
lum1nar wants to merge 3 commits into
sudo-tee:mainfrom
lum1nar:fix/diff-fence-language
Open

lum1nar wants to merge 3 commits into
sudo-tee:mainfrom
lum1nar:fix/diff-fence-language

Conversation

@lum1nar

@lum1nar lum1nar commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Problem

Code fences in the output and permission dialog are labelled with a string that
is not always a treesitter language name, so some files never get syntax
highlighting. .rs was labelled rs (not rust), .yaml became yml, etc.

Root cause

vim.filetype.match (Neovim core) already resolves almost every path to a
filetype. Neovim then resolves a fence's info string with has_parser(name) /
get_lang(name), where get_lang maps a filetype to a language — not an
extension.

The two code paths had also diverged:

  • The formatter already reused the helper.
  • The permission dialog was written separately and derived the fence language
    from the raw extension (fnamemodify(path, ':e')), so .rs became rs,
    which no parser resolves.

The helper itself used a hand-written override table that was incomplete and
partly wrong, and duplicated what get_lang already does.

Fix

  • permission_window: reuse the helper instead of fnamemodify(path, ':e').
  • util.get_markdown_language (renamed from get_markdown_filetype): resolve
    path -> vim.filetype.match -> vim.treesitter.language.get_lang, falling
    back to the extension.
  • The context/formatter callers use the renamed function.

The old table was incomplete (only a handful of the filetype != language
pairs) and wrong:

  • yaml -> yml: no parser, and it turned a case that resolved on its own
    (yaml) into a broken one.
  • javascipt -> js: a typo that never matched.
  • typescript -> ts, javascriptreact -> jsx: only worked because
    nvim-treesitter aliases those extensions.
  • text -> txt: not a language.

Why the small table exists

get_lang only maps filetypes to languages when nvim-treesitter is
installed — Neovim core registers almost nothing. The plugin keeps a small
table for the common name mismatches so setups without nvim-treesitter still
highlight them. The two overlap and complement each other:

without nvim-treesitter with nvim-treesitter
plugin table (common ~21) only source redundant (same value as get_lang)
get_lang no-op (returns the filetype) all 43 entries, including ones the table omits

Verify by listing every filetype whose name is not its language:

for _, ft in ipairs(vim.fn.getcompletion('', 'filetype')) do
  local lang = vim.treesitter.language.get_lang(ft)
  if lang ~= ft then print(ft, '->', lang) end
end
  • Without nvim-treesitter: prints nothing — every filetype falls back to
    itself, so sh, cs, javascriptreact, ... have no language and would not
    highlight without the table.
  • With nvim-treesitter: 43 entries, e.g. sh -> bash, cs -> c_sharp,
    svg -> xml, javascriptreact -> javascript, pandoc -> markdown.

Verification

  • New unit cases for the path -> language mapping: rs -> rust, sh -> bash,
    svg -> xml, tsx, jsx, empty filename, unknown extension.
  • Full suite green (minimal / unit / replay). Four replay snapshots were
    regenerated because the fence label changed (txt -> text).

- permission_window: the permission dialog derived the fence language from
  the raw extension (fnamemodify(path, ':e')) instead of the helper the
  formatter already used, so `.rs` was labelled `rs` and never highlighted.
  Reuse the helper, which resolves the full language name (.rs -> rust).
- util: rename get_markdown_filetype to get_markdown_language and resolve
  path -> filetype (vim.filetype.match) -> treesitter language
  (vim.treesitter.language.get_lang) instead of the hand-written override
  table, which was incomplete and wrong (yaml -> yml has no parser and
  turned a case that resolved on its own into a broken one; javascipt -> js
  was a typo that never matched; text -> txt is not a language; typescript
  -> ts / javascriptreact -> jsx only worked because nvim-treesitter aliases
  those extensions).
- util: get_lang only maps filetypes to languages when nvim-treesitter is
  installed, so keep a small filetype -> language table for the common name
  mismatches (sh -> bash, cs -> c_sharp, svg -> xml, ...) as a fallback for
  setups without it. With nvim-treesitter, get_lang already resolves those
  and the table changes nothing. rs needs no entry: vim.filetype.match
  already turns .rs into the rust filetype.
- context/formatter callers: use the renamed function.
- tests: cover the filetype -> language mapping (rs -> rust, sh -> bash,
  svg -> xml, tsx, jsx) and regenerate the snapshots whose fence label
  changes (txt -> text).
- permission_window: the command/pattern fence used the permission type
  directly. v1 sets `permission = "bash"` (a real language), while v2 sets
  `action = "shell"`, which has no parser — so a v2 shell permission never
  highlighted. Map shell -> bash for the fence only; the `*shell*` label
  stays.
- tests/unit: the v2 spec had asserted the broken `shell` fence; assert
  `bash` instead (the `*shell*` label is still asserted).
@sudo-tee

Copy link
Copy Markdown
Owner

@lum1nar

I think the test snapshots needs to be updated to match the new filetypes.

- util: keep `text -> txt`. Neovim nightly reports the `text` filetype for
  .txt while older versions fall back to the `txt` extension; without the
  mapping the fence label (and its snapshot) depends on the Neovim version.
  The table is checked before get_lang, so this holds with or without
  nvim-treesitter.
- tests/data: revert the diff/permission/redo snapshots regenerated in the
  fence-language commit. The label is stable again, so they need no change.
@lum1nar

lum1nar commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Same issue as 313acb8 — I've reverted those snapshots.

@lum1nar

lum1nar commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

I think we can avoid per-version expected snapshots here — text/txt highlight the same (neither has a parser), so normalizing to one label should be enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants