Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(popup)!: moved="any", enter default, callback=fn #622

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions POPUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ stablization and any required features are merged into Neovim, we can upstream
this and expose the API in vimL to create better compatibility.

## Notices
- **2024-09-19:** change `enter` default to false to follow Vim.
- **2021-09-19:** we now follow Vim's convention of the first line/column of the screen being indexed 1, so that 0 can be used for centering.
- **2021-08-19:** we now follow Vim's default to `noautocmd` on popup creation. This can be overriden with `vim_options.noautocmd=false`

Expand All @@ -34,19 +35,26 @@ Unlikely (due to technical difficulties):
- textprop
- textpropwin
- textpropid
- [ ] "close"
- But this is mostly because I don't know how to use mouse APIs in nvim. If someone knows. please make an issue in the repo, and maybe we can get it sorted out.

Unlikely (due to not sure if people are using):
- [ ] tabpage

## Progress

Suported Functions:

- [x] popup.create
- [x] popup.move
- [ ] popup.close
- [ ] popup.clear


Suported Features:

- [x] what
- string
- list of strings
- bufnr
- [x] popup_create-arguments
- [x] border
- [x] borderchars
Expand All @@ -69,6 +77,25 @@ Suported Features:
- [x] title
- [x] wrap
- [x] zindex
- [x] callback
- [ ] mousemoved
- [ ] "any"
- [ ] "word"
- [ ] "WORD"
- [ ] "expr"
- [ ] (list options)
- [?] close
- [ ] "button"
- [ ] "click"
- [x] "none"


Additional Features:

- [x] enter
- [x] focusable
- [x] noautocmd
- [x] finalize_callback

## All known unimplemented vim features at the moment

Expand All @@ -79,10 +106,7 @@ Suported Features:
- filter
- filtermode
- mapping
- callback
- mouse:
- mousemoved
- close
- drag
- resize

Expand Down
125 changes: 95 additions & 30 deletions lua/plenary/popup/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ popup._hidden = {}
-- Keep track of popup borders, so we don't have to pass them between functions
popup._borders = {}

-- Callbacks to be called later by popup.execute_callback. Indexed by win_id.
popup._callback_fn = {}

-- Result is passed to the callback. Indexed by win_id. See popup_win_closed.
popup._result = {}

local function dict_default(options, key, default)
if options[key] == nil then
return default[key]
Expand All @@ -34,9 +40,6 @@ local function dict_default(options, key, default)
end
end

-- Callbacks to be called later by popup.execute_callback
popup._callbacks = {}

-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts}
-- If an option is not given in {vim_options}, fall back to {default_opts}
local function add_position_config(win_opts, vim_options, default_opts)
Expand Down Expand Up @@ -112,6 +115,69 @@ local function add_position_config(win_opts, vim_options, default_opts)
-- , contents on the screen. Set to TRUE to disable this.
end

--- Closes the popup window
--- Adapted from vim.lsp.util.close_preview_autocmd
---
---@param winnr integer window id of popup window
---@param bufnrs table|nil optional list of ignored buffers
local function close_window(winnr, bufnrs)
vim.schedule(function()
-- exit if we are in one of ignored buffers
if bufnrs and vim.list_contains(bufnrs, vim.api.nvim_get_current_buf()) then
return
end

local augroup = "popup_window_" .. winnr
pcall(vim.api.nvim_del_augroup_by_name, augroup)
pcall(vim.api.nvim_win_close, winnr, true)
end)
end

--- Creates autocommands to close a popup window when events happen.
---
---@param events table list of events
---@param winnr integer window id of popup window
---@param bufnrs table list of buffers where the popup window will remain visible, {popup, parent}
---@see autocmd-events
local function close_window_autocmd(events, winnr, bufnrs)
local augroup = vim.api.nvim_create_augroup("popup_window_" .. winnr, {
clear = true,
})

-- close the popup window when entered a buffer that is not
-- the floating window buffer or the buffer that spawned it
vim.api.nvim_create_autocmd("BufEnter", {
group = augroup,
callback = function()
close_window(winnr, bufnrs)
end,
})

if #events > 0 then
vim.api.nvim_create_autocmd(events, {
group = augroup,
buffer = bufnrs[2],
callback = function()
close_window(winnr)
end,
})
end
end
--- End of code adapted from vim.lsp.util.close_preview_autocmd

