feat: new navigation view (#235)

This commit is contained in:
Steven Arcangeli 2023-03-30 19:29:04 -07:00
parent 8d0915b1f8
commit 4b725dc8e5
15 changed files with 653 additions and 12 deletions

View file

@ -168,6 +168,9 @@ it](https://github.com/stevearc/aerial.nvim/issues/new?assignees=stevearc&labels
| `[count]AerialPrev` | | Jump backwards [count] symbols (default 1). |
| `[count]AerialGo[!]` | | Jump to the [count] symbol (default 1). |
| `AerialInfo` | | Print out debug info related to aerial. |
| `AerialNavToggle` | | Open or close the aerial nav window. |
| `AerialNavOpen` | | Open the aerial nav window. |
| `AerialNavClose` | | Close the aerial nav window. |
## Options
@ -455,6 +458,31 @@ require("aerial").setup({
end,
},
-- Options for the floating nav windows
nav = {
border = "rounded",
max_height = 0.9,
min_height = { 10, 0.1 },
max_width = 0.5,
min_width = { 0.2, 20 },
win_opts = {
cursorline = true,
winblend = 10,
},
-- Jump to symbol in source window when the cursor moves
autojump = false,
-- Keymaps in the nav window
keymaps = {
["<CR>"] = "actions.jump",
["<2-LeftMouse>"] = "actions.jump",
["<C-v>"] = "actions.jump_vsplit",
["<C-s>"] = "actions.jump_split",
["h"] = "actions.left",
["l"] = "actions.right",
["<C-c>"] = "actions.close",
},
},
lsp = {
-- Fetch document symbols when LSP diagnostics update.
-- If false, will update on buffer changes.
@ -646,6 +674,10 @@ hi AerialGuide2 guifg=Blue
- [tree_open(opts)](doc/api.md#tree_openopts)
- [tree_close(opts)](doc/api.md#tree_closeopts)
- [tree_toggle(opts)](doc/api.md#tree_toggleopts)
- [nav_is_open()](doc/api.md#nav_is_open)
- [nav_open()](doc/api.md#nav_open)
- [nav_close()](doc/api.md#nav_close)
- [nav_toggle()](doc/api.md#nav_toggle)
- [sync_folds(bufnr)](doc/api.md#sync_foldsbufnr)
- [info()](doc/api.md#info)
- [num_symbols(bufnr)](doc/api.md#num_symbolsbufnr)

View file

@ -293,6 +293,31 @@ OPTIONS *aerial-option
end,
},
-- Options for the floating nav windows
nav = {
border = "rounded",
max_height = 0.9,
min_height = { 10, 0.1 },
max_width = 0.5,
min_width = { 0.2, 20 },
win_opts = {
cursorline = true,
winblend = 10,
},
-- Jump to symbol in source window when the cursor moves
autojump = false,
-- Keymaps in the nav window
keymaps = {
["<CR>"] = "actions.jump",
["<2-LeftMouse>"] = "actions.jump",
["<C-v>"] = "actions.jump_vsplit",
["<C-s>"] = "actions.jump_split",
["h"] = "actions.left",
["l"] = "actions.right",
["<C-c>"] = "actions.close",
},
},
lsp = {
-- Fetch document symbols when LSP diagnostics update.
-- If false, will update on buffer changes.
@ -364,6 +389,15 @@ AerialCloseAll *:AerialCloseAl
AerialInfo *:AerialInfo*
Print out debug info related to aerial.
AerialNavToggle *:AerialNavToggle*
Open or close the aerial nav window.
AerialNavOpen *:AerialNavOpen*
Open the aerial nav window.
AerialNavClose *:AerialNavClose*
Close the aerial nav window.
--------------------------------------------------------------------------------
API *aerial-api*
@ -551,6 +585,22 @@ tree_toggle({opts}) *aerial.tree_toggl
{bubble} `nil|boolean` If true and current symbol has no children,
perform the action on the nearest parent (default true)
nav_is_open(): boolean *aerial.nav_is_open*
Check if the nav windows are open
nav_open() *aerial.nav_open*
Open the nav windows
nav_close() *aerial.nav_close*
Close the nav windows
nav_toggle() *aerial.nav_toggle*
Toggle the nav windows open/closed
sync_folds({bufnr}) *aerial.sync_folds*
Sync code folding with the current tree state.

View file

@ -26,6 +26,10 @@
- [tree_open(opts)](#tree_openopts)
- [tree_close(opts)](#tree_closeopts)
- [tree_toggle(opts)](#tree_toggleopts)
- [nav_is_open()](#nav_is_open)
- [nav_open()](#nav_open)
- [nav_close()](#nav_close)
- [nav_toggle()](#nav_toggle)
- [sync_folds(bufnr)](#sync_foldsbufnr)
- [info()](#info)
- [num_symbols(bufnr)](#num_symbolsbufnr)
@ -266,6 +270,30 @@ Toggle the collapsed state at the selected location
| | recurse | `nil\|boolean` | If true, perform the action recursively on all children (default false) |
| | bubble | `nil\|boolean` | If true and current symbol has no children, perform the action on the nearest parent (default true) |
## nav_is_open()
`nav_is_open(): boolean` \
Check if the nav windows are open
## nav_open()
`nav_open()` \
Open the nav windows
## nav_close()
`nav_close()` \
Close the nav windows
## nav_toggle()
`nav_toggle()` \
Toggle the nav windows open/closed
## sync_folds(bufnr)
`sync_folds(bufnr)` \

View file

@ -2,6 +2,9 @@
:AerialCloseAll aerial.txt /*:AerialCloseAll*
:AerialGo aerial.txt /*:AerialGo*
:AerialInfo aerial.txt /*:AerialInfo*
:AerialNavClose aerial.txt /*:AerialNavClose*
:AerialNavOpen aerial.txt /*:AerialNavOpen*
:AerialNavToggle aerial.txt /*:AerialNavToggle*
:AerialNext aerial.txt /*:AerialNext*
:AerialOpen aerial.txt /*:AerialOpen*
:AerialOpenAll aerial.txt /*:AerialOpenAll*
@ -26,6 +29,10 @@ aerial.focus aerial.txt /*aerial.focus*
aerial.get_location aerial.txt /*aerial.get_location*
aerial.info aerial.txt /*aerial.info*
aerial.is_open aerial.txt /*aerial.is_open*
aerial.nav_close aerial.txt /*aerial.nav_close*
aerial.nav_is_open aerial.txt /*aerial.nav_is_open*
aerial.nav_open aerial.txt /*aerial.nav_open*
aerial.nav_toggle aerial.txt /*aerial.nav_toggle*
aerial.next aerial.txt /*aerial.next*
aerial.next_up aerial.txt /*aerial.next_up*
aerial.num_symbols aerial.txt /*aerial.num_symbols*

View file

@ -6,7 +6,7 @@ M.show_help = {
desc = "Show default keymaps",
callback = function()
local config = require("aerial.config")
require("aerial.keymap_util").show_help(config.keymaps)
require("aerial.keymap_util").show_help("aerial.actions", config.keymaps)
end,
}

View file

@ -120,4 +120,16 @@ M.info = function(params)
print(string.format("Show symbols: %s", data.filter_kind_map))
end
M.nav_toggle = function()
require("aerial.nav_view").toggle()
end
M.nav_open = function()
require("aerial.nav_view").open()
end
M.nav_close = function()
require("aerial.nav_view").close()
end
return M

View file

@ -279,6 +279,31 @@ local default_options = {
end,
},
-- Options for the floating nav windows
nav = {
border = "rounded",
max_height = 0.9,
min_height = { 10, 0.1 },
max_width = 0.5,
min_width = { 0.2, 20 },
win_opts = {
cursorline = true,
winblend = 10,
},
-- Jump to symbol in source window when the cursor moves
autojump = false,
-- Keymaps in the nav window
keymaps = {
["<CR>"] = "actions.jump",
["<2-LeftMouse>"] = "actions.jump",
["<C-v>"] = "actions.jump_vsplit",
["<C-s>"] = "actions.jump_split",
["h"] = "actions.left",
["l"] = "actions.right",
["<C-c>"] = "actions.close",
},
},
lsp = {
-- Fetch document symbols when LSP diagnostics update.
-- If false, will update on buffer changes.

View file

@ -208,6 +208,7 @@ function M.set_symbols(buf, items)
local prev = prev_by_level[item.level]
if prev then
prev.next_sibling = item
item.prev_sibling = prev
end
for j = item.level + 1, max_level do
prev_by_level[j] = nil

View file

@ -87,6 +87,27 @@ local commands = {
desc = "Print out debug info related to aerial.",
},
},
{
cmd = "AerialNavToggle",
func = "nav_toggle",
defn = {
desc = "Open or close the aerial nav window.",
},
},
{
cmd = "AerialNavOpen",
func = "nav_open",
defn = {
desc = "Open the aerial nav window.",
},
},
{
cmd = "AerialNavClose",
func = "nav_close",
defn = {
desc = "Close the aerial nav window.",
},
},
}
local do_setup
@ -367,6 +388,19 @@ M.tree_close = lazy("tree", "close")
--- bubble nil|boolean If true and current symbol has no children, perform the action on the nearest parent (default true)
M.tree_toggle = lazy("tree", "toggle")
---Check if the nav windows are open
---@return boolean
M.nav_is_open = lazy("nav_view", "is_open")
---Open the nav windows
M.nav_open = lazy("nav_view", "open")
---Close the nav windows
M.nav_close = lazy("nav_view", "close")
---Toggle the nav windows open/closed
M.nav_toggle = lazy("nav_view", "toggle")
---Sync code folding with the current tree state.
---@param bufnr nil|integer
---@note

View file

@ -1,10 +1,10 @@
local actions = require("aerial.actions")
local util = require("aerial.util")
local M = {}
local function resolve(rhs)
local function resolve(action_module, rhs)
if type(rhs) == "string" and vim.startswith(rhs, "actions.") then
return resolve(actions[vim.split(rhs, ".", true)[2]])
local mod = require(action_module)
return resolve(action_module, mod[vim.split(rhs, ".", true)[2]])
elseif type(rhs) == "table" then
local opts = vim.deepcopy(rhs)
opts.callback = nil
@ -13,16 +13,23 @@ local function resolve(rhs)
return rhs, {}
end
M.set_keymaps = function(mode, keymaps, bufnr)
M.set_keymaps = function(mode, action_module, keymaps, bufnr, ...)
local args = vim.F.pack_len(...)
for k, v in pairs(keymaps) do
local rhs, opts = resolve(v)
local rhs, opts = resolve(action_module, v)
if rhs then
if type(rhs) == "function" and args.n > 0 then
local _rhs = rhs
rhs = function()
_rhs(vim.F.unpack_len(args))
end
end
vim.keymap.set(mode, k, rhs, vim.tbl_extend("keep", { buffer = bufnr }, opts))
end
end
end
M.show_help = function(keymaps)
M.show_help = function(action_module, keymaps)
local rhs_to_lhs = {}
local lhs_to_all_lhs = {}
for k, rhs in pairs(keymaps) do
@ -43,7 +50,7 @@ M.show_help = function(keymaps)
for k, rhs in pairs(keymaps) do
local all_lhs = lhs_to_all_lhs[k]
if all_lhs then
local _, opts = resolve(rhs)
local _, opts = resolve(action_module, rhs)
local keystr = table.concat(all_lhs, "/")
max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr))
table.insert(col_left, { str = keystr, all_lhs = all_lhs })

View file

@ -13,6 +13,27 @@ local function calc_float(value, max_value)
end
end
---@return integer
M.get_editor_width = function()
return vim.o.columns
end
---@return integer
M.get_editor_height = function()
local editor_height = vim.o.lines - vim.o.cmdheight
-- Subtract 1 if tabline is visible
if vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1) then
editor_height = editor_height - 1
end
-- Subtract 1 if statusline is visible
if
vim.o.laststatus >= 2 or (vim.o.laststatus == 1 and #vim.api.nvim_tabpage_list_wins(0) > 1)
then
editor_height = editor_height - 1
end
return editor_height
end
local function calc_list(values, max_value, aggregator, limit)
local ret = limit
if type(values) == "table" then
@ -44,7 +65,7 @@ end
local function get_max_width(relative, winid)
if relative == "editor" then
return vim.o.columns
return M.get_editor_width()
else
return vim.api.nvim_win_get_width(winid or 0)
end
@ -52,7 +73,7 @@ end
local function get_max_height(relative, winid)
if relative == "editor" then
return vim.o.lines - vim.o.cmdheight
return M.get_editor_height()
else
return vim.api.nvim_win_get_height(winid or 0)
end

View file

@ -0,0 +1,75 @@
local aerial = require("aerial")
local M = {}
M.jump = {
desc = "Jump to the symbol under the cursor",
callback = function(nav)
local symbol = nav:get_current_symbol()
nav:close()
if symbol then
require("aerial.navigation").select_symbol(symbol, nav.winid, nav.bufnr, { jump = true })
end
end,
}
M.jump_vsplit = {
desc = "Jump to the symbol in a vertical split",
callback = function(nav)
local symbol = nav:get_current_symbol()
nav:close()
if symbol then
require("aerial.navigation").select_symbol(
symbol,
nav.winid,
nav.bufnr,
{ jump = true, split = "vertical" }
)
end
end,
}
M.jump_split = {
desc = "Jump to the symbol in a horizontal split",
callback = function(nav)
local symbol = nav:get_current_symbol()
nav:close()
if symbol then
require("aerial.navigation").select_symbol(
symbol,
nav.winid,
nav.bufnr,
{ jump = true, split = "horizontal" }
)
end
end,
}
M.left = {
desc = "Navigate to parent symbol",
callback = function(nav)
local symbol = nav:get_current_symbol()
if symbol and symbol.parent then
nav:focus_symbol(symbol.parent)
end
end,
}
M.right = {
desc = "Navigate to child symbol",
callback = function(nav)
local symbol = nav:get_current_symbol()
if symbol and symbol.children and not vim.tbl_isempty(symbol.children) then
nav:focus_symbol(symbol.children[1])
end
end,
}
M.close = {
desc = "Close the nav windows",
callback = function()
aerial.nav_close()
end,
}
return M

342
lua/aerial/nav_view.lua Normal file
View file

@ -0,0 +1,342 @@
local backends = require("aerial.backends")
local config = require("aerial.config")
local data = require("aerial.data")
local keymap_util = require("aerial.keymap_util")
local layout = require("aerial.layout")
local navigation = require("aerial.navigation")
local util = require("aerial.util")
local window = require("aerial.window")
local M = {}
---@class aerial.NavPanel
---@field winid integer
---@field bufnr integer
---@field width integer
---@field height integer
---@field symbols aerial.Symbol[]
---@class aerial.Nav
---@field left aerial.NavPanel
---@field main aerial.NavPanel
---@field right aerial.NavPanel
---@field bufnr integer
---@field autocmds integer[]
local AerialNav = {}
local _active_nav = nil
---@return integer
local function create_buf()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].buftype = "nofile"
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].modifiable = false
vim.bo[bufnr].filetype = "aerial-nav"
return bufnr
end
function AerialNav.new(bufnr, winid)
local left_buf = create_buf()
local width = math.floor((layout.get_editor_width() - 6) / 3)
local left_win = vim.api.nvim_open_win(left_buf, false, {
relative = "editor",
row = 1,
col = 1,
width = width,
height = 20,
border = config.nav.border,
style = "minimal",
})
local main_buf = create_buf()
local main_win = vim.api.nvim_open_win(main_buf, true, {
relative = "editor",
row = 1,
col = width + 2,
width = width,
height = 20,
-- If you want a rounded border, convert the main window border to 'single' so the center joints
-- have a cleaner look
border = config.nav.border == "rounded" and "single" or config.nav.border,
style = "minimal",
zindex = 51,
})
local right_buf = create_buf()
local right_win = vim.api.nvim_open_win(right_buf, false, {
relative = "editor",
row = 1,
col = 2 * (width + 2),
width = width,
height = 20,
border = config.nav.border,
style = "minimal",
})
local nav = setmetatable({
winid = winid,
bufnr = bufnr,
left = {
bufnr = left_buf,
winid = left_win,
width = 80,
height = 20,
symbols = {},
},
main = {
bufnr = main_buf,
winid = main_win,
width = 80,
height = 20,
symbols = {},
},
right = {
bufnr = right_buf,
winid = right_win,
width = 80,
height = 20,
symbols = {},
},
autocmds = {},
}, {
__index = AerialNav,
})
keymap_util.set_keymaps("", "aerial.nav_actions", config.nav.keymaps, main_buf, nav)
vim.api.nvim_create_autocmd("WinLeave", {
desc = "Close Aerial nav window on leave",
nested = true,
once = true,
callback = function()
M.close()
end,
})
vim.api.nvim_create_autocmd("BufLeave", {
desc = "Close Aerial nav window on leave",
nested = true,
once = true,
buffer = main_buf,
callback = function()
M.close()
end,
})
-- Defer the CursorMoved autocmd so it doesn't fire immediately
vim.schedule(function()
vim.api.nvim_create_autocmd("CursorMoved", {
desc = "Update symbols on cursor move",
buffer = main_buf,
callback = function()
local symbol = nav:get_current_symbol()
if symbol then
if config.nav.autojump then
navigation.select_symbol(symbol, winid, bufnr, { jump = false })
end
nav:focus_symbol(symbol)
end
end,
})
end)
table.insert(
nav.autocmds,
vim.api.nvim_create_autocmd("VimResized", {
desc = "Update aerial nav view",
callback = function()
nav:relayout()
end,
})
)
return nav
end
---@return nil|aerial.Symbol
function AerialNav:get_current_symbol()
local lnum = vim.api.nvim_win_get_cursor(self.main.winid)[1]
return self.main.symbols[lnum]
end
---@param symbol nil|aerial.Symbol
---@return aerial.Symbol[]
---@return integer
local function get_all_siblings(symbol)
local ret = {}
if not symbol then
return ret, 1
end
table.insert(ret, symbol)
local i = 1
local iter = symbol
while iter.prev_sibling do
iter = iter.prev_sibling
i = i + 1
table.insert(ret, 1, iter)
end
iter = symbol
while iter.next_sibling do
iter = iter.next_sibling
table.insert(ret, iter)
end
return ret, i
end
---@param panel aerial.NavPanel
local function render_symbols(panel)
local bufnr = panel.bufnr
local lines = {}
local highlights = {}
local max_len = 1
for i, item in ipairs(panel.symbols) do
local kind = config.get_icon(bufnr, item.kind)
local text = util.remove_newlines(string.format("%s %s", kind, item.name))
table.insert(lines, text)
local text_cols = vim.api.nvim_strwidth(text)
table.insert(highlights, { "Aerial" .. item.kind .. "Icon", i - 1, 0, kind:len() })
table.insert(highlights, { "Aerial" .. item.kind, i - 1, kind:len(), -1 })
max_len = math.max(max_len, text_cols)
end
-- If there are no symbols in this section, add some indicator of that
if #lines == 0 then
table.insert(lines, "<none>")
table.insert(highlights, { "Comment", 0, 0, -1 })
end
vim.bo[bufnr].modifiable = true
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.bo[bufnr].modifiable = false
vim.bo[bufnr].modified = false
local ns = vim.api.nvim_create_namespace("aerial")
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
for _, hl in ipairs(highlights) do
vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
end
panel.width = max_len
panel.height = #lines
end
---@param symbol aerial.Symbol
function AerialNav:focus_symbol(symbol)
local siblings, lnum = get_all_siblings(symbol)
self.main.symbols = siblings
self.left.symbols = get_all_siblings(symbol.parent)
self.right.symbols = symbol.children or {}
render_symbols(self.left)
render_symbols(self.main)
render_symbols(self.right)
if vim.api.nvim_win_is_valid(self.main.winid) then
vim.api.nvim_win_set_cursor(self.main.winid, { lnum, 0 })
end
self:relayout()
end
function AerialNav:relayout()
local total_width = layout.get_editor_width()
local total_height = layout.get_editor_height()
local main_width = layout.calculate_width("editor", self.main.width, config.nav, 0)
local desired_height = math.max(self.left.height, math.max(self.main.height, self.right.height))
local height = layout.calculate_height("editor", desired_height, config.nav, 0)
local main_col = math.floor((total_width - main_width) / 2)
local main_row = math.floor((total_height - height) / 2)
vim.api.nvim_win_set_config(self.main.winid, {
relative = "editor",
width = main_width,
height = height,
row = main_row,
col = main_col,
})
local border_width = config.nav.border == "none" and 0 or 1
local width_remaining = math.floor((total_width - main_width) / 2)
if config.nav.border ~= "none" then
width_remaining = width_remaining - (2 * border_width)
end
local left_width = layout.calculate_width("editor", self.left.width, config.nav, 0)
if left_width > width_remaining then
left_width = width_remaining
end
vim.api.nvim_win_set_config(self.left.winid, {
relative = "editor",
width = left_width,
height = height,
row = main_row,
col = main_col - left_width - border_width,
})
local right_width = layout.calculate_width("editor", self.right.width, config.nav, 0)
if right_width > width_remaining then
right_width = width_remaining
end
vim.api.nvim_win_set_config(self.right.winid, {
relative = "editor",
width = right_width,
height = height,
row = main_row,
col = main_col + main_width + border_width,
})
for k, v in pairs(config.nav.win_opts) do
vim.wo[self.main.winid][k] = v
-- Hack: we generally don't want the left/right to have cursorline enabled
if k ~= "cursorline" then
vim.wo[self.left.winid][k] = v
vim.wo[self.right.winid][k] = v
end
end
end
function AerialNav:close()
for _, winid in ipairs({ self.left.winid, self.main.winid, self.right.winid }) do
if vim.api.nvim_win_is_valid(winid) then
vim.api.nvim_win_close(winid, true)
end
end
for _, id in ipairs(self.autocmds) do
vim.api.nvim_del_autocmd(id)
end
self.autocmds = {}
end
---@return boolean
M.is_open = function()
return _active_nav ~= nil
end
M.open = function()
if M.is_open() then
return
end
local bufnr = vim.api.nvim_get_current_buf()
local winid = vim.api.nvim_get_current_win()
local cursor = vim.api.nvim_win_get_cursor(0)
local backend = backends.get(bufnr)
if not backend then
backends.log_support_err()
return
end
if not data.has_symbols(bufnr) then
backend.fetch_symbols(bufnr)
end
_active_nav = AerialNav.new(bufnr, winid)
local bufdata = data.get(bufnr)
if bufdata then
local pos = window.get_symbol_position(bufdata, cursor[1], cursor[2], true)
_active_nav:focus_symbol(pos.closest_symbol)
end
end
M.toggle = function()
if M.is_open() then
M.close()
else
M.open()
end
end
M.close = function()
if not M.is_open() then
return
end
local nav = _active_nav
_active_nav = nil
nav:close()
end
return M

View file

@ -196,8 +196,15 @@ M.select = function(opts)
error(string.format("Symbol %s is outside the bounds", opts.index))
return
end
local bufnr, _ = util.get_buffers()
M.select_symbol(item, winid, bufnr, opts)
end
---@param item aerial.Symbol
---@param winid integer
---@param bufnr integer
---@param opts table
M.select_symbol = function(item, winid, bufnr, opts)
if opts.jump and config.close_on_select then
window.close()
end

View file

@ -15,7 +15,7 @@ local function create_aerial_buffer(bufnr)
end
local aer_bufnr = vim.api.nvim_create_buf(false, true)
keymap_util.set_keymaps("", config.keymaps, aer_bufnr)
keymap_util.set_keymaps("", "aerial.actions", config.keymaps, aer_bufnr)
vim.api.nvim_buf_set_var(bufnr, "aerial_buffer", aer_bufnr)
-- Set buffer options
vim.api.nvim_buf_set_var(aer_bufnr, "source_buffer", bufnr)