fix(parser): detect filetype from file content (shebang/modeline)

When a file has no extension but contains a shebang (e.g., `#!/bin/bash`),
filetype detection now reads the first 10 lines from disk and uses
`vim.filetype.match({ filename, contents })` for content-based detection.

This enables syntax highlighting for files like `build` scripts that rely
on shebang detection, even when the file isn't open in a buffer.

Detection order:
1. Existing buffer's filetype (already implemented in #69)
2. File content (shebang/modeline) - NEW
3. Filename extension only

Also adds `filetype on` to test helpers to ensure `vim.g.ft_ignore_pat`
is set, which is required for shell detection.
This commit is contained in:
Barrett Ruth 2026-02-05 01:04:23 -05:00
parent 0e6871b167
commit 33c58c7498
3 changed files with 103 additions and 3 deletions

View file

@ -13,12 +13,33 @@ local M = {}
local dbg = require('diffs.log').dbg
---@param filepath string
---@param n integer
---@return string[]?
local function read_first_lines(filepath, n)
local f = io.open(filepath, 'r')
if not f then
return nil
end
local lines = {}
for _ = 1, n do
local line = f:read('*l')
if not line then
break
end
table.insert(lines, line)
end
f:close()
return #lines > 0 and lines or nil
end
---@param filename string
---@param repo_root string?
---@return string?
local function get_ft_from_filename(filename, repo_root)
if repo_root then
local full_path = vim.fs.joinpath(repo_root, filename)
local buf = vim.fn.bufnr(full_path)
if buf ~= -1 then
local ft = vim.api.nvim_get_option_value('filetype', { buf = buf })
@ -30,10 +51,25 @@ local function get_ft_from_filename(filename, repo_root)
end
local ft = vim.filetype.match({ filename = filename })
if not ft then
dbg('no filetype for: %s', filename)
if ft then
dbg('filetype from filename: %s', ft)
return ft
end
return ft
if repo_root then
local full_path = vim.fs.joinpath(repo_root, filename)
local contents = read_first_lines(full_path, 10)
if contents then
ft = vim.filetype.match({ filename = filename, contents = contents })
if ft then
dbg('filetype from file content: %s', ft)
return ft
end
end
end
dbg('no filetype for: %s', filename)
return nil
end
---@param ft string