--- Only used from 'WinClosed' autocommand
--- Cleanup after popup window closes.
---@param win_id integer window id of popup window
local function popup_win_closed(win_id)
-- Invoke the callback with the win_id and result.
if popup._callback_fn[win_id] then
pcall(popup._callback_fn[win_id], win_id, popup._result[win_id])
popup._callback_fn[win_id] = nil
end
-- Forget about this window.
popup._result[win_id] = nil
end

function popup.create(what, vim_options)
vim_options = vim.deepcopy(vim_options)

Expand Down Expand Up @@ -236,19 +302,38 @@ function popup.create(what, vim_options)

local win_id
if vim_options.hidden then
assert(false, "I have not implemented this yet and don't know how")
assert(false, "hidden: not implemented yet and don't know how")
else
win_id = vim.api.nvim_open_win(bufnr, false, win_opts)
end

-- Set the default result. Also serves to indicate active popups.
popup._result[win_id] = -1
-- Always catch the popup's close
local augroup = vim.api.nvim_create_augroup("popup_close_" .. win_id, {
clear = true,
})
vim.api.nvim_create_autocmd("WinClosed", {
group = augroup,
pattern = tostring(win_id),
callback = function()
pcall(vim.api.nvim_del_augroup_by_name, augroup)
popup_win_closed(win_id)
end,
})

-- Moved, handled after since we need the window ID
if vim_options.moved then
if vim_options.moved == "any" then
vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id)
-- elseif vim_options.moved == "word" then
-- TODO: Handle word, WORD, expr, and the range functions... which seem hard?
close_window_autocmd({ "CursorMoved", "CursorMovedI" }, win_id, { bufnr, vim.fn.bufnr() })
--[[
else
-- TODO: Handle word, WORD, expr, and the range functions... which seem hard?
assert(false, "moved ~= 'any': not implemented yet and don't know how")
]]
end
else
-- TODO: If the buffer's deleted close the window. Is this needed?
local silent = false
vim.cmd(
string.format(
Expand Down Expand Up @@ -397,7 +482,7 @@ function popup.create(what, vim_options)
-- enter
local should_enter = vim_options.enter
if should_enter == nil then
should_enter = true
should_enter = false
end

if should_enter then
Expand All @@ -412,22 +497,10 @@ function popup.create(what, vim_options)

-- callback
if vim_options.callback then
popup._callbacks[bufnr] = function()
-- (jbyuki): Giving win_id is pointless here because it's closed right afterwards
-- but it might make more sense once hidden is implemented
local row, _ = unpack(vim.api.nvim_win_get_cursor(win_id))
vim_options.callback(win_id, what[row])
vim.api.nvim_win_close(win_id, true)
end
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<CR>",
'<cmd>lua require"plenary.popup".execute_callback(' .. bufnr .. ")<CR>",
{ noremap = true }
)
popup._callback_fn[win_id] = vim_options.callback
end

-- TODO: Wonder what this is about? Debug? Convenience to get bufnr?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what this does if I'm not mistaken @tjdevries @Conni2461 it's that checks for vim_options.finalize_callback to be (it could be null) and if does exists calls it, maybe there's a part of the code where it removes it or it's set to false, will look into that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vim_options.finalize_callback

The question is about why it exists at all. It was added with #513.

@xactlyblue Could you clarify the purpose of #513? It doesn't seem to have anything to do with popups.

Copy link
Contributor

@AlejandroSuero AlejandroSuero Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah why it exists, that idk, if @xactlyblue could clarify on that regard, would be nice. I tried to find where it was being used or defined and this is what I got.

I added this to log when it's called or not:

image

Results from make test:

Screenshot 2024-10-09 at 18 04 38

Video demo when using Telescope which creates a popup:

finalize_callback-demo.mp4

Note

As you can see it adds the not called log twice because I think Telescope calls it twice for each one it creates. I used Telescope 3 times, and got 6 not called logs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried both in this PR and in master btw, in case it was related to something this PR was removing.

Copy link
Contributor

@xactlyblue xactlyblue Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if @xactlyblue could clarify on that regard

Hey.

I made that change a while ago to address a problem I was having where I needed to access some information about the buffer created for the popup (I forgot the details to be honest, but if you need them I could probably go through my dotfiles commit history to figure out what my specific use-case was at the time).

It just creates an easier way for the user to manually access the bufnr and win_id of the popup for whatever reason they might need it. There isn't any internal implementation or usage of it.

I probably should've added a comment explaining the changes to clear the purpose up, sorry for any confusion!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creates an easier way for the user to manually access the bufnr and win_id

Thanks, no problem. Debug convenience. Do you agree there's no reason to keep it in a final product?

Consider nvim_win_get_buf(win_id)

Copy link
Contributor

@xactlyblue xactlyblue Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I should have clarified: this change was meant to be accessible by the user (for whatever reason they may need to access the bufnr and win_id of a created popup).

I'm not really familiar with the inner-workings of plenary or it's API, so I apologize if this change was unnecessary, but I couldn't find any existing method to easily access the bufnr of a created popup via a callback, so I figured the simplest solution would be to add an optional callback to allow the user to access the bufnr and win_id as soon as they're available.

If I recall correctly my use-case had something to do with zindex/overlap issues in a plugin that allowed you to supply popup arguments to plenary, which is why I made it a callback (I guess I figured it was more useful/elegant to add a general callback for plenary instead of modifying the plugin itself) the user can supply.

I looked through my NeoVim configuration and I'm not currently using it so I personally don't mind if it's removed but I'm not sure if anyone else is making use of it in their own configurations (though I'd guess the odds of that are fairly slim considering it's not really documented anywhere and has fairly specific use-cases).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood (I think). I didn't see a reason for it, other than convenience.

simple, existing method to access the bufnr of a created popup

I'm new to neovim, I don't know what was available when. Currently nvim_win_get_buf(win_id) or winbufnr() gets the buffer number from a window. May be other ways.

I'm not asking you to remove it, just wanted to make sure there wasn't something I missed.

if vim_options.finalize_callback then
vim_options.finalize_callback(win_id, bufnr)
end
Expand Down Expand Up @@ -478,12 +551,4 @@ function popup.move(win_id, vim_options)
end
end

function popup.execute_callback(bufnr)
if popup._callbacks[bufnr] then
local wrapper = popup._callbacks[bufnr]
wrapper()
popup._callbacks[bufnr] = nil
end
end

return popup
3 changes: 3 additions & 0 deletions lua/plenary/popup/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ utils.bounded = function(value, min, max)
return value
end

-- TODO: Should defaults get deepcopy before table values are used?
-- utils.apply_defaults is never used AFAICT.
-- So I guess this comment is about plenary/tbl.lua.
utils.apply_defaults = function(original, defaults)
if original == nil then
original = {}
Expand Down
76 changes: 76 additions & 0 deletions tests/plenary/popup_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,82 @@ describe("plenary.popup", function()
})
end)

