From ded17258cda821713cf24568f14a93417ad74e4b Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 20 Feb 2026 16:56:17 -0500 Subject: [PATCH] feat(icon): add opt-in filetype detection via file contents Problem: files without standard extensions (e.g. scripts with shebangs, Makefile, Dockerfile) got incorrect or default icons since icon providers match by filename or extension only. Solution: add use_slow_filetype_detection option to the icon column config. When enabled, reads the first 16 lines of each file and passes them to vim.filetype.match for content-based detection. The detected filetype is forwarded to mini.icons or nvim-web-devicons as a trailing parameter, preserving backwards compatibility with existing icon provider implementations. Based on: stevearc/oil.nvim#618 --- doc/oil.txt | 4 ++++ lua/oil/columns.lua | 17 +++++++++++++++-- lua/oil/util.lua | 15 ++++++++++++--- scripts/generate.py | 5 +++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/doc/oil.txt b/doc/oil.txt index 55da34b..307dd1e 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -438,6 +438,10 @@ icon *column-ico {directory} `string` Icon for directories {add_padding} `boolean` Set to false to remove the extra whitespace after the icon + {use_slow_filetype_detection} `boolean` Set to true to detect filetypes + by reading the first lines of each file (e.g. shebangs). + This improves icon accuracy for extensionless scripts but + performs synchronous I/O per file per render. size *column-size* Adapters: files, ssh, s3 diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 975576f..6d76674 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -153,7 +153,7 @@ end local icon_provider = util.get_icon_provider() if icon_provider then M.register("icon", { - render = function(entry, conf) + render = function(entry, conf, bufnr) local field_type = entry[FIELD_TYPE] local name = entry[FIELD_NAME] local meta = entry[FIELD_META] @@ -168,7 +168,20 @@ if icon_provider then if meta and meta.display_name then name = meta.display_name end - local icon, hl = icon_provider(field_type, name, conf) + + local ft = nil + if conf and conf.use_slow_filetype_detection and field_type == "file" then + local bufname = vim.api.nvim_buf_get_name(bufnr) + local _, path = util.parse_url(bufname) + if path then + local lines = vim.fn.readfile(path .. name, "", 16) + if lines and #lines > 0 then + ft = vim.filetype.match({ filename = name, contents = lines }) + end + end + end + + local icon, hl = icon_provider(field_type, name, conf, ft) if not conf or conf.add_padding ~= false then icon = icon .. " " end diff --git a/lua/oil/util.lua b/lua/oil/util.lua index 0ec1acd..dc3ac14 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -8,7 +8,7 @@ local FIELD_NAME = constants.FIELD_NAME local FIELD_TYPE = constants.FIELD_TYPE local FIELD_META = constants.FIELD_META ----@alias oil.IconProvider fun(type: string, name: string, conf: table?): (icon: string, hl: string) +---@alias oil.IconProvider fun(type: string, name: string, conf: table?, ft: string?): (icon: string, hl: string) ---@param url string ---@return nil|string @@ -934,7 +934,10 @@ M.get_icon_provider = function() local _, mini_icons = pcall(require, "mini.icons") ---@diagnostic disable-next-line: undefined-field if _G.MiniIcons then -- `_G.MiniIcons` is a better check to see if the module is setup - return function(type, name) + return function(type, name, conf, ft) + if ft then + return mini_icons.get("filetype", ft) + end return mini_icons.get(type == "directory" and "directory" or "file", name) end end @@ -942,10 +945,16 @@ M.get_icon_provider = function() -- fallback to `nvim-web-devicons` local has_devicons, devicons = pcall(require, "nvim-web-devicons") if has_devicons then - return function(type, name, conf) + return function(type, name, conf, ft) if type == "directory" then return conf and conf.directory or "", "OilDirIcon" else + if ft then + local ft_icon, ft_hl = devicons.get_icon_by_filetype(ft) + if ft_icon and ft_icon ~= "" then + return ft_icon, ft_hl + end + end local icon, hl = devicons.get_icon(name) icon = icon or (conf and conf.default_file or "") return icon, hl diff --git a/scripts/generate.py b/scripts/generate.py index adcab9e..eb30f85 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -154,6 +154,11 @@ COL_DEFS = [ "boolean", "Set to false to remove the extra whitespace after the icon", ), + LuaParam( + "use_slow_filetype_detection", + "boolean", + "Set to true to detect filetypes by reading the first lines of each file (e.g. shebangs).", + ), ], ), ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", UNIVERSAL + []),