grapp-dev/nui-components.nvim

`Select` not showing proper amount of options on different renders

vague2k opened this issue · 8 comments

Hello, I've noticed unexpected behavior while using the Select component, that the amount of options presented can differ between each render throughout different sessions. Unfortunately this doesn't cause anything to error, so I have no reproducible steps.

here's a couple of screenshots showing the issue

1st time entering neovim, this is what my picker looked like. It is missing 1 option, being oxocarbon
Screenshot 2024-04-30 at 12 59 34 PM

when quitting neovim, and entering again (2nd time) I get what I expect to see al the time, a list of all installed colorschemes excluding the defaults. Notice how oxocarbon is now an option in the component
Screenshot 2024-04-30 at 12 59 46 PM

after quitting yet again and rendering (now the 3rd time) many more options are missing almost half in fact.
Screenshot 2024-04-30 at 12 59 56 PM

The way I'm getting the colorschemes is done in a synchronous manner, so I should have all of the expected options by the time the renderer renders the component tree.

@vague2k could you provide a repro, please?

@vague2k could you provide a repro, please?

here's my code snippet. I have a suspicion that it has something to do with the signal.

local test_conf = require("huez.picker.renderer")

-- FIXME: why does select component randomly render different amount of options on each neovim instance
local renderer = n.create_renderer(test_conf._calc_pos())

local signal = n.create_signal({
  query = "",
  data = helpers.tonodes(colorscheme.installed()),
})

local get_data = function()
  return signal.data:dup():combine_latest(signal.query:debounce(0):start_with(""), function(items, query)
    return fn.ifilter(items, function(item)
      return string.find(item.name:lower(), query:lower())
    end)
  end)
end

local body = function()
  return n.columns(n.rows(
    -- { flex = 1 },
    n.prompt({
      id = PROMPT_ID,
      autofocus = true,
      prefix = " ::: ",
      value = signal.query,
      size = 1,
      border_label = {
        text = "󰌁 Huez",
        align = "center",
      },
      on_change = function(curr)
        signal.query = curr
      end,
      on_submit = function()
        if signal.query:get_value() ~= "" then
          local top_query_match = renderer:get_component_by_id(SELECT_ID):get_props().data[1].name
          colorscheme.save(top_query_match)
          renderer:close()
          log.notify("Selected " .. top_query_match, "info")
        end
      end,
    }),

    n.select({
      id = SELECT_ID,
      flex = 1,
      autofocus = false,
      border_label = "Themes",
      data = get_data(),
      on_change = function(theme)
        vim.cmd("colorscheme " .. theme.name)
      end,
      on_select = function(theme)
        colorscheme.save(theme.name)
        renderer:close()
        log.notify("Selected " .. theme.name, "info")
      end,
      on_focus = function(self)
        local first_theme_from_options = self:get_props().data[1].name
        vim.cmd("colorscheme " .. first_theme_from_options)
      end,
      on_blur = function()
        vim.cmd("colorscheme " .. colorscheme.get())
      end,
    })
  ))
end

-- TODO: extract this to a function that returns a table of mappings
renderer:add_mappings({
  {
    mode = "n",
    key = "q",
    handler = function()
      renderer:close()
    end,
  },
})

renderer:on_unmount(function()
  vim.cmd("colorscheme " .. colorscheme.get())
end)

local function pick_colorscheme()
  renderer:render(body)
end

return {
  pick_colorscheme = pick_colorscheme,
}

hello @vague2k 👋 Please provide a reproducible example that I can easily copy and test.

For reference:

  • test_conf._calc_pos() - add a sample renderer position
  • helpers.tonodes(colorscheme.installed()) - provide mocked data instead

@mobily when mocking the data, this behavior isn't present. if you'd still like to use mock data I think the issue will be seen the most if passing a function call to signal.data.

local function installed()

  local themes = vim.fn.getcompletion("", "color", true)
  local manually_installed = {}

  for _, theme in pairs(themes) do
      local node = n.option(theme, {name = theme}) 
      table.insert(manually_installed, node)
  end

  return manually_installed
end

local signal = n.create_signal({
  -- ... ,
  data = installed(),
})

For the renderer you can use this snippet

local renderer = n.create_renderer({
      width = 40,
      height = vim.api.nvim_win_get_height(0),
      relative = "editor",
      position = {
        row = 0,
        col = vim.api.nvim_win_get_height(0) + 40,
      },
})

