rebelot / heirline.nvim

Heirline.nvim is a no-nonsense Neovim Statusline plugin designed around recursive inheritance to be exceptionally fast and versatile.
MIT License
1.01k stars 41 forks source link

[Feat] Expandable components #13

Closed rebelot closed 2 years ago

rebelot commented 2 years ago

You can test expandable components on branch experimental. The following is an example to illustrate the basic usage.

The factory function is make_elastic_component(priority, providers). it accepts the priority integer value and a list of providers which can be either strings or functions. Providers must be in decreasing length.

lowest priority: first to contract, last to expand highest priority: last to contract, first to expand components with same priority are contracted/expanded together.

You also need to plug the elastic_before callback when calling setup.

I need some feedback on this to test performance and usability.

Cheers!

    local u = require("heirline.utils")

    local elastic1 = u.make_elastic_component(1, { string.rep("A", 40), function(self) return string.rep("B", 20) end, string.rep("C", 5) })
    local elastic2 = u.clone(elastic1, { static = { priority = 2 } })

    local statusline = {

        {
            provider = string.rep("S", 20),
        },
        {
            provider = "%=",
        },
        elastic1,
        {
            provider = "%=",
        },
        elastic2,
        {
            provider = "%=",
        },
        elastic1,
        {
            provider = "%=",
        },
        {
            provider = string.rep("S", 20),
        },
    }

    require("heirline").setup(statusline, { before = u.elastic_before })
arsham commented 2 years ago

Thank your for the new design. I can see it is headed for the right direction.

The priority is getting confused in some cases. In the following example, the icon and the filetype on the right (blue) have priority of 1:

    heirline.make_elastic_component(1, {
      function(self)
        return " " .. (self.icon or "") .. " " .. string.upper(vim.bo.filetype)
      end,
      function(self)
        return " " .. (self.icon or "")
      end,
      "",
    }),

And the filename priority at the centre of the status line is 1, 2 and 4. 1 1

2 2

4 3

As you can see, the filename when it has the highest priority is not retracted.

rebelot commented 2 years ago

1) fixed issue where priorities had to be continuous. Now can be any integer (except 0) 2) make_elastic_component now accepts components

Below some examples taking advantage of some other utility functions and general heirline inheritance-based functionality. Notes: 1) you can wrap elastic components in other components providing some scope for providers 2) when you pass components to make_elastic_components as arguments, their condition, init and static will be affected. To avoid troubles, it's best to shield your actual component by enclosing them into a "dummy" parent. Like this: make_elastic_component(3, {my_component}, {my_other_component}) (see expandable_2 in the example, where condition is protected)

Perhaps the most common usecase wuold be expandable_3 in this example.

    local long = { provider = string.rep("A", 40) }
    local medium = { provider = string.rep("B", 20) }
    local short = { provider = string.rep("C", 5) }
    local null = { provider = "" }
    local static = { provider = string.rep("S", 10) }
    local fill = { provider = "%=" }

    local expandable_1 = u.make_elastic_component(1, long, medium, short)

    local expandable_2 = u.make_elastic_component(2)
    expandable_2 = u.insert(expandable_2, {
        u.clone(long, {
            condition = function()
                return true
            end,
            hl = { bg = "green" },
        }),
    }, null)

    local expandable_3 = {
        condition = function(self)
            return vim.bo.filetype == "lua"
        end,
        static = { icon = "!" },
        u.make_elastic_component(3, {
            provider = function(self)
                return self.icon .. "loooooooooooong"
            end,
        }, {
            provider = function(self)
                return self.icon .. "short"
            end,
        }),
    }

    local statusline = {

        static,
        fill,
        { hl = { fg = "blue" }, expandable_1 },
        fill,
        expandable_2,
        fill,
        expandable_3,
        fill,
        static,
    }

    require("heirline").setup(statusline, { before = u.elastic_before })
rebelot commented 2 years ago

IMPORTANT: in the experimental branch, stop_at_first has become stop_when(self, out). I am not sure I will keep this. To retain the same functionality, you should change

...
stop_at_first = true
...

to this:

...
stop_when = function(self, out)
  return out ~= ''
end
...
arsham commented 2 years ago

