mirror of
https://github.com/neovim/neovim.git
synced 2025-10-09 03:16:31 +00:00
feat(runtime): undotree #35627
Problem No builtin way to visualize and navigate the undo-tree. Solution Include an "opt" plugin.
This commit is contained in:
@@ -196,6 +196,7 @@ EDITOR
|
|||||||
"(v)iew" then run `:trust`.
|
"(v)iew" then run `:trust`.
|
||||||
• |gx| in help buffers opens the online documentation for the tag under the
|
• |gx| in help buffers opens the online documentation for the tag under the
|
||||||
cursor.
|
cursor.
|
||||||
|
• |:Undotree| for visually navigating the |undo-tree|
|
||||||
|
|
||||||
EVENTS
|
EVENTS
|
||||||
|
|
||||||
|
@@ -38,6 +38,7 @@ Help-link Loaded Short description ~
|
|||||||
|pi_zip.txt| Yes Zip archive explorer
|
|pi_zip.txt| Yes Zip archive explorer
|
||||||
|spellfile.vim| Yes Install spellfile if missing
|
|spellfile.vim| Yes Install spellfile if missing
|
||||||
|tohtml| Yes Convert buffer to html, syntax included
|
|tohtml| Yes Convert buffer to html, syntax included
|
||||||
|
|undotree| No Interactive textual undotree
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
Builtin plugin: editorconfig *editorconfig*
|
Builtin plugin: editorconfig *editorconfig*
|
||||||
@@ -157,4 +158,33 @@ tohtml({winid}, {opt}) *tohtml.tohtml()*
|
|||||||
(`string[]`)
|
(`string[]`)
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
Builtin plugin: undotree *undotree*
|
||||||
|
|
||||||
|
open({opts}) *undotree.open()*
|
||||||
|
Open a window that displays a textual representation of the undotree.
|
||||||
|
|
||||||
|
While in the window, moving the cursor changes the undo.
|
||||||
|
|
||||||
|
Load the plugin with this command: >
|
||||||
|
packadd nvim.undotree
|
||||||
|
<
|
||||||
|
|
||||||
|
Can also be shown with `:Undotree`. *:Undotree*
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {opts} (`table?`) A table with the following fields:
|
||||||
|
• {bufnr} (`integer?`) Buffer to draw the tree into. If
|
||||||
|
omitted, a new buffer is created.
|
||||||
|
• {winid} (`integer?`) Window id to display the tree buffer
|
||||||
|
in. If omitted, a new window is created with {command}.
|
||||||
|
• {command} (`string?`) Vimscript command to create the
|
||||||
|
window. Default value is "30vnew". Only used when {winid} is
|
||||||
|
nil.
|
||||||
|
• {title} (`string|fun(bufnr:integer):string?`) Title of the
|
||||||
|
window. If a function, it accepts the buffer number of the
|
||||||
|
source buffer as its only argument and should return a
|
||||||
|
string.
|
||||||
|
|
||||||
|
|
||||||
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:
|
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:
|
||||||
|
406
runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua
vendored
Normal file
406
runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua
vendored
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
--- @class (private) vim.undotree.tree.entry
|
||||||
|
--- @field child integer[]
|
||||||
|
--- @field time integer
|
||||||
|
|
||||||
|
--- @alias vim.undotree.tree {[integer]: vim.undotree.tree.entry}
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local ns = vim.api.nvim_create_namespace('nvim.undotree')
|
||||||
|
|
||||||
|
--- @param buf integer
|
||||||
|
--- @return vim.fn.undotree.entry[]
|
||||||
|
--- @return integer
|
||||||
|
local function get_undotree_entries(buf)
|
||||||
|
local undotree = vim.fn.undotree(buf)
|
||||||
|
local entries = undotree.entries
|
||||||
|
|
||||||
|
--Maybe: `:undo 0` and then `undotree` to get seq 0 time
|
||||||
|
table.insert(entries, 1, { seq = 0, time = -1 })
|
||||||
|
|
||||||
|
return entries, undotree.seq_cur
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param ent vim.fn.undotree.entry[]
|
||||||
|
--- @param _tree vim.undotree.tree?
|
||||||
|
--- @param _last integer?
|
||||||
|
--- @return vim.undotree.tree
|
||||||
|
local function treefy(ent, _tree, _last)
|
||||||
|
local tree = _tree or {}
|
||||||
|
local last = _last or nil
|
||||||
|
|
||||||
|
for idx, v in ipairs(ent) do
|
||||||
|
local seq = v.seq
|
||||||
|
|
||||||
|
if last then
|
||||||
|
table.insert(tree[last].child, seq)
|
||||||
|
else
|
||||||
|
assert(idx == 1 and not _tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
tree[seq] = { child = {}, time = v.time }
|
||||||
|
if v.alt then
|
||||||
|
assert(last)
|
||||||
|
treefy(v.alt, tree, last)
|
||||||
|
end
|
||||||
|
last = seq
|
||||||
|
end
|
||||||
|
|
||||||
|
return tree
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @class (private) vim.undotree.graph_line
|
||||||
|
--- @field kind 'node'|'remove'|'branch'|'remove+branch'|'nochange_remove'
|
||||||
|
--- @field index integer
|
||||||
|
--- @field node_count integer
|
||||||
|
--- @field node integer|integer[]
|
||||||
|
--- @field index2 integer? -- for branch-index in `remove+branch`
|
||||||
|
|
||||||
|
--- @param tree vim.undotree.tree
|
||||||
|
--- @return vim.undotree.graph_line[]
|
||||||
|
local function tree_to_graph_lines(tree)
|
||||||
|
--- @type vim.undotree.graph_line[]
|
||||||
|
local graph_lines = {}
|
||||||
|
|
||||||
|
assert(tree[0], "tree doesn't have 0-th node")
|
||||||
|
--- @type (integer[]|integer)[]
|
||||||
|
local nodes = { 0 }
|
||||||
|
|
||||||
|
while #nodes > 0 do
|
||||||
|
local minseq = math.huge
|
||||||
|
--- @type integer
|
||||||
|
local index
|
||||||
|
--- @type integer
|
||||||
|
local node_index
|
||||||
|
|
||||||
|
for k, v in ipairs(nodes) do
|
||||||
|
if type(v) == 'table' then
|
||||||
|
for i, j in ipairs(v) do
|
||||||
|
if j < minseq then
|
||||||
|
minseq = j
|
||||||
|
index = k
|
||||||
|
node_index = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif v < minseq then
|
||||||
|
assert(type(v) == 'number')
|
||||||
|
minseq = v
|
||||||
|
index = k
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = nodes[index]
|
||||||
|
|
||||||
|
--- @param kind 'node'|'remove'|'branch'|'nochange_remove'
|
||||||
|
local function add_graph_line(kind)
|
||||||
|
table.insert(graph_lines, { kind = kind, index = index, node_count = #nodes, node = node })
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(node) == 'number' then
|
||||||
|
add_graph_line('node')
|
||||||
|
|
||||||
|
local child = tree[node].child
|
||||||
|
if #child == 0 then
|
||||||
|
if index ~= #nodes then
|
||||||
|
add_graph_line('remove')
|
||||||
|
else
|
||||||
|
add_graph_line('nochange_remove')
|
||||||
|
end
|
||||||
|
|
||||||
|
table.remove(nodes, index)
|
||||||
|
elseif #child == 1 then
|
||||||
|
nodes[index] = child[1]
|
||||||
|
else
|
||||||
|
nodes[index] = child
|
||||||
|
end
|
||||||
|
else
|
||||||
|
assert(type(node) == 'table')
|
||||||
|
|
||||||
|
add_graph_line('branch')
|
||||||
|
|
||||||
|
table.remove(nodes, index)
|
||||||
|
if #node == 2 then
|
||||||
|
table.insert(nodes, index, math.min(unpack(node)))
|
||||||
|
table.insert(nodes, index, math.max(unpack(node)))
|
||||||
|
elseif #node > 2 then
|
||||||
|
table.insert(nodes, index, node[node_index])
|
||||||
|
table.insert(nodes, index, node)
|
||||||
|
table.remove(node, node_index)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for k, v in ipairs(graph_lines) do
|
||||||
|
if v.kind == 'remove' and (graph_lines[k + 1] or {}).kind == 'branch' then
|
||||||
|
v.kind = 'remove+branch'
|
||||||
|
v.index2 = graph_lines[k + 1].index
|
||||||
|
table.remove(graph_lines, k + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return graph_lines
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param time integer
|
||||||
|
--- @return string
|
||||||
|
local function undo_fmt_time(time)
|
||||||
|
if time == -1 then
|
||||||
|
return 'origin'
|
||||||
|
end
|
||||||
|
|
||||||
|
local diff = os.time() - time
|
||||||
|
|
||||||
|
if diff >= 100 then
|
||||||
|
if diff < (60 * 60 * 12) then
|
||||||
|
return os.date('%H:%M:%S', time) --[[@as string]]
|
||||||
|
else
|
||||||
|
return os.date('%Y/%m/%d %H:%M:%S', time) --[[@as string]]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return ('%d second%s ago'):format(diff, diff == 1 and '' or 's')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param tree vim.undotree.tree
|
||||||
|
--- @param graph_lines vim.undotree.graph_line[]
|
||||||
|
--- @param buf integer
|
||||||
|
--- @param meta {[integer]:integer}
|
||||||
|
--- @param find_seq? integer
|
||||||
|
--- @return integer?
|
||||||
|
local function buf_apply_graph_lines(tree, graph_lines, buf, meta, find_seq)
|
||||||
|
-- As in io-buffer, not vim-buffer
|
||||||
|
local line_buffer = {}
|
||||||
|
local extmark_buffer = {}
|
||||||
|
|
||||||
|
--- @type integer?
|
||||||
|
local found_seq
|
||||||
|
|
||||||
|
for k, v in ipairs(graph_lines) do
|
||||||
|
local is_last = k == #graph_lines
|
||||||
|
|
||||||
|
--- @type string?
|
||||||
|
local line
|
||||||
|
if v.kind == 'node' then
|
||||||
|
line = ('| '):rep(v.index - 1)
|
||||||
|
.. '*'
|
||||||
|
.. (' |'):rep(v.node_count - v.index)
|
||||||
|
.. ' '
|
||||||
|
.. v.node
|
||||||
|
.. ' ('
|
||||||
|
.. undo_fmt_time(tree[v.node].time)
|
||||||
|
.. ')'
|
||||||
|
elseif v.kind == 'remove' then
|
||||||
|
line = ('| '):rep(v.index - 1) .. (' /'):rep(v.node_count - v.index)
|
||||||
|
elseif v.kind == 'branch' then
|
||||||
|
line = ('| '):rep(v.index - 1) .. '|\\' .. (' \\'):rep(v.node_count - v.index)
|
||||||
|
elseif v.kind == 'remove+branch' then
|
||||||
|
if v.index2 < v.index then
|
||||||
|
line = ('| '):rep(v.index2 - 1)
|
||||||
|
.. '|\\'
|
||||||
|
.. (' \\'):rep(v.index - v.index2 - 1)
|
||||||
|
.. ' '
|
||||||
|
.. (' |'):rep(v.node_count - v.index)
|
||||||
|
else
|
||||||
|
line = ('| '):rep(v.index - 1)
|
||||||
|
.. (' /'):rep(v.index2 - v.index)
|
||||||
|
.. ' /|'
|
||||||
|
.. (' |'):rep(v.node_count - v.index2 - 1)
|
||||||
|
end
|
||||||
|
elseif v.kind == 'nochange_remove' then
|
||||||
|
line = nil
|
||||||
|
else
|
||||||
|
error 'unreachable'
|
||||||
|
end
|
||||||
|
|
||||||
|
if v.kind == 'node' then
|
||||||
|
table.insert(line_buffer, line)
|
||||||
|
table.insert(meta, v.node)
|
||||||
|
|
||||||
|
if v.node == find_seq then
|
||||||
|
found_seq = #meta
|
||||||
|
end
|
||||||
|
elseif line then
|
||||||
|
table.insert(extmark_buffer, { { line, 'Comment' } })
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(extmark_buffer) and (v.kind == 'node' or is_last) then
|
||||||
|
local row = vim.api.nvim_buf_line_count(buf)
|
||||||
|
vim.api.nvim_buf_set_extmark(buf, ns, row - 1, 0, { virt_lines = extmark_buffer })
|
||||||
|
extmark_buffer = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(line_buffer) and (v.kind ~= 'node' or is_last) then
|
||||||
|
vim.api.nvim_buf_set_lines(buf, -1, -1, true, line_buffer)
|
||||||
|
|
||||||
|
if #line_buffer > 3 then
|
||||||
|
local end_ = vim.api.nvim_buf_line_count(buf) - 1
|
||||||
|
local start = end_ - #line_buffer + 3
|
||||||
|
vim.api.nvim_buf_call(buf, function()
|
||||||
|
vim.cmd.fold { range = { start, end_ } }
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
line_buffer = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, 1, true, {})
|
||||||
|
|
||||||
|
return found_seq
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param inbuf integer
|
||||||
|
---@param outbuf integer
|
||||||
|
---@return {[integer]:integer}
|
||||||
|
local function draw(inbuf, outbuf)
|
||||||
|
local entries, curseq = get_undotree_entries(inbuf)
|
||||||
|
local tree = treefy(entries)
|
||||||
|
local graph_lines = tree_to_graph_lines(tree)
|
||||||
|
|
||||||
|
local meta = {}
|
||||||
|
vim.bo[outbuf].modifiable = true
|
||||||
|
vim.api.nvim_buf_set_lines(outbuf, 0, -1, true, {})
|
||||||
|
vim.api.nvim_buf_clear_namespace(outbuf, ns, 0, -1)
|
||||||
|
local curseq_line = buf_apply_graph_lines(tree, graph_lines, outbuf, meta, curseq)
|
||||||
|
vim.bo[outbuf].modifiable = false
|
||||||
|
|
||||||
|
if vim.api.nvim_win_is_valid(vim.b[outbuf].nvim_is_undotree) then
|
||||||
|
vim.api.nvim_win_set_cursor(vim.b[outbuf].nvim_is_undotree, { curseq_line, 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
return meta
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @class vim.undotree.opts
|
||||||
|
--- @inlinedoc
|
||||||
|
---
|
||||||
|
--- Buffer to draw the tree into. If omitted, a new buffer is created.
|
||||||
|
--- @field bufnr integer?
|
||||||
|
---
|
||||||
|
--- Window id to display the tree buffer in. If omitted, a new window is
|
||||||
|
--- created with {command}.
|
||||||
|
--- @field winid integer?
|
||||||
|
---
|
||||||
|
--- Vimscript command to create the window. Default value is "30vnew".
|
||||||
|
--- Only used when {winid} is nil.
|
||||||
|
--- @field command string?
|
||||||
|
---
|
||||||
|
--- Title of the window. If a function, it accepts the buffer number of the
|
||||||
|
--- source buffer as its only argument and should return a string.
|
||||||
|
--- @field title (string|fun(bufnr:integer):string|nil)
|
||||||
|
|
||||||
|
--- Open a window that displays a textual representation of the undotree.
|
||||||
|
---
|
||||||
|
--- While in the window, moving the cursor changes the undo.
|
||||||
|
---
|
||||||
|
--- Load the plugin with this command:
|
||||||
|
--- ```
|
||||||
|
--- packadd nvim.undotree
|
||||||
|
--- ```
|
||||||
|
---
|
||||||
|
--- Can also be shown with `:Undotree`. [:Undotree]()
|
||||||
|
---
|
||||||
|
--- @param opts vim.undotree.opts?
|
||||||
|
function M.open(opts)
|
||||||
|
-- The following lines of code was copied from
|
||||||
|
-- `vim.treesitter.dev.inspect_tree` and then modified to fit
|
||||||
|
|
||||||
|
vim.validate('opts', opts, 'table', true)
|
||||||
|
|
||||||
|
opts = opts or {}
|
||||||
|
|
||||||
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
|
if vim.b[buf].nvim_undotree then
|
||||||
|
local w = vim.b[buf].nvim_undotree
|
||||||
|
if vim.api.nvim_win_is_valid(w) then
|
||||||
|
vim.api.nvim_win_close(w, true)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
elseif vim.b[buf].nvim_is_undotree then
|
||||||
|
local w = vim.b[buf].nvim_is_undotree
|
||||||
|
if vim.api.nvim_win_is_valid(w) then
|
||||||
|
vim.api.nvim_win_close(w, true)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local w = opts.winid
|
||||||
|
if not w then
|
||||||
|
vim.cmd(opts.command or '30vnew')
|
||||||
|
w = vim.api.nvim_get_current_win()
|
||||||
|
end
|
||||||
|
|
||||||
|
local b = opts.bufnr
|
||||||
|
if b then
|
||||||
|
vim.api.nvim_win_set_buf(w, b)
|
||||||
|
else
|
||||||
|
b = vim.api.nvim_win_get_buf(w)
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.b[buf].nvim_undotree = w
|
||||||
|
vim.b[b].nvim_is_undotree = w
|
||||||
|
|
||||||
|
local title --- @type string?
|
||||||
|
local opts_title = opts.title
|
||||||
|
if not opts_title then
|
||||||
|
local bufname = vim.api.nvim_buf_get_name(buf)
|
||||||
|
title = string.format('Undo tree for %s', vim.fn.fnamemodify(bufname, ':.'))
|
||||||
|
elseif type(opts_title) == 'function' then
|
||||||
|
title = opts_title(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(type(title) == 'string', 'Window title must be a string')
|
||||||
|
vim.api.nvim_buf_set_name(b, title)
|
||||||
|
|
||||||
|
vim.wo[w][0].scrolloff = 5
|
||||||
|
vim.wo[w][0].wrap = false
|
||||||
|
vim.wo[w][0].foldmethod = 'manual'
|
||||||
|
vim.wo[w][0].foldenable = true
|
||||||
|
vim.wo[w][0].cursorline = true
|
||||||
|
vim.bo[b].buflisted = false
|
||||||
|
vim.bo[b].buftype = 'nofile'
|
||||||
|
vim.bo[b].bufhidden = 'wipe'
|
||||||
|
vim.bo[b].swapfile = false
|
||||||
|
|
||||||
|
local meta = draw(buf, b)
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_cursor(w, { vim.api.nvim_buf_line_count(b), 0 })
|
||||||
|
|
||||||
|
local group = vim.api.nvim_create_augroup('nvim.undotree', { clear = false })
|
||||||
|
vim.api.nvim_clear_autocmds({ buffer = b })
|
||||||
|
vim.api.nvim_clear_autocmds({ buffer = buf })
|
||||||
|
|
||||||
|
vim.api.nvim_win_call(w, function()
|
||||||
|
vim.cmd.syntax('region Comment start="(" end=")"')
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd('CursorMoved', {
|
||||||
|
group = group,
|
||||||
|
buffer = b,
|
||||||
|
callback = function()
|
||||||
|
local row = vim.fn.line('.')
|
||||||
|
vim.api.nvim_buf_call(buf, function()
|
||||||
|
vim.cmd.undo { meta[row], mods = { silent = true } }
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
|
||||||
|
group = group,
|
||||||
|
buffer = buf,
|
||||||
|
callback = function()
|
||||||
|
if not vim.api.nvim_buf_is_valid(b) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
meta = draw(buf, b)
|
||||||
|
|
||||||
|
if vim.api.nvim_win_is_valid(w) then
|
||||||
|
vim.wo[w][0].foldlevel = 99
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
8
runtime/pack/dist/opt/nvim.undotree/plugin/undotree.lua
vendored
Normal file
8
runtime/pack/dist/opt/nvim.undotree/plugin/undotree.lua
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
if vim.g.loaded_undotree_plugin ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.g.loaded_undotree_plugin = true
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command('Undotree', function()
|
||||||
|
require 'undotree'.open()
|
||||||
|
end, {})
|
@@ -415,10 +415,12 @@ local config = {
|
|||||||
section_order = {
|
section_order = {
|
||||||
'editorconfig.lua',
|
'editorconfig.lua',
|
||||||
'tohtml.lua',
|
'tohtml.lua',
|
||||||
|
'undotree.lua',
|
||||||
},
|
},
|
||||||
files = {
|
files = {
|
||||||
'runtime/lua/editorconfig.lua',
|
'runtime/lua/editorconfig.lua',
|
||||||
'runtime/lua/tohtml.lua',
|
'runtime/lua/tohtml.lua',
|
||||||
|
'runtime/pack/dist/opt/nvim.undotree/lua/undotree.lua',
|
||||||
},
|
},
|
||||||
fn_xform = function(fun)
|
fn_xform = function(fun)
|
||||||
if fun.module == 'editorconfig' then
|
if fun.module == 'editorconfig' then
|
||||||
|
136
test/functional/plugin/undotree_spec.lua
Normal file
136
test/functional/plugin/undotree_spec.lua
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
local t = require('test.testutil')
|
||||||
|
local n = require('test.functional.testnvim')()
|
||||||
|
|
||||||
|
local clear = n.clear
|
||||||
|
local eq = t.eq
|
||||||
|
local exec = n.exec
|
||||||
|
local api = n.api
|
||||||
|
local dedent = t.dedent
|
||||||
|
|
||||||
|
---@param reverse_tree {[integer]:integer}
|
||||||
|
local function generate_undo_tree_from_rev(reverse_tree)
|
||||||
|
for k, v in ipairs(reverse_tree) do
|
||||||
|
exec('undo ' .. v)
|
||||||
|
api.nvim_buf_set_lines(0, 0, -1, true, { tostring(k) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
---@param buf integer
|
||||||
|
---@return string
|
||||||
|
local function buf_get_lines_and_extmark(buf)
|
||||||
|
local lines = api.nvim_buf_get_lines(buf, 0, -1, true)
|
||||||
|
local ns = api.nvim_create_namespace('nvim.undotree')
|
||||||
|
local extmarks = api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true })
|
||||||
|
for i = #extmarks, 1, -1 do
|
||||||
|
local extmark = extmarks[i]
|
||||||
|
---@type nil,integer,nil,vim.api.keyset.extmark_details
|
||||||
|
local _, row, _, opts = unpack(extmark)
|
||||||
|
local virt_lines = assert(opts.virt_lines)
|
||||||
|
for _, v in ipairs(virt_lines) do
|
||||||
|
local virt_line = v[1][1]
|
||||||
|
table.insert(lines, row + 2, virt_line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(lines, '\n')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function strip_time(text)
|
||||||
|
return text:gsub('%s-%(.-%)', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe(':Undotree', function()
|
||||||
|
before_each(function()
|
||||||
|
clear({ args = { '--clean' } })
|
||||||
|
exec 'packadd nvim.undotree'
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('works', function()
|
||||||
|
api.nvim_set_current_line('foo')
|
||||||
|
exec 'Undotree'
|
||||||
|
local buf = api.nvim_get_current_buf()
|
||||||
|
local win = api.nvim_get_current_win()
|
||||||
|
eq(
|
||||||
|
dedent [[
|
||||||
|
* 0
|
||||||
|
* 1]],
|
||||||
|
strip_time(buf_get_lines_and_extmark(buf))
|
||||||
|
)
|
||||||
|
eq(2, api.nvim_win_get_cursor(win)[1])
|
||||||
|
exec 'wincmd w'
|
||||||
|
|
||||||
|
-- Doing changes moves cursor in undotree
|
||||||
|
exec 'undo'
|
||||||
|
eq(1, api.nvim_win_get_cursor(win)[1])
|
||||||
|
api.nvim_set_current_line('bar')
|
||||||
|
eq(3, api.nvim_win_get_cursor(win)[1])
|
||||||
|
|
||||||
|
eq(
|
||||||
|
dedent [[
|
||||||
|
* 0
|
||||||
|
|\
|
||||||
|
| * 1
|
||||||
|
* 2]],
|
||||||
|
strip_time(buf_get_lines_and_extmark(buf))
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Moving the cursor in undotree changes the buffer
|
||||||
|
eq('bar', api.nvim_get_current_line())
|
||||||
|
exec 'wincmd w'
|
||||||
|
exec '2'
|
||||||
|
exec 'wincmd w'
|
||||||
|
eq('foo', api.nvim_get_current_line())
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('branch+remove is correctly graphed', function()
|
||||||
|
it('when branching left', function()
|
||||||
|
generate_undo_tree_from_rev({ 0, 1, 2, 3, 1, 3, 4, 3, 2, 0 })
|
||||||
|
exec 'Undotree'
|
||||||
|
eq(
|
||||||
|
dedent([[
|
||||||
|
* 0
|
||||||
|
|\
|
||||||
|
| * 1
|
||||||
|
| |\
|
||||||
|
| | * 2
|
||||||
|
| | |\
|
||||||
|
| | | * 3
|
||||||
|
| | | |\
|
||||||
|
| | | | * 4
|
||||||
|
| * | | | 5
|
||||||
|
| / /| |]] --[[This is the line being tested, e.g. remove&branch left]] .. '\n' .. [[
|
||||||
|
| | | * | 6
|
||||||
|
| | | /
|
||||||
|
| | | * 7
|
||||||
|
| | * 8
|
||||||
|
| * 9
|
||||||
|
* 10]]),
|
||||||
|
strip_time(buf_get_lines_and_extmark(0))
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('when branching right', function()
|
||||||
|
generate_undo_tree_from_rev({ 0, 1, 2, 3, 3, 1, 4, 2, 1, 0 })
|
||||||
|
exec 'Undotree'
|
||||||
|
eq(
|
||||||
|
dedent([[
|
||||||
|
* 0
|
||||||
|
|\
|
||||||
|
| * 1
|
||||||
|
| |\
|
||||||
|
| | * 2
|
||||||
|
| | |\
|
||||||
|
| | | * 3
|
||||||
|
| | | |\
|
||||||
|
| | | | * 4
|
||||||
|
| | | * | 5
|
||||||
|
| |\ \ |]] --[[This is the line being tested, e.g. remove&branch right]] .. '\n' .. [[
|
||||||
|
| | * | | 6
|
||||||
|
| | / /
|
||||||
|
| | | * 7
|
||||||
|
| | * 8
|
||||||
|
| * 9
|
||||||
|
* 10]]),
|
||||||
|
strip_time(buf_get_lines_and_extmark(0))
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
@@ -138,7 +138,7 @@ endfunc
|
|||||||
|
|
||||||
func Test_help_completion()
|
func Test_help_completion()
|
||||||
call feedkeys(":help :undo\<C-A>\<C-B>\"\<CR>", 'tx')
|
call feedkeys(":help :undo\<C-A>\<C-B>\"\<CR>", 'tx')
|
||||||
call assert_equal('"help :undo :undoj :undol :undojoin :undolist', @:)
|
call assert_equal('"help :undo :undoj :undol :undojoin :undolist :Undotree', @:)
|
||||||
endfunc
|
endfunc
|
||||||
|
|
||||||
" Test for the :helptags command
|
" Test for the :helptags command
|
||||||
|
Reference in New Issue
Block a user