From fb0faa8fce01bfe54afeae5164a9a5606649db93 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Thu, 25 Jul 2024 11:49:38 +0300 Subject: [PATCH] feat(icons)!: use direct `get('extension', ext)` in "file" resolution This approach is better for the following reasons: - Simplifies resolution logic. - Allows to stop manual tracking of common extensions for speed. - Caches ALL extensions instead of only manually tracked ones. Results in better performance in real world cases (about 50% more speed). - Does not use **all** cached extensions. This was a problem because a single `get('extension', 'xxx')` call could overshadow more general filetype matching resolution. Instead, using only recognizable extensions (i.e. not coming from fallback) is better in most cases (although results in always going through until calling `vim.filetype.match()` for unknown extensions). The speed improvement also relies on the assumption that there are more recognizable extensions than fallback ones: in first case cache will be used more frequently, in second one `vim.filetype.match()` will be used twice on the uncached unrecognizable extension (first to resolve extension, second to resolve "file"). Still present issues (to be solved in upcoming commits): - It blocks matches from `vim.filetype.match()` in case file name or pattern contains recognizable extension. Previously it was only an issue for manually tracked and cached extensions, but now it can happen in many more cases. For example: - Exact file names like 'requirements.txt', 'CMakeLists.txt'. Will be solved by manually tracking Neovim's built-in ones while directing users to add those during `setup()`. - Patterns containing extension, like for 'yaml.ansible' filetype ('.*/roles/.*/tasks/.*%.ya?ml') or 'query' ('queries/.*%.scm'). Will be solved by adding `use_file_extension` callable config entry (`function(ext, basename, name) return true end` as default) for users to tweak this manually. --- doc/mini-icons.txt | 6 ++-- lua/mini/icons.lua | 30 ++++++------------ tests/test_icons.lua | 72 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 74 insertions(+), 34 deletions(-) diff --git a/doc/mini-icons.txt b/doc/mini-icons.txt index b20046ab..d5fff3bc 100644 --- a/doc/mini-icons.txt +++ b/doc/mini-icons.txt @@ -319,10 +319,8 @@ Parameters ~ Icon data is attempted to be resolved in the following order: - List of built-in and user configured file names (matched to basename of the input exactly). Run `:=MiniIcons.list('flle')` to see them. - - List of built-in and user configured extensions (matched to extension - in basename ignoring case). Run `:=MiniIcons.list('extension')` to - see them. This step also includes already cached extensions. - Uses icon data from "extension" category. + - Basename extension(s) matched directly as `get('extension', ext)`. + Only recognizable extensions (i.e. not default fallback) are used. - Filetype as a result of |vim.filetype.match()| with full input (not basename) as `filename`. Uses icon data from "filetype" category. diff --git a/lua/mini/icons.lua b/lua/mini/icons.lua index 0964f40c..b3090aab 100644 --- a/lua/mini/icons.lua +++ b/lua/mini/icons.lua @@ -333,10 +333,8 @@ MiniIcons.config = { --- Icon data is attempted to be resolved in the following order: --- - List of built-in and user configured file names (matched to basename --- of the input exactly). Run `:=MiniIcons.list('flle')` to see them. ---- - List of built-in and user configured extensions (matched to extension ---- in basename ignoring case). Run `:=MiniIcons.list('extension')` to ---- see them. This step also includes already cached extensions. ---- Uses icon data from "extension" category. +--- - Basename extension(s) matched directly as `get('extension', ext)`. +--- Only recognizable extensions (i.e. not default fallback) are used. --- - Filetype as a result of |vim.filetype.match()| with full input (not --- basename) as `filename`. Uses icon data from "filetype" category. --- @@ -1970,8 +1968,10 @@ H.get_impl = { default = function(name) H.error(vim.inspect(name) .. ' is not a supported category.') end, directory = function(name) return H.directory_icons[name] end, extension = function(name) - local icon, hl = H.get_from_extension(name) - if icon ~= nil then return icon, hl end + -- Built-in extensions + local icon_data = H.extension_icons[name] + if type(icon_data) == 'string' then return MiniIcons.get('filetype', icon_data) end + if icon_data ~= nil then return icon_data, icon_data.hl end -- Fall back to built-in filetype matching using generic filename local ft = H.filetype_match('aaa.' .. name) @@ -1982,6 +1982,7 @@ H.get_impl = { -- Built-in file names local icon_data = H.file_icons[basename] + if type(icon_data) == 'string' then return MiniIcons.get('filetype', icon_data) end if icon_data ~= nil then return icon_data end -- User configured file names @@ -1991,14 +1992,9 @@ H.get_impl = { -- (as the latter is slow-ish; like 0.1 ms in Neovim<0.11) local dot = string.find(basename, '%..', 2) while dot ~= nil do - local ext = basename:sub(dot + 1):lower() - - local cached = H.cache_get('extension', ext) - if cached ~= nil then return cached[1], cached[2] end - - local icon, hl = H.get_from_extension(ext) - if icon ~= nil then return H.cache_set('extension', ext, icon, hl) end - + local ext = basename:sub(dot + 1) + local icon, hl, is_default = MiniIcons.get('extension', ext) + if not is_default then return icon, hl end dot = string.find(basename, '%..', dot + 1) end @@ -2012,12 +2008,6 @@ H.get_impl = { os = function(name) return H.os_icons[name] end, } -H.get_from_extension = function(ext) - local icon_data = H.extension_icons[ext] - if type(icon_data) == 'string' then return MiniIcons.get('filetype', icon_data) end - if icon_data ~= nil then return H.style_icon(icon_data.glyph, ext), icon_data.hl end -end - H.style_icon = function(glyph, name) if MiniIcons.config.style ~= 'ascii' then return glyph end -- Use `vim.str_byteindex()` and `vim.fn.toupper()` for multibyte characters diff --git a/tests/test_icons.lua b/tests/test_icons.lua index 3200cff7..3bae8ab8 100644 --- a/tests/test_icons.lua +++ b/tests/test_icons.lua @@ -312,9 +312,8 @@ T['get()']['caches output'] = function() eq(durations.cache <= 0.02 * durations.no_cache_2, true) end -T['get()']['adds to cache resolved output source'] = function() +T['get()']['adds to cache resolved output in its original category'] = function() -- NOTES: - -- - Only manually tracked extensions can be target of resolve. -- - There should also be caching of both "file" and "extension" category -- resolving to "filetype", but as "filetype" is already very fast without -- caching, the benchmarking is not stable. @@ -325,20 +324,73 @@ T['get()']['adds to cache resolved output source'] = function() return vim.loop.hrtime() - start_time end - local ext_no_cache = bench('extension', 'lua') - - -- "file" category resolving to "extension" + -- "file" category resolving to manually tracked "extension" + local ext_manual_no_cache = bench('extension', 'lua') MiniIcons.get('file', 'hello.py') - local ext_cache_after_file = bench('extension', 'py') + local ext_manual_cache = bench('extension', 'py') + + -- "file" category resolving to known (i.e. not fallback) "extension" + local ext_known_no_cache = bench('extension', 'txt') + MiniIcons.get('file', 'hello.yml') + local ext_known_cache = bench('extension', 'yml') + + -- "file" category resolving to unknown "extension" + local ext_unknown_no_cache = bench('extension', 'myext') + MiniIcons.get('file', 'hello.myotherext') + local ext_unknown_cache = bench('extension', 'myotherext') + + return { + ext_manual_no_cache = ext_manual_no_cache, + ext_manual_cache = ext_manual_cache, + ext_known_no_cache = ext_known_no_cache, + ext_known_cache = ext_known_cache, + ext_unknown_no_cache = ext_unknown_no_cache, + ext_unknown_cache = ext_unknown_cache, + } + ]]) + + -- Resolution with manually tracked data is usually fast, hence higher coeff + eq(durations.ext_manual_cache < 0.7 * durations.ext_manual_no_cache, true) + + -- There is a full effect of caching for not manually tracked + eq(durations.ext_known_cache < 0.1 * durations.ext_known_no_cache, true) + eq(durations.ext_unknown_cache < 0.1 * durations.ext_unknown_no_cache, true) +end + +T['get()']['uses cached extension during "file" resolution'] = function() + local durations = child.lua([[ + local bench = function(category, name) + local start_time = vim.loop.hrtime() + MiniIcons.get(category, name) + return vim.loop.hrtime() - start_time + end + + -- Known extension (i.e. not falling back to default) + local file_known_ext_no_cache = bench('file', 'hello.txt') + MiniIcons.get('extension', 'yml') + local file_known_ext_cache = bench('file', 'world.yml') + + -- Unknown extension (i.e. falling back to default) + local file_unknown_ext_no_cache = bench('file', 'hello.myext') + MiniIcons.get('file', 'hello.myotherext') + local file_unknown_ext_cache = bench('file', 'world.myotherext') return { - ext_no_cache = ext_no_cache, - ext_cache_after_file = ext_cache_after_file, + file_known_ext_no_cache = file_known_ext_no_cache, + file_known_ext_cache = file_known_ext_cache, + file_unknown_ext_no_cache = file_unknown_ext_no_cache, + file_unknown_ext_cache = file_unknown_ext_cache, } ]]) - -- Resolution with manually tracked data is usually fast, hence high coeff - eq(durations.ext_cache_after_file < 0.7 * durations.ext_no_cache, true) + -- Known extensions are used as output resulting in no `vim.filetype.match()` + -- call for file name itself + eq(durations.file_known_ext_cache < 0.1 * durations.file_known_ext_no_cache, true) + + -- Unknown extensions are NOT used as output, but they are still cached which + -- results in no extra `vim.filetype.match()` call to resolve itself inside + -- "extension" category + eq(durations.file_unknown_ext_cache < 0.7 * durations.file_unknown_ext_no_cache, true) end T['get()']['prefers user configured data over `vim.filetype.match()`'] = function()