Nice work!

The overall functionality works really well. However there are a few things that can be worked on:

  1. Nesting elastic components doesn't work. It is intended?
  2. The performance is degraded when there are a few elastic components in the statusline. I've noticed in places they are inactive the performance is better.
  3. The spacing is incorrect.

As you can see the components retract when there is still enough space left. spacing

rebelot commented 2 years ago

there's a current bug where all elastic components contribute to the "decision making" process of whether they would fit or not, disregarding the fact that the component might be in an hidden statusline (stop_at_first/stop_when).

If you try the example script you'll see that the spacing works correctly, but if you implement, say, an inactive statusline containing expandable components you'll see the wrong spacing behavior.

I am working on it, shouldn't be too too hard.

for the nesting, keep in mind that the statusline structure is a recursive inheritance hell and my brain hurts at thinking what would happen if you nest them.

The problem:

One option could be pretending to forget about this feature and call it bloat. Or I could just fix it piece-by-piece, until "it works", but I can already see it's becoming spaghetti. I would be happy already with some working spaghetti, but I am considering a third way, using vim.loop libuv asynchronous events and allow some kind of async communication between a main event loop and each component.

arsham commented 2 years ago

Fair enough.

How do you feel about keeping a table of elastic component in descending order of priorities and calculate the length in two passes? Something like this.

rebelot commented 2 years ago

try it now. ;)

483c56a2736e4775862f9b44820ce605df12dd87

rebelot commented 2 years ago

it is now possible to nest expandable components. Just make sure that the inner expandable component has a lower priority than the outer (i.e. it needs to be updated before its own container).

performance should also be improved as the calls to eval have been significantly reduced.

I am still thinking about small optimisations/refactoring, but the API should remain unchanged.

5828b3d4d5fef336bf918145956ba131aa6cb2ac

Thanks a lot @arsham as your feedback greatly improved this plugin.

arsham commented 2 years ago

No problem mate, I'm glad to help.

So, everything is fine, here is the results (the same setup): 1

This setup doesn't have any nested components, I will refactor and test it.

The performance is noticeably better, although can be improved.

Thank you for the amazing work!

arsham commented 2 years ago

I found a couple of new things: 1

  1. The folder name is not retracted. I might have ignored one of your notes above, I'm knackered!
  2. In the last example when I comment the middle section out, it ignores everything if there is no space left. I've checked with different priorities, the outcome was the same.
rebelot commented 2 years ago

Thanks again for the feedback, I know what's causing this and I can solve it already. It is a bit complicated, but has to do with the fact that the current logic is to always evaluate the longest options and then try to contract. For this reason expandable components that are not at index = 1 will never have the chance to notify their existence. The other option is to try to contract or expand instead of always try to contract. I discarded the first option after some benchmarkin showing the execution time was 0.5 times higher and I could not foresee benefits. I need to clean the code a bit then I can push that. It shouldn't take long and hopefully this is solved (for good???).

rebelot commented 2 years ago

update: we're almost there. Evaluation time has sped up again, as now the total evaluations per cycle are: 1 full evaluation and at most N evaluations of just the expandable components.

ATM I'm trying to make sense out of sorting priorities in case of nesting. It is becoming confusing.

rebelot commented 2 years ago

update: It should now be possible to nest expandable components indefinitely.

    local a = { provider = string.rep("A", 40) }
    local b = { provider = string.rep("B", 30) }
    local c = { provider = string.rep("C", 20) }
    local d = { provider = string.rep("D", 10) }
    local e = { provider = string.rep("E", 8) }
    local f = { provider = string.rep("F", 4) }

    local nest_madness = {
        u.make_elastic_component(1,
            a,
            u.make_elastic_component(nil,
                b,
                u.make_elastic_component(nil, c, d),
                e
            ),
            f
        ),
        { provider = "%=" },
        u.make_elastic_component(4,
            a,
            u.make_elastic_component(nil,
                b,
                u.make_elastic_component(nil, c, d),
                e
            ),
            f
        ),
    }

    require("heirline").setup(nest_madness, { before = u.elastic_before, after = u.elastic_after })

tl;dr Only care about the outer expandable component priority and use large number gaps between expandable components.