From 087bc8266f538bd49c9b9b02639b88739f43335e Mon Sep 17 00:00:00 2001 From: Famiu Haque Date: Sun, 3 Oct 2021 21:10:03 +0600 Subject: [PATCH] feat: add smart component truncation (#132) --- USAGE.md | 60 ++++++++++++++ lua/feline/generator.lua | 145 ++++++++++++++++++++++++++++------ lua/feline/statusline_ffi.lua | 69 ++++++++++++++++ 3 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 lua/feline/statusline_ffi.lua diff --git a/USAGE.md b/USAGE.md index cabbdcf..a9707e5 100644 --- a/USAGE.md +++ b/USAGE.md @@ -164,6 +164,66 @@ end If you omit the provider value, it will be set to an empty string. A component with no provider or an empty provider may be useful for things like [applying a highlight to section gaps](#highlight-section-gaps) or just having an icon or separator as a component. +#### Truncation + +Feline has an automatic smart truncation system where components can be automatically truncated if the statusline doesn't fit within the window. There's a few component values associated with truncation. + +##### Component short provider + +`short_provider` is an optional component value that allows you to take advantage of Feline's truncation system. Note that this should only be defined if you want to enable truncation for the component, otherwise it's absolutely fine to omit it. + +`short_provider` works just like the `provider` value, but is activated only when the component is being truncated due to the statusline not fitting within the window. `short_provider` is independent from the `provider` value so it can be a different provider altogether, or it can be a shortened version of the same provider or the same provider but with a different `opts` value. For example: + +```lua +-- In this component, short provider uses same provider but with different opts +local file_info_component = { + provider = { + name = 'file_info', + opts = { + type = 'full-path' + } + }, + short_provider = { + name = 'file_info', + opts = { + type = 'short-path' + } + } +} + +-- Short provider can also be an independent value / function +local my_component = { + provider = 'loooooooooooooooong', + short_provider = 'short' +} +``` + +Feline doesn't set `short_provider` to any component by default, so it must be provided manually. + +##### Hide components during truncation + +If you wish to allow Feline to hide a component entirely if necessary during truncation, you may set the `truncate_hide` component value to `true`. By default, `truncate_hide` is `false` for every component. + +##### Component priority + +When components are being truncated by Feline, you can choose to give some components a higher priority over the other components. The `priority` component value just takes a number. By default, the priority of a component is `0`. Components are truncated in ascending order of priority. So components with lower priority are truncated first, while components with higher priority are truncated later on. For example: + +```lua +-- This component has the default priority +local my_component = { + provider = 'loooooooooooooooong', + short_provider = 'short' +} +-- This component has a higher priority, so it will be truncated after the previous component +local high_priority_component = { + provider = 'long provider with high priority', + short_provider = 'short', + priority = 1 +} +``` + +Priority can also be set to a negative number, which can be used to make a component be truncated earlier than the ones with default priority. + #### Conditionally enable components The `enabled` value of a component can be a boolean or function. This value determines if the component is enabled or not. If false, the component is not shown in the statusline. For example: diff --git a/lua/feline/generator.lua b/lua/feline/generator.lua index b65ff2b..e5b6e4c 100644 --- a/lua/feline/generator.lua +++ b/lua/feline/generator.lua @@ -10,6 +10,8 @@ local separators = feline.separators local disable = feline.disable local force_inactive = feline.force_inactive +local get_statusline_expr_width = require('feline.statusline_ffi').get_statusline_expr_width + local M = { highlights = {} } @@ -253,8 +255,7 @@ local function parse_provider(provider, component) return provider, icon end --- Parses a component to return the component string -local function parse_component(component) +local function parse_component(component, use_short_provider) local enabled if component.enabled then enabled = component.enabled else enabled = true end @@ -277,10 +278,16 @@ local function parse_component(component) hl = parse_hl(hl) end - local str, icon + local provider, str, icon + + if use_short_provider then + provider = component.short_provider + else + provider = component.provider + end - if component.provider then - str, icon = parse_provider(component.provider, component) + if provider then + str, icon = parse_provider(provider, component) else str = '' end @@ -315,6 +322,32 @@ local function parse_component(component) ) end +-- Wrapper around parse_component that handles any errors that happen while parsing the components +-- and points to the location of the component in case of any errors +local function parse_component_handle_errors( + component, + use_short_provider, + statusline_type, + component_section, + component_number +) + local ok, result = pcall(parse_component, component, use_short_provider) + + if not ok then + api.nvim_err_writeln(string.format( + "Feline: error while processing component number %d on section %d of type '%s': %s", + component_number, + component_section, + statusline_type, + result + )) + + return '' + end + + return result +end + -- Generate statusline by parsing all components and return a string function M.generate_statusline(is_active) -- Generate default highlights for the statusline @@ -338,34 +371,100 @@ function M.generate_statusline(is_active) return '' end - -- Concatenate all components strings of each section to get a string for each section - local section_strs = {} + -- Iterate through all the components, parse them and store the component strings, each + -- component's indices and width in separate tables, while also calculating the statusline width + local component_strs = {} + local component_indices = {} + local component_widths = {} + local statusline_width = 0 for i, section in ipairs(sections) do - local component_strs = {} + component_strs[i] = {} + component_widths[i] = {} for j, component in ipairs(section) do - -- Handle any errors that happen while parsing the components - -- and point to the location of the component in case of any erros - local ok, result = pcall(parse_component, component) - - if not ok then - api.nvim_err_writeln(string.format( - "Feline: error while processing component number %d on section %d " .. - "of type '%s': %s", - j, i, statusline_type, result - )) - - result = '' + local component_str = parse_component_handle_errors( + component, false, statusline_type, i, j + ) + + local component_width = get_statusline_expr_width(component_str) + + component_strs[i][j] = component_str + component_widths[i][j] = component_width + statusline_width = statusline_width + component_width + + component_indices[#component_indices+1] = {i, j} + end + end + + local window_width = api.nvim_win_get_width(0) + + -- If statusline width is greater than the window width, begin the truncation process + if statusline_width > window_width then + -- First, sort the component indices in ascending order of the priority of the components + -- that the indices refer to + table.sort(component_indices, function(first, second) + local first_priority = sections[first[1]][first[2]].priority or 0 + local second_priority = sections[second[1]][second[2]].priority or 0 + + return first_priority < second_priority + end) + + -- Then, iterate through the sorted indices to access the components in order of priority, + -- and if the component has a short_provider, use it instead of the normal provider to + -- truncate the component + for _, indices in ipairs(component_indices) do + local section, number = indices[1], indices[2] + local component = sections[section][number] + + if component.short_provider then + local component_str = parse_component_handle_errors( + component, true, statusline_type, section, number + ) + + local component_width = get_statusline_expr_width(component_str) + + -- Calculate how much the width of the statusline decreases if the provider is + -- replaced with the short_provider, and if it's greater than 0 (which implies that + -- the statusline decreased in width), replace the provider with the short_provider + -- and update the statusline_width variable to reflect the change + local width_difference = component_widths[section][number] - component_width + + if width_difference > 0 then + statusline_width = statusline_width - width_difference + component_strs[section][number] = component_str + component_widths[section][number] = component_width + end + end + + if statusline_width <= window_width then break end + end + end + + -- If statusline still doesn't fit within window, remove components with truncate_hide set to + -- true until it does + if statusline_width > window_width then + for _, indices in ipairs(component_indices) do + local section, number = indices[1], indices[2] + + if sections[section][number].truncate_hide then + statusline_width = statusline_width - component_widths[section][number] + component_strs[section][number] = '' + component_widths[section][number] = 0 end - component_strs[j] = result + if statusline_width <= window_width then break end end + end + + -- Concatenate all component strings in each section to get a string for each section + local section_strs = {} - section_strs[i] = table.concat(component_strs) + for i, section_component_strs in ipairs(component_strs) do + section_strs[i] = table.concat(section_component_strs) end - -- Then concatenate all the sections to get the statusline string and return it + -- Finally, concatenate all sections to get the statusline string, and return it return table.concat(section_strs, '%=') end diff --git a/lua/feline/statusline_ffi.lua b/lua/feline/statusline_ffi.lua new file mode 100644 index 0000000..8b0c678 --- /dev/null +++ b/lua/feline/statusline_ffi.lua @@ -0,0 +1,69 @@ +-- This module provides an interface to Neovim's statusline generator using Lua FFI +local M = {} + +local ffi = require('ffi') + +-- Definitions required to use Neovim's build_stl_str_hl function to expand statusline expressions +ffi.cdef [[ +typedef unsigned char char_u; +typedef struct window_S win_T; +typedef struct {} stl_hlrec_t; +typedef struct {} StlClickRecord; + +extern win_T *curwin; + +int build_stl_str_hl( + win_T *wp, + char_u *out, + size_t outlen, + char_u *fmt, + int use_sandbox, + char_u fillchar, + int maxwidth, + stl_hlrec_t **hltab, + StlClickRecord **tabtab +); +]] + +-- Used CType values stored in a local variable to avoid redefining them and improve performance +local char_u_buf_t = ffi.typeof('char_u[?]') +local char_u_str_t = ffi.typeof('char_u*') + +-- Statusline string buffer +local stlbuf_len = 256 +local stlbuf = char_u_buf_t(stlbuf_len) + +-- Expand statusline expression, returns a Lua string containing plaintext with only the characters +-- that'll be displayed in the statusline +function M.expand_statusline_expr(expr) + ffi.C.build_stl_str_hl( + ffi.C.curwin, + stlbuf, + stlbuf_len, + ffi.cast(char_u_str_t, expr), + 0, + 0, + 0, + nil, + nil + ) + + return ffi.string(stlbuf) +end + +-- Get display width of statusline expression +function M.get_statusline_expr_width(expr) + return tonumber(ffi.C.build_stl_str_hl( + ffi.C.curwin, + stlbuf, + stlbuf_len, + ffi.cast(char_u_str_t, expr), + 0, + 0, + 0, + nil, + nil + )) +end + +return M