mirror of
https://github.com/stevearc/aerial.nvim
synced 2024-09-16 14:34:08 +02:00
First draft
This commit is contained in:
parent
ddf6f0ce6b
commit
3faa3993f1
11 changed files with 1054 additions and 1 deletions
61
README.md
61
README.md
|
@ -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
215
doc/aerial.txt
Normal 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
31
doc/tags
Normal 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
165
lua/aerial.lua
Normal 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
|
68
lua/aerial/autocommands.lua
Normal file
68
lua/aerial/autocommands.lua
Normal 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
51
lua/aerial/callbacks.lua
Normal 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
107
lua/aerial/config.lua
Normal 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
7
lua/aerial/data.lua
Normal 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
143
lua/aerial/navigation.lua
Normal 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
52
lua/aerial/util.lua
Normal 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
155
lua/aerial/window.lua
Normal 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
|
Loading…
Reference in a new issue