First draft

This commit is contained in:
Steven Arcangeli 2020-09-19 17:29:26 -07:00
parent ddf6f0ce6b
commit 3faa3993f1
11 changed files with 1054 additions and 1 deletions

View file

@ -1,2 +1,61 @@
# aerial.nvim
Neovim plugin for a table-of-contents navigation window
Show a table-of-contents pane next to your code for quick navigation
TODO: screenshots
## Requirements
Neovim 0.5 (nightly)
It's powered by LSP, so you'll need to have that already set up and working.
## Installation
aerial.nvim works with [Pathogen](https://github.com/tpope/vim-pathogen)
```sh
cd ~/.vim/bundle/
git clone https://github.com/stevearc/aerial.nvim
```
and [vim-plug](https://github.com/junegunn/vim-plug)
```vim
Plug 'stevearc/aerial.nvim'
```
## Configuration
Step one is to get a Neovim LSP set up, which is beyond the scope of this guide.
See [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) for instructions.
After you have a functioning LSP setup, you will need to customize the
`on_attach` callback.
```lua
local aerial = require'aerial'
local custom_attach = function(client)
aerial.on_attach(client)
-- Aerial does not set any mappings by default, so you'll want to set some up
local mapper = function(mode, key, result)
vim.fn.nvim_buf_set_keymap(0, mode, key, result, {noremap = true, silent = true})
end
-- Toggle the aerial pane with <leader>a
mapper('n', '<leader>a', '<cmd>lua require"aerial".toggle()<CR>')
-- Jump forwards/backwards with '[[' and ']]'
mapper('n', '[[', '<cmd>lua require"aerial".prev_item()<CR>zzzv')
mapper('v', '[[', '<cmd>lua require"aerial".prev_item()<CR>zzzv')
mapper('n', ']]', '<cmd>lua require"aerial".next_item()<CR>zzzv')
mapper('v', ']]', '<cmd>lua require"aerial".next_item()<CR>zzzv')
-- This is a great place to set up all your other LSP mappings
end
-- Set up your LSP clients here, using the custom on_attach method
require'nvim_lsp'.vimls.setup{
on_attach = custom_attach,
}
```
A full list of commands and options can be found [in the
docs](https://github.com/stevearc/aerial.nvim/blob/master/doc/aerial.txt)

215
doc/aerial.txt Normal file
View file

@ -0,0 +1,215 @@
*aerial.txt*
*Aerial* *aerial* *aerial.nvim*
===============================================================================
CONTENTS *aerial-contents*
1. Intro...........................................|aerial-intro|
2. Configure.......................................|aerial-configure|
3. Commands........................................|aerial-commands|
4. Options.........................................|aerial-options|
===============================================================================
INTRO *aerial-intro*
Aerial is a table-of-contents pane next to your code for quick navigation.
It's powered by the documentSymbols information from LSP, and uses the
built-in Neovim |lsp| client.
===============================================================================
CONFIGURE *aerial-configure*
This is a minimal configuration to get you started:
>
local aerial = require'aerial'
local custom_attach = function(client)
aerial.on_attach(client)
-- Aerial does not set any mappings by default, so you'll want to set some up
local mapper = function(mode, key, result)
vim.fn.nvim_buf_set_keymap(0, mode, key, result, {noremap = true, silent = true})
end
-- Toggle the aerial pane with <leader>a
mapper('n', '<leader>a', '<cmd>lua require"aerial".toggle()<CR>')
-- Jump forwards/backwards with '[[' and ']]'
mapper('n', '[[', '<cmd>lua require"aerial".prev_item()<CR>zzzv')
mapper('v', '[[', '<cmd>lua require"aerial".prev_item()<CR>zzzv')
mapper('n', ']]', '<cmd>lua require"aerial".next_item()<CR>zzzv')
mapper('v', ']]', '<cmd>lua require"aerial".next_item()<CR>zzzv')
-- This is a great place to set up all your other LSP mappings
end
-- Set up your LSP clients here, using the custom on_attach method
require'nvim_lsp'.vimls.setup{
on_attach = custom_attach,
}
The aerial pane itself has some sane default bindings, however you can easily
override them. The easiest way to is to use a ftplugin. For example, you can
create a file .vim/ftplugin/aerial.vim:
>
" These are the default bindings.
nnoremap <buffer> <CR> <cmd>lua require'aerial'.jump_to_loc()<CR>zzzv
nnoremap <buffer> <C-v> <cmd>lua require'aerial'.jump_to_loc(2)<CR>zzzv
nnoremap <buffer> <C-s> <cmd>lua require'aerial'.jump_to_loc(2, 'belowright split')<CR>zzzv
nnoremap <buffer> <C-j> j<cmd>lua require'aerial'.scroll_to_loc()<CR>
nnoremap <buffer> <C-k> k<cmd>lua require'aerial'.scroll_to_loc()<CR>
nnoremap <buffer> p <cmd>lua require'aerial'.scroll_to_loc()<CR>
nnoremap <buffer> q <cmd>lua require"aerial".close()<CR>
By default, the symbols information in the pane should stay updated, but if
you'd like to tweak the behavior see |g:aerial_diagnostics_trigger_update| and
you can manually call |vim.lsp.buf.document_symbol()|.
As a side note you will probably want to 'set sessionoptions-=blank' to avoid
storing aerial buffers (and other scratch buffers) when you call `:mksession`.
See 'sessionoptions' for more info
===============================================================================
COMMANDS *aerial-commands*
aerial.on_attach({client}, [{opts}] *aerial.on_attach()*
This must be called in the on_attach of your LSP client configuration. The
{opts} dictionary can contain the following entries:
preserve_callback boolean. If true, will add to the
textDocument/documentSymbol callback instead of
replacing it.
aerial.open([{focus}], [{direction}]) *aerial.open()*
Open the aerial pane for the current buffer. {focus} is a boolean that, if
true, will also jump your cursor to the aerial buffer. {direction} can be
either "<" or ">", to indicate which direction of vsplit to use (default
will try to autodetect which to use).
aerial.close() *aerial.close()*
Close the aerial pane for the current buffer.
aerial.toggle([{focus}], [{direction}]) *aerial.toggle()*
Same as |aerial.open()|, but will close the pane if it is already open.
aerial.focus() *aerial.focus()*
Jump to the aerial pane for the current buffer if it exists.
aerial.is_open() *aerial.is_open()*
Returns true if the aerial pane is open for the current buffer.
aerial.jump_to_loc([{virt_winnr}], [{split_cmd}]) *aerial.jump_to_loc()*
When in the aerial buffer, jump to the location under the cursor.
{virt_winnr} is the "virtual" winnr to jump to. 1 (the default) will be
the source buffer with the lowest winnr. 2 will be the second-lowest, etc.
If no buffer can be found with that window position, a new split will be
created. {split_cmd} will define the command used to create the split
(default is "belowright vsplit").
aerial.scroll_to_loc([{virt_winnr}], [{split_cmd}]) *aerial.scroll_to_loc()*
Same as |aerial.jump_to_loc()| but keep the cursor in the aerial buffer.
aerial.next_item() *aerial.next_item()*
Jump to the position of the next item in the aerial list.
aerial.prev_item() *aerial.prev_item()*
Jump to the position of the previous item in the aerial list.
aerial.skip_item({delta}) *aerial.skip_item()*
Jump {delta} steps in the aerial list.
===============================================================================
OPTIONS *aerial-options*
g:aerial_width *g:aerial_width*
The width of the aerial pane. Default 40.
g:aerial_update_when_errors *g:aerial_update_when_errors*
Update the aerial pane even when your file has errors. Default true.
g:aerial_diagnostics_trigger_update *g:aerial_diagnostics_trigger_update*
Call |vim.lsp.buf.document_symbol()| to update symbols whenenever the LSP
client receives diagnostics. Default true.
g:aerial_highlight_on_jump *g:aerial_highlight_on_jump*
Briefly highlight the line jumped from |aerial.jump_to_loc()|. Default
true.
g:aerial_highlight_mode *g:aerial_highlight_mode*
Valid values are "split_width", "full_width" or "last".
split_width Each open buffer will have its cursor location marked in
the aerial buffer. Each line will only be partially
highlighted to indicate which pane is at that location.
This is the default.
full_width Each open buffer will have its cursor location marked as
a full-width highlight in the aerial buffer.
last Only the most-recently focused pane will have its
location marked in the aerial buffer.
g:aerial_highlight_group *g:aerial_highlight_group*
The highlight group used for the active line in the aerial buffer and also
used for the brief highlight when jumping to a location. Default
|QuickFixLine|
aerial.set_open_automatic({filetype}, {bool}) *aerial.set_open_automatic()*
aerial.set_open_automatic({mapping})
This is used to automatically open aerial when a new buffer is loaded.
Disabled by default. The options are set per-filetype like so:
>
aerial.set_open_automatic('vim', true)
aerial.set_open_automatic('rust', false)
-- Or you can specify the mapping
aerial.set_open_automatic{
['_'] = true, -- use underscore to set the default behavior
['vim'] = false,
['lua'] = false,
}
g:aerial_open_automatic_min_lines *g:aerial_open_automatic_min_lines*
When using |aerial.set_open_automatic()|, you can set this value to only
automatically open aerial on files greater than a certain length.
g:aerial_automatic_direction *g:aerial_automatic_direction*
When using |aerial.set_open_automatic()|, the aerial pane will be opened
with this {direction} (see |aerial.open()| for details).
aerial.set_kind_abbr({kind}, {abbr}) *aerial.set_kind_abbr()*
aerial.set_kind_abbr({mapping})
Use these abbreviations for the SymbolKind of the items.
See https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol
for a complete list of the possible SymbolKind values.
>
aerial.set_kind_abbr('Function', 'F')
aerial.set_kind_abbr{
['Function'] = 'F',
['Method'] = 'M',
}
aerial.set_filter_kind({kinds}) *aerial.set_filter_kind()*
Only display these SymbolKind symbols in the aerial buffer. {kinds} is a
list-like table.
>
-- These are the default values
aerial.set_filter_kind{
'Function',
'Class',
'Constructor',
'Method',
'Struct',
'Enum',
}
===============================================================================
TODO
* Make next_item/prev_item work in aerial buffer
* Dynamic width
* Show hierarchy (will require parsing the symbols result ourselves)
* Add some vim.validate in function calls
* Scroll summary when updating position -- blocked by https://github.com/neovim/neovim/issues/10822
* Remove items & positions from cache when they are no longer relevant
* Make symbols available to fuzzy finder
===============================================================================
vim:ft=help:et:ts=2:sw=2:sts=2:norl

31
doc/tags Normal file
View file

@ -0,0 +1,31 @@
Aerial aerial.txt /*Aerial*
aerial aerial.txt /*aerial*
aerial-commands aerial.txt /*aerial-commands*
aerial-configure aerial.txt /*aerial-configure*
aerial-contents aerial.txt /*aerial-contents*
aerial-intro aerial.txt /*aerial-intro*
aerial-options aerial.txt /*aerial-options*
aerial.close() aerial.txt /*aerial.close()*
aerial.focus() aerial.txt /*aerial.focus()*
aerial.is_open() aerial.txt /*aerial.is_open()*
aerial.jump_to_loc() aerial.txt /*aerial.jump_to_loc()*
aerial.next_item() aerial.txt /*aerial.next_item()*
aerial.nvim aerial.txt /*aerial.nvim*
aerial.on_attach() aerial.txt /*aerial.on_attach()*
aerial.open() aerial.txt /*aerial.open()*
aerial.prev_item() aerial.txt /*aerial.prev_item()*
aerial.scroll_to_loc() aerial.txt /*aerial.scroll_to_loc()*
aerial.set_filter_kind() aerial.txt /*aerial.set_filter_kind()*
aerial.set_kind_abbr() aerial.txt /*aerial.set_kind_abbr()*
aerial.set_open_automatic() aerial.txt /*aerial.set_open_automatic()*
aerial.skip_item() aerial.txt /*aerial.skip_item()*
aerial.toggle() aerial.txt /*aerial.toggle()*
aerial.txt aerial.txt /*aerial.txt*
g:aerial_automatic_direction aerial.txt /*g:aerial_automatic_direction*
g:aerial_diagnostics_trigger_update aerial.txt /*g:aerial_diagnostics_trigger_update*
g:aerial_highlight_group aerial.txt /*g:aerial_highlight_group*
g:aerial_highlight_mode aerial.txt /*g:aerial_highlight_mode*
g:aerial_highlight_on_jump aerial.txt /*g:aerial_highlight_on_jump*
g:aerial_open_automatic_min_lines aerial.txt /*g:aerial_open_automatic_min_lines*
g:aerial_update_when_errors aerial.txt /*g:aerial_update_when_errors*
g:aerial_width aerial.txt /*g:aerial_width*

165
lua/aerial.lua Normal file
View file

@ -0,0 +1,165 @@
local callbacks = require 'aerial.callbacks'
local config = require 'aerial.config'
local data = require 'aerial.data'
local nav = require 'aerial.navigation'
local util = require 'aerial.util'
local window = require 'aerial.window'
local M = {}
M.is_open = function(bufnr)
local aer_bufnr = util.get_aerial_buffer()
if aer_bufnr == -1 then
return false
else
local winid = vim.fn.bufwinid(aer_bufnr)
return winid ~= -1
end
end
M.close = function()
if util.is_aerial_buffer() then
vim.cmd('close')
return
end
local aer_bufnr = util.get_aerial_buffer()
local winnr = vim.fn.bufwinnr(aer_bufnr)
if winnr ~= -1 then
vim.cmd(winnr .. "close")
end
end
M._maybe_open_automatic = function()
if vim.fn.line('$') < config.get_open_automatic_min_lines() then
return
end
M.open(false, config.get_automatic_direction())
end
M.open = function(focus, direction)
if vim.lsp.buf_get_clients() == 0 then
error("Cannot open aerial. No LSP clients")
return
end
bufnr = vim.api.nvim_get_current_buf()
local aer_bufnr = util.get_aerial_buffer(bufnr)
if M.is_open() then
if focus then
local winid = vim.fn.bufwinid(aer_bufnr)
vim.api.nvim_set_current_win(winid)
end
return
end
local direction = direction or util.detect_split_direction()
local start_winid = vim.fn.win_getid()
if aer_bufnr == -1 then
window.create_aerial_buffer(bufnr, direction)
aer_bufnr = vim.api.nvim_get_current_buf()
else
window.create_aerial_window(bufnr, direction)
vim.api.nvim_set_current_buf(aer_bufnr)
end
vim.api.nvim_set_current_win(start_winid)
if data.items_by_buf[bufnr] == nil then
vim.lsp.buf.document_symbol()
end
nav._update_position()
if focus then
vim.api.nvim_set_current_win(vim.fn.bufwinid(aer_bufnr))
end
end
M.focus = function()
if not M.is_open() then
return
end
bufnr = vim.api.nvim_get_current_buf()
local aer_bufnr = util.get_aerial_buffer(bufnr)
local winid = vim.fn.bufwinid(aer_bufnr)
vim.api.nvim_set_current_win(winid)
end
M.toggle = function(focus, direction)
if util.is_aerial_buffer() then
vim.cmd('close')
return
end
if M.is_open() then
M.close()
else
M.open(focus, direction)
end
end
M.jump_to_loc = function(virt_winnr, split_cmd)
nav.jump_to_loc(virt_winnr, split_cmd)
end
M.scroll_to_loc = function(virt_winnr, split_cmd)
nav.scroll_to_loc(virt_winnr, split_cmd)
end
M.next_item = function()
nav.skip_item(1)
end
M.prev_item = function()
nav.skip_item(-1)
end
M.skip_item = function(delta)
nav.skip_item(delta)
end
M.on_attach = function(client, opts)
local opts = opts or {}
local old_callback = vim.lsp.callbacks['textDocument/documentSymbol']
local new_callback = callbacks.symbol_callback
if opts.preserve_callback then
new_callback = function(idk1, idk2, result, idk3, bufnr)
callbacks.symbol_callback(idk1, idk2, result, idk3, bufnr)
old_callback(idk1, idk2, result, idk3, bufnr)
end
end
vim.lsp.callbacks['textDocument/documentSymbol'] = new_callback
if config.get_diagnostics_trigger_update() then
vim.cmd("autocmd User LspDiagnosticsChanged lua require'aerial.autocommands'.request_symbols_if_diagnostics_changed()")
end
vim.cmd("autocmd InsertLeave <buffer> lua vim.lsp.buf.document_symbol()")
vim.cmd("autocmd BufWritePost <buffer> lua vim.lsp.buf.document_symbol()")
vim.cmd("autocmd CursorMoved <buffer> lua require'aerial.navigation'._update_position()")
vim.cmd("autocmd BufLeave <buffer> lua require'aerial.autocommands'.on_buf_leave()")
if config.get_open_automatic() then
M._maybe_open_automatic()
vim.cmd("autocmd BufWinEnter <buffer> lua require'aerial'._maybe_open_automatic()")
end
end
M.set_open_automatic = function(ft_or_mapping, bool)
if type(ft_or_mapping) == 'table' then
config.open_automatic = ft_or_mapping
else
config.open_automatic[ft_or_mapping] = bool
end
end
M.set_kind_abbr = function(kind_or_mapping, abbr)
if type(kind_or_mapping) == 'table' then
config.kind_abbr = kind_or_mapping
else
config.kind_abbr[kind_or_mapping] = abbr
end
end
M.set_filter_kind = function(list)
config.filter_kind = {}
for _,kind in pairs(list) do
config.filter_kind[kind] = true
end
end
return M

View file

@ -0,0 +1,68 @@
-- Functions that are called in response to autocommands
local config = require 'aerial.config'
local data = require 'aerial.data'
local util = require 'aerial.util'
local window = require 'aerial.window'
local M = {}
M.on_enter_aerial_buffer = function()
local bufnr = util.get_source_buffer()
if bufnr == -1 then
-- Quit if source buffer is gone
vim.cmd('q!')
return
else
visible_buffers = vim.fn.tabpagebuflist()
-- Quit if the source buffer is no longer visible
if not vim.tbl_contains(visible_buffers, bufnr) then
vim.cmd('q!')
return
end
end
-- Hack to ignore winwidth
vim.cmd('vertical resize ' .. config.get_width())
-- Move cursor to nearest matching line
local row = data.last_position_by_buf[bufnr]
if row ~= nil then
vim.fn.setpos('.', {0, row, 1, 0})
end
end
M.on_buf_leave = function()
bufnr = vim.api.nvim_get_current_buf()
local aer_bufnr = util.get_aerial_buffer(bufnr)
if aer_bufnr == -1 then
return
end
window.update_highlights(bufnr)
local maybe_close_aerial = function()
local winid = vim.fn.bufwinid(bufnr)
-- If there are no windows left with the source buffer,
if winid == -1 then
local winnr = vim.fn.bufwinnr(aer_bufnr)
-- And there is a window left for the aerial buffer
if winnr ~= -1 then
vim.cmd(winnr .. "close")
end
end
end
-- We have to defer this because if we :q out of a buffer with a aerial open,
-- and we *synchronously* close the aerial buffer, it will cause the :q
-- command to fail (presumably because it would cause vim to 'unexpectedly'
-- exit).
vim.defer_fn(maybe_close_aerial, 5)
end
M.request_symbols_if_diagnostics_changed = function()
local errors = vim.lsp.util.buf_diagnostics_count("Error")
-- if no errors, refresh symbols
if errors == 0 then
vim.lsp.buf.document_symbol()
end
end
return M

51
lua/aerial/callbacks.lua Normal file
View file

@ -0,0 +1,51 @@
local config = require 'aerial.config'
local data = require 'aerial.data'
local nav = require 'aerial.navigation'
local window = require 'aerial.window'
local M = {}
function filter_symbol_predicate(item)
return config.filter_kind[item.kind]
end
function sort_symbol(a, b)
return a.lnum < b.lnum
end
M.symbol_callback = function(_, _, result, _, bufnr)
if not result or vim.tbl_isempty(result) then return end
local items = vim.lsp.util.symbols_to_items(result, bufnr)
items = vim.tbl_filter(filter_symbol_predicate, items)
table.sort(items, sort_symbol)
local had_items = data.items_by_buf[bufnr] ~= nil
data.items_by_buf[bufnr] = items
-- Don't update if there are diagnostics errors (or override by setting)
local error_count = M._buf_diagnostics_count(bufnr, 'Error') or 0
if not config.get_update_when_errors() and error_count > 0 then
return
end
window.update_aerial_buffer(bufnr)
if not had_items and vim.api.nvim_get_current_buf() == bufnr then
nav._update_position()
else
window.update_highlights(bufnr)
end
end
-- This is mostly copied from Neovim source, but adjusted to accept a bufnr
function M._buf_diagnostics_count(bufnr, kind)
local diagnostics = vim.lsp.util.diagnostics_by_buf[bufnr]
if not diagnostics then return end
local count = 0
for _, diagnostic in pairs(diagnostics) do
if vim.lsp.protocol.DiagnosticSeverity[kind] == diagnostic.severity then
count = count + 1
end
end
return count
end
return M

107
lua/aerial/config.lua Normal file
View file

@ -0,0 +1,107 @@
local M = {}
M.open_automatic = {
['_'] = false,
}
M.filter_kind = {
['Function'] = true,
['Class'] = true,
['Constructor'] = true,
['Method'] = true,
['Struct'] = true,
['Enum'] = true,
}
M.kind_abbr = {
File = 'File';
Module = 'Mod';
Namespace = 'NS';
Package = 'Pkg';
Class = 'C';
Method = 'M';
Property = 'P';
Field = 'Fld';
Constructor = 'Co';
Enum = 'E';
Interface = 'I';
Function = 'F';
Variable = 'V';
Constant = 'const';
String = 'str';
Number = 'num';
Boolean = 'bool';
Array = 'arr';
Object = 'obj';
Key = 'K';
Null = 'null';
EnumMember = 'em';
Struct = 'S';
Event = 'Ev';
Operator = 'Op';
TypeParameter = 'T';
}
M.get_highlight_on_jump = function()
local value = vim.g.aerial_highlight_on_jump
if value == nil then return true else return value end
end
M.get_update_when_errors = function()
local val = vim.g.aerial_update_when_errors
if val == nil then return true else return val end
end
M.get_open_automatic = function(bufnr)
local ft = vim.api.nvim_buf_get_option(bufnr or 0, 'filetype')
local ret = M.open_automatic[ft]
if ret == nil then
return M.open_automatic['_']
end
return ret
end
M.get_open_automatic_min_lines = function()
local min_lines = vim.g.aerial_open_automatic_min_lines
if min_lines == nil then return 0 else return min_lines end
end
M.get_automatic_direction = function()
return vim.g.aerial_automatic_direction
end
M.get_diagnostics_trigger_update = function()
local update = vim.g.aerial_diagnostics_trigger_update
if update == nil then return true else return update end
end
M.get_highlight_mode = function()
local mode = vim.g.aerial_highlight_mode
if mode == nil then
return 'split_width'
elseif mode == 'last' or mode == 'full_width' or mode == 'split_width' then
return mode
end
error("Unrecognized highlight mode '" .. mode .. "'")
return 'split_width'
end
M.get_highlight_group = function()
local hl = vim.g.aerial_highlight_group
if hl == nil then return 'QuickFixLine' else return hl end
end
M.get_width = function()
local width = vim.g.aerial_width
if width == nil then return 40 else return width end
end
M.get_kind_abbr = function(kind)
abbr = M.kind_abbr[kind]
if abbr == nil then
return kind
end
return abbr
end
return M

7
lua/aerial/data.lua Normal file
View file

@ -0,0 +1,7 @@
local M = {}
M.items_by_buf = {}
M.positions_by_buf = {}
M.last_position_by_buf = {}
return M

143
lua/aerial/navigation.lua Normal file
View file

@ -0,0 +1,143 @@
local window = require 'aerial.window'
local data = require 'aerial.data'
local util = require 'aerial.util'
local config = require 'aerial.config'
local M = {}
M._get_virt_winid = function(bufnr, virt_winnr)
local vwin = 1
for i=1,vim.fn.winnr('$'),1 do
if vim.fn.winbufnr(i) == bufnr then
if vwin == virt_winnr then
return vim.fn.win_getid(i)
end
vwin = vwin + 1
end
end
return -1
end
M._get_current_lnum = function()
local bufnr = vim.fn.bufnr()
local aer_bufnr = util.get_aerial_buffer(bufnr)
if aer_bufnr == -1 then
return nil
end
local items = data.items_by_buf[bufnr]
if items == nil then
return nil
end
local pos = vim.fn.getcurpos()
local selected = 1
local relative = 'above'
for idx,item in ipairs(items) do
if item.lnum > pos[2] then
break
elseif item.lnum == pos[2] then
relative = 'exact'
else
relative = 'below'
end
selected = idx
end
return {
['lnum'] = selected,
['relative'] = relative
}
end
M._update_position = function()
local pos = M._get_current_lnum()
if pos == nil then
return
end
local bufnr = vim.fn.bufnr()
local aer_bufnr = util.get_aerial_buffer(bufnr)
local mywin = vim.fn.win_getid()
data.positions_by_buf[bufnr] = data.positions_by_buf[bufnr] or {}
data.positions_by_buf[bufnr][mywin] = pos.lnum
data.last_position_by_buf[bufnr] = pos.lnum
window.update_highlights(bufnr)
end
M._jump_to_loc = function(item_no, virt_winnr, split_cmd)
local virt_winnr = virt_winnr or 1
local split_cmd = split_cmd or 'belowright vsplit'
local bufnr = util.get_source_buffer()
local items = data.items_by_buf[bufnr]
if items == nil then
return
end
local item = items[item_no]
if item == nil then
error("Could not find item at position " .. item_no)
return
end
local bufnr = util.get_source_buffer()
local winid = M._get_virt_winid(bufnr, virt_winnr)
if winid == -1 then
-- Create a new split for the source window
local winid = vim.fn.bufwinid(bufnr)
if winid ~= -1 then
vim.fn.win_gotoid(winid)
vim.cmd(split_cmd)
else
vim.cmd(split_cmd)
vim.api.nvim_set_current_buf(bufnr)
end
vim.fn.setpos('.', {bufnr, item.lnum, item.col, 0})
else
vim.fn.win_gotoid(winid)
vim.fn.setpos('.', {bufnr, item.lnum, item.col, 0})
end
return item
end
-- TODO: make this work in source & aerial buffers
M.jump_to_loc = function(virt_winnr, split_cmd)
local pos = vim.fn.getcurpos()
local item = M._jump_to_loc(pos[2], virt_winnr, split_cmd)
if config.get_highlight_on_jump() then
util.flash_highlight(vim.api.nvim_get_current_buf(), item.lnum)
end
end
-- TODO: make this work in source & aerial buffers
M.scroll_to_loc = function(virt_winnr, split_cmd)
M.jump_to_loc(virt_winnr, split_cmd)
M._update_position()
vim.cmd('normal zzzv')
vim.cmd('wincmd p')
end
-- TODO: make this work in source & aerial buffers
M.skip_item = function(delta)
local pos = M._get_current_lnum()
if pos == nil then
return
end
local items = data.items_by_buf[bufnr]
local count = 0
for _ in pairs(items) do count = count + 1 end
local new_num = pos.lnum + delta
-- If we're not *exactly* on a location, make sure we hit the nearest location
-- first even if we're currently considered to be "on" it
if delta < 0 and pos.relative == 'below' then
new_num = new_num + 1
elseif delta > 0 and pos.relative == 'above' then
new_num = new_num - 1
end
while new_num < 1 do
new_num = new_num + count
end
while new_num > count do
new_num = new_num - count
end
local item = items[new_num]
vim.fn.setpos('.', {0, item.lnum, item.col, 0})
end
return M

52
lua/aerial/util.lua Normal file
View file

@ -0,0 +1,52 @@
local config = require 'aerial.config'
local M = {}
M.is_aerial_buffer = function(bufnr)
local ft = vim.api.nvim_buf_get_option(bufnr or 0, 'filetype')
return ft == 'aerial'
end
M.get_aerial_buffer = function(bufnr)
return M.get_buffer_from_var(bufnr or 0, 'aerial_buffer')
end
M.get_source_buffer = function(bufnr)
return M.get_buffer_from_var(bufnr or 0, 'source_buffer')
end
M.get_buffer_from_var = function(bufnr, varname)
local status, result_bufnr = pcall(vim.api.nvim_buf_get_var, bufnr, varname)
if not status or result_bufnr == nil then
return -1
end
return vim.fn.bufnr(result_bufnr)
end
M.flash_highlight = function(bufnr, lnum, hl_group, durationMs)
local hl_group = hl_group or config.get_highlight_group()
local durationMs = durationMs or 300
local ns = vim.api.nvim_buf_add_highlight(bufnr, 0, hl_group, lnum - 1, 0, -1)
local remove_highlight = function()
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
end
vim.defer_fn(remove_highlight, durationMs)
end
M.detect_split_direction = function()
bufnr = vim.api.nvim_get_current_buf()
-- If we are the first window default to left side
if vim.fn.winbufnr(1) == bufnr then
return '<'
end
-- If we are the last window default to right side
local lastwin = vim.fn.winnr('$')
if vim.fn.winbufnr(lastwin) == bufnr then
return '>'
end
return '<'
end
return M

155
lua/aerial/window.lua Normal file
View file

@ -0,0 +1,155 @@
local data = require 'aerial.data'
local util = require 'aerial.util'
local config = require 'aerial.config'
local M = {}
M.create_aerial_window = function(bufnr, direction)
if direction ~= '<' and direction ~= '>' then
error("Expected direction to be '<' or '>'")
end
local winnr
for i=1,vim.fn.winnr('$'),1 do
if vim.fn.winbufnr(i) == bufnr then
winnr = i
if direction == '<' then
break
end
end
end
if winnr ~= vim.fn.winnr() then
vim.api.nvim_set_current_win(vim.fn.win_getid(winnr))
end
if direction == '<' then
vim.cmd('vertical leftabove new')
elseif direction == '>' then
vim.cmd('vertical rightbelow new')
else
error("Unknown aerial window direction " .. direction)
return
end
vim.cmd('vertical resize ' .. config.get_width())
vim.api.nvim_win_set_option(0, 'winfixwidth', true)
vim.api.nvim_win_set_option(0, 'number', false)
vim.api.nvim_win_set_option(0, 'relativenumber', false)
end
M.create_aerial_buffer = function(bufnr, direction)
M.create_aerial_window(bufnr, direction)
win = vim.api.nvim_get_current_win()
buf = vim.api.nvim_get_current_buf()
-- Set up default mappings
local mapper = function(mode, key, result)
vim.fn.nvim_buf_set_keymap(0, mode, key, result, {noremap = true, silent = true})
end
mapper('n', '<CR>', "<cmd>lua require'aerial'.jump_to_loc()<CR>zzzv")
mapper('n', '<C-v>', "<cmd>lua require'aerial'.jump_to_loc(2)<CR>zzzv")
mapper('n', '<C-s>', "<cmd>lua require'aerial'.jump_to_loc(2, 'belowright split')<CR>zzzv")
mapper('n', '<C-j>', "j<cmd>lua require'aerial'.scroll_to_loc()<CR>")
mapper('n', '<C-k>', "k<cmd>lua require'aerial'.scroll_to_loc()<CR>")
mapper('n', 'p', "<cmd>lua require'aerial'.scroll_to_loc()<CR>")
mapper('n', 'q', '<cmd>lua require"aerial".close()<CR>')
-- Set buffer options
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {"Loading..."})
vim.api.nvim_buf_set_var(bufnr, 'aerial_buffer', buf)
vim.api.nvim_buf_set_var(buf, 'source_buffer', bufnr)
vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile')
vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe')
vim.api.nvim_buf_set_option(buf, 'buflisted', false)
vim.api.nvim_buf_set_option(buf, 'swapfile', false)
vim.api.nvim_buf_set_option(buf, 'modifiable', false)
vim.api.nvim_buf_set_option(buf, 'filetype', 'aerial')
vim.api.nvim_win_set_option(win, 'wrap', false)
vim.api.nvim_win_set_option(win, 'cursorline', true)
M.update_aerial_buffer(bufnr)
vim.cmd("autocmd BufEnter <buffer> lua require'aerial.autocommands'.on_enter_aerial_buffer()")
end
-- Update the aerial buffer from cached symbols
M.update_aerial_buffer = function(bufnr)
local aer_bufnr = util.get_aerial_buffer(bufnr)
if aer_bufnr == -1 then
return
end
local items = data.items_by_buf[bufnr]
if items == nil then
return
end
local lines = {}
for _,item in ipairs(items) do
local text = string.gsub(item.text, item.kind, config.get_kind_abbr(item.kind), 1)
if string.len(text) < config.get_width() then
text = text .. string.rep(' ', config.get_width() - string.len(text))
end
table.insert(lines, text)
end
vim.api.nvim_buf_set_option(aer_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(aer_bufnr, 0, -1, false, lines)
vim.api.nvim_buf_set_option(aer_bufnr, 'modifiable', false)
end
-- Update the highlighted lines in the aerial buffer
M.update_highlights = function(bufnr)
local positions = data.positions_by_buf[bufnr]
if positions == nil then
return
end
local winids = {}
local win_count = 0
for k in pairs(positions) do
local winnr = vim.fn.win_id2win(k)
if winnr ~= 0 and vim.fn.winbufnr(k) == bufnr then
win_count = win_count + 1
table.insert(winids, k)
end
end
local sortWinId = function(a, b)
return vim.fn.win_id2win(a) < vim.fn.win_id2win(b)
end
table.sort(winids, sortWinId)
local ns = vim.api.nvim_create_namespace('aerial')
local aer_bufnr = util.get_aerial_buffer(bufnr)
if aer_bufnr == -1 then
return
end
vim.api.nvim_buf_clear_namespace(aer_bufnr, ns, 0, -1)
local hl_width = math.floor(config.get_width() / win_count)
local hl_mode = config.get_highlight_mode()
if hl_mode == 'last' then
local row = data.last_position_by_buf[bufnr]
vim.api.nvim_buf_add_highlight(
aer_bufnr,
ns,
config.get_highlight_group(),
row - 1,
0,
-1)
return
end
if win_count == 1 or hl_mode == 'full_width' then
-- Will make end_hl -1, which is the special value for "entire line"
hl_width = -2
end
local start_hl = 0
local end_hl = hl_width
for _,winid in ipairs(winids) do
vim.api.nvim_buf_add_highlight(
aer_bufnr,
ns,
config.get_highlight_group(),
positions[winid] - 1,
start_hl,
end_hl)
if hl_mode ~= 'full_width' then
start_hl = end_hl
end_hl = end_hl + hl_width
end
end
end
return M