diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35f5728..3556027 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -79,6 +79,10 @@
([#37](https://github.com/hedyhli/outline.nvim/issues/37))
- Added `get_symbol` and `get_breadcrumb` functions (useful in
statusline/winbar) ([#24](https://github.com/hedyhli/outline.nvim/issues/24))
+- New "Live Preview" feature which allows editing in the preview buffer. This
+ allows navigating some symbol away from cursor location and make quick edits in
+ the other position using the preview window. This feature is currently
+ experimental and opt-in. Enable with `preview_window.live = true`
### Fixes
@@ -116,6 +120,9 @@
- Follow cursor algorithm significantly improved
- Highlight hovered item and initial opening of outline has been rewritten and
performance improved
+- Revamped various provider-related modules such as rename/code-actions/hover
+ to delegate the task to specific providers
+- Revamped the preview module for better per-tab outline support and more features
### Others
diff --git a/README.md b/README.md
index d81db01..f7e60f8 100644
--- a/README.md
+++ b/README.md
@@ -309,6 +309,9 @@ Pass a table to the setup call with your configuration options.
winhl = 'NormalFloat:',
-- Pseudo-transparency of the preview window, see ':h winblend'
winblend = 0
+ -- Experimental feature that let's you edit the source content live
+ -- in the preview window. Like VS Code's "peek editor".
+ live = false
},
-- These keymaps can be a string or a table for multiple keys.
@@ -1036,6 +1039,27 @@ symbols = {

+### Live, editable previews
+
+Press `K` to open the preview, press `K` again to focus on the preview window
+to make any quick edits, similar to VS Code's reference window "peek editor".
+
+Then use `:q` to close the window, and continue browsing the outline.
+
+```lua
+preview_window = {
+ live = true,
+}
+```
+
+Note that this feature is experimental and may be unstable.
+
+https://github.com/hedyhli/outline.nvim/assets/50042066/183fc5f9-b369-41e2-a831-a4185704d76d
+
+Auto-preview with the feature is also supported, set `auto_preview = true` and
+press `K` to focus on the auto-opened preview window. `:q` to quit the window.
+
+
---
diff --git a/lua/outline/config.lua b/lua/outline/config.lua
index 4a5ca25..9d19f98 100644
--- a/lua/outline/config.lua
+++ b/lua/outline/config.lua
@@ -48,6 +48,7 @@ M.defaults = {
center_on_jump = true,
},
preview_window = {
+ live = false,
auto_preview = false,
width = 50,
min_width = 50,
diff --git a/lua/outline/init.lua b/lua/outline/init.lua
index 8179277..83d4b2d 100644
--- a/lua/outline/init.lua
+++ b/lua/outline/init.lua
@@ -24,8 +24,12 @@ local function setup_global_autocmd()
pattern = '*',
callback = function()
local s = M._get_sidebar()
+ local w = vim.api.nvim_get_current_win()
if s and s.preview then
- s.preview:close()
+ -- Don't close preview when entering preview!
+ if not s.preview.win or s.preview.win ~= w then
+ s.preview:close()
+ end
end
end,
})
diff --git a/lua/outline/preview.lua b/lua/outline/preview.lua
index 3d2ddca..0107c99 100644
--- a/lua/outline/preview.lua
+++ b/lua/outline/preview.lua
@@ -1,7 +1,8 @@
-local cfg = require('outline.config')
-local providers = require('outline.providers')
-
-local conf
+-- A floating window to preview the location of a symbol from the outline.
+-- Classical preview reads entire lines into a new buffer for preview. Live
+-- preview sets the buffer of floating window to the code buffer, which allows
+-- focusing by pressing the preview keymap again, to edit the buffer at that
+-- position.
---@class outline.Preview
local Preview = {}
@@ -9,77 +10,53 @@ local Preview = {}
---@class outline.Preview
---@field buf integer
---@field win integer
----@field width integer
---@field height integer
+---@field width integer
---@field outline_height integer
---@field s outline.Sidebar
+---@field conf table
----@param s outline.Sidebar
-function Preview:new(s)
- -- Config must have been setup when calling Preview:new
- conf = cfg.o.preview_window
- return setmetatable({
- buf = nil,
- win = nil,
- s = s,
- width = nil,
- height = nil,
- }, { __index = Preview })
-end
+---@class outline.LivePreview
+local LivePreview = {}
----Creates new preview window and sets the content. Calls setup and set_lines.
-function Preview:create()
- self.buf = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_attach(self.buf, false, {
- on_detach = function()
- self.buf = nil
- self.win = nil
- end,
- })
- -- FIXME: Re-calculate dimensions on update-preview, in case outline window
- -- was resized between last preview and next preview?
- self.outline_height = vim.api.nvim_win_get_height(self.s.view.win)
- self.width = conf.width
- self.height = math.max(math.ceil(self.outline_height / 2), conf.min_height)
- self.win = vim.api.nvim_open_win(self.buf, false, {
- relative = 'editor',
- height = self.height,
- width = self.width,
- bufpos = { 0, 0 },
- col = self:calc_col(),
- row = self:calc_row(),
- border = conf.border,
- })
- self:setup()
- self:set_lines()
-end
+---@class outline.LivePreview
+---@field win integer
+---@field codewin integer
+---@field codebuf integer
+---@field height integer
+---@field width integer
+---@field outline_height integer
+---@field s outline.Sidebar
+---@field last_node outline.FlatSymbol
+---@field initial_cursorline boolean
+---@field conf table
----Set up highlights, window, and buffer options
-function Preview:setup()
- vim.api.nvim_win_set_option(self.win, 'winhl', conf.winhl)
- vim.api.nvim_win_set_option(self.win, 'winblend', conf.winblend)
-
- local code_buf = self.s.code.buf
- local ft = vim.api.nvim_buf_get_option(code_buf, 'filetype')
- vim.api.nvim_buf_set_option(self.buf, 'syntax', ft)
-
- local ts_highlight_fn = vim.treesitter.start
- if not _G._outline_nvim_has[8] then
- local ok, ts_highlight = pcall(require, 'nvim-treesitter.highlight')
- if ok then
- ts_highlight_fn = ts_highlight.attach
- end
+---@param conf table
+function Preview:new(conf)
+ if conf.live == true then
+ return setmetatable({
+ conf = conf,
+ win = nil,
+ width = nil,
+ height = nil,
+ }, { __index = LivePreview })
+ else
+ return setmetatable({
+ conf = conf,
+ buf = nil,
+ win = nil,
+ width = nil,
+ height = nil,
+ }, { __index = Preview })
end
- pcall(ts_highlight_fn, self.buf, ft)
-
- vim.api.nvim_buf_set_option(self.buf, 'bufhidden', 'delete')
- vim.api.nvim_win_set_option(self.win, 'cursorline', true)
- vim.api.nvim_buf_set_option(self.buf, 'modifiable', false)
end
---Get the correct column to place the floating window based on relative
---positions of the outline and the code window.
-function Preview:calc_col()
+---@param self outline.Preview|outline.LivePreview
+local function calc_col(self)
+ -- TODO: Re-calculate dimensions on update-preview, in case outline window
+ -- was resized between last preview and next preview?
---@type integer
local outline_winnr = self.s.view.win
local outline_col = vim.api.nvim_win_get_position(outline_winnr)[2]
@@ -99,20 +76,63 @@ function Preview:calc_col()
end
---Get the vertically center-aligned row for preview window
-function Preview:calc_row()
+---@param self outline.Preview|outline.LivePreview
+local function calc_row(self)
local offset = math.floor((self.outline_height - self.height) / 2) - 1
return vim.api.nvim_win_get_position(self.s.view.win)[1] + offset
end
----Set and update preview buffer content
-function Preview:set_lines()
- -- TODO: Editable, savable buffer in the preview like VS Code for quick
- -- edits? It can be like LSP. Trigger preview to open, trigger again to focus
- -- (so buffer can be edited).
- -- This can be achieved by simply opening the buffer from inside the preview
- -- window.
- -- This also removes the need of manually setting highlights, treesitter etc.
- -- The preview window will look exactly the same as in the code window.
+function Preview:create()
+ self.buf = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_buf_attach(self.buf, false, {
+ on_detach = function()
+ self.buf = nil
+ self.win = nil
+ end,
+ })
+ self.outline_height = vim.api.nvim_win_get_height(self.s.view.win)
+ self.width = self.conf.width
+ self.height = math.max(math.ceil(self.outline_height / 2), self.conf.min_height)
+ self.win = vim.api.nvim_open_win(self.buf, false, {
+ relative = 'editor',
+ height = self.height,
+ width = self.width,
+ bufpos = { 0, 0 },
+ col = calc_col(self),
+ row = calc_row(self),
+ border = self.conf.border,
+ focusable = false,
+ })
+ self:setup()
+ self:update()
+end
+
+
+---Set buf & win options, and setup highlight
+function Preview:setup()
+ vim.api.nvim_win_set_option(self.win, 'winhl', self.conf.winhl)
+ vim.api.nvim_win_set_option(self.win, 'winblend', self.conf.winblend)
+
+ local code_buf = self.s.code.buf
+ local ft = vim.api.nvim_buf_get_option(code_buf, 'filetype')
+ vim.api.nvim_buf_set_option(self.buf, 'syntax', ft)
+
+ local ts_highlight_fn = vim.treesitter.start
+ if not _G._outline_nvim_has[8] then
+ local ok, ts_highlight = pcall(require, 'nvim-treesitter.highlight')
+ if ok then
+ ts_highlight_fn = ts_highlight.attach
+ end
+ end
+ pcall(ts_highlight_fn, self.buf, ft)
+
+ vim.api.nvim_buf_set_option(self.buf, 'bufhidden', 'delete')
+ vim.api.nvim_buf_set_option(self.buf, 'modifiable', false)
+ vim.api.nvim_win_set_option(self.win, 'cursorline', true)
+end
+
+
+function Preview:update()
local node = self.s:_current_node()
if not node then
return
@@ -133,24 +153,17 @@ function Preview:show()
return
end
- if self.buf and self.win then
- self:set_lines()
- else
+ if not self.buf or not self.win then
self:create()
- end
-
- if conf.open_hover_on_preview then
- providers.action(self.s, 'show_hover', { self.s })
+ else
+ self:update()
end
end
function Preview:close()
- -- TODO: Why was this in symbols-outline.nvim?
- -- if self.s:has_code_win() then
if self.win ~= nil and vim.api.nvim_win_is_valid(self.win) then
vim.api.nvim_win_close(self.win, true)
end
- -- end
end
function Preview:toggle()
@@ -161,4 +174,102 @@ function Preview:toggle()
end
end
+---Creates new preview window and sets the content. Calls setup and set_lines.
+function LivePreview:create()
+ self.codewin = self.s.code.win
+ self.initial_cursorline = vim.api.nvim_win_get_option(self.s.code.win, 'cursorline')
+ self.outline_height = vim.api.nvim_win_get_height(self.s.view.win)
+ self.width = self.conf.width
+ self.height = math.max(math.ceil(self.outline_height / 2), self.conf.min_height)
+ self.win = vim.api.nvim_open_win(self.s.code.buf, false, {
+ relative = 'editor',
+ height = self.height,
+ width = self.width,
+ bufpos = { 0, 0 },
+ col = calc_col(self),
+ row = calc_row(self),
+ border = self.conf.border,
+ -- Setting this to disallow using other methods to focus on this window,
+ -- because currently the autocmds from setup() isn't triggering if user did
+ -- not use close() and focus().
+ focusable = false,
+ })
+ self:setup()
+end
+
+---Set buf & win options, and autocmds
+function LivePreview:setup()
+ vim.api.nvim_win_set_option(self.win, 'winhl', self.conf.winhl)
+ vim.api.nvim_win_set_option(self.win, 'winblend', self.conf.winblend)
+ vim.api.nvim_win_set_option(self.win, 'cursorline', true)
+
+ vim.api.nvim_create_autocmd('WinClosed', {
+ pattern = tostring(self.win),
+ once = true,
+ callback = function()
+ self.s.code.win = self.codewin
+ self.win = nil
+ end
+ })
+ vim.api.nvim_create_autocmd('WinEnter', {
+ pattern = tostring(self.win),
+ once = true,
+ callback = function()
+ -- This doesn't work at all?
+ vim.api.nvim_win_set_option(self.win, 'cursorline', self.initial_cursorline)
+ end
+ })
+end
+
+function LivePreview:update(node)
+ vim.api.nvim_win_set_buf(self.win, self.s.code.buf)
+ vim.api.nvim_win_set_cursor(self.win, { node.line + 1, node.character })
+end
+
+function LivePreview:focus()
+ vim.api.nvim_set_current_win(self.win)
+ -- Remove this when the autocmd for WinEnter works above
+ vim.api.nvim_win_set_option(self.win, 'cursorline', self.initial_cursorline)
+end
+
+---Create, focus, or update preview
+function LivePreview:show()
+ if not self.s:has_focus() or #vim.api.nvim_list_wins() < 2 then
+ return
+ end
+
+ local node = self.s:_current_node()
+ if not node then
+ return
+ end
+
+ if not self.win then
+ self:create()
+ vim.api.nvim_win_set_cursor(self.win, { node.line + 1, node.character })
+ self.last_node = node
+ return
+ end
+
+ if node == self.last_node then
+ self:focus()
+ else
+ self:update(node)
+ end
+
+ self.last_node = node
+end
+
+function LivePreview:close()
+ if self.win ~= nil then
+ vim.api.nvim_win_close(self.win, true)
+ -- autocmd from setup is not triggered here?
+ self.win = nil
+ self.s.code.win = self.codewin
+ end
+end
+
+function LivePreview:toggle()
+ self:show()
+end
+
return Preview
diff --git a/lua/outline/sidebar.lua b/lua/outline/sidebar.lua
index 1373741..45c7e66 100644
--- a/lua/outline/sidebar.lua
+++ b/lua/outline/sidebar.lua
@@ -26,13 +26,13 @@ local Sidebar = {}
---@field code outline.SidebarCodeState
---@field augroup integer
---@field provider outline.Provider?
----@field preview outline.Preview
+---@field preview outline.Preview|outline.LivePreview
function Sidebar:new(id)
return setmetatable({
id = id,
view = View:new(),
- preview = Preview:new(),
+ preview = Preview:new(cfg.o.preview_window),
code = { buf = 0, win = 0 },
items = {},
flats = {},
@@ -138,7 +138,6 @@ function Sidebar:setup_keymaps()
goto_and_close = { '_goto_and_close', {} },
down_and_jump = { '_move_and_jump', { 'down' } },
up_and_jump = { '_move_and_jump', { 'up' } },
- toggle_preview = { function() self.preview:toggle() end, {} },
fold_toggle = { '_toggle_fold', {} },
fold = { '_set_folded', { true } },
unfold = { '_set_folded', { false } },
@@ -159,6 +158,14 @@ function Sidebar:setup_keymaps()
---@diagnostic disable-next-line param-type-mismatch
self:nmap(name, meth[1], meth[2])
end
+
+ local toggle_preview
+ if cfg.o.preview_window.auto_preview and cfg.o.preview_window.live then
+ toggle_preview = { function() self.preview:focus() end, {} }
+ else
+ toggle_preview = { function() self.preview:toggle() end, {} }
+ end
+ self:nmap('toggle_preview', toggle_preview[1], toggle_preview[2])
end
-- stylua: ignore end
@@ -240,7 +247,6 @@ function Sidebar:update_cursor_style()
-- Set cursor color to CursorLine in normal mode
if hide_cursor then
- self.original_cursor = vim.o.guicursor
local cur = vim.o.guicursor:match('n.-:(.-)[-,]')
vim.opt.guicursor:append('n:' .. cur .. '-Cursorline')
end