Hi, any updates? I've tried debugging my own code to see if somehow it was something wrong with my implementation, but I'm almost convinced it has some unexpected behavior with signals

For example, according to my mock functions I provided above, if you pass installed directly to the data prop in select, the amount of expected nodes appear in the component every single time.

But when passing the same function to a signal, then passing the signal's prop the data field, the behavior arises again.

Thought it would be worth making a follow up comment.

hello @vague2k 👋 I couldn't reproduce the issue you reported, which makes me think that the problem might be on your end. However, upon reviewing your code, I noticed an issue with setting the initial value of the Prompt component. This issue was resolved in the latest PR (#43), but it seems unrelated to the problem you reported (but please give it a try and let me know!).

Here is the command I used to retrieve all installed color schemes.

:lua vim.notify(vim.inspect(vim.fn.getcompletion("", "color", true)))

Result:

CleanShot 2024-05-04 at 16 52 52@2x

Here's the screen recording of a few attempts. In each attempt, the Lua table with the installed color schemes remains unchanged (in the Select component).

CleanShot.2024-05-04.at.16.57.54-converted.mp4

The code snippet I've been using:

local fn = require("nui-components.utils.fn")
local n = require("nui-components")

local renderer = n.create_renderer({
  width = 40,
  height = vim.api.nvim_win_get_height(0),
  relative = "editor",
  position = {
    row = 0,
    col = vim.api.nvim_win_get_height(0) + 40,
  },
})

local PROMPT_ID = "prompt"
local SELECT_ID = "select"

local function installed()
  local themes = vim.fn.getcompletion("", "color", true)
  local manually_installed = {}

  for _, theme in pairs(themes) do
    local node = n.option(theme, { name = theme })
    table.insert(manually_installed, node)
  end

  return manually_installed
end

local signal = n.create_signal({
  query = "",
  data = installed(),
})

local get_data = function()
  return signal.data
    :dup()
    -- 🔥 added signal.query:get_value() to set the initial value correctly
    :combine_latest(signal.query:debounce(0):start_with(signal.query:get_value()), function(items, query)
      return fn.ifilter(items, function(item)
        return string.find(item.name:lower(), query:lower())
      end)
    end)
end

local body = function()
  return n.columns(n.rows(
    -- { flex = 1 },
    n.prompt({
      id = PROMPT_ID,
      autofocus = true,
      prefix = " ::: ",
      value = signal.query,
      size = 1,
      border_label = {
        text = "󰌁 Huez",
        align = "center",
      },
      on_change = function(value)
        signal.query = value
      end,
    }),
    n.select({
      id = SELECT_ID,
      flex = 1,
      autofocus = false,
      border_label = "Themes",
      data = get_data(),
      on_change = function(theme)
        vim.cmd("colorscheme " .. theme.name)
      end,
      on_select = function(theme)
        fn.log({ theme.name })
      end,
      on_focus = function(self)
        local first_theme_from_options = renderer:get_component_by_id(SELECT_ID):get_props().data[1].name
        vim.cmd("colorscheme " .. first_theme_from_options)
      end,
      on_blur = function() end,
    })
  ))
end

-- TODO: extract this to a function that returns a table of mappings
renderer:add_mappings({
  {
    mode = "n",
    key = "q",
    handler = function()
      renderer:close()
    end,
  },
})

renderer:on_unmount(function() end)

local function pick_colorscheme()
  renderer:render(body)
end

vim.keymap.set("n", "<leader>fg", pick_colorscheme)

@mobily After throwing around some print statements, I noticed that the specific function that I am calling to get the options colorscheme.installed(), When I put a print statement at the bottom of the function, that function gets executed on nvim startup, and thus being the source of the issue, printing the different amounts of themes.

So it seems like it's an issue with my implementation of setup, although that begs that next question. Why is the function getting executed during startup when it isn't explicitly called?

A very subtle oversight that was hard to catch unless I knew what i was looking for, and in all honesty makes feel a bit embarassed considering it ended up having nothing to do with nui-components.

If you'd like to help me with that, this is the specific branch of my plugin which uses nui-components.

going to close this, as simply adding scheduling the user command that has my api as a dependency solves the issue

sorry again for the drawn out issue, I really do appreciate your time and efforts ! :)