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

feat: auto load popups and auto populate help popup #601

Merged
merged 10 commits into from
Jul 18, 2023
2 changes: 1 addition & 1 deletion lua/neogit/lib/buffer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local Ui = require("neogit.lib.ui")

---@class Buffer
---@field handle number
---@field mmanager any
---@field mmanager MappingsManager
---@field ui Ui
---@field kind string
local Buffer = {
Expand Down
35 changes: 21 additions & 14 deletions lua/neogit/lib/mappings_manager.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
local managers = {}

local function invoke(id, map_id)
---@alias Mapping string|function|MappingTable

---@class MappingTable
---@field [1] string mode
---@field [2] string|function func
---@field [3] boolean Escape visual mode

---@class MappingsManager
---@field mappings table<string, Mapping>
local MappingsManager = {}

function MappingsManager.invoke(id, map_id)
local manager = managers[id]
local k = manager.map_id_to_key[map_id]
local mapping = manager.mappings[k]
Expand All @@ -12,11 +23,16 @@ local function invoke(id, map_id)
end
end

local function build_call_string(id, k)
function MappingsManager.build_call_string(id, k)
return string.format([[<cmd>lua require 'neogit.lib.mappings_manager'.invoke(%d, %d)<CR>]], id, k)
end

local function new(id)
function MappingsManager.delete(id)
managers[id] = nil
end

---@return MappingsManager
function MappingsManager.new(id)
local mappings = {}
local map_id_to_key = {}
local manager = {
Expand All @@ -26,7 +42,7 @@ local function new(id)
register = function()
for k, mapping in pairs(mappings) do
local map_id = #map_id_to_key + 1
local f_call = build_call_string(id, map_id)
local f_call = MappingsManager.build_call_string(id, map_id)
if type(mapping) == "table" then
for _, m in pairs(vim.split(mapping[1], "")) do
if type(mapping[2]) == "string" then
Expand Down Expand Up @@ -61,13 +77,4 @@ local function new(id)
return manager
end

local function delete(id)
managers[id] = nil
end

return {
new = new,
build_call_string = build_call_string,
delete = delete,
invoke = invoke,
}
return MappingsManager
6 changes: 4 additions & 2 deletions lua/neogit/lib/popup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ end

-- Closes the popup buffer
function M:close()
self.buffer:close()
self.buffer = nil
if self.buffer then
self.buffer:close()
self.buffer = nil
end
end

-- Determines the correct highlight group for a switch based on it's state.
Expand Down
17 changes: 0 additions & 17 deletions lua/neogit/popups.lua

This file was deleted.

7 changes: 7 additions & 0 deletions lua/neogit/popups/echo/actions.lua
Copy link
Member

Choose a reason for hiding this comment

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

Is this just for testing?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it is for a unit test inside M.test

CKolkey marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
local M = {}
function M.echo(value)
local notification = require("neogit.lib.notification")
notification.create("Echo: " .. value)
end

return M
22 changes: 22 additions & 0 deletions lua/neogit/popups/echo/init.lua
CKolkey marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
local popup = require("neogit.lib.popup")
local actions = require("neogit.popups.echo.actions")

local M = {}

function M.create(...)
local args = { ... }
local p = popup.builder():name("NeogitEchoPopup")
for k, v in ipairs(args) do
p:action(tostring(k), tostring(v), function()
actions.echo(v)
end)
end

local p = p:build()

p:show()

return p
end

return M
37 changes: 12 additions & 25 deletions lua/neogit/popups/help/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ local function present(commands)
local presenter = util.map(commands, function(command)
local cmd, name, fn = unpack(command)

--- Handle the longer table mapping form (mode, func, esc)
if type(fn) == "table" then
fn = fn[2]
end

return { name = name, key = status_mappings[cmd], fn = fn }
end)

Expand All @@ -19,40 +24,22 @@ local function present(commands)
return presenter
end

M.popups = function(env)
M.popups = function()
local popups = require("neogit.popups")

return present {
{ "HelpPopup", "Help", popups.help.create },
{ "DiffPopup", "Diff", popups.diff.create },
{ "PullPopup", "Pull", popups.pull.create },
{ "RebasePopup", "Rebase", popups.rebase.create },
{ "MergePopup", "Merge", popups.merge.create },
{ "PushPopup", "Push", popups.push.create },
{ "CommitPopup", "Commit", popups.commit.create },
{ "LogPopup", "Log", popups.log.create },
{ "CherryPickPopup", "Apply", popups.cherry_pick.create },
{ "BranchPopup", "Branch", popups.branch.create },
{ "FetchPopup", "Fetch", popups.fetch.create },
{ "ResetPopup", "Reset", popups.reset.create },
{ "RevertPopup", "Revert", popups.revert.create },
{ "RemotePopup", "Remote", popups.remote.create },
{ "InitRepo", "Init", require("neogit.lib.git").init.init_repo },
{
"StashPopup",
"Stash",
function()
popups.stash.create(env.get_stash())
end,
},
local items = vim.list_extend({
{

"CommandHistory",
"History",
function()
require("neogit.buffers.git_command_history"):new():show()
end,
},
}
{ "InitRepo", "Init", require("neogit.lib.git").init.init_repo },
}, popups.mappings_table())

return present(items)
end

M.actions = function()
Expand Down
2 changes: 1 addition & 1 deletion lua/neogit/popups/help/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ local M = {}
function M.create(env)
local p = popup.builder():name("NeogitHelpPopup"):group_heading("Commands")

local popups = actions.popups(env)
local popups = actions.popups()
for i, cmd in ipairs(popups) do
p = p:action(cmd.key, cmd.name, cmd.fn)

Expand Down
109 changes: 109 additions & 0 deletions lua/neogit/popups/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
local M = {}

---@param name string
---@param f nil|fun(create: fun(...)): any
--- Creates a curried function which will open the popup with the given name when called
function M.open(name, f)
f = f or function(f)
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain how this works and why? 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes absolutely.

It is a function which given a popup name will return a function (which takes 0 arguments) which will when called load the popup neogit.popups/{name} module, and then pass its create function to the second argument f.

f can then call the create function with any arguments it likes, or whenever it wants.

I originally had it be plain argument, and create was called inside the open returned function instead, but this allows async and sleeping, as well as aborting the popup using the supplied function.

Essentially, this function will create a keymapping ready function, which will supply f with the means to open the correct popup.

the f = f or function(create) create() end will simply call the create function directly with no arguments if no second argument is supplied. Most popups don't need any extra arguments, but some do, such as stashing.

The normal flow is

open loads popup module
uses f to help calling the popup.create method.

In most cases, if the popup.create requires no special loading or arguments,

popup.open("my_awesome_popup") -> function() which opens popup is enough

This is usually called lifting and creating a function which "fills in" some arguments and returns a function, eg function(a,b) -> function(b) is called currying (partial application)

f()
end

return function()
local ok, value = pcall(require, "neogit.popups." .. name)
if ok then
assert(value, "popup is not nil")
assert(value.create, "popup has create function")

f(value.create)
else
local notification = require("neogit.lib.notification")
notification.create(string.format("No such popup: %q", name), vim.log.levels.ERROR)
end
end
end

--- Returns an array useful for creating mappings for the available popups
---@return table<string, Mapping>
function M.mappings_table()
local config = require("neogit.config")
local async = require("plenary.async")
return {
{
"HelpPopup",
"Help",
M.open("help", function(f)
f {
use_magit_keybindings = config.values.use_magit_keybindings,
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little torn on this pattern of reaching into the status buffer for stuff. On the one hand, its a little cleaner than needing to pass the line into the constructor, but on the other hand it makes this function dependent on external state instead of passing that state in as an argument.

Not saying to change this, just voicing my thoughts

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I am as well.

I usually go about coding with a domain driven approach. Some code lives in a certain domain/space, and can enter a smaller contained domain. Code usually becomes messier when a code enters a larger domain than which is was called from. Think of a graph which has to remain acyclic, like a tree.

The status buffer would need a bit of splitting like you did for the repository.

Copy link
Member Author

Choose a reason for hiding this comment

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

Perhaps splitting it as well into an init/view and actions and then using actions in other places.

Copy link
Member

Choose a reason for hiding this comment

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

Splitting the status buffer up is a longer-term goal of mine, but it clearly will take some doing. It also pre-dates the buffer lib, so doesn't really take advantage of the library that all the popups/buffers do. Eventually I'd like the status buffer to not have a special place like it currently does, and just be one more view (albeit the "main" view), but given the size/complexity of it, thats not a simple undertaking. So, small bits here and there.

It may be a good middle-step to use the init/view/actions pattern - I'm not against that.

}
end),
},
{ "DiffPopup", "Diff", M.open("diff") },
{ "PullPopup", "Pull", M.open("pull") },
{
"RebasePopup",
"Rebase",
M.open("rebase", function(f)
local status = require("neogit.status")
local line = status.status_buffer:get_current_line()
f { line[1]:match("^(%x%x%x%x%x%x%x+)") }
end),
},
{ "MergePopup", "Merge", M.open("merge") },
{ "PushPopup", "Push", M.open("push") },
{ "CommitPopup", "Commit", M.open("commit") },
{ "LogPopup", "Log", M.open("log") },
{
"CherryPickPopup",
"Cherry Pick",
{
"nv",
M.open(
"cherry_pick",
async.void(function(f)
Copy link
Member

Choose a reason for hiding this comment

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

Can you verify that this needs to be async.void? I'm not sure that it's needed, especially considering the revert popup isn't, and... they're real similar.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it needs to either, it was how it was at present.

The difference is that async.void may run on the next microtick, though not exactly sure. It should be immediate unless it needs that microframe delay for getting the visual selection. Will remove and tset

Copy link
Member

Choose a reason for hiding this comment

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

Yea, pretty sure I added that because I had no idea how async stuff worked when I did.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tested it and it works without.

There is an error on master when doing AA on a visual selection. More testing (again) would catch it

Error executing vim.schedule lua callback: ...share/nvim/lazy/plenary.nvim/lua/plenary/async/async.lua:18: The coroutine failed with this message: ...rs/teiroberts/dev/nvim/neogit/lua/neogit/lib/git/cli.lua:749: invalid value (table) at index 8 in table for 'concat'
stack traceback:
	[C]: in function 'error'
	...share/nvim/lazy/plenary.nvim/lua/plenary/async/async.lua:18: in function 'callback_or_next'
	...share/nvim/lazy/plenary.nvim/lua/plenary/async/async.lua:45: in function <...share/nvim/lazy/plenary.nvim/lua/plenary/async/async.lua:44>

local selection = nil

if vim.api.nvim_get_mode().mode == "V" then
local status = require("neogit.status")
selection = status.get_selected_commits()
end

f { commits = selection }
end)
),
true,
},
},
{ "BranchPopup", "Branch", M.open("branch") },
{ "FetchPopup", "Fetch", M.open("fetch") },
{ "ResetPopup", "Reset", M.open("reset") },
{
"RevertPopup",
"Revert",
M.open("revert", function(f)
local status = require("neogit.status")
local line = status.status_buffer:get_current_line()
f { commits = { line[1]:match("^(%x%x%x%x%x%x%x+)") } }
end),
},
{ "RemotePopup", "Remote", M.open("remote") },
{
"StashPopup",
"Stash",
M.open("stash", function(f)
local status = require("neogit.status")
local line = status.status_buffer:get_current_line()
f {
name = line[1]:match("^(stash@{%d+})"),
}
end),
},
}
end

function M.test()
M.open("echo", function(f)
f("a", "b")
end)()
end

return M
Loading