feat: Live previews (peek editor)

Disabled by default, but if this feature turns out to be useful and
stable, it will replace the old preview window.
This commit is contained in:
hedy
2023-11-28 20:50:08 +08:00
parent 20cb9ef8dc
commit 884486f3d0
6 changed files with 242 additions and 89 deletions

View File

@@ -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

View File

@@ -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 = {
<div align=center><img width="500" alt="outline with disabled icon for String" src="https://github.com/hedyhli/outline.nvim/assets/50042066/26d258c6-9530-43d4-b88b-963304e3bf2d"></div>
### 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.
<!-- panvimdoc-ignore-start -->
---

View File

@@ -48,6 +48,7 @@ M.defaults = {
center_on_jump = true,
},
preview_window = {
live = false,
auto_preview = false,
width = 50,
min_width = 50,

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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