├── .gitignore ├── LICENSE ├── README.md ├── autoload └── undotree.vim ├── doc ├── _static │ └── undotree.png └── undotree.txt ├── plugin └── undotree.vim └── syntax └── undotree.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, the VIM undotree plug-in authors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [Project on Vim.org](http://www.vim.org/scripts/script.php?script_id=4177) 2 | 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mbbill/undotree?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ### Screenshot 6 | 7 | ![](doc/_static/undotree.png) 8 | 9 | ### Table of Contents 10 | 11 | 12 | 13 | - [Description](#description) 14 | - [Download and Install](#download-and-install) 15 | - [Usage](#usage) 16 | - [Configuration](#configuration) 17 | - [Debug](#debug) 18 | - [License](#license) 19 | - [Author](#author) 20 | 21 | 22 | 23 | ### Description 24 | 25 | Undotree visualizes the undo history and makes it easy to browse and switch between different undo branches. You may be wondering, _what are undo "branches" anyway?_ They're a feature of Vim that allow you to go back to a prior state even after it has been overwritten by later edits. For example: In most editors, if you make some change A, followed by change B, then go back to A and make another change C, normally you wouldn't be able to go back to change B because the undo history is linear. That's not the case with Vim, however. Vim internally stores the entire edit history for each file as a single, monolithic tree structure; this plug-in exposes that tree to you so that you can not only switch back and forth between older and more recent edits linearly but can also switch between diverging branches. 26 | 27 | > Note: use `:help 'undolevels'` in Vim for information on configuring the size of the undo history 28 | 29 | 30 | ### How it works 31 | 32 | Some users may have questions about whether file contents change when switching between undo history states. With undotree, you don't need to worry about data loss or disk writes. The plugin will **never** save your data or write to disk. Instead, it modifies the current buffer temporarily, just like auto-completion plugins do. This means that any changes made by undotree are reversible with a single click, allowing you to easily revert to any prior state. 33 | 34 | Vim's undo/redo feature is a great way to protect your work from accidental changes or data loss. Unlike other editors, where undoing and then accidentally typing something can cause you to lose your edits, Vim allows you to revert to previous states without losing any data, as long as you keep the Vim session alive. If you want to keep your undo history permanently, Vim offers a persistent undo feature. This feature saves your undo history to a file on disk, allowing you to preserve your undo history across editing sessions. To enable persistent undo, refer to the instructions below. This can be a useful option for those who want to maintain a long-term undo history for a file or project. 35 | 36 | ### Persisting the undo history 37 | 38 | Undo/redo functionality is a useful feature for most editors, including Vim. However, by default, Vim's undo history is only available during the current editing session, as it is stored in memory and lost once the process exits. While tools such as undotree can aid in accessing historical states, it does not offer a permanent solution. For some users, it may be safer or more convenient to persist the undo history across editing sessions, and that's where Vim's persistent undo feature comes in. 39 | 40 | Persistent undo saves the undo history in a file on disk, rather than in RAM. Whenever a change is made, Vim saves the edited file with its current state, while also saving the entire undo history to a separate file on disk that includes all states. This means that even after exiting Vim, the undo history is still available when you reopen the file, allowing you to continue to undo/redo changes. The undo history file is incremental and saves every change permanently, similar to Git. 41 | 42 | If you're worried about the potential storage space used by persistent undo files, undotree provides an option to clean them up. Additionally, undotree is written in pure Vim script, making it lightweight, simple, and fast, and only runs when needed. To enable persistent undo, simply type `:h persistent-undo` in Vim, or follow the instructions provided in the *Usage* section below. 43 | 44 | ### Download and Install 45 | 46 | Using Vim's built-in package manager: 47 | 48 | ```sh 49 | mkdir -p ~/.vim/pack/mbbill/start 50 | cd ~/.vim/pack/mbbill/start 51 | git clone https://github.com/mbbill/undotree.git 52 | vim -u NONE -c "helptags undotree/doc" -c q 53 | ``` 54 | 55 | Use whatever plug-in manager to pull the master branch. I've included 2 examples of the most used: 56 | 57 | - *Vundle:* `Plugin 'mbbill/undotree'` 58 | - *Vim-Plug:* `Plug 'mbbill/undotree'` 59 | - *Packer:* `use 'mbbill/undotree'` 60 | 61 | And install them with the following: 62 | 63 | - *Vundle:* `:PluginInstall` 64 | - *Vim-Plug:* `:PlugInstall` 65 | - *Packer:* `:PackerSync` 66 | 67 | ### Usage 68 | 69 | 1. Use `:UndotreeToggle` to toggle the undo-tree panel. 70 | 71 | You may want to map this command to whatever hotkey by adding the following line to your vimrc, take `F5` for example. 72 | 73 | ```vim 74 | nnoremap :UndotreeToggle 75 | ``` 76 | 77 | Or the equivalent mapping if using Neovim and Lua script. 78 | 79 | ```lua 80 | vim.keymap.set('n', '', vim.cmd.UndotreeToggle) 81 | ``` 82 | 83 | 2. Markers 84 | * Every change has a sequence number and it is displayed before timestamps. 85 | * The current state is marked as `> number <`. 86 | * The next state which will be restored by `:redo` or `` is marked as `{ number }`. 87 | * The `[ number ]` marks the most recent change. 88 | * The undo history is sorted by timestamps. 89 | * Saved changes are marked as `s` and the big `S` indicates the most recent saved change. 90 | 3. Press `?` in undotree window for quick help. 91 | 4. Persistent undo 92 | * Usually, I would like to store the undo files in a separate place like below. 93 | 94 | ```vim 95 | if has("persistent_undo") 96 | let target_path = expand('~/.undodir') 97 | 98 | " create the directory and any parent directories 99 | " if the location does not exist. 100 | if !isdirectory(target_path) 101 | call mkdir(target_path, "p", 0700) 102 | endif 103 | 104 | let &undodir=target_path 105 | set undofile 106 | endif 107 | ``` 108 | * Alternatively, if you wish to persist the undo history for a currently 109 | open file only, you can use the `:UndotreePersistUndo` command. 110 | 111 | #### Configuration 112 | 113 | [Here](https://github.com/mbbill/undotree/blob/master/plugin/undotree.vim#L27) is a list of options. 114 | 115 | #### Debug 116 | 117 | 1. Create a file under $HOME with the name `undotree_debug.log` 118 | * `$touch ~/undotree_debug.log` 119 | 2. Run vim, and the log will automatically be appended to the file, and you may watch it using `tail`: 120 | * `$tail -F ~/undotree_debug.log` 121 | 3. If you want to disable debug, just delete that file. 122 | 123 | ### License 124 | 125 | **BSD** 126 | 127 | ### Author 128 | 129 | Ming Bai <mbbill AT gmail DOT COM> 130 | -------------------------------------------------------------------------------- /autoload/undotree.vim: -------------------------------------------------------------------------------- 1 | "================================================= 2 | " File: autoload/undotree.vim 3 | " Description: Manage your undo history in a graph. 4 | " Author: David Knoble 5 | " License: BSD 6 | 7 | " Avoid installing twice. 8 | if exists('g:autoloaded_undotree') 9 | finish 10 | endif 11 | let g:autoloaded_undotree = 0 12 | 13 | " At least version 7.3 with 005 patch is needed for undo branches. 14 | " Refer to https://github.com/mbbill/undotree/issues/4 for details. 15 | " Thanks kien 16 | if v:version < 703 17 | finish 18 | endif 19 | if (v:version == 703 && !has("patch005")) 20 | finish 21 | endif 22 | let g:loaded_undotree = 1 " Signal plugin availability with a value of 1. 23 | 24 | " Short time indicators 25 | if g:undotree_ShortIndicators == 1 26 | let s:timeSecond = '1 s' 27 | let s:timeSeconds = ' s' 28 | 29 | let s:timeMinute = '1 m' 30 | let s:timeMinutes = ' m' 31 | 32 | let s:timeHour = '1 h' 33 | let s:timeHours = ' h' 34 | 35 | let s:timeDay = '1 d' 36 | let s:timeDays = ' d' 37 | 38 | let s:timeOriginal = 'Orig' 39 | else 40 | let s:timeSecond = '1 second ago' 41 | let s:timeSeconds = ' seconds ago' 42 | 43 | let s:timeMinute = '1 minute ago' 44 | let s:timeMinutes = ' minutes ago' 45 | 46 | let s:timeHour = '1 hour ago' 47 | let s:timeHours = ' hours ago' 48 | 49 | let s:timeDay = '1 day ago' 50 | let s:timeDays = ' days ago' 51 | 52 | let s:timeOriginal = 'Original' 53 | endif 54 | 55 | "================================================= 56 | " Help text 57 | let s:helpmore = ['" ===== Marks ===== ', 58 | \'" >num< : The current state', 59 | \'" {num} : The next redo state', 60 | \'" [num] : The latest state', 61 | \'" =num= : The diff mark', 62 | \'" s : Saved states', 63 | \'" S : The last saved state', 64 | \'" ===== Hotkeys ====='] 65 | if !g:undotree_HelpLine 66 | let s:helpless = [] 67 | else 68 | let s:helpless = ['" Press ? for help.'] 69 | endif 70 | 71 | "Custom key mappings: add this function to your vimrc. 72 | "You can define whatever mapping as you like, this is a hook function which 73 | "will be called after undotree window initialized. 74 | " 75 | "function g:Undotree_CustomMap() 76 | " map J 77 | " map K 78 | "endfunction 79 | 80 | " Keymap 81 | let s:keymap = [] 82 | " action, key, help. 83 | let s:keymap += [['Help','?','Toggle quick help']] 84 | let s:keymap += [['Close','q','Close undotree panel']] 85 | let s:keymap += [['FocusTarget','','Set Focus back to the editor']] 86 | let s:keymap += [['ClearHistory','C','Clear undo history (with confirmation)']] 87 | let s:keymap += [['TimestampToggle','T','Toggle relative timestamp']] 88 | let s:keymap += [['DiffToggle','D','Toggle the diff panel']] 89 | let s:keymap += [['DiffMark','=','Set the diff marker']] 90 | let s:keymap += [['ClearDiffMark','M','Clear the diff marker']] 91 | let s:keymap += [['NextState','K','Move to the next undo state']] 92 | let s:keymap += [['PreviousState','J','Move to the previous undo state']] 93 | let s:keymap += [['NextSavedState','>','Move to the next saved state']] 94 | let s:keymap += [['PreviousSavedState','<','Move to the previous saved state']] 95 | let s:keymap += [['Redo','','Redo']] 96 | let s:keymap += [['Undo','u','Undo']] 97 | let s:keymap += [['Enter','<2-LeftMouse>','Move to the current state']] 98 | let s:keymap += [['Enter','','Move to the current state']] 99 | 100 | " 'Diff' sign definitions. There are two 'delete' signs; a 'normal' one and one 101 | " that is used if the very end of the buffer has been deleted (in which case the 102 | " deleted text is actually beyond the end of the current buffer version and therefore 103 | " it is not possible to place a sign on the exact line - because it doesn't exist. 104 | " Instead, a 'special' delete sign is placed on the (existing) last line of the 105 | " buffer) 106 | exe 'sign define UndotreeAdd text=++ texthl='.undotree_HighlightSyntaxAdd 107 | exe 'sign define UndotreeChg text=~~ texthl='.undotree_HighlightSyntaxChange 108 | exe 'sign define UndotreeDel text=-- texthl='.undotree_HighlightSyntaxDel 109 | exe 'sign define UndotreeDelEnd text=-v texthl='.undotree_HighlightSyntaxDel 110 | 111 | " Id to use for all signs. This is an arbitrary number that is hoped to be unique 112 | " within the instance of vim. There is no way of guaranteeing it IS unique, which 113 | " is a shame because it needs to be! 114 | " 115 | " Note that all signs are placed with the same Id - as long as we keep a count of 116 | " how many we have placed (so we can remove them all again), this is ok 117 | let s:signId = 2123654789 118 | 119 | "================================================= 120 | function! s:new(obj) abort 121 | let newobj = deepcopy(a:obj) 122 | call newobj.Init() 123 | return newobj 124 | endfunction 125 | 126 | " Get formatted time 127 | function! s:gettime(time) abort 128 | if a:time == 0 129 | return s:timeOriginal 130 | endif 131 | if !g:undotree_RelativeTimestamp 132 | let today = substitute(strftime("%c",localtime())," .*$",'','g') 133 | if today == substitute(strftime("%c",a:time)," .*$",'','g') 134 | return strftime("%H:%M:%S",a:time) 135 | else 136 | return strftime("%H:%M:%S %b%d %Y",a:time) 137 | endif 138 | else 139 | let sec = localtime() - a:time 140 | if sec < 0 141 | let sec = 0 142 | endif 143 | if sec < 60 144 | if sec == 1 145 | return s:timeSecond 146 | else 147 | return sec.s:timeSeconds 148 | endif 149 | endif 150 | if sec < 3600 151 | if (sec/60) == 1 152 | return s:timeMinute 153 | else 154 | return (sec/60).s:timeMinutes 155 | endif 156 | endif 157 | if sec < 86400 "3600*24 158 | if (sec/3600) == 1 159 | return s:timeHour 160 | else 161 | return (sec/3600).s:timeHours 162 | endif 163 | endif 164 | if (sec/86400) == 1 165 | return s:timeDay 166 | else 167 | return (sec/86400).s:timeDays 168 | endif 169 | endif 170 | endfunction 171 | 172 | function! s:exec(cmd) abort 173 | call s:log("s:exec() ".a:cmd) 174 | silent exe a:cmd 175 | endfunction 176 | 177 | " Don't trigger any events(like BufEnter which could cause redundant refresh) 178 | function! s:exec_silent(cmd) abort 179 | call s:log("s:exec_silent() ".a:cmd) 180 | let ei_bak= &eventignore 181 | set eventignore=BufEnter,BufLeave,BufWinLeave,InsertLeave,CursorMoved,BufWritePost 182 | silent exe a:cmd 183 | let &eventignore = ei_bak 184 | endfunction 185 | 186 | " Return a unique id each time. 187 | let s:cntr = 0 188 | function! s:getUniqueID() abort 189 | let s:cntr = s:cntr + 1 190 | return s:cntr 191 | endfunction 192 | 193 | " Set to 1 to enable debug log 194 | let s:debug = 0 195 | let s:debugfile = $HOME.'/undotree_debug.log' 196 | " If debug file exists, enable debug output. 197 | if filewritable(s:debugfile) 198 | let s:debug = 1 199 | exec 'redir >> '. s:debugfile 200 | silent echo "=======================================\n" 201 | redir END 202 | endif 203 | 204 | function! s:log(msg) abort 205 | if s:debug 206 | exec 'redir >> ' . s:debugfile 207 | silent echon strftime('%H:%M:%S') . ': ' . string(a:msg) . "\n" 208 | redir END 209 | endif 210 | endfunction 211 | 212 | function! s:ObserveOptions() 213 | augroup Undotree_OptionsObserver 214 | try 215 | autocmd! 216 | if exists('+fdo') 217 | let s:open_folds = &fdo =~# 'undo' 218 | if exists('##OptionSet') 219 | autocmd OptionSet foldopen let s:open_folds = v:option_new =~# 'undo' 220 | endif 221 | endif 222 | finally 223 | augroup END 224 | endtry 225 | endfunction 226 | 227 | " Whether to open folds on undo/redo. 228 | " Is 1 when 'undo' is in &fdo (see :help 'foldopen'). 229 | " default: 1 230 | let s:open_folds = 1 231 | 232 | if exists('v:vim_did_enter') 233 | if !v:vim_did_enter 234 | autocmd VimEnter * call s:ObserveOptions() 235 | else 236 | call s:ObserveOptions() 237 | endif 238 | else 239 | autocmd VimEnter * call s:ObserveOptions() 240 | call s:ObserveOptions() 241 | endif 242 | 243 | "================================================= 244 | "Base class for panels. 245 | let s:panel = {} 246 | 247 | function! s:panel.Init() abort 248 | let self.bufname = "invalid" 249 | endfunction 250 | 251 | function! s:panel.SetFocus() abort 252 | let winnr = bufwinnr(self.bufname) 253 | " already focused. 254 | if winnr == winnr() 255 | return 256 | endif 257 | if winnr == -1 258 | echoerr "Fatal: window does not exist!" 259 | return 260 | endif 261 | call s:log("SetFocus() winnr:".winnr." bufname:".self.bufname) 262 | " wincmd would cause cursor outside window. 263 | call s:exec_silent("norm! ".winnr."\\") 264 | endfunction 265 | 266 | function! s:panel.IsVisible() abort 267 | if bufwinnr(self.bufname) != -1 268 | return 1 269 | else 270 | return 0 271 | endif 272 | endfunction 273 | 274 | function! s:panel.Hide() abort 275 | call s:log(self.bufname." Hide()") 276 | if !self.IsVisible() 277 | return 278 | endif 279 | call self.SetFocus() 280 | call s:exec("quit") 281 | endfunction 282 | 283 | "================================================= 284 | " undotree panel class. 285 | " extended from panel. 286 | " 287 | 288 | " {rawtree} 289 | " | 290 | " | ConvertInput() {seq2index}--> [seq1:index1] 291 | " v [seq2:index2] ---+ 292 | " {tree} ... | 293 | " | [asciimeta] | 294 | " | Render() | | 295 | " v v | 296 | " [asciitree] --> [" * | SEQ DDMMYY "] <==> [node1{seq,time,..}] | 297 | " [" |/ "] [node2{seq,time,..}] <---+ 298 | " ... ... 299 | 300 | let s:undotree = s:new(s:panel) 301 | 302 | function! s:undotree.Init() abort 303 | let self.bufname = "undotree_".s:getUniqueID() 304 | " Increase to make it unique. 305 | let self.width = g:undotree_SplitWidth 306 | let self.opendiff = g:undotree_DiffAutoOpen 307 | let self.diffmark = -1 " Marker for the diff view 308 | let self.targetid = -1 309 | let self.targetBufnr = -1 310 | let self.rawtree = {} "data passed from undotree() 311 | let self.tree = {} "data converted to internal format. 312 | let self.seq_last = -1 313 | let self.save_last = -1 314 | let self.save_last_bak = -1 315 | 316 | " seqs 317 | let self.seq_cur = -1 318 | let self.seq_curhead = -1 319 | let self.seq_newhead = -1 320 | let self.seq_saved = {} "{saved value -> seq} pair 321 | 322 | "backup, for mark 323 | let self.seq_cur_bak = -1 324 | let self.seq_curhead_bak = -1 325 | let self.seq_newhead_bak = -1 326 | 327 | let self.asciitree = [] "output data. 328 | let self.asciimeta = [] "meta data behind ascii tree. 329 | let self.seq2index = {} "table used to convert seq to index. 330 | let self.showHelp = 0 331 | endfunction 332 | 333 | function! s:undotree.BindKey() abort 334 | if v:version > 703 || (v:version == 703 && has("patch1261")) 335 | let map_options = ' ' 336 | else 337 | let map_options = '' 338 | endif 339 | let map_options = map_options.' ' 340 | for i in s:keymap 341 | silent exec 'nmap '.map_options.i[1].' Undotree'.i[0] 342 | silent exec 'nnoremap '.map_options.'Undotree'.i[0] 343 | \ .' :call undotreeAction("'.i[0].'")' 344 | endfor 345 | if exists('*g:Undotree_CustomMap') 346 | call g:Undotree_CustomMap() 347 | endif 348 | endfunction 349 | 350 | function! s:undotree.BindAu() abort 351 | " Auto exit if it's the last window 352 | augroup Undotree_Main 353 | au! 354 | au BufEnter call s:exitIfLast() 355 | au BufEnter,BufLeave if exists('t:undotree') | 356 | \let t:undotree.width = winwidth(winnr()) | endif 357 | au BufWinLeave if exists('t:diffpanel') | 358 | \call t:diffpanel.Hide() | endif 359 | augroup end 360 | endfunction 361 | 362 | function! s:undotree.Action(action) abort 363 | call s:log("undotree.Action() ".a:action) 364 | if !self.IsVisible() || !exists('b:isUndotreeBuffer') 365 | echoerr "Fatal: window does not exist." 366 | return 367 | endif 368 | if !has_key(self,'Action'.a:action) 369 | echoerr "Fatal: Action does not exist!" 370 | return 371 | endif 372 | silent exec 'call self.Action'.a:action.'()' 373 | endfunction 374 | 375 | " Helper function, do action in target window, and then update itself. 376 | function! s:undotree.ActionInTarget(cmd) abort 377 | if !self.SetTargetFocus() 378 | return 379 | endif 380 | " Target should be a normal buffer. 381 | if (&bt == '' || &bt == 'acwrite') && (&modifiable == 1) && (mode() == 'n') 382 | call s:exec(a:cmd) 383 | " Open folds so that the change being undone/redone is visible. 384 | if s:open_folds 385 | call s:exec('normal! zv') 386 | endif 387 | call self.Update() 388 | endif 389 | " Update not always set current focus. 390 | call self.SetFocus() 391 | endfunction 392 | 393 | function! s:undotree.ActionHelp() abort 394 | let self.showHelp = !self.showHelp 395 | call self.Draw() 396 | call self.MarkSeqs(1) 397 | endfunction 398 | 399 | function! s:undotree.ActionFocusTarget() abort 400 | call self.SetTargetFocus() 401 | endfunction 402 | 403 | function! s:undotree.ActionEnter() abort 404 | let index = self.Screen2Index(line('.')) 405 | if index < 0 406 | return 407 | endif 408 | let seq = self.asciimeta[index].seq 409 | if seq == -1 410 | return 411 | endif 412 | if seq == 0 413 | call self.ActionInTarget('norm 9999u') 414 | return 415 | endif 416 | call self.ActionInTarget('u '.self.asciimeta[index].seq) 417 | endfunction 418 | 419 | function! s:undotree.ActionUndo() abort 420 | call self.ActionInTarget('undo') 421 | endfunction 422 | 423 | function! s:undotree.ActionRedo() abort 424 | call self.ActionInTarget("redo") 425 | endfunction 426 | 427 | function! s:undotree.ActionPreviousState() abort 428 | call self.ActionInTarget('earlier') 429 | endfunction 430 | 431 | function! s:undotree.ActionNextState() abort 432 | call self.ActionInTarget('later') 433 | endfunction 434 | 435 | function! s:undotree.ActionPreviousSavedState() abort 436 | call self.ActionInTarget('earlier 1f') 437 | endfunction 438 | 439 | function! s:undotree.ActionNextSavedState() abort 440 | call self.ActionInTarget('later 1f') 441 | endfunction 442 | 443 | function! s:undotree.ActionDiffMark() abort 444 | let index = self.Screen2Index(line('.')) 445 | if index < 0 446 | return 447 | endif 448 | let seq = self.asciimeta[index].seq 449 | if seq == -1 450 | return 451 | endif 452 | if seq == self.diffmark 453 | let self.diffmark = -1 454 | else 455 | let self.diffmark = seq 456 | endif 457 | call self.UpdateDiff() 458 | call self.Draw() 459 | call self.MarkSeqs(0) 460 | endfunction 461 | 462 | function! s:undotree.ActionClearDiffMark() abort 463 | let self.diffmark = -1 464 | call self.UpdateDiff() 465 | call self.Draw() 466 | call self.MarkSeqs(1) 467 | endfunction 468 | 469 | function! s:undotree.ActionDiffToggle() abort 470 | let self.opendiff = !self.opendiff 471 | call t:diffpanel.Toggle() 472 | call self.UpdateDiff() 473 | endfunction 474 | 475 | function! s:undotree.ActionTimestampToggle() abort 476 | if !self.SetTargetFocus() 477 | return 478 | endif 479 | let g:undotree_RelativeTimestamp = !g:undotree_RelativeTimestamp 480 | let self.targetBufnr = -1 "force update 481 | call self.Update() 482 | " Update not always set current focus. 483 | call self.SetFocus() 484 | endfunction 485 | 486 | function! s:undotree.ActionClearHistory() abort 487 | if input("Clear ALL undo history? Type \"YES\" to continue: ") != "YES" 488 | return 489 | endif 490 | if !self.SetTargetFocus() 491 | return 492 | endif 493 | let ul_bak = &undolevels 494 | let mod_bak = &modified 495 | let &undolevels = -1 496 | call s:exec("norm! a \\") 497 | let &undolevels = ul_bak 498 | let &modified = mod_bak 499 | unlet ul_bak mod_bak 500 | let self.targetBufnr = -1 "force update 501 | call self.Update() 502 | endfunction 503 | 504 | function! s:undotree.ActionClose() abort 505 | call self.Toggle() 506 | endfunction 507 | 508 | function! s:undotree.UpdateDiff() abort 509 | call s:log("undotree.UpdateDiff()") 510 | if !t:diffpanel.IsVisible() 511 | return 512 | endif 513 | call t:diffpanel.Update(self.seq_cur,self.targetBufnr,self.targetid,self.diffmark) 514 | endfunction 515 | 516 | " May fail due to target window closed. 517 | function! s:undotree.SetTargetFocus() abort 518 | for winnr in range(1, winnr('$')) "winnr starts from 1 519 | if getwinvar(winnr,'undotree_id') == self.targetid 520 | if winnr() != winnr 521 | call s:exec("norm! ".winnr."\\") 522 | return 1 523 | endif 524 | endif 525 | endfor 526 | return 0 527 | endfunction 528 | 529 | function! s:undotree.Toggle() abort 530 | "Global auto commands to keep undotree up to date. 531 | let auEvents = "BufEnter,InsertLeave,CursorMoved,BufWritePost" 532 | 533 | call s:log(self.bufname." Toggle()") 534 | if self.IsVisible() 535 | call self.Hide() 536 | call t:diffpanel.Hide() 537 | call self.SetTargetFocus() 538 | augroup Undotree 539 | autocmd! 540 | augroup END 541 | else 542 | call self.Show() 543 | if !g:undotree_SetFocusWhenToggle 544 | call self.SetTargetFocus() 545 | endif 546 | augroup Undotree 547 | au! 548 | exec "au! ".auEvents." * call undotree#UndotreeUpdate()" 549 | augroup END 550 | endif 551 | endfunction 552 | 553 | function! s:undotree.GetStatusLine() abort 554 | if self.seq_cur != -1 555 | let seq_cur = self.seq_cur 556 | else 557 | let seq_cur = 'None' 558 | endif 559 | if self.seq_curhead != -1 560 | let seq_curhead = self.seq_curhead 561 | else 562 | let seq_curhead = 'None' 563 | endif 564 | return 'current: '.seq_cur.' redo: '.seq_curhead 565 | endfunction 566 | 567 | function! s:undotree.Show() abort 568 | call s:log("undotree.Show()") 569 | if self.IsVisible() 570 | return 571 | endif 572 | 573 | let self.targetid = w:undotree_id 574 | 575 | " Create undotree window. 576 | if exists("g:undotree_CustomUndotreeCmd") 577 | let cmd = g:undotree_CustomUndotreeCmd . ' ' . 578 | \self.bufname 579 | elseif g:undotree_WindowLayout == 1 || g:undotree_WindowLayout == 2 580 | let cmd = "topleft vertical" . 581 | \self.width . ' new ' . self.bufname 582 | else 583 | let cmd = "botright vertical" . 584 | \self.width . ' new ' . self.bufname 585 | endif 586 | call s:exec("silent keepalt ".cmd) 587 | call self.SetFocus() 588 | 589 | " We need a way to tell if the buffer is belong to undotree, 590 | " bufname() is not always reliable. 591 | let b:isUndotreeBuffer = 1 592 | 593 | setlocal winfixwidth 594 | setlocal noswapfile 595 | setlocal buftype=nowrite 596 | setlocal bufhidden=delete 597 | setlocal nowrap 598 | setlocal nolist 599 | setlocal foldcolumn=0 600 | setlocal nobuflisted 601 | setlocal nospell 602 | setlocal nonumber 603 | setlocal norelativenumber 604 | if g:undotree_CursorLine 605 | setlocal cursorline 606 | else 607 | setlocal nocursorline 608 | endif 609 | setlocal nomodifiable 610 | setlocal statusline=%!t:undotree.GetStatusLine() 611 | setfiletype undotree 612 | 613 | call self.BindKey() 614 | call self.BindAu() 615 | 616 | let ei_bak= &eventignore 617 | set eventignore=all 618 | 619 | call self.SetTargetFocus() 620 | let self.targetBufnr = -1 "force update 621 | call self.Update() 622 | 623 | let &eventignore = ei_bak 624 | 625 | if self.opendiff 626 | call t:diffpanel.Show() 627 | call self.UpdateDiff() 628 | endif 629 | endfunction 630 | 631 | " called outside undotree window 632 | function! s:undotree.Update() abort 633 | if !self.IsVisible() 634 | return 635 | endif 636 | " do nothing if we're in the undotree or diff panel 637 | if exists('b:isUndotreeBuffer') 638 | return 639 | endif 640 | if (&bt != '' && &bt != 'acwrite') || (&modifiable == 0) || (mode() != 'n') 641 | if &bt == 'quickfix' || &bt == 'nofile' 642 | "Do nothing for quickfix and q: 643 | call s:log("undotree.Update() ignore quickfix") 644 | return 645 | endif 646 | if self.targetBufnr == bufnr('%') && self.targetid == w:undotree_id 647 | call s:log("undotree.Update() invalid buffer NOupdate") 648 | return 649 | endif 650 | let emptybuf = 1 "This is not a valid buffer, could be help or something. 651 | call s:log("undotree.Update() invalid buffer update") 652 | else 653 | let emptybuf = 0 654 | "update undotree,set focus 655 | if self.targetBufnr == bufnr('%') 656 | let self.targetid = w:undotree_id 657 | let newrawtree = undotree() 658 | if self.rawtree == newrawtree 659 | return 660 | endif 661 | 662 | " same buffer, but seq changed. 663 | if newrawtree.seq_last == self.seq_last 664 | call s:log("undotree.Update() update seqs") 665 | let self.rawtree = newrawtree 666 | call self.ConvertInput(0) "only update seqs. 667 | if (self.seq_cur == self.seq_cur_bak) && 668 | \(self.seq_curhead == self.seq_curhead_bak)&& 669 | \(self.seq_newhead == self.seq_newhead_bak)&& 670 | \(self.save_last == self.save_last_bak) 671 | return 672 | endif 673 | call self.SetFocus() 674 | call self.MarkSeqs(1) 675 | call self.UpdateDiff() 676 | return 677 | endif 678 | endif 679 | endif 680 | call s:log("undotree.Update() update whole tree") 681 | 682 | let self.targetBufnr = bufnr('%') 683 | let self.targetid = w:undotree_id 684 | if emptybuf " Show an empty undo tree instead of do nothing. 685 | let self.rawtree = {'seq_last':0,'entries':[],'time_cur':0,'save_last':0,'synced':1,'save_cur':0,'seq_cur':0} 686 | else 687 | let self.rawtree = undotree() 688 | endif 689 | let self.seq_last = self.rawtree.seq_last 690 | let self.seq_cur = -1 691 | let self.seq_curhead = -1 692 | let self.seq_newhead = -1 693 | call self.ConvertInput(1) "update all. 694 | call self.Render() 695 | call self.SetFocus() 696 | call self.Draw() 697 | call self.MarkSeqs(1) 698 | call self.UpdateDiff() 699 | endfunction 700 | 701 | function! s:undotree.AppendHelp() abort 702 | if self.showHelp 703 | call append(0,'') "empty line 704 | for i in s:keymap 705 | call append(0,'" '.i[1].' : '.i[2]) 706 | endfor 707 | call append(0,s:helpmore) 708 | else 709 | if g:undotree_HelpLine 710 | call append(0,'') 711 | endif 712 | call append(0,s:helpless) 713 | endif 714 | endfunction 715 | 716 | function! s:undotree.Index2Screen(index) abort 717 | " index starts from zero 718 | let index_padding = 1 719 | let empty_line = 1 720 | let lineNr = a:index + index_padding + empty_line 721 | " calculate line number according to the help text. 722 | " index starts from zero and lineNr starts from 1 723 | if self.showHelp 724 | let lineNr += len(s:keymap) + len(s:helpmore) 725 | else 726 | let lineNr += len(s:helpless) 727 | if !g:undotree_HelpLine 728 | let lineNr -= empty_line 729 | endif 730 | endif 731 | return lineNr 732 | endfunction 733 | 734 | " <0 if index is invalid. e.g. current line is in help text. 735 | function! s:undotree.Screen2Index(line) abort 736 | let index_padding = 1 737 | let empty_line = 1 738 | let index = a:line - index_padding - empty_line 739 | 740 | if self.showHelp 741 | let index -= len(s:keymap) + len(s:helpmore) 742 | else 743 | let index -= len(s:helpless) 744 | if !g:undotree_HelpLine 745 | let index += empty_line 746 | endif 747 | endif 748 | return index 749 | endfunction 750 | 751 | " Current window must be undotree. 752 | function! s:undotree.Draw() abort 753 | " remember the current cursor position. 754 | let savedview = winsaveview() 755 | 756 | setlocal modifiable 757 | " Delete text into blackhole register. 758 | call s:exec('1,$ d _') 759 | call append(0,self.asciitree) 760 | 761 | call self.AppendHelp() 762 | 763 | "remove the last empty line 764 | call s:exec('$d _') 765 | 766 | " restore previous cursor position. 767 | call winrestview(savedview) 768 | 769 | setlocal nomodifiable 770 | endfunction 771 | 772 | function! s:undotree.MarkSeqs(move_cursor) abort 773 | call s:log("bak(cur,curhead,newhead): ". 774 | \self.seq_cur_bak.' '. 775 | \self.seq_curhead_bak.' '. 776 | \self.seq_newhead_bak) 777 | call s:log("(cur,curhead,newhead): ". 778 | \self.seq_cur.' '. 779 | \self.seq_curhead.' '. 780 | \self.seq_newhead) 781 | setlocal modifiable 782 | " reset bak seq lines. 783 | if self.seq_cur_bak != -1 784 | let index = self.seq2index[self.seq_cur_bak] 785 | call setline(self.Index2Screen(index),self.asciitree[index]) 786 | endif 787 | if self.seq_curhead_bak != -1 788 | let index = self.seq2index[self.seq_curhead_bak] 789 | call setline(self.Index2Screen(index),self.asciitree[index]) 790 | endif 791 | if self.seq_newhead_bak != -1 792 | let index = self.seq2index[self.seq_newhead_bak] 793 | call setline(self.Index2Screen(index),self.asciitree[index]) 794 | endif 795 | " mark save seqs 796 | for i in keys(self.seq_saved) 797 | let index = self.seq2index[self.seq_saved[i]] 798 | let lineNr = self.Index2Screen(index) 799 | call setline(lineNr,substitute(self.asciitree[index], 800 | \' \d\+ \zs \ze','s','')) 801 | endfor 802 | let max_saved_num = max(keys(self.seq_saved)) 803 | if max_saved_num > 0 804 | let lineNr = self.Index2Screen(self.seq2index[self.seq_saved[max_saved_num]]) 805 | call setline(lineNr,substitute(getline(lineNr),'s','S','')) 806 | endif 807 | " mark new seqs. 808 | if self.seq_cur != -1 809 | let index = self.seq2index[self.seq_cur] 810 | let lineNr = self.Index2Screen(index) 811 | call setline(lineNr,substitute(getline(lineNr), 812 | \'\zs \(\d\+\) \ze [sS ] ','>\1<','')) 813 | if a:move_cursor 814 | " move cursor to that line. 815 | call s:exec("normal! " . lineNr . "G") 816 | endif 817 | endif 818 | if self.seq_curhead != -1 819 | let index = self.seq2index[self.seq_curhead] 820 | let lineNr = self.Index2Screen(index) 821 | call setline(lineNr,substitute(getline(lineNr), 822 | \'\zs \(\d\+\) \ze [sS ] ','{\1}','')) 823 | endif 824 | if self.seq_newhead != -1 825 | let index = self.seq2index[self.seq_newhead] 826 | let lineNr = self.Index2Screen(index) 827 | call setline(lineNr,substitute(getline(lineNr), 828 | \'\zs \(\d\+\) \ze [sS ] ','[\1]','')) 829 | endif 830 | " mark diff marker 831 | if self.diffmark != -1 832 | let index = self.seq2index[self.diffmark] 833 | let lineNr = self.Index2Screen(index) 834 | call setline(lineNr, substitute(getline(lineNr), 835 | \ '\zs \(\d\+\) \ze [sS ]', '=\1=', '')) 836 | endif 837 | setlocal nomodifiable 838 | endfunction 839 | 840 | " tree node class 841 | let s:node = {} 842 | 843 | function! s:node.Init() abort 844 | let self.seq = -1 845 | let self.p = [] 846 | let self.time = -1 847 | endfunction 848 | 849 | function! s:undotree._parseNode(in,out) abort 850 | " type(in) == type([]) && type(out) == type({}) 851 | if empty(a:in) "empty 852 | return 853 | endif 854 | let curnode = a:out 855 | for i in a:in 856 | if has_key(i,'alt') 857 | call self._parseNode(i.alt,curnode) 858 | endif 859 | let newnode = s:new(s:node) 860 | let newnode.seq = i.seq 861 | let newnode.time = i.time 862 | if has_key(i,'newhead') 863 | let self.seq_newhead = i.seq 864 | endif 865 | if has_key(i,'curhead') 866 | let self.seq_curhead = i.seq 867 | let self.seq_cur = curnode.seq 868 | endif 869 | if has_key(i,'save') 870 | let self.seq_saved[i.save] = i.seq 871 | endif 872 | call extend(curnode.p,[newnode]) 873 | let curnode = newnode 874 | endfor 875 | endfunction 876 | 877 | "Sample: 878 | "let s:test={'seq_last': 4, 'entries': [{'seq': 3, 'alt': [{'seq': 1, 'time': 1345131443}, {'seq': 2, 'time': 1345131445}], 'time': 1345131490}, {'seq': 4, 'time': 1345131492, 'newhead': 1}], 'time_cur': 1345131493, 'save_last': 0, 'synced': 0, 'save_cur': 0, 'seq_cur': 4} 879 | 880 | " updatetree: 0: no update, just assign seqs; 1: update and assign seqs. 881 | function! s:undotree.ConvertInput(updatetree) abort 882 | "reset seqs 883 | let self.seq_cur_bak = self.seq_cur 884 | let self.seq_curhead_bak = self.seq_curhead 885 | let self.seq_newhead_bak = self.seq_newhead 886 | let self.save_last_bak = self.save_last 887 | 888 | let self.seq_cur = -1 889 | let self.seq_curhead = -1 890 | let self.seq_newhead = -1 891 | let self.seq_saved = {} 892 | 893 | "Generate root node 894 | let root = s:new(s:node) 895 | let root.seq = 0 896 | let root.time = 0 897 | 898 | call self._parseNode(self.rawtree.entries,root) 899 | 900 | let self.save_last = self.rawtree.save_last 901 | " Note: Normally, the current node should be the one that seq_cur points to, 902 | " but in fact it's not. May be bug, bug anyway I found a workaround: 903 | " first try to find the parent node of 'curhead', if not found, then use 904 | " seq_cur. 905 | if self.seq_cur == -1 906 | let self.seq_cur = self.rawtree.seq_cur 907 | endif 908 | " undo history is cleared 909 | if empty(self.rawtree.entries) 910 | let self.seq_cur = 0 911 | endif 912 | if a:updatetree 913 | let self.tree = root 914 | endif 915 | endfunction 916 | 917 | "================================================= 918 | " Ascii undo tree generator 919 | " 920 | " Example: 921 | " 6 8 7 922 | " |/ | 923 | " 2 4 924 | " \ | 925 | " 1 3 5 926 | " \ | / 927 | " 0 928 | 929 | " Tree sieve, p:fork, x:none 930 | " 931 | " x 8 932 | " 8x | 7 933 | " 87 \ \ 934 | " x87 6 | | 935 | " 687 |/ / 936 | " p7x | | 5 937 | " p75 | 4 | 938 | " p45 | 3 | 939 | " p35 | |/ 940 | " pp 2 | 941 | " 2p 1 | 942 | " 1p |/ 943 | " p 0 944 | " 0 945 | " 946 | " Data sample: 947 | "let example = {'seq':0,'p':[{'seq':1,'p':[{'seq':2,'p':[{'seq':6,'p':[]},{'seq':8,'p':[]}]}]},{'seq':3,'p':[{'seq':4,'p':[{'seq':7,'p':[]}]}]},{'seq':5,'p':[]}]} 948 | " 949 | " Convert self.tree -> self.asciitree 950 | function! s:undotree.Render() abort 951 | " We gonna modify self.tree so we'd better make a copy first. 952 | " Cannot make a copy because variable nested too deep, gosh.. okay, 953 | " fine.. 954 | " let tree = deepcopy(self.tree) 955 | let tree = self.tree 956 | let slots = [tree] 957 | let out = [] 958 | let outmeta = [] 959 | let seq2index = {} 960 | let TYPE_E = type({}) 961 | let TYPE_P = type([]) 962 | let TYPE_X = type('x') 963 | while slots != [] 964 | "find next node 965 | let foundx = 0 " 1 if x element is found. 966 | let index = 0 " Next element to be print. 967 | 968 | " Find x element first. 969 | for i in range(len(slots)) 970 | if type(slots[i]) == TYPE_X 971 | let foundx = 1 972 | let index = i 973 | break 974 | endif 975 | endfor 976 | 977 | " Then, find the element with minimum seq. 978 | let minseq = 99999999 979 | let minnode = {} 980 | if foundx == 0 981 | "assume undo level isn't more than this... of course 982 | for i in range(len(slots)) 983 | if type(slots[i]) == TYPE_E 984 | if slots[i].seq < minseq 985 | let minseq = slots[i].seq 986 | let index = i 987 | let minnode = slots[i] 988 | continue 989 | endif 990 | endif 991 | if type(slots[i]) == TYPE_P 992 | for j in slots[i] 993 | if j.seq < minseq 994 | let minseq = j.seq 995 | let index = i 996 | let minnode = j 997 | continue 998 | endif 999 | endfor 1000 | endif 1001 | endfor 1002 | endif 1003 | 1004 | " output. 1005 | let onespace = " " 1006 | let newline = onespace 1007 | let newmeta = {} 1008 | let node = slots[index] 1009 | if type(node) == TYPE_X 1010 | let newmeta = s:new(s:node) "invalid node. 1011 | if index+1 != len(slots) " not the last one, append '\' 1012 | for i in range(len(slots)) 1013 | if i < index 1014 | let newline = newline.g:undotree_TreeVertShape.' ' 1015 | endif 1016 | if i > index 1017 | let newline = newline.' '.g:undotree_TreeReturnShape 1018 | endif 1019 | endfor 1020 | endif 1021 | call remove(slots,index) 1022 | endif 1023 | if type(node) == TYPE_E 1024 | let newmeta = node 1025 | let seq2index[node.seq]=len(out) 1026 | for i in range(len(slots)) 1027 | if index == i 1028 | let newline = newline.g:undotree_TreeNodeShape.' ' 1029 | else 1030 | let newline = newline.g:undotree_TreeVertShape.' ' 1031 | endif 1032 | endfor 1033 | let newline = newline.' '.(node.seq).' '. 1034 | \'('.s:gettime(node.time).')' 1035 | " update the printed slot to its child. 1036 | if empty(node.p) 1037 | let slots[index] = 'x' 1038 | endif 1039 | if len(node.p) == 1 "only one child. 1040 | let slots[index] = node.p[0] 1041 | endif 1042 | if len(node.p) > 1 "insert p node 1043 | let slots[index] = node.p 1044 | endif 1045 | let node.p = [] "cut reference. 1046 | endif 1047 | if type(node) == TYPE_P 1048 | let newmeta = s:new(s:node) "invalid node. 1049 | for k in range(len(slots)) 1050 | if k < index 1051 | let newline = newline.g:undotree_TreeVertShape." " 1052 | endif 1053 | if k == index 1054 | let newline = newline.g:undotree_TreeVertShape.g:undotree_TreeSplitShape." " 1055 | endif 1056 | if k > index 1057 | let newline = newline.g:undotree_TreeSplitShape." " 1058 | endif 1059 | endfor 1060 | call remove(slots,index) 1061 | if len(node) == 2 1062 | if node[0].seq > node[1].seq 1063 | call insert(slots,node[1],index) 1064 | call insert(slots,node[0],index) 1065 | else 1066 | call insert(slots,node[0],index) 1067 | call insert(slots,node[1],index) 1068 | endif 1069 | endif 1070 | " split P to E+P if elements in p > 2 1071 | if len(node) > 2 1072 | call remove(node,index(node,minnode)) 1073 | call insert(slots,minnode,index) 1074 | call insert(slots,node,index) 1075 | endif 1076 | endif 1077 | unlet node 1078 | if newline != onespace 1079 | let newline = substitute(newline,'\s*$','','g') "remove trailing space. 1080 | call insert(out,newline,0) 1081 | call insert(outmeta,newmeta,0) 1082 | endif 1083 | endwhile 1084 | let self.asciitree = out 1085 | let self.asciimeta = outmeta 1086 | " revert index. 1087 | let totallen = len(out) 1088 | for i in keys(seq2index) 1089 | let seq2index[i] = totallen - 1 - seq2index[i] 1090 | endfor 1091 | let self.seq2index = seq2index 1092 | endfunction 1093 | 1094 | "================================================= 1095 | "diff panel 1096 | let s:diffpanel = s:new(s:panel) 1097 | 1098 | function! s:diffpanel.Update(seq,targetBufnr,targetid,diffmark) abort 1099 | call s:log('diffpanel.Update(),seq:'.a:seq.' bufname:'.bufname(a:targetBufnr).' diffmark:'.a:diffmark) 1100 | if !self.diffexecutable 1101 | return 1102 | endif 1103 | let diffresult = [] 1104 | let self.changes.add = 0 1105 | let self.changes.del = 0 1106 | 1107 | if a:seq == 0 1108 | let diffresult = [] 1109 | else 1110 | if has_key(self.cache,a:targetBufnr.'_'.a:seq.'_'.a:diffmark) 1111 | call s:log("diff cache hit.") 1112 | let diffresult = self.cache[a:targetBufnr.'_'.a:seq.'_'.a:diffmark] 1113 | else 1114 | " Double check the target winnr and bufnr 1115 | let targetWinnr = -1 1116 | for winnr in range(1, winnr('$')) "winnr starts from 1 1117 | if (getwinvar(winnr,'undotree_id') == a:targetid) 1118 | \&& winbufnr(winnr) == a:targetBufnr 1119 | let targetWinnr = winnr 1120 | endif 1121 | endfor 1122 | if targetWinnr == -1 1123 | return 1124 | endif 1125 | 1126 | let ei_bak = &eventignore 1127 | set eventignore=all 1128 | 1129 | call s:exec_silent(targetWinnr." wincmd w") 1130 | 1131 | " remember and restore cursor and window position. 1132 | let savedview = winsaveview() 1133 | 1134 | let new = [] 1135 | let old = [] 1136 | let diff_dist = 1 1137 | 1138 | if a:diffmark != -1 1139 | let diff_dist = a:seq - a:diffmark 1140 | if diff_dist > 0 1141 | let new = getbufline(a:targetBufnr,'^','$') 1142 | execute 'silent earlier ' . diff_dist 1143 | let old = getbufline(a:targetBufnr,'^','$') 1144 | execute 'silent later ' . diff_dist 1145 | else 1146 | let old = getbufline(a:targetBufnr,'^','$') 1147 | execute 'silent later ' . (-diff_dist) 1148 | let new = getbufline(a:targetBufnr,'^','$') 1149 | execute 'silent earlier ' . (-diff_dist) 1150 | endif 1151 | else 1152 | let new = getbufline(a:targetBufnr,'^','$') 1153 | silent undo 1154 | let old = getbufline(a:targetBufnr,'^','$') 1155 | silent redo 1156 | endif 1157 | 1158 | call winrestview(savedview) 1159 | 1160 | " diff files. 1161 | let tempfile1 = tempname() 1162 | let tempfile2 = tempname() 1163 | if writefile(old,tempfile1) == -1 1164 | echoerr "Can not write to temp file:".tempfile1 1165 | endif 1166 | if writefile(new,tempfile2) == -1 1167 | echoerr "Can not write to temp file:".tempfile2 1168 | endif 1169 | let diffresult = split(system(g:undotree_DiffCommand.' '.tempfile1.' '.tempfile2),"\n") 1170 | call s:log("diffresult: ".string(diffresult)) 1171 | if delete(tempfile1) != 0 1172 | echoerr "Can not delete temp file:".tempfile1 1173 | endif 1174 | if delete(tempfile2) != 0 1175 | echoerr "Can not delete temp file:".tempfile2 1176 | endif 1177 | let &eventignore = ei_bak 1178 | "Update cache 1179 | let self.cache[a:targetBufnr.'_'.a:seq.'_'.a:diffmark] = diffresult 1180 | endif 1181 | endif 1182 | 1183 | call self.ParseDiff(diffresult, a:targetBufnr) 1184 | 1185 | call self.SetFocus() 1186 | 1187 | setlocal modifiable 1188 | call s:exec('1,$ d _') 1189 | 1190 | call append(0,diffresult) 1191 | if a:diffmark == -1 || a:seq == a:diffmark 1192 | call append(0,'+ seq: '.a:seq.' +') 1193 | elseif a:seq > a:diffmark 1194 | call append(0,'+ seq: '.a:seq.' +') 1195 | call append(0,'- seq: '.a:diffmark.' -') 1196 | else 1197 | call append(0,'+ seq: '.a:diffmark.' +') 1198 | call append(0,'- seq: '.a:seq.' -') 1199 | endif 1200 | 1201 | "remove the last empty line 1202 | if getline("$") == "" 1203 | call s:exec('$d _') 1204 | endif 1205 | call s:exec('norm! gg') "move cursor to line 1. 1206 | setlocal nomodifiable 1207 | call t:undotree.SetFocus() 1208 | endfunction 1209 | 1210 | function! s:diffpanel.ParseDiff(diffresult, targetBufnr) abort 1211 | " set target focus first. 1212 | call t:undotree.SetTargetFocus() 1213 | 1214 | " If 'a:diffresult' is empty then there are no new signs to place. However, 1215 | " we need to ensure any old signs are removed. This is especially important 1216 | " if we are at the very first sequence, otherwise signs get left 1217 | if (exists("w:undotree_diffsigns")) 1218 | while w:undotree_diffsigns > 0 1219 | exe 'sign unplace '.s:signId 1220 | let w:undotree_diffsigns -= 1 1221 | endwhile 1222 | endif 1223 | 1224 | if empty(a:diffresult) 1225 | return 1226 | endif 1227 | 1228 | " clear previous highlighted syntax 1229 | " matchadd associates with windows. 1230 | if exists("w:undotree_diffmatches") 1231 | for i in w:undotree_diffmatches 1232 | silent! call matchdelete(i) 1233 | endfor 1234 | endif 1235 | 1236 | let w:undotree_diffmatches = [] 1237 | let w:undotree_diffsigns = 0 1238 | let lineNr = 0 1239 | let l:lastLine = line('$') 1240 | for line in a:diffresult 1241 | let matchnum = matchstr(line,'^[0-9,\,]*[acd]\zs\d*\ze') 1242 | if !empty(matchnum) 1243 | let lineNr = str2nr(matchnum) 1244 | let matchwhat = matchstr(line,'^[0-9,\,]*\zs[acd]\ze\d*') 1245 | if matchwhat ==# 'd' 1246 | if g:undotree_HighlightChangedWithSign 1247 | " Normally, for a 'delete' change, the line number we have is always 1 less than the line we 1248 | " need to place the sign at, hence '+ 1' 1249 | " However, if the very end of the buffer has been deleted then this is not possible (because 1250 | " that bit of the buffer no longer exists), so we place a 'special' version of the 'delete' 1251 | " sign on what is the last available line) 1252 | exe 'sign place '.s:signId.' line='.((lineNr < l:lastLine) ? lineNr + 1 : l:lastLine).' name='.((lineNr < l:lastLine) ? 'UndotreeDel' : 'UndotreeDelEnd').' buffer='.a:targetBufnr 1253 | let w:undotree_diffsigns += 1 1254 | endif 1255 | 1256 | let matchnum = 0 1257 | let matchwhat = '' 1258 | endif 1259 | continue 1260 | endif 1261 | if matchstr(line,'^<.*$') != '' 1262 | let self.changes.del += 1 1263 | endif 1264 | 1265 | let matchtext = matchstr(line,'^>\zs .*$') 1266 | if empty(matchtext) 1267 | continue 1268 | endif 1269 | 1270 | let self.changes.add += 1 1271 | if g:undotree_HighlightChangedText 1272 | if matchtext != ' ' 1273 | let matchtext = '\%'.lineNr.'l\V'.escape(matchtext[1:],'"\') "remove beginning space. 1274 | call s:log("matchadd(".matchwhat.") -> ".matchtext) 1275 | call add(w:undotree_diffmatches,matchadd((matchwhat ==# 'a' ? g:undotree_HighlightSyntaxAdd : g:undotree_HighlightSyntaxChange),matchtext)) 1276 | endif 1277 | endif 1278 | 1279 | if g:undotree_HighlightChangedWithSign 1280 | exe 'sign place '.s:signId.' line='.lineNr.' name='.(matchwhat ==# 'a' ? 'UndotreeAdd' : 'UndotreeChg').' buffer='.a:targetBufnr 1281 | let w:undotree_diffsigns += 1 1282 | endif 1283 | 1284 | let lineNr = lineNr+1 1285 | endfor 1286 | endfunction 1287 | 1288 | function! s:diffpanel.GetStatusLine() abort 1289 | let max = winwidth(0) - 4 1290 | let sum = self.changes.add + self.changes.del 1291 | if sum > max 1292 | let add = self.changes.add * max / sum + 1 1293 | let del = self.changes.del * max / sum + 1 1294 | else 1295 | let add = self.changes.add 1296 | let del = self.changes.del 1297 | endif 1298 | return string(sum).' '.repeat('+',add).repeat('-',del) 1299 | endfunction 1300 | 1301 | function! s:diffpanel.Init() abort 1302 | let self.bufname = "diffpanel_".s:getUniqueID() 1303 | let self.cache = {} 1304 | let self.changes = {'add':0, 'del':0} 1305 | let self.diffexecutable = executable(g:undotree_DiffCommand) 1306 | if !self.diffexecutable 1307 | " If the command contains parameters, strip out the executable itself 1308 | let cmd = matchstr(g:undotree_DiffCommand.' ', '.\{-}\ze\s.*') 1309 | let self.diffexecutable = executable(cmd) 1310 | if !self.diffexecutable 1311 | echoerr '"'.cmd.'" is not executable.' 1312 | endif 1313 | endif 1314 | endfunction 1315 | 1316 | function! s:diffpanel.Toggle() abort 1317 | call s:log(self.bufname." Toggle()") 1318 | if self.IsVisible() 1319 | call self.Hide() 1320 | else 1321 | call self.Show() 1322 | endif 1323 | endfunction 1324 | 1325 | function! s:diffpanel.Show() abort 1326 | call s:log("diffpanel.Show()") 1327 | if self.IsVisible() 1328 | return 1329 | endif 1330 | " Create diffpanel window. 1331 | call t:undotree.SetFocus() "can not exist without undotree 1332 | " remember and restore cursor and window position. 1333 | let savedview = winsaveview() 1334 | 1335 | let ei_bak= &eventignore 1336 | set eventignore=all 1337 | 1338 | if exists("g:undotree_CustomDiffpanelCmd") 1339 | let cmd = g:undotree_CustomDiffpanelCmd.' '.self.bufname 1340 | elseif g:undotree_WindowLayout == 1 || g:undotree_WindowLayout == 3 1341 | let cmd = 'belowright '.g:undotree_DiffpanelHeight.'new '.self.bufname 1342 | else 1343 | let cmd = 'botright '.g:undotree_DiffpanelHeight.'new '.self.bufname 1344 | endif 1345 | call s:exec_silent(cmd) 1346 | 1347 | let b:isUndotreeBuffer = 1 1348 | 1349 | setlocal winfixwidth 1350 | setlocal winfixheight 1351 | setlocal noswapfile 1352 | setlocal buftype=nowrite 1353 | setlocal bufhidden=delete 1354 | setlocal nowrap 1355 | setlocal nolist 1356 | setlocal nobuflisted 1357 | setlocal nospell 1358 | setlocal nonumber 1359 | setlocal norelativenumber 1360 | setlocal nocursorline 1361 | setlocal nomodifiable 1362 | setlocal statusline=%!t:diffpanel.GetStatusLine() 1363 | 1364 | let &eventignore = ei_bak 1365 | 1366 | " syntax need filetype autocommand 1367 | setfiletype diff 1368 | setlocal foldcolumn=0 1369 | setlocal nofoldenable 1370 | 1371 | call self.BindAu() 1372 | call t:undotree.SetFocus() 1373 | call winrestview(savedview) 1374 | endfunction 1375 | 1376 | function! s:diffpanel.BindAu() abort 1377 | " Auto exit if it's the last window or undotree closed. 1378 | augroup Undotree_Diff 1379 | au! 1380 | au BufEnter call s:exitIfLast() 1381 | au BufEnter if !t:undotree.IsVisible() 1382 | \|call t:diffpanel.Hide() |endif 1383 | augroup end 1384 | endfunction 1385 | 1386 | function! s:diffpanel.CleanUpHighlight() abort 1387 | call s:log("CleanUpHighlight()") 1388 | " save current position 1389 | let curwinnr = winnr() 1390 | let savedview = winsaveview() 1391 | 1392 | " clear w:undotree_diffmatches in all windows. 1393 | let winnum = winnr('$') 1394 | for i in range(1,winnum) 1395 | call s:exec_silent("norm! ".i."\\") 1396 | if exists("w:undotree_diffmatches") 1397 | for j in w:undotree_diffmatches 1398 | silent! call matchdelete(j) 1399 | endfor 1400 | let w:undotree_diffmatches = [] 1401 | endif 1402 | if (exists("w:undotree_diffsigns")) 1403 | while w:undotree_diffsigns > 0 1404 | exe 'sign unplace '.s:signId 1405 | let w:undotree_diffsigns -= 1 1406 | endwhile 1407 | endif 1408 | endfor 1409 | 1410 | "restore position 1411 | call s:exec_silent("norm! ".curwinnr."\\") 1412 | call winrestview(savedview) 1413 | endfunction 1414 | 1415 | function! s:diffpanel.Hide() abort 1416 | call s:log(self.bufname." Hide()") 1417 | if !self.IsVisible() 1418 | return 1419 | endif 1420 | call self.SetFocus() 1421 | call s:exec("quit") 1422 | call self.CleanUpHighlight() 1423 | endfunction 1424 | 1425 | "================================================= 1426 | " It will set the target of undotree window to the current editing buffer. 1427 | function! s:undotreeAction(action) abort 1428 | call s:log("undotreeAction()") 1429 | if !exists('t:undotree') 1430 | echoerr "Fatal: t:undotree does not exist!" 1431 | return 1432 | endif 1433 | call t:undotree.Action(a:action) 1434 | endfunction 1435 | 1436 | function! s:exitIfLast() abort 1437 | let num = 0 1438 | if exists('t:undotree') && t:undotree.IsVisible() 1439 | let num = num + 1 1440 | endif 1441 | if exists('t:diffpanel') && t:diffpanel.IsVisible() 1442 | let num = num + 1 1443 | endif 1444 | if winnr('$') == num 1445 | if exists('t:undotree') 1446 | call t:undotree.Hide() 1447 | endif 1448 | if exists('t:diffpanel') 1449 | call t:diffpanel.Hide() 1450 | endif 1451 | endif 1452 | endfunction 1453 | 1454 | "================================================= 1455 | " User command functions 1456 | "called outside undotree window 1457 | function! undotree#UndotreeUpdate() abort 1458 | if !exists('t:undotree') 1459 | return 1460 | endif 1461 | if !exists('w:undotree_id') 1462 | let w:undotree_id = 'id_'.s:getUniqueID() 1463 | call s:log("Unique window id assigned: ".w:undotree_id) 1464 | endif 1465 | " assume window layout won't change during updating. 1466 | let thiswinnr = winnr() 1467 | call t:undotree.Update() 1468 | " focus moved 1469 | if winnr() != thiswinnr 1470 | call s:exec("norm! ".thiswinnr."\\") 1471 | endif 1472 | endfunction 1473 | 1474 | function! undotree#UndotreeToggle() abort 1475 | try 1476 | call s:log(">>> UndotreeToggle()") 1477 | if !exists('w:undotree_id') 1478 | let w:undotree_id = 'id_'.s:getUniqueID() 1479 | call s:log("Unique window id assigned: ".w:undotree_id) 1480 | endif 1481 | if !exists('t:undotree') 1482 | let t:undotree = s:new(s:undotree) 1483 | endif 1484 | if !exists('t:diffpanel') 1485 | let t:diffpanel = s:new(s:diffpanel) 1486 | endif 1487 | call t:undotree.Toggle() 1488 | call s:log("<<< UndotreeToggle() leave") 1489 | catch /^Vim\%((\a\+)\)\?:E11/ 1490 | echohl ErrorMsg 1491 | echom v:exception 1492 | echohl NONE 1493 | endtry 1494 | endfunction 1495 | 1496 | function! undotree#UndotreeIsVisible() abort 1497 | return (exists('t:undotree') && t:undotree.IsVisible()) 1498 | endfunction 1499 | 1500 | function! undotree#UndotreeHide() abort 1501 | if undotree#UndotreeIsVisible() 1502 | try 1503 | call undotree#UndotreeToggle() 1504 | catch /^Vim\%((\a\+)\)\?:E11/ 1505 | echohl ErrorMsg 1506 | echom v:exception 1507 | echohl NONE 1508 | endtry 1509 | endif 1510 | endfunction 1511 | 1512 | function! undotree#UndotreeShow() abort 1513 | try 1514 | if ! undotree#UndotreeIsVisible() 1515 | call undotree#UndotreeToggle() 1516 | else 1517 | call t:undotree.SetFocus() 1518 | endif 1519 | catch /^Vim\%((\a\+)\)\?:E11/ 1520 | echohl ErrorMsg 1521 | echom v:exception 1522 | echohl NONE 1523 | endtry 1524 | endfunction 1525 | 1526 | function! undotree#UndotreeFocus() abort 1527 | if undotree#UndotreeIsVisible() 1528 | try 1529 | call t:undotree.SetFocus() 1530 | catch /^Vim\%((\a\+)\)\?:E11/ 1531 | echohl ErrorMsg 1532 | echom v:exception 1533 | echohl NONE 1534 | endtry 1535 | endif 1536 | endfunction 1537 | 1538 | function! undotree#UndotreePersistUndo(goSetUndofile) abort 1539 | call s:log("undotree#UndotreePersistUndo(" . a:goSetUndofile . ")") 1540 | if ! &undofile 1541 | if !isdirectory(g:undotree_UndoDir) 1542 | call mkdir(g:undotree_UndoDir, 'p', 0700) 1543 | call s:log(" > [Dir " . g:undotree_UndoDir . "] created.") 1544 | endif 1545 | exe "set undodir=" . fnameescape(g:undotree_UndoDir) 1546 | call s:log(" > [set undodir=" . g:undotree_UndoDir . "] executed.") 1547 | if filereadable(undofile(expand('%'))) || a:goSetUndofile 1548 | setlocal undofile 1549 | call s:log(" > [setlocal undofile] executed") 1550 | endif 1551 | if a:goSetUndofile 1552 | silent! write 1553 | echo "A persistence undo file has been created." 1554 | endif 1555 | else 1556 | call s:log(" > Undofile has been set. Do nothing.") 1557 | endif 1558 | endfunction 1559 | 1560 | " vim: set et fdm=marker sts=4 sw=4: 1561 | -------------------------------------------------------------------------------- /doc/_static/undotree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbbill/undotree/b951b87b46c34356d44aa71886aecf9dd7f5788a/doc/_static/undotree.png -------------------------------------------------------------------------------- /doc/undotree.txt: -------------------------------------------------------------------------------- 1 | *undotree.txt* The undo history visualizer for VIM 2 | 3 | Author: Ming Bai 4 | Licence: BSD 5 | Homepage: https://github.com/mbbill/undotree/ 6 | 7 | ============================================================================== 8 | CONTENTS *undotree-contents* 9 | 10 | 1. Intro ................................ |undotree-intro| 11 | 2. Usage ................................ |undotree-usage| 12 | 3. Configuration ........................ |undotree-config| 13 | 3.1 undotree_WindowLayout .......... |undotree_WindowLayout| 14 | 3.2 undotree_CustomUndotreeCmd...... |undotree_CustomUndotreeCmd| 15 | undotree_CustomDiffpanelCmd..... |undotree_CustomDiffpanelCmd| 16 | 3.3 undotree_SplitWidth ............ |undotree_SplitWidth| 17 | 3.4 undotree_DiffpanelHeight ....... |undotree_DiffpanelHeight| 18 | 3.5 undotree_DiffAutoOpen .......... |undotree_DiffAutoOpen| 19 | 3.6 undotree_SetFocusWhenToggle .... |undotree_SetFocusWhenToggle| 20 | 3.7 undotree_TreeNodeShape ......... |undotree_TreeNodeShape| 21 | undotree_TreeVertShape ......... |undotree_TreeVertShape| 22 | undotree_TreeSplitShape ........ |undotree_TreeSplitShape| 23 | undotree_TreeReturnShape ....... |undotree_TreeReturnShape| 24 | 3.8 undotree_DiffCommand ........... |undotree_DiffCommand| 25 | 3.9 undotree_RelativeTimestamp ..... |undotree_RelativeTimestamp| 26 | 3.10 undotree_ShortIndicators ....... |undotree_ShortIndicators| 27 | 3.11 undotree_HighlightChangedText .. |undotree_HighlightChangedText| 28 | 3.12 undotree_HighlightSyntaxAdd .... |undotree_HighlightSyntaxAdd| 29 | undotree_HighlightSyntaxChange . |undotree_HighlightSyntaxChange| 30 | 3.13 Undotree_CustomMap ............. |Undotree_CustomMap| 31 | 3.14 undotree_HelpLine .............. |undotree_HelpLine| 32 | 3.15 undotree_CursorLine ............ |undotree_CursorLine| 33 | 3.16 undotree_UndoDir................ |undotree_UndoDir| 34 | 4. Bugs ................................. |undotree-bugs| 35 | 5. Changelog ............................ |undotree-changelog| 36 | 6. License .............................. |undotree-license| 37 | 38 | ============================================================================== 39 | 1. Intro *undotree-intro* 40 | 41 | The plug-in visualizes undo history and makes it easier to browse and switch 42 | between different undo branches. You might wonder what are undo "branches"? 43 | It's vim feature that allows you to go back to a state when it is overwritten 44 | by a latest edit. For most editors, if you make a change A, then B, then go 45 | back to A and make change C, normally you won't be able to go back to B 46 | because undo history is linear. That's not the case for Vim because it 47 | internally keeps all the edit history like a tree structure, and this plug-in 48 | exposes the tree to you so that you not only can switch back and forth but 49 | also can switch between branches. 50 | 51 | Some people have questions about file contents being changed when switching 52 | between undo history states. Don't worry, undotree will NEVER save your data 53 | or write to disk. All it does is to change the current buffer little bit, just 54 | like those auto-completion plug-ins do. It just adds or removes something in 55 | the buffer temporarily, and if you don't like you can always go back to the 56 | last state easily. Let's say, you made some change but didn't save, then you 57 | use undotree and go back to an arbitrary version, your unsaved change doesn't 58 | get lost - it stores in the latest undo history node. Clicking that node on 59 | undotree will bring you back instantly. Play with undo/redo on other editors 60 | is always dangerous because when you step back and accidentally typed 61 | something, boom! You lose your edits. But don't worry, that won't happen in 62 | Vim. Then you might ask what if I make some changes without saving and switch 63 | back to an old version and then exit? Well, imagine what would happen if you 64 | don't have undotree? You lose your latest edits and the file on disk is your 65 | last saved version. This behaviour remains the same with undotree. So, if you 66 | saved, you won't lose anything. 67 | 68 | We all know that usually undo/redo is only for the current edit session. It's 69 | stored in memory and once the process exits, the undo history is lost. 70 | Although undotree makes switching between history states easier, it doesn't do 71 | more than that. Sometimes it would be much safer or more convenient to keep 72 | the undo history across edit sessions. In this case you might need to enable a 73 | Vim feature called persistent undo. Let me explain how persistent undo works: 74 | instead of keeping undo history in RAM, persistent undo keeps undo history in 75 | file. Let's say you make a change A, then B, then go back to A and make change 76 | C, then you save the file. Now Vim save the file with content state C, and in 77 | the mean time it saves the entire undo history to a file including state A, B 78 | and C. Next time when you open the file, Vim will also restore undo history. 79 | So you can still go back to B. The history file is incremental, and every 80 | change will be recorded permanently, kind of like Git. You might think that's 81 | too much, well, undotree does provide a way to clean them up. If you need to 82 | enable persistent undo, type :h persistent-undo or follow the instructions 83 | below. 84 | 85 | Undotree is written in pure Vim script and doesn't rely on any third party 86 | tools. It's lightweight, simple and fast. It only does what it supposed to do, 87 | and it only runs when you need it. 88 | 89 | ============================================================================== 90 | 2. Usage *undotree-usage* 91 | 92 | Use :UndotreeToggle to toggle the undo-tree panel. You may want to map this 93 | command to whatever hotkey by adding the following line to your vimrc, take F5 94 | for example. 95 | > 96 | nnoremap :UndotreeToggle 97 | < 98 | Markers 99 | 100 | * Every change has a sequence number and it is displayed before timestamps. 101 | 102 | * The current state is marked as > number <. 103 | 104 | * The next state which will be restored by :redo or is marked as 105 | { number }. 106 | 107 | * The [ number ] marks the most recent change. 108 | 109 | * The undo history is sorted by timestamps. 110 | 111 | * Saved changes are marked as s and the big S indicates the most recent 112 | saved change. 113 | 114 | * The = number = marks user chosen change. When this mark is set the 115 | diff panel displays diff between current seq and marked seq. 116 | 117 | * Press ? in undotree window for quick help. 118 | 119 | Persistent undo 120 | 121 | Usually I would like to store the undo files in a separate place like below. 122 | > 123 | if has("persistent_undo") 124 | let target_path = expand('~/.undodir') 125 | 126 | " create the directory and any parent directories 127 | " if the location does not exist. 128 | if !isdirectory(target_path) 129 | call mkdir(target_path, "p", 0700) 130 | endif 131 | 132 | let &undodir=target_path 133 | set undofile 134 | endif 135 | < 136 | Alternatively, the persistent undofile can be activated by using the 137 | :UndotreePersistUndo command. This will enable the undofile for the current 138 | buffer only, as undotree utilizes the setlocal undofile function. Once this 139 | command is executed and the persistence undofile is created, the "setlocal 140 | undofile" function will automatically be executed the next time the file is 141 | reopened. 142 | 143 | See |undotree_UndoDir| 144 | 145 | You may want to map the command to whatever hotkey by adding this following 146 | line to your vimrc, for instance: 147 | 148 | `nnoremap :UndotreePersistUndo` 149 | 150 | ============================================================================== 151 | 3. Configuration *undotree-config* 152 | 153 | ------------------------------------------------------------------------------ 154 | 3.1 g:undotree_WindowLayout *undotree_WindowLayout* 155 | 156 | Set the undotree window layout. 157 | 158 | Style 1 159 | > 160 | +----------+------------------------+ 161 | | | | 162 | | | | 163 | | undotree | | 164 | | | | 165 | | | | 166 | +----------+ | 167 | | | | 168 | | diff | | 169 | | | | 170 | +----------+------------------------+ 171 | < 172 | Style 2 173 | > 174 | +----------+------------------------+ 175 | | | | 176 | | | | 177 | | undotree | | 178 | | | | 179 | | | | 180 | +----------+------------------------+ 181 | | | 182 | | diff | 183 | | | 184 | +-----------------------------------+ 185 | < 186 | Style 3 187 | > 188 | +------------------------+----------+ 189 | | | | 190 | | | | 191 | | | undotree | 192 | | | | 193 | | | | 194 | | +----------+ 195 | | | | 196 | | | diff | 197 | | | | 198 | +------------------------+----------+ 199 | < 200 | Style 4 201 | > 202 | +------------------------+----------+ 203 | | | | 204 | | | | 205 | | | undotree | 206 | | | | 207 | | | | 208 | +------------------------+----------+ 209 | | | 210 | | diff | 211 | | | 212 | +-----------------------------------+ 213 | < 214 | Default: 1 215 | 216 | ------------------------------------------------------------------------------ 217 | 3.2 g:undotree_CustomUndotreeCmd *undotree_CustomUndotreeCmd* 218 | g:undotree_CustomDiffpanelCmd *undotree_CustomDiffpanelCmd* 219 | 220 | Set up custom window layout. 221 | 222 | Setting |undotree_CustomUndotreeCmd| will ignore |undotree_SplitWidth|, and 223 | setting |undotree_CustomDiffpanelCmd| will ignore |undotree_DiffpanelHeight|. 224 | 225 | An |undotree_CustomUndotreeCmd| will always open the undotree window relative 226 | to the tracked window and |undotree_CustomDiffpanelCmd| will always open the 227 | diffpanel relative to the undotree window. 228 | 229 | Useful when 230 | 231 | * absolute positioning commands (|topleft|, |botright|) don't play well 232 | with other plugins 233 | 234 | * you have a preferred split window layout and would like to use 235 | UndoTree relative to one specific window only 236 | 237 | Examples: 238 | 239 | * To recreate Style 1: 240 | > 241 | let g:undotree_CustomUndotreeCmd = 'topleft vertical 30 new' 242 | let g:undotree_CustomDiffpanelCmd = 'belowright 10 new' 243 | < 244 | * To recreate Style 2: 245 | > 246 | let g:undotree_CustomUndotreeCmd = 'topleft vertical 30 new' 247 | let g:undotree_CustomDiffpanelCmd = 'botright 10 new' 248 | < 249 | * A custom layout example: 250 | > 251 | +------------------------+----------+ 252 | | | | 253 | | | w | 254 | | | i | 255 | | | n | 256 | | window_1 | d | 257 | | | o | 258 | | | w | 259 | | | | | 260 | | | 2 | 261 | | | | 262 | +------------------------+----------+ 263 | | | 264 | | window_3 | 265 | | | 266 | +-----------------------------------+ 267 | < 268 | Using the following setup wouldn't mess up the current layout as it 269 | does not use absolute positioning: 270 | > 271 | let g:undotree_CustomUndotreeCmd = 'vertical 32 new' 272 | let g:undotree_CustomDiffpanelCmd= 'belowright 12 new' 273 | < 274 | Issuing :UndotreeToggle now in window_1 would result in: 275 | > 276 | +--------+---------------+----------+ 277 | | | | | 278 | | u | | w | 279 | | n | | i | 280 | | d | | n | 281 | | o | window_1 | d | 282 | | | | o | 283 | +--------+ | w | 284 | | | | | | 285 | | diff | | 2 | 286 | | | | | 287 | +--------+---------------+----------+ 288 | | | 289 | | window_3 | 290 | | | 291 | +-----------------------------------+ 292 | < 293 | Executing :UndotreeToggle again would turn off UndoTree (independently 294 | of which window was active at the time). Moving between window1, window_2 295 | and window_3 would result in showing the respective window's changelog 296 | in the undotree panel. 297 | 298 | CAVEAT: To avoid the Vim's default behaviour of equalizing window sizes 299 | when closing a window, set the 'noequalalways' option. 300 | 301 | ------------------------------------------------------------------------------ 302 | 3.3 g:undotree_SplitWidth *undotree_SplitWidth* 303 | 304 | Set the undotree window width. 305 | 306 | Default: 30 307 | 308 | ------------------------------------------------------------------------------ 309 | 3.4 g:undotree_DiffpanelHeight *undotree_DiffpanelHeight* 310 | 311 | Set the diff window height. 312 | 313 | Default: 10 314 | 315 | ------------------------------------------------------------------------------ 316 | 3.5 g:undotree_DiffAutoOpen *undotree_DiffAutoOpen* 317 | 318 | Set this to 1 to auto open the diff window. 319 | 320 | Default: 1 321 | 322 | ------------------------------------------------------------------------------ 323 | 3.6 g:undotree_SetFocusWhenToggle *undotree_SetFocusWhenToggle* 324 | 325 | If set to 1, the undotree window will get focus after being opened, otherwise 326 | focus will stay in current window. 327 | 328 | Default: 0 329 | 330 | ------------------------------------------------------------------------------ 331 | 3.7 g:undotree_TreeNodeShape *undotree_TreeNodeShape* 332 | g:undotree_TreeVertShape *undotree_TreeVertShape* 333 | g:undotree_TreeSplitShape *undotree_TreeSplitShape* 334 | g:undotree_TreeReturnShape *undotree_TreeReturnShape* 335 | 336 | Set the characters used to draw the tree. Although you can put any character 337 | you want in these shape variables, the only Unicode box drawing characters 338 | that work well are these three. Make sure your font will render them; you can 339 | easily see if that's the case in the last column of this table. 340 | 341 | Variable | Default | Unicode Box Character 342 | ---------------------------+---------------+----------------------- 343 | g:undotree_TreeNodeShape | * | 344 | g:undotree_TreeReturnShape | \ (backslash) | ╲ (U+2572) 345 | g:undotree_TreeVertShape | | (pipe) | │ (U+2502) 346 | g:undotree_TreeSplitShape | / (slash) | ╱ (U+2571) 347 | 348 | ------------------------------------------------------------------------------ 349 | 3.8 g:undotree_DiffCommand *undotree_DiffCommand* 350 | 351 | Set the command used to get the diff output. 352 | 353 | Default: "diff" 354 | 355 | ------------------------------------------------------------------------------ 356 | 3.9 g:undotree_RelativeTimestamp *undotree_RelativeTimestamp* 357 | 358 | Set to 1 to use relative timestamp. 359 | 360 | Default: 1 361 | 362 | ------------------------------------------------------------------------------ 363 | 3.10 g:undotree_ShortIndicators *undotree_ShortIndicators* 364 | 365 | Set to 1 to get short timestamps when |undotree_RelativeTimestamp| is also 366 | enabled: 367 | > 368 | Before | After 369 | =========================== 370 | (5 seconds ago) | (5 s) 371 | ----------------|---------- 372 | (1 minute ago) | (1 m) 373 | ----------------|---------- 374 | (2 minutes ago) | (2 m) 375 | ----------------|---------- 376 | (1 hour ago) | (1 h) 377 | ----------------|---------- 378 | (Original) | (Orig) 379 | < 380 | Default: 0 381 | 382 | ------------------------------------------------------------------------------ 383 | 3.11 g:undotree_HighlightChangedText *undotree_HighlightChangedText* 384 | 385 | Set to 1 to highlight the changed text. 386 | 387 | Default: 1 388 | 389 | ------------------------------------------------------------------------------ 390 | 3.12 g:undotree_HighlightSyntaxAdd *undotree_HighlightSyntaxAdd* 391 | g:undotree_HighlightSyntaxDel *undotree_HighlightSyntaxDel* 392 | g:undotree_HighlightSyntaxChange *undotree_HighlightSyntaxChange* 393 | 394 | Set the highlight linked syntax type. 395 | You may chose your favorite through ":hi" command. 396 | 397 | Default: "DiffAdd", "DiffDelete" and "DiffChange" 398 | 399 | ------------------------------------------------------------------------------ 400 | 3.13 g:Undotree_CustomMap *Undotree_CustomMap* 401 | 402 | There are two ways of changing the default key mappings: 403 | The first way is to define global mappings as the following example: 404 | > 405 | nmap J UndotreeNextState 406 | nmap K UndotreePreviousState 407 | < 408 | A better approach is to define the callback function g:Undotree_CustomMap(). 409 | The function will be called after the undotree windows is initialized, so the 410 | key mappings only works on the undotree windows. 411 | > 412 | function g:Undotree_CustomMap() 413 | nmap J UndotreeNextState 414 | nmap K UndotreePreviousState 415 | endfunc 416 | < 417 | List of the commands available for redefinition. 418 | > 419 | UndotreeHelp 420 | UndotreeClose 421 | UndotreeFocusTarget 422 | UndotreeClearHistory 423 | UndotreeTimestampToggle 424 | UndotreeDiffToggle 425 | UndotreeNextState 426 | UndotreePreviousState 427 | UndotreeNextSavedState 428 | UndotreePreviousSavedState 429 | UndotreeRedo 430 | UndotreeUndo 431 | UndotreeEnter 432 | UndotreeDiffMark 433 | UndotreeClearDiffMark 434 | < 435 | 436 | ------------------------------------------------------------------------------ 437 | 3.14 g:undotree_HelpLine *undotree_HelpLine* 438 | 439 | Set to 0 to hide "Press ? for help". 440 | 441 | Default: 1 442 | 443 | ------------------------------------------------------------------------------ 444 | 3.15 g:undotree_CursorLine 445 | 446 | Set to 0 to disable cursorline. 447 | 448 | Default: 1 449 | 450 | ------------------------------------------------------------------------------ 451 | 3.16 g:undotree_UndoDir *undotree_UndoDir* 452 | 453 | Set the path for the persistence undo directory. Due to the differing formats 454 | of the persistence undo files between nvim and vim, the default undodir for 455 | both has been intentionally given different paths. This ensures that users who 456 | use both nvim and vim do not accidentally lose their persistence undo files 457 | due to the different ways in which vim and nvim handle these files. 458 | 459 | If the g:undotree_UndoDir is not set and undodir has been set to "." (vim's 460 | default undodir value), then the g:undotree_UndoDir value will be set to: 461 | 462 | - vim: $HOME/.local/state/undo/vim/ 463 | - nvim: $HOME/.local/state/undo/nvim/ 464 | 465 | ============================================================================== 466 | 4. Bugs *undotree-bugs* 467 | 468 | Post any issue and feature request here: 469 | https://github.com/mbbill/undotree/issues 470 | 471 | ============================================================================== 472 | 5. Changelog *undotree-changelog* 473 | 474 | Further changes will not be recorded. Please go to github page for more 475 | information. 476 | 477 | 4.4 (2017-10-15) 478 | - Autoload plugin functions 479 | 480 | 4.3 (2013-02-18) 481 | - Several fixes and enhancements. 482 | 483 | 4.2 (2012-11-24) 484 | - Fixed some small issue. 485 | 486 | 4.1 (2012-09-05) 487 | - Enhanced tree style. 488 | - Multi-window switching support. 489 | 490 | 4.0 (2012-08-30) 491 | - Live updated highlight for changed text. 492 | - Customizable key mappings. 493 | - Fixed some minor bugs. 494 | 495 | 3.1 (2012-08-25) 496 | - Add saved status. 497 | - Add relative timestamp. 498 | - Add ability of clear undo history. 499 | 500 | 3.0 (2012-08-24) 501 | - Add diff panel. 502 | - Performance improvement. 503 | 504 | 2.2 (2012-08-21) 505 | - Stable version. 506 | 507 | 2.1 (2012-08-20) 508 | - Fixed some annoying issues. 509 | 510 | 2.0 (2012-08-19) 511 | - Hotkey support. 512 | - Handle undo levels. 513 | - Auto refresh. 514 | - And so on. 515 | 516 | 1.0 (2012-08-18) 517 | - Initial upload 518 | 519 | ============================================================================== 520 | 6. License *undotree-license* 521 | 522 | BSD 523 | 524 | vim:tw=78:ts=8:ft=help:norl: 525 | -------------------------------------------------------------------------------- /plugin/undotree.vim: -------------------------------------------------------------------------------- 1 | "================================================= 2 | " File: plugin/undotree.vim 3 | " Description: Manage your undo history in a graph. 4 | " Author: Ming Bai 5 | " License: BSD 6 | 7 | " Avoid installing twice. 8 | if exists('g:loaded_undotree') 9 | finish 10 | endif 11 | let g:loaded_undotree = 0 12 | 13 | " At least version 7.3 with 005 patch is needed for undo branches. 14 | " Refer to https://github.com/mbbill/undotree/issues/4 for details. 15 | " Thanks kien 16 | if v:version < 703 17 | command! -nargs=0 -bar UndotreeToggle :echoerr "undotree.vim needs Vim version >= 7.3" 18 | finish 19 | endif 20 | if (v:version == 703 && !has("patch005")) 21 | command! -nargs=0 -bar UndotreeToggle :echoerr "undotree.vim needs vim7.3 with patch005 applied." 22 | finish 23 | endif 24 | let g:loaded_undotree = 1 " Signal plugin availability with a value of 1. 25 | 26 | "================================================= 27 | "Options: 28 | 29 | " Window layout 30 | " style 1 31 | " +----------+------------------------+ 32 | " | | | 33 | " | | | 34 | " | undotree | | 35 | " | | | 36 | " | | | 37 | " +----------+ | 38 | " | | | 39 | " | diff | | 40 | " | | | 41 | " +----------+------------------------+ 42 | " Style 2 43 | " +----------+------------------------+ 44 | " | | | 45 | " | | | 46 | " | undotree | | 47 | " | | | 48 | " | | | 49 | " +----------+------------------------+ 50 | " | | 51 | " | diff | 52 | " | | 53 | " +-----------------------------------+ 54 | " Style 3 55 | " +------------------------+----------+ 56 | " | | | 57 | " | | | 58 | " | | undotree | 59 | " | | | 60 | " | | | 61 | " | +----------+ 62 | " | | | 63 | " | | diff | 64 | " | | | 65 | " +------------------------+----------+ 66 | " Style 4 67 | " +-----------------------++----------+ 68 | " | | | 69 | " | | | 70 | " | | undotree | 71 | " | | | 72 | " | | | 73 | " +------------------------+----------+ 74 | " | | 75 | " | diff | 76 | " | | 77 | " +-----------------------------------+ 78 | if !exists('g:undotree_WindowLayout') 79 | let g:undotree_WindowLayout = 1 80 | endif 81 | 82 | " e.g. using 'd' instead of 'days' to save some space. 83 | if !exists('g:undotree_ShortIndicators') 84 | let g:undotree_ShortIndicators = 0 85 | endif 86 | 87 | " undotree window width 88 | if !exists('g:undotree_SplitWidth') 89 | if g:undotree_ShortIndicators == 1 90 | let g:undotree_SplitWidth = 24 91 | else 92 | let g:undotree_SplitWidth = 30 93 | endif 94 | endif 95 | 96 | " diff window height 97 | if !exists('g:undotree_DiffpanelHeight') 98 | let g:undotree_DiffpanelHeight = 10 99 | endif 100 | 101 | " auto open diff window 102 | if !exists('g:undotree_DiffAutoOpen') 103 | let g:undotree_DiffAutoOpen = 1 104 | endif 105 | 106 | " if set, let undotree window get focus after being opened, otherwise 107 | " focus will stay in current window. 108 | if !exists('g:undotree_SetFocusWhenToggle') 109 | let g:undotree_SetFocusWhenToggle = 0 110 | endif 111 | 112 | " tree node shape. 113 | if !exists('g:undotree_TreeNodeShape') 114 | let g:undotree_TreeNodeShape = '*' 115 | endif 116 | 117 | " tree vertical shape. 118 | if !exists('g:undotree_TreeVertShape') 119 | let g:undotree_TreeVertShape = '|' 120 | endif 121 | 122 | " tree split shape. 123 | if !exists('g:undotree_TreeSplitShape') 124 | let g:undotree_TreeSplitShape = '/' 125 | endif 126 | 127 | " tree return shape. 128 | if !exists('g:undotree_TreeReturnShape') 129 | let g:undotree_TreeReturnShape = '\' 130 | endif 131 | 132 | if !exists('g:undotree_DiffCommand') 133 | let g:undotree_DiffCommand = "diff" 134 | endif 135 | 136 | " relative timestamp 137 | if !exists('g:undotree_RelativeTimestamp') 138 | let g:undotree_RelativeTimestamp = 1 139 | endif 140 | 141 | " Highlight changed text 142 | if !exists('g:undotree_HighlightChangedText') 143 | let g:undotree_HighlightChangedText = 1 144 | endif 145 | 146 | " Highlight changed text using signs in the gutter 147 | if !exists('g:undotree_HighlightChangedWithSign') 148 | let g:undotree_HighlightChangedWithSign = 1 149 | endif 150 | 151 | " Highlight linked syntax type. 152 | " You may chose your favorite through ":hi" command 153 | if !exists('g:undotree_HighlightSyntaxAdd') 154 | let g:undotree_HighlightSyntaxAdd = "DiffAdd" 155 | endif 156 | if !exists('g:undotree_HighlightSyntaxChange') 157 | let g:undotree_HighlightSyntaxChange = "DiffChange" 158 | endif 159 | if !exists('g:undotree_HighlightSyntaxDel') 160 | let g:undotree_HighlightSyntaxDel = "DiffDelete" 161 | endif 162 | 163 | " Deprecates the old style configuration. 164 | if exists('g:undotree_SplitLocation') 165 | echo "g:undotree_SplitLocation is deprecated, 166 | \ please use g:undotree_WindowLayout instead." 167 | endif 168 | 169 | " Show help line 170 | if !exists('g:undotree_HelpLine') 171 | let g:undotree_HelpLine = 1 172 | endif 173 | 174 | " Show cursorline 175 | if !exists('g:undotree_CursorLine') 176 | let g:undotree_CursorLine = 1 177 | endif 178 | 179 | " Define the default persistence undo directory if not defined in vim/nvim 180 | " startup script. 181 | if !exists('g:undotree_UndoDir') 182 | let s:undoDir = &undodir 183 | let s:subdir = has('nvim') ? 'nvim' : 'vim' 184 | if s:undoDir == "." 185 | let s:undoDir = $HOME . '/.local/state/' . s:subdir . '/undo/' 186 | endif 187 | let g:undotree_UndoDir = s:undoDir 188 | endif 189 | 190 | augroup undotreeDetectPersistenceUndo 191 | au! 192 | au BufReadPost * call undotree#UndotreePersistUndo(0) 193 | augroup END 194 | 195 | "================================================= 196 | " User commands. 197 | command! -nargs=0 -bar UndotreeToggle :call undotree#UndotreeToggle() 198 | command! -nargs=0 -bar UndotreeHide :call undotree#UndotreeHide() 199 | command! -nargs=0 -bar UndotreeShow :call undotree#UndotreeShow() 200 | command! -nargs=0 -bar UndotreeFocus :call undotree#UndotreeFocus() 201 | command! -nargs=0 -bar UndotreePersistUndo :call undotree#UndotreePersistUndo(1) 202 | 203 | " vim: set et fdm=marker sts=4 sw=4: 204 | -------------------------------------------------------------------------------- /syntax/undotree.vim: -------------------------------------------------------------------------------- 1 | "================================================= 2 | " File: undotree.vim 3 | " Description: undotree syntax 4 | " Author: Ming Bai 5 | " License: BSD 6 | 7 | execute "syn match UndotreeNode ' \\zs".escape(g:undotree_TreeNodeShape,'*')."\\ze '" 8 | execute "syn match UndotreeNodeCurrent '\\zs".escape(g:undotree_TreeNodeShape,'*')."\\ze.*>\d\+<'" 9 | syn match UndotreeTimeStamp '(.*)$' 10 | syn match UndotreeFirstNode 'Original' 11 | execute "syn match UndotreeBranch '[".escape(g:undotree_TreeVertShape.g:undotree_TreeSplitShape.g:undotree_TreeReturnShape,'\')."]'" 12 | syn match UndotreeSeq ' \zs\d\+\ze ' 13 | syn match UndotreeCurrent '>\d\+<' 14 | syn match UndotreeNext '{\d\+}' 15 | syn match UndotreeHead '\[\d\+]' 16 | syn match UndotreeHelp '^".*$' contains=UndotreeHelpKey,UndotreeHelpTitle 17 | syn match UndotreeHelpKey '^" \zs.\{-}\ze:' contained 18 | syn match UndotreeHelpTitle '===.*===' contained 19 | syn match UndotreeSavedSmall ' \zss\ze ' 20 | syn match UndotreeSavedBig ' \zsS\ze ' 21 | 22 | hi def link UndotreeNode Question 23 | hi def link UndotreeNodeCurrent Statement 24 | hi def link UndotreeTimeStamp Function 25 | hi def link UndotreeFirstNode Function 26 | hi def link UndotreeBranch Constant 27 | hi def link UndotreeSeq Comment 28 | hi def link UndotreeCurrent Statement 29 | hi def link UndotreeNext Type 30 | hi def link UndotreeHead Identifier 31 | hi def link UndotreeHelp Comment 32 | hi def link UndotreeHelpKey Function 33 | hi def link UndotreeHelpTitle Type 34 | hi def link UndotreeSavedSmall WarningMsg 35 | hi def link UndotreeSavedBig MatchParen 36 | 37 | " vim: set et fdm=marker sts=4 sw=4: 38 | --------------------------------------------------------------------------------