Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

Commit

Permalink
feat: add smart component truncation (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
famiu committed Oct 3, 2021
1 parent 8a221af commit 087bc82
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 23 deletions.
60 changes: 60 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
145 changes: 122 additions & 23 deletions lua/feline/generator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
69 changes: 69 additions & 0 deletions lua/feline/statusline_ffi.lua
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 087bc82

Please sign in to comment.