Skip to content

Commit

Permalink
feat(ui): show keys/help in an overlay and added scrolling hint
Browse files Browse the repository at this point in the history
  • Loading branch information
folke committed Jul 15, 2024
1 parent af7a30f commit 50b2c43
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 83 deletions.
2 changes: 1 addition & 1 deletion lua/which-key/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ local defaults = {
g = true, -- bindings for prefixed with g
},
},
---@type wk.Win
---@type wk.Win.opts
win = {
-- don't allow the popup to overlap with the cursor
no_overlap = true,
Expand Down
2 changes: 1 addition & 1 deletion lua/which-key/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
---@field mode? string|string[]
---@field cond? boolean|fun():boolean?

---@class wk.Win: vim.api.keyset.win_config
---@class wk.Win.opts: vim.api.keyset.win_config
---@field width? wk.Dim
---@field height? wk.Dim
---@field wo? vim.wo
Expand Down
158 changes: 77 additions & 81 deletions lua/which-key/view.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ local Icons = require("which-key.icons")
local Layout = require("which-key.layout")
local Plugins = require("which-key.plugins")
local State = require("which-key.state")
local Text = require("which-key.text")
local Tree = require("which-key.tree")
local Util = require("which-key.util")
local Win = require("which-key.win")

---@alias

local M = {}
M.buf = nil ---@type number
M.win = nil ---@type number
M.view = nil ---@type wk.Win?
M.footer = nil ---@type wk.Win?
M.timer = (vim.uv or vim.loop).new_timer()

---@alias wk.Sorter fun(node:wk.Item): (string|number)
Expand Down Expand Up @@ -48,18 +52,22 @@ M.fields = {
end,
}

---@param key string
function M.format(key)
local inner = key:match("^<(.*)>$")
if not inner then
return key
end
local parts = vim.split(inner, "-", { plain = true })
parts[1] = Config.icons.keys[parts[1]] or parts[1]
if parts[2] and not parts[2]:match("^%w$") then
parts[2] = Config.icons.keys[parts[2]] or parts[2]
end
return table.concat(parts, "")
---@param lhs string
function M.format(lhs)
local keys = Util.keys(lhs)
local ret = vim.tbl_map(function(key)
local inner = key:match("^<(.*)>$")
if not inner then
return key
end
local parts = vim.split(inner, "-", { plain = true })
parts[1] = Config.icons.keys[parts[1]] or parts[1]
if parts[2] and not parts[2]:match("^%w$") then
parts[2] = Config.icons.keys[parts[2]] or parts[2]
end
return table.concat(parts, "")
end, keys)
return table.concat(ret, "")
end

---@param nodes wk.Item[]
Expand All @@ -83,7 +91,7 @@ function M.sort(nodes, fields)
end

function M.valid()
return M.buf and vim.api.nvim_buf_is_valid(M.buf) and M.win and vim.api.nvim_win_is_valid(M.win) or false
return M.view and M.view:valid()
end

---@param opts? {delay?: number, schedule?: boolean, waited?: number}
Expand Down Expand Up @@ -117,28 +125,17 @@ function M.update(opts)
end

function M.hide()
if not (M.buf or M.win) then
return
if M.view then
M.view:hide()
M.view = nil
end

---@type number?, number?
local buf, win = M.buf, M.win
M.buf, M.win = nil, nil

local function try_close()
pcall(vim.api.nvim_win_close, win, true)
pcall(vim.api.nvim_buf_delete, buf, { force = true })
win = win and vim.api.nvim_win_is_valid(win) and win or nil
buf = buf and vim.api.nvim_buf_is_valid(buf) and buf or nil
if win or buf then
vim.schedule(try_close)
end
if M.footer then
M.footer:hide()
M.footer = nil
end

try_close()
end

---@return wk.Win
---@return wk.Win.opts
function M.opts()
return vim.tbl_deep_extend("force", { col = 0, row = math.huge }, Config.win, {
relative = "editor",
Expand All @@ -161,25 +158,6 @@ function M.opts()
})
end

---@param opts wk.Win
function M.mount(opts)
local win_opts = vim.deepcopy(opts)
win_opts.wo = nil
win_opts.bo = nil
win_opts.padding = nil
win_opts.no_overlap = nil

if M.valid() then
win_opts.noautocmd = nil
return vim.api.nvim_win_set_config(M.win, win_opts)
end

M.buf = vim.api.nvim_create_buf(false, true)
Util.bo(M.buf, opts.bo)
M.win = vim.api.nvim_open_win(M.buf, false, win_opts)
Util.wo(M.win, opts.wo)
end

---@param field string
---@param value string
---@return string
Expand Down Expand Up @@ -272,7 +250,7 @@ function M.show()
M.hide()
return
end
local text = require("which-key.text").new()
local text = Text.new()

---@type wk.Node[]
local children = vim.tbl_values(state.node.children or {})
Expand Down Expand Up @@ -323,7 +301,7 @@ function M.show()

local t = Layout.new({ cols = cols, rows = items })

local opts = M.opts()
local opts = Win.defaults(Config.win)
local container = {
width = Layout.dim(vim.o.columns, vim.o.columns, opts.width),
height = Layout.dim(vim.o.lines, vim.o.lines, opts.height),
Expand Down Expand Up @@ -409,35 +387,63 @@ function M.show()
opts.height = opts.height - bw
M.check_overlap(opts)

M.view = M.view or Win.new(opts)
M.view:show(opts)

if Config.show_help or show_keys then
text:nl()
local footer = Text.new()
if show_keys then
text:append(" ")
footer:append(" ")
for _, segment in ipairs(M.trail(state.node) or {}) do
text:append(segment[1], segment[2])
footer:append(segment[1], segment[2])
end
end
if Config.show_help then
local col = text:col({ display = true })
local ws = string.rep(" ", math.floor((opts.width - 30) / 2) - col)
text:append(ws)
text:append("<esc>", "WhichKey"):append(" close", "WhichKeySeparator")
text:append(" ")
text:append("<bs>", "WhichKey"):append(" go up a level", "WhichKeySeparator")
---@type {key: string, desc: string}[]
local keys = {
{ key = "<esc>", desc = "close" },
}
if state.node.parent then
keys[#keys + 1] = { key = "<bs>", desc = "back" }
end
if opts.height < text:height() then
keys[#keys + 1] = { key = "<c-d>/<c-u>", desc = "scroll" }
end
local help = Text.new()
for k, key in ipairs(keys) do
help:append(M.replace("key", Util.norm(key.key)), "WhichKey"):append(" " .. key.desc, "WhichKeySeparator")
if k < #keys then
help:append(" ")
end
end
local col = footer:col({ display = true })
local ws = string.rep(" ", math.floor((opts.width - help:width()) / 2) - col)
footer:append(ws)
footer:append(help._lines[1])
end
end
text:trim()

M.mount(opts)

text:render(M.buf)
vim.api.nvim_win_call(M.win, function()
footer:trim()
M.footer = M.footer or Win.new()
M.footer:show({
relative = "win",
win = M.view.win,
col = 0,
row = opts.height - 1,
width = opts.width,
height = 1,
zindex = M.view.opts.zindex + 1,
})
footer:render(M.footer.buf)
end

text:render(M.view.buf)
vim.api.nvim_win_call(M.view.win, function()
vim.fn.winrestview({ topline = 1 })
end)
vim.cmd.redraw()
end

---@param opts wk.Win
---@param opts wk.Win.opts
function M.check_overlap(opts)
if Config.win.no_overlap == false then
return
Expand All @@ -459,17 +465,7 @@ end

---@param up boolean
function M.scroll(up)
assert(M.valid(), "invalid view")
local height = vim.api.nvim_win_get_height(M.win)
local delta = math.ceil((up and -1 or 1) * height / 2)
local view = vim.api.nvim_win_call(M.win, vim.fn.winsaveview)
local top = view.topline ---@type number
top = top + delta
top = math.max(top, 1)
top = math.min(top, vim.api.nvim_buf_line_count(M.buf) - height + 1)
vim.api.nvim_win_call(M.win, function()
vim.fn.winrestview({ topline = top, lnum = top })
end)
return M.view and M.view:scroll(up)
end

return M
111 changes: 111 additions & 0 deletions lua/which-key/win.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
local Util = require("which-key.util")

---@class wk.Win
---@field win? number
---@field buf? number
---@field opts wk.Win.opts
local M = {}
M.__index = M

---@class wk.Win.opts
local override = {
relative = "editor",
style = "minimal",
focusable = false,
noautocmd = true,
wo = {
scrolloff = 0,
foldenable = false,
winhighlight = "Normal:WhichKeyNormal,FloatBorder:WhichKeyBorder,FloatTitle:WhichKeyTitle",
winbar = "",
statusline = "",
wrap = false,
},
bo = {
buftype = "nofile",
bufhidden = "wipe",
filetype = "wk",
},
}

---@type wk.Win.opts
local defaults = { col = 0, row = math.huge, zindex = 1000 }

---@param opts? wk.Win.opts
function M.defaults(opts)
return vim.tbl_deep_extend("force", {}, defaults, opts or {}, override)
end

---@param opts? wk.Win.opts
function M.new(opts)
local self = setmetatable({}, M)
self.opts = M.defaults(opts)
return self
end

function M:valid()
return self.buf and vim.api.nvim_buf_is_valid(self.buf) and self.win and vim.api.nvim_win_is_valid(self.win) or false
end

function M:hide()
if not (self.buf or self.win) then
return
end

---@type number?, number?
local buf, win = self.buf, self.win
self.buf, self.win = nil, nil

local function try_close()
pcall(vim.api.nvim_win_close, win, true)
pcall(vim.api.nvim_buf_delete, buf, { force = true })
win = win and vim.api.nvim_win_is_valid(win) and win or nil
buf = buf and vim.api.nvim_buf_is_valid(buf) and buf or nil
if win or buf then
vim.schedule(try_close)
end
end

try_close()
end

---@param opts? wk.Win.opts
function M:show(opts)
if opts then
self.opts = vim.tbl_deep_extend("force", self.opts, opts)
end
local win_opts = vim.deepcopy(self.opts)
win_opts.wo = nil
win_opts.bo = nil
win_opts.padding = nil
win_opts.no_overlap = nil

if self:valid() then
win_opts.noautocmd = nil
return vim.api.nvim_win_set_config(self.win, win_opts)
end

self.buf = vim.api.nvim_create_buf(false, true)
Util.bo(self.buf, self.opts.bo or {})
self.win = vim.api.nvim_open_win(self.buf, false, win_opts)
Util.wo(self.win, self.opts.wo or {})
end

---@param up boolean
function M:scroll(up)
if not self:valid() then
return
end
local height = vim.api.nvim_win_get_height(self.win)
local delta = math.ceil((up and -1 or 1) * height / 2)
local view = vim.api.nvim_win_call(self.win, vim.fn.winsaveview)
local top = view.topline ---@type number
top = top + delta
top = math.max(top, 1)
top = math.min(top, vim.api.nvim_buf_line_count(self.buf) - height + 1)
vim.api.nvim_win_call(self.win, function()
vim.fn.winrestview({ topline = top, lnum = top })
end)
end

return M

1 comment on commit 50b2c43

@max397574
Copy link
Contributor

@max397574 max397574 commented on 50b2c43 Jul 15, 2024

Choose a reason for hiding this comment

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

👀
vim.fn.winsaveview and vm.fn.winrestview are really useful functions which I didn't know existed
Definitely will be useful
Until now I used vim.cmd.normal("zt")` for this usecase

Please sign in to comment.