Skip to content

Commit

Permalink
feat(icons)!: use direct get('extension', ext) in "file" resolution
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
echasnovski committed Jul 26, 2024
1 parent 4d11d9f commit fb0faa8
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 34 deletions.
6 changes: 2 additions & 4 deletions doc/mini-icons.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
30 changes: 10 additions & 20 deletions lua/mini/icons.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
---
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
72 changes: 62 additions & 10 deletions tests/test_icons.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand Down

0 comments on commit fb0faa8

Please sign in to comment.