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 = {
outline with disabled icon for String
+### 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