From fea12d3bbf079821c4623483e9d4ac4f4f38141f Mon Sep 17 00:00:00 2001 From: Famiu Haque Date: Sat, 25 Sep 2021 09:30:28 +0600 Subject: [PATCH] feat: add smart component truncation --- USAGE.md | 50 +++++++++ lua/feline/generator.lua | 236 +++++++++++++++++++++++++++------------ 2 files changed, 217 insertions(+), 69 deletions(-) diff --git a/USAGE.md b/USAGE.md index 9ba73e6..83c16ec 100644 --- a/USAGE.md +++ b/USAGE.md @@ -167,6 +167,56 @@ provider = function(_, _, opts) end ``` +#### Component short provider + +Feline has an automatic smart truncation system where components can be automatically truncated if the statusline doesn't fit within the window. In order to make use of this truncation system, you have to define the `short_provider` component value. + +`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' +} +``` + +#### 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. If it's a function, it can take either the window handler as an argument, or it can take no arguments. For example: diff --git a/lua/feline/generator.lua b/lua/feline/generator.lua index 6e794fe..b12e259 100644 --- a/lua/feline/generator.lua +++ b/lua/feline/generator.lua @@ -3,12 +3,14 @@ local api = vim.api local feline = require('feline') local providers = feline.providers -local components = feline.components +local components_table = feline.components local colors = feline.colors local separators = feline.separators local disable = feline.disable local force_inactive = feline.force_inactive +local strwidth = api.nvim_strwidth + local M = { highlights = {} } @@ -143,22 +145,22 @@ local function get_hlname(hl, parent_hl) return hlname end --- Parse component seperator +-- Parse component seperator to return parsed string and length -- By default, foreground color of separator is background color of parent -- and background color is set to default background color local function parse_sep(sep, parent_bg, is_component_empty) - if sep == nil then return '' end + if sep == nil then return {str = '', len = 0} end local hl local str if type(sep) == 'string' then - if is_component_empty then return '' end + if is_component_empty then return {str = '', len = 0} end str = sep hl = {fg = parent_bg, bg = colors.bg} else - if is_component_empty and not sep.always_visible then return '' end + if is_component_empty and not sep.always_visible then return {str = '', len = 0} end str = sep.str or '' hl = sep.hl or {fg = parent_bg, bg = colors.bg} @@ -166,83 +168,110 @@ local function parse_sep(sep, parent_bg, is_component_empty) if separators[str] then str = separators[str] end - return string.format('%%#%s#%s', get_hlname(hl), str) + return { + str = string.format('%%#%s#%s', get_hlname(hl), str), + len = strwidth(str) + } end -- Either parse a single separator or a list of separators with different highlights local function parse_sep_list(sep_list, parent_bg, is_component_empty, winid) - if sep_list == nil then return '' end + if sep_list == nil then return {str = '', len = 0} end if (type(sep_list) == 'table' and sep_list[1] and (type(sep_list[1]) == 'function' or type(sep_list[1]) == 'table' or type(sep_list[1]) == 'string')) then local sep_strs = {} + local total_len = 0 for _,v in ipairs(sep_list) do - sep_strs[#sep_strs+1] = parse_sep( + local sep = parse_sep( evaluate_if_function(v, winid), parent_bg, is_component_empty ) + + sep_strs[#sep_strs+1] = sep.str + total_len = total_len + sep.len end - return table.concat(sep_strs) + return {str = table.concat(sep_strs), len = total_len} else return parse_sep(evaluate_if_function(sep_list, winid), parent_bg, is_component_empty) end end --- Parse component icon +-- Parse component icon and return parsed string alongside length -- By default, icon inherits component highlights local function parse_icon(icon, parent_hl, is_component_empty) - if icon == nil then return '' end + if icon == nil then return {str = '', len = 0} end local hl local str - if type(icon) == "string" then - if is_component_empty then return '' end + if type(icon) == 'string' then + if is_component_empty then return {str = '', len = 0} end str = icon hl = parent_hl else - if is_component_empty and not icon.always_visible then return '' end + if is_component_empty and not icon.always_visible then return {str = '', len = 0} end str = icon.str or '' hl = icon.hl or parent_hl end - return string.format('%%#%s#%s', get_hlname(hl, parent_hl), str) + return { + str = string.format('%%#%s#%s', get_hlname(hl, parent_hl), str), + len = strwidth(str) + } end --- Parse component provider +-- Parse component provider to return the provider string, icon and length of provider string local function parse_provider(provider, winid, component) local icon -- If provider is a string and its name matches the name of a registered provider, use it - if type(provider) == "string" and providers[provider] then + if type(provider) == 'string' and providers[provider] then provider, icon = providers[provider](winid, component, {}) -- If provider is a function, just evaluate it normally - elseif type(provider) == "function" then + elseif type(provider) == 'function' then provider, icon = provider(winid, component) -- If provider is a table, get the provider name and opts and evaluate the provider - elseif type(provider) == "table" then + elseif type(provider) == 'table' then provider, icon = providers[provider.name](winid, component, provider.opts or {}) end - return provider, icon + if type(provider) ~= 'string' then + api.nvim_err_writeln(string.format( + "Provider must evaluate to string, got type '%s' instead", + type(provider) + )) + end + + return {str = provider, len = strwidth(provider)}, icon end --- Parses a component alongside its highlight -local function parse_component(component, winid) +-- Parses a component alongside its highlight to return the component string and length +local function parse_component(component, winid, use_short_provider) local enabled if component.enabled then enabled = component.enabled else enabled = true end enabled = evaluate_if_function(enabled, winid) - if not enabled then return '' end + if not enabled then return {str = '', len = 0} end - local str, icon = parse_provider(component.provider, winid, component) + local provider + + if use_short_provider then + provider = component.short_provider + else + provider = component.provider + end + + local icon + + provider, icon = parse_provider(provider, winid, component) local hl = evaluate_if_function(component.hl, winid) or {} local hlname @@ -258,68 +287,58 @@ local function parse_component(component, winid) hl = parse_hl(hl) end - local is_component_empty = str == '' + local is_component_empty = provider.str == '' - local left_sep_str = parse_sep_list( + local left_sep = parse_sep_list( component.left_sep, hl.bg, is_component_empty, winid ) - local right_sep_str = parse_sep_list( + local right_sep = parse_sep_list( component.right_sep, hl.bg, is_component_empty, winid ) - icon = parse_icon(evaluate_if_function(component.icon or icon, winid), hl, is_component_empty) + icon = parse_icon( + evaluate_if_function(component.icon or icon, winid), + hl, + is_component_empty + ) if is_component_empty then - return string.format( - '%s%s%s', - left_sep_str, - icon, - right_sep_str - ) + return { + str = string.format( + '%s%s%s', + left_sep.str, + icon.str, + right_sep.str + ), + len = left_sep.len + icon.len + right_sep.len + } else - return string.format( - '%s%s%%#%s#%s%s', - left_sep_str, - icon, - hlname or get_hlname(hl), - str, - right_sep_str - ) + return { + str = string.format( + '%s%s%%#%s#%s%s', + left_sep.str, + icon.str, + hlname or get_hlname(hl), + provider.str, + right_sep.str + ), + len = left_sep.len + icon.len + provider.len + right_sep.len + } end end --- Parse components of a section of the statusline -local function parse_statusline_section(section, winid, statusline_type, section_index) - local section_components = {} - - for i, component in ipairs(section) do - local ok, result = pcall(parse_component, component, winid) - - if ok then - section_components[#section_components+1] = result - else - api.nvim_err_writeln(string.format( - "Feline: error while processing component number %d on section %d of type '%s': %s", - i, section_index, statusline_type, result - )) - end - end - - return table.concat(section_components) -end - -- Generate statusline by parsing all components and return a string function M.generate_statusline(winid) local statusline_str = '' - if components and not is_disabled(winid) then + if components_table and not is_disabled(winid) then local statusline_type if winid == api.nvim_get_current_win() and not is_forced_inactive() then @@ -328,17 +347,96 @@ function M.generate_statusline(winid) statusline_type='inactive' end - local statusline = components[statusline_type] + local sections = components_table[statusline_type] + + -- Flatten sections to a single table containing all components. Parse the components and + -- store the parsed value alongside the original value. Also store the section number and + -- the component number of the component so that the components can later be placed in order + -- Also calculate the length of the statusline while doing all of that + local all_components = {} + local statusline_length = 0 + + for i, section in ipairs(sections) do + for j, component in ipairs(section) do + local ok, result = pcall(parse_component, component, winid) + + 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 = '' + end + + local component_info = { + value = component, + parsed_value = result, + section_index = i, + component_index = j + } + + all_components[#all_components+1] = component_info + statusline_length = statusline_length + component_info.parsed_value.len + end + end + + local win_width = api.nvim_win_get_width(winid) + + -- If statusline length is larger than the window width, sort the components in ascending + -- order of priority and then truncate the components one by one using by their + -- short_provider until the statusline fits within the window + if statusline_length > win_width then + table.sort(all_components, function(a, b) + return (a.value.priority or 0) < (b.value.priority or 0) + end) + + for _, component_info in ipairs(all_components) do + if component_info.value.short_provider then + -- Get new parsed value using the short provider and calculate the length + -- difference between the two values, and if it's greater than 0, use the new + -- value instead of the old one + local new_parsed_value = parse_component(component_info.value, winid, true) + local length_difference = component_info.parsed_value.len - new_parsed_value.len + + if length_difference > 0 then + -- Update statusline length and replace old parsed value with new one + statusline_length = statusline_length - length_difference + component_info.parsed_value = new_parsed_value + end + end + + if statusline_length <= win_width then break end + end + end + + -- Now use the section number and component number to put the components strings in their + -- respective locations + local parsed_sections = {} - if statusline then - local sections = {} + for _, component_info in ipairs(all_components) do + local section_index, component_index, component_str = component_info.section_index, + component_info.component_index, + component_info.parsed_value.str - for i, section in ipairs(statusline) do - sections[#sections+1] = parse_statusline_section(section, winid, statusline_type, i) + if not parsed_sections[section_index] then + parsed_sections[section_index] = {} end - statusline_str = table.concat(sections, string.format('%%#%s#%%=', defhl())) + parsed_sections[section_index][component_index] = component_str end + + -- Concatenate all components in each section to get a string for each section + local section_strs = {} + + for i, parsed_section in ipairs(parsed_sections) do + section_strs[i] = table.concat(parsed_section) + end + + -- Finally, concatenate all sections to get the statusline string + -- Append defhl to each section so that the gaps between each section use the default hl + statusline_str = table.concat(section_strs, string.format('%%#%s#%%=', defhl())) end -- Never return an empty string since setting statusline to an empty string will make it