feat: Add follow_cursor and restore_location

New lua API function: follow_cursor (supports opts.focus_outline).
This sets location in outline to match location in code.

New keymap: restore_location (C-g) by default.
This provides the same functionality as follow_cursor.

I've also refactored other lua API functions for consistency of using
`opts.focus_outline`. If opts is not provided, focus_outline is
defaulted to true. To change this behaviour, set it to false.
This commit is contained in:
hedy
2023-11-06 09:20:52 +08:00
parent ceb9a4841d
commit 9e96b54de9
7 changed files with 225 additions and 90 deletions

View File

@@ -45,7 +45,8 @@ I do not merge PRs from the original repo that I don't personally need.
- Fold all operation too slow: simrat39/symbols-outline.nvim#223 (simrat39/symbols-outline.nvim#224) - Fold all operation too slow: simrat39/symbols-outline.nvim#223 (simrat39/symbols-outline.nvim#224)
- "Invalid buffer id" error simrat39/symbols-outline.nvim#177 - "Invalid buffer id" error simrat39/symbols-outline.nvim#177
- Open handler triggering multiple times ends up in messy state with errors - Open handler triggering multiple times ends up in messy state with errors
simrat39/symbols-outline.nvim#235 simrat39/symbols-outline.nvim#235
- Fixed `_highlight_current_item` function checking provider on outline window
## 🛑 Breaking changes ## 🛑 Breaking changes
@@ -74,6 +75,7 @@ features:
- **Behaviour**: For `auto_preview=true`, previously preview is only shown after - **Behaviour**: For `auto_preview=true`, previously preview is only shown after
some delay. Now preview is shown instantly every time the cursor moves. some delay. Now preview is shown instantly every time the cursor moves.
## Features ## Features
[Skip this section](#symbols-outlinenvim) [Skip this section](#symbols-outlinenvim)
@@ -107,6 +109,11 @@ Features/Changes:
- Auto jump config option (see config `auto_goto`) - Auto jump config option (see config `auto_goto`)
(simrat39/symbols-outline.nvim#229, simrat39/symbols-outline.nvim#228). (simrat39/symbols-outline.nvim#229, simrat39/symbols-outline.nvim#228).
- New Follow command, opposite of `goto_location`/`focus_location`
- New restore location keymap option to go back to corresponding outline
location synced with code (see config `restore_location`).
## PRs ## PRs
[Skip this section](#symbols-outlinenvim) [Skip this section](#symbols-outlinenvim)
@@ -193,7 +200,7 @@ Key:
(#101 by druskus20) (#101 by druskus20)
### TODO ## TODO
[Skip this section](#symbols-outlinenvim) [Skip this section](#symbols-outlinenvim)
@@ -225,9 +232,12 @@ Items will be moved to above list when complete.
(simrat39/symbols-outline#128) (simrat39/symbols-outline#128)
- `[/]` Configurable width and height of preview window (simrat39/symbols-outline#130) - `[/]` Configurable width and height of preview window (simrat39/symbols-outline#130)
- `[/]` Outline window customizations (simrat39/symbols-outline#137) - View
- `[/]` Outline window customizations (simrat39/symbols-outline#137)
- `[/]` Option to show line number next to symbols
### Related plugins
## Related plugins
- nvim-navic - nvim-navic
- nvim-navbuddy - nvim-navbuddy
@@ -403,19 +413,25 @@ local opts = {
-- Jump to symbol under cursor but keep focus on outline window. -- Jump to symbol under cursor but keep focus on outline window.
-- Renamed in this fork! -- Renamed in this fork!
peek_location = "o", peek_location = "o",
-- Only in this fork:
-- Change cursor position of outline window to the current location in code.
-- "Opposite" of goto/peek_location.
restore_location = "<C-g>",
-- Open LSP/provider-dependent symbol hover information
hover_symbol = "<C-space>", hover_symbol = "<C-space>",
-- Preview symbol under cursor -- Preview location code of the symbol under cursor
toggle_preview = "K", toggle_preview = "K",
-- Symbol actions
rename_symbol = "r", rename_symbol = "r",
code_actions = "a", code_actions = "a",
-- These fold actions are collapsing tree nodes, not code folding -- These fold actions are collapsing tree nodes, not code folding
fold = "h", fold = "h",
unfold = "l",
fold_toggle = '<Tab>', -- Only in this fork fold_toggle = '<Tab>', -- Only in this fork
-- Toggle folds for all nodes. -- Toggle folds for all nodes.
-- If at least one node is folded, this action will fold all nodes. -- If at least one node is folded, this action will fold all nodes.
-- If all nodes are folded, this action will unfold all nodes. -- If all nodes are folded, this action will unfold all nodes.
fold_toggle_all = '<S-Tab>', -- Only in this fork fold_toggle_all = '<S-Tab>', -- Only in this fork
unfold = "l",
fold_all = "W", fold_all = "W",
unfold_all = "E", unfold_all = "E",
fold_reset = "R", fold_reset = "R",
@@ -504,6 +520,20 @@ local opts = {
Display current provider and outline window status in the messages area. Display current provider and outline window status in the messages area.
- **:SymbolsOutlineFollow[!]**
Go to corresponding node in outline based on cursor position in code, and
focus on the outline window.
With bang, retain focus on the code window.
This can be understood as the converse of `goto_location` (see keymaps).
`goto_location` sets cursor of code window to the position of outline window,
whereas this command sets position in outline window to the cursor position of
code window.
With bang, it can be understood as the converse of `focus_location`.
### Lua API ### Lua API
@@ -516,13 +546,13 @@ require'symbols-outline'
Toggle opening/closing of outline window. Toggle opening/closing of outline window.
If `opts.bang` is true, keep focus on previous window. If `opts.focus_outline=false`, keep focus on previous window.
- **open_outline(opts)** - **open_outline(opts)**
Open the outline window. Open the outline window.
If `opts.bang` is true, keep focus on previous window. If `opts.focus_outline=false`, keep focus on previous window.
- **close_outline()** - **close_outline()**
@@ -548,22 +578,36 @@ require'symbols-outline'
Display current provider and outline window status in the messages area. Display current provider and outline window status in the messages area.
- **has_provider()**
Returns whether a provider is available for current window.
- **follow_cursor(opts)**
Go to corresponding node in outline based on cursor position in code, and
focus on the outline window.
With `opts.focus_outline=false`, cursor focus will remain on code window.
## Default keymaps ## Default keymaps
These mappings are active for the outline window.
| Key | Action | | Key | Action |
| ---------- | -------------------------------------------------- | | ---------- | -------------------------------------------------- |
| Escape | Close outline | | Escape | Close outline |
| ? | Show help message | | ? | Show help message |
| Enter | Go to symbol location in code | | Enter | Go to symbol location in code |
| o | Go to symbol location in code without losing focus | | o | Go to symbol location in code without losing focus |
| Ctrl+g | Go to code location in outline window |
| Ctrl+Space | Hover current symbol | | Ctrl+Space | Hover current symbol |
| K | Toggles the current symbol preview | | K | Toggles the current symbol preview |
| r | Rename symbol | | r | Rename symbol |
| a | Code actions | | a | Code actions |
| h | fold symbol | | h | Fold symbol or parent symbol |
| Tab | toggle fold under cursor | | Tab | Toggle fold under cursor |
| Shift+Tab | toggle all folds | | Shift+Tab | Toggle all folds |
| l | Unfold symbol | | l | Unfold symbol |
| W | Fold all symbols | | W | Fold all symbols |
| E | Unfold all symbols | | E | Unfold all symbols |

View File

@@ -37,6 +37,7 @@ M.defaults = {
close = { '<Esc>', 'q' }, close = { '<Esc>', 'q' },
goto_location = '<Cr>', goto_location = '<Cr>',
peek_location = 'o', peek_location = 'o',
restore_location = "<C-g>",
hover_symbol = '<C-space>', hover_symbol = '<C-space>',
toggle_preview = 'K', toggle_preview = 'K',
rename_symbol = 'r', rename_symbol = 'r',

View File

@@ -39,31 +39,6 @@ local function setup_global_autocmd()
}) })
end end
local function setup_buffer_autocmd()
if config.options.auto_preview then
vim.api.nvim_create_autocmd('CursorMoved', {
buffer = 0,
callback = require('symbols-outline.preview').show,
})
else
vim.api.nvim_create_autocmd('CursorMoved', {
buffer = 0,
callback = require('symbols-outline.preview').close,
})
end
end
local function setup_commands()
local cmd = function(n, c, o) vim.api.nvim_create_user_command('SymbolsOutline'..n, c, o) end
cmd('', M.toggle_outline, { nargs = 0, bang = true })
cmd('Open', M.open_outline, { nargs = 0, bang = true })
cmd('Close', M.close_outline, { nargs = 0 })
cmd('FocusOutline', M.focus_outline, { nargs = 0 })
cmd('FocusCode', M.focus_code, { nargs = 0 })
cmd('Focus', M.focus_toggle, { nargs = 0 })
cmd('Status', M.show_status, { nargs = 0 })
end
------------------------- -------------------------
-- STATE -- STATE
------------------------- -------------------------
@@ -71,7 +46,6 @@ M.state = {
outline_items = {}, outline_items = {},
flattened_outline_items = {}, flattened_outline_items = {},
code_win = 0, code_win = 0,
opts = {}
} }
local function wipe_state() local function wipe_state()
@@ -91,7 +65,9 @@ local function _merge_items(items)
end end
local function __refresh() local function __refresh()
if M.view:is_open() then local current_buffer_is_outline = M.view.bufnr
== vim.api.nvim_get_current_buf()
if M.view:is_open() and current_buffer_is_outline then
local function refresh_handler(response) local function refresh_handler(response)
if response == nil or type(response) ~= 'table' then if response == nil or type(response) ~= 'table' then
return return
@@ -153,25 +129,6 @@ function M._toggle_fold(move_cursor, node_index)
end end
end end
local function setup_buffer_autocmd()
if config.options.auto_preview then
vim.api.nvim_create_autocmd('CursorHold', {
buffer = 0,
callback = require('symbols-outline.preview').show,
})
else
vim.api.nvim_create_autocmd('CursorMoved', {
buffer = 0,
callback = function()
require('symbols-outline.preview').close()
if config.options.auto_goto then
M._goto_location(false)
end
end,
})
end
end
local function setup_buffer_autocmd() local function setup_buffer_autocmd()
if config.options.auto_preview then if config.options.auto_preview then
vim.api.nvim_create_autocmd('CursorMoved', { vim.api.nvim_create_autocmd('CursorMoved', {
@@ -252,32 +209,25 @@ function M._set_all_folded(folded, nodes)
end end
function M._highlight_current_item(winnr) function M._highlight_current_item(winnr)
local has_provider = providers.has_provider() local has_provider = M.has_provider()
local has_outline_open = M.view:is_open()
local is_current_buffer_the_outline = M.view.bufnr local current_buffer_is_outline = M.view.bufnr
== vim.api.nvim_get_current_buf() == vim.api.nvim_get_current_buf()
local doesnt_have_outline_buf = not M.view.bufnr if not has_provider then
return
local should_exit = not has_provider
or doesnt_have_outline_buf
or is_current_buffer_the_outline
-- Make a special case if we have a window number
-- Because we might use this to manually focus so we dont want to quit this
-- function
if winnr then
should_exit = false
end end
if should_exit then if
not has_outline_open -- Outline not open
-- Not currently on outline window, but no code window is given.
or (not winnr and not current_buffer_is_outline)
then
return return
end end
local win = winnr or vim.api.nvim_get_current_win() local win = winnr or vim.api.nvim_get_current_win()
local hovered_line = vim.api.nvim_win_get_cursor(win)[1] - 1 local hovered_line = vim.api.nvim_win_get_cursor(win)[1] - 1
local leaf_node = nil local leaf_node = nil
local cb = function(value) local cb = function(value)
@@ -318,6 +268,10 @@ local function setup_keymaps(bufnr)
map(config.options.keymaps.peek_location, function() map(config.options.keymaps.peek_location, function()
M._goto_location(false) M._goto_location(false)
end) end)
-- Navigate to corresponding outline location for current code location
map(config.options.keymaps.restore_location, function()
M._map_follow_cursor()
end)
-- Move down/up in outline and peek that location in code -- Move down/up in outline and peek that location in code
map(config.options.keymaps.down_and_goto, function() map(config.options.keymaps.down_and_goto, function()
M._move_and_goto('down') M._move_and_goto('down')
@@ -381,7 +335,7 @@ local function setup_keymaps(bufnr)
end) end)
end end
local function handler(response) local function handler(response, opts)
if response == nil or type(response) ~= 'table' or M.view:is_open() then if response == nil or type(response) ~= 'table' or M.view:is_open() then
return return
end end
@@ -408,42 +362,122 @@ local function handler(response)
M._highlight_current_item(M.state.code_win) M._highlight_current_item(M.state.code_win)
if not config.options.focus_on_open or (M.state.opts and M.state.opts.bang) then if not config.options.focus_on_open or (opts and not opts.focus_outline) then
vim.fn.win_gotoid(M.state.code_win) vim.fn.win_gotoid(M.state.code_win)
end end
end end
---Set position of outline window to match cursor position in code, return
---whether the window is just newly opened (previously not open).
---@param opts table? Field `focus_outline` = `false` or `nil` means don't focus on outline window after following cursor. If opts is not provided, focus will be on outline window after following cursor.
---@return boolean ok Whether it was successful. If ok=false, either the outline window is not open or the code window cannot be found.
function M.follow_cursor(opts)
if not M.view:is_open() then
return false
end
if require('symbols-outline.preview').has_code_win() then
M._highlight_current_item(M.state.code_win)
else
return false
end
if not opts then
opts = { focus_outline = true }
end
if opts.focus_outline then
M.focus_outline()
end
return true
end
local function _cmd_follow_cursor(opts)
local fnopts = { focus_outline = true }
if opts.bang then
fnopts.focus_outline = false
end
M.follow_cursor(fnopts)
end
function M._map_follow_cursor()
if not M.follow_cursor({ focus_outline = true }) then
vim.notify(
"Code window no longer active. Try closing and reopening the outline.",
vim.log.levels.ERROR
)
end
end
---Toggle the outline window, and return whether the outline window is open
---after this operation.
---@param opts table? Table of options, @see open_outline
---@return boolean is_open Whether outline window is open
function M.toggle_outline(opts) function M.toggle_outline(opts)
if M.view:is_open() then if M.view:is_open() then
M.close_outline() M.close_outline()
return false
else else
M.open_outline(opts) M.open_outline(opts)
return true
end end
end end
-- Used for SymbolsOutline user command
local function _cmd_toggle_outline(opts)
if opts.bang then
M.toggle_outline({ focus_outline = false })
else
M.toggle_outline({ focus_outline = true })
end
end
---Open the outline window.
---@param opts table? Field focus_outline=false means don't focus on outline window after opening. If opts is not provided, focus will be on outline window after opening.
function M.open_outline(opts) function M.open_outline(opts)
if not opts then
opts = { focus_outline = true }
end
if not M.view:is_open() then if not M.view:is_open() then
M.state.opts = opts providers.request_symbols(handler, opts)
providers.request_symbols(handler)
end end
end end
local function _cmd_open_outline(opts)
if opts.bang then
M.open_outline({ focus_outline = false })
else
M.open_outline({ focus_outline = true })
end
end
---Close the outline window.
function M.close_outline() function M.close_outline()
M.view:close() M.view:close()
end end
---Set cursor to focus on the outline window, return whether the window is currently open..
---@return boolean is_open Whether the window is open
function M.focus_outline() function M.focus_outline()
if M.view:is_open() then if M.view:is_open() then
vim.fn.win_gotoid(M.view.winnr) vim.fn.win_gotoid(M.view.winnr)
return true
end end
return false
end end
---Set cursor to focus on the code window, return whether this operation was successful.
---@return boolean ok Whether it was successful. If unsuccessful, it might mean that the attached code window has been closed or is no longer valid.
function M.focus_code() function M.focus_code()
if require('symbols-outline.preview').has_code_win() then if require('symbols-outline.preview').has_code_win() then
vim.fn.win_gotoid(M.state.code_win) vim.fn.win_gotoid(M.state.code_win)
return true
end end
return false
end end
---Toggle focus between outline and code window, returns whether it was successful.
---@return boolean ok Whether it was successful. If `ok=false`, either the outline window is not open or the code window is no longer valid.
function M.focus_toggle() function M.focus_toggle()
if M.view:is_open() and require('symbols-outline.preview').has_code_win() then if M.view:is_open() and require('symbols-outline.preview').has_code_win() then
local winid = vim.fn.win_getid() local winid = vim.fn.win_getid()
@@ -452,27 +486,80 @@ function M.focus_toggle()
else else
vim.fn.win_gotoid(M.state.code_win) vim.fn.win_gotoid(M.state.code_win)
end end
return true
end end
return false
end end
---Whether the outline window is currently open.
---@return boolean is_open
function M.is_open() function M.is_open()
return M.view:is_open() return M.view:is_open()
end end
---Display outline window status in the message area.
function M.show_status() function M.show_status()
if providers.has_provider() and _G._symbols_outline_current_provider then if M.has_provider() then
print("Current provider:") print("Current provider:")
print(_G._symbols_outline_current_provider.name) print(' ' .. _G._symbols_outline_current_provider.name)
if M.view:is_open() then if M.view:is_open() then
print("Outline window is open") print("Outline window is open.")
else else
print("Outline window is not open") print("Outline window is not open.")
end
if require('symbols-outline.preview').has_code_win() then
print("Code window is active.")
else
print("Warning: code window is either closed or invalid. Please close and reopen the outline window.")
end end
else else
print("No providers") print("No providers")
end end
end end
---Whether there is currently an available provider.
---@return boolean has_provider
function M.has_provider()
local winid = vim.fn.win_getid()
if M.view:is_open() and winid == M.view.winnr then
return _G._symbols_outline_current_provider ~= nil
end
return providers.has_provider() and _G._symbols_outline_current_provider
end
local function setup_commands()
local cmd = function(n, c, o)
vim.api.nvim_create_user_command('SymbolsOutline'..n, c, o)
end
cmd('', _cmd_toggle_outline, {
desc = "Toggle the outline window. \
With bang, keep focus on initial window after opening.",
nargs = 0,
bang = true,
})
cmd('Open', _cmd_open_outline, {
desc = "With bang, keep focus on initial window after opening.",
nargs = 0,
bang = true,
})
cmd('Close', M.close_outline, { nargs = 0 })
cmd('FocusOutline', M.focus_outline, { nargs = 0 })
cmd('FocusCode', M.focus_code, { nargs = 0 })
cmd('Focus', M.focus_toggle, { nargs = 0 })
cmd('Status', M.show_status, {
desc = "Show a message about the current status of the outline window.",
nargs = 0,
})
cmd('Follow', _cmd_follow_cursor, {
desc = "Update position of outline with position of cursor. \
With bang, don't switch cursor focus to outline window.",
nargs = 0,
bang = true,
})
end
---Set up configuration options for symbols-outline.
function M.setup(opts) function M.setup(opts)
config.setup(opts) config.setup(opts)
ui.setup_highlights() ui.setup_highlights()

View File

@@ -83,11 +83,12 @@ local function convert_symbols(result)
end end
---@param on_symbols function ---@param on_symbols function
function M.request_symbols(on_symbols) ---@param opts table
function M.request_symbols(on_symbols, opts)
vim.fn.call('CocActionAsync', { vim.fn.call('CocActionAsync', {
'documentSymbols', 'documentSymbols',
function(_, symbols) function(_, symbols)
on_symbols { [1000000] = { result = convert_symbols(symbols) } } on_symbols({ [1000000] = { result = convert_symbols(symbols) } }, opts)
end, end,
}) })
end end

View File

@@ -21,13 +21,13 @@ function M.has_provider()
end end
---@param on_symbols function ---@param on_symbols function
function M.request_symbols(on_symbols) function M.request_symbols(on_symbols, opts)
for _, value in ipairs(providers) do for _, value in ipairs(providers) do
local provider = require(value) local provider = require(value)
if provider.should_use_provider(0) then if provider.should_use_provider(0) then
_G._symbols_outline_current_provider = provider _G._symbols_outline_current_provider = provider
_G._symbols_outline_current_provider.name = value _G._symbols_outline_current_provider.name = value
provider.request_symbols(on_symbols) provider.request_symbols(on_symbols, opts)
break break
end end
end end

View File

@@ -17,8 +17,9 @@ function M.hover_info(_, _, on_info)
end end
---@param on_symbols function ---@param on_symbols function
function M.request_symbols(on_symbols) ---@param opts table
on_symbols(md_parser.handle_markdown()) function M.request_symbols(on_symbols, opts)
on_symbols(md_parser.handle_markdown(), opts)
end end
return M return M

View File

@@ -69,13 +69,14 @@ function M.postprocess_symbols(response)
end end
---@param on_symbols function ---@param on_symbols function
function M.request_symbols(on_symbols) function M.request_symbols(on_symbols, opts)
vim.lsp.buf_request_all( vim.lsp.buf_request_all(
0, 0,
'textDocument/documentSymbol', 'textDocument/documentSymbol',
getParams(), getParams(),
function (response) function (response)
on_symbols(M.postprocess_symbols(response)) response = M.postprocess_symbols(response)
on_symbols(response, opts)
end end
) )
end end