describe("callback option", function()
local callback_result
local function callback(wid, result)
callback_result = result
end

it("without a callback", function()
callback_result = nil
local popup_wid = popup.create("hello there", {})
vim.api.nvim_win_close(popup_wid, true)

eq(nil, callback_result)
end)

it("with a callback", function()
callback_result = nil
local popup_wid = popup.create("hello there", {
callback = callback,
})
vim.api.nvim_win_close(popup_wid, true)

eq(-1, callback_result)
end)
end)

describe("enter option", function()
it("enter not specified", function()
local main_wid = vim.fn.win_getid()
-- same as enter = false
local popup_wid = popup.create("hello there", {})
cur_wid = vim.fn.win_getid()
-- current window should still be the main window
eq(main_wid, cur_wid)
vim.api.nvim_win_close(popup_wid, true)
end)

it("enter = false", function()
local main_wid = vim.fn.win_getid()
local popup_wid = popup.create("hello there", {
enter = false,
})
cur_wid = vim.fn.win_getid()
-- current window should still be the main window
eq(main_wid, cur_wid)
vim.api.nvim_win_close(popup_wid, true)
end)

it("enter = true", function()
local main_wid = vim.fn.win_getid()
local popup_wid = popup.create("hello there", {
enter = true,
})
cur_wid = vim.fn.win_getid()
-- current window should be the popup
eq(popup_wid, cur_wid)
vim.api.nvim_win_close(popup_wid, true)
end)
end)

describe("moved option", function()
local callback_result
local function callback(wid, result)
callback_result = result
end

it("with moved but not used", function()
callback_result = nil
local popup_wid = popup.create("hello there", {
moved = "any",
callback = callback,
})
vim.api.nvim_win_close(popup_wid, true)
eq(-1, callback_result)
end)
end)

describe("what", function()
it("can be an existing bufnr", function()
local bufnr = vim.api.nvim_create_buf(false, false)
Expand Down