# HG changeset patch # User Bram Moolenaar # Date 1274650476 -7200 # Node ID f8222d1f9a735f15fbffde01e5d2c2071c45ac1c # Parent 0e0e99d1092e39ece5bed883175b60c0c9f52c1b Included patch for persistent undo. Lots of changes and added test. diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt --- a/runtime/doc/cmdline.txt +++ b/runtime/doc/cmdline.txt @@ -805,6 +805,7 @@ Note: these are typed literally, they ar *filename-modifiers* *:_%:* *::8* *::p* *::.* *::~* *::h* *::t* *::r* *::e* *::s* *::gs* + *%:8* *%:p* *%:.* *%:~* *%:h* *%:t* *%:r* *%:e* *%:s* *%:gs* The file name modifiers can be used after "%", "#", "#n", "", "", "" or "". They are also used with the |fnamemodify()| function. These are not available when Vim has been compiled without the |+modify_fname| diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -6053,6 +6053,7 @@ os2 OS/2 version of Vim. osfiletype Compiled with support for osfiletypes |+osfiletype| path_extra Compiled with up/downwards search in 'path' and 'tags' perl Compiled with Perl interface. +persistent_undo Compiled with support for persistent undo history. postscript Compiled with PostScript file printing. printer Compiled with |:hardcopy| support. profile Compiled with |:profile| support. diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -7220,6 +7220,33 @@ A jump table for the options with a shor global Alias for 'term', see above. + *'undodir'* *'udir'* +'undodir' 'udir' string (default ".") + global + {not in Vi} + {only when compiled with the +persistent_undo feature} + List of directory names for undo files, separated with commas. + See |'backupdir'| for the format. Specifically, "." means using the + directory of the file. + When writing: The first directory that exists is used. "." always + works, no directories after "." will be used for writing. + When reading all entries are tried to find an undo file. The first + undo file that exists is used. When it cannot be read an error is + given, no further entry is used. + See |undo-persistence|. + + *'undofile'* *'udf'* +'undofile' 'udf' boolean (default off) + local to buffer + {not in Vi} + {only when compiled with the +persistent_undo feature} + When on, Vim automatically saves undo history to an undo file when + writing a buffer to a file, and restores undo history from the same + file on buffer read. + The name of the undo file is specified by 'undodir'. + See |undo-persistence|. + WARNING: this is a very new feature. Use at your own risc! + *'undolevels'* *'ul'* 'undolevels' 'ul' number (default 100, 1000 for Unix, VMS, Win32 and OS/2) diff --git a/runtime/doc/tags b/runtime/doc/tags --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -10,6 +10,16 @@ $VIM-use version5.txt /*$VIM-use* $VIMRUNTIME starting.txt /*$VIMRUNTIME* % motion.txt /*%* +%:. cmdline.txt /*%:.* +%:8 cmdline.txt /*%:8* +%:e cmdline.txt /*%:e* +%:gs cmdline.txt /*%:gs* +%:h cmdline.txt /*%:h* +%:p cmdline.txt /*%:p* +%:r cmdline.txt /*%:r* +%:s cmdline.txt /*%:s* +%:t cmdline.txt /*%:t* +%:~ cmdline.txt /*%:~* & change.txt /*&* ' motion.txt /*'* '' motion.txt /*''* @@ -1005,7 +1015,11 @@ 'tw' options.txt /*'tw'* 'tx' options.txt /*'tx'* 'uc' options.txt /*'uc'* +'udf' options.txt /*'udf'* +'udir' options.txt /*'udir'* 'ul' options.txt /*'ul'* +'undodir' options.txt /*'undodir'* +'undofile' options.txt /*'undofile'* 'undolevels' options.txt /*'undolevels'* 'updatecount' options.txt /*'updatecount'* 'updatetime' options.txt /*'updatetime'* @@ -1163,6 +1177,7 @@ +path_extra various.txt /*+path_extra* +perl various.txt /*+perl* +perl/dyn various.txt /*+perl\/dyn* ++persistent_undo various.txt /*+persistent_undo* +postscript various.txt /*+postscript* +printer various.txt /*+printer* +profile various.txt /*+profile* @@ -2582,6 +2597,7 @@ 90.5 usr_90.txt /*90.5* :rubydo if_ruby.txt /*:rubydo* :rubyf if_ruby.txt /*:rubyf* :rubyfile if_ruby.txt /*:rubyfile* +:rundo undo.txt /*:rundo* :runtime repeat.txt /*:runtime* :rv starting.txt /*:rv* :rviminfo starting.txt /*:rviminfo* @@ -2964,6 +2980,7 @@ 90.5 usr_90.txt /*90.5* :write_f editing.txt /*:write_f* :ws workshop.txt /*:ws* :wsverb workshop.txt /*:wsverb* +:wundo undo.txt /*:wundo* :wv starting.txt /*:wv* :wviminfo starting.txt /*:wviminfo* :x editing.txt /*:x* @@ -6913,6 +6930,7 @@ perl-overview if_perl.txt /*perl-overvie perl-patterns pattern.txt /*perl-patterns* perl-using if_perl.txt /*perl-using* perl.vim syntax.txt /*perl.vim* +persistent-undo undo.txt /*persistent-undo* pexpr-option print.txt /*pexpr-option* pfn-option print.txt /*pfn-option* pheader-option print.txt /*pheader-option* @@ -7835,6 +7853,7 @@ undo undo.txt /*undo* undo-blocks undo.txt /*undo-blocks* undo-branches undo.txt /*undo-branches* undo-commands undo.txt /*undo-commands* +undo-persistence undo.txt /*undo-persistence* undo-redo undo.txt /*undo-redo* undo-remarks undo.txt /*undo-remarks* undo-tree undo.txt /*undo-tree* diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt --- a/runtime/doc/todo.txt +++ b/runtime/doc/todo.txt @@ -30,6 +30,9 @@ be worked on, but only if you sponsor Vi *known-bugs* -------------------- Known bugs and current work ----------------------- +When Vim crashes it may run out of stack while executing autocommands. Patch +to not run autocommands when leaving Vim? (James Vega, 2010 May 23) + Cursor positioning wrong with 0x200e character. (John Becket, 2010 May 6) E315 when trying to change a file in FileChangedRO autocommand event. @@ -1082,6 +1085,18 @@ restored. (Luc St-Louis) Vim 7.3: Patches to include: +8 Persistent undo bugs / fixes: + - Add tests. Also with different 'enc' + - Add undofile(name): get undo file name for buffer "name". +- Extend test62 for gettabvar() and settabvar(). (Yegappan Lakshmanan, 2010 + May 23) +- Also crypt the undo file. +- Also crypt the swap file, each block separately. Change mf_write() and + mf_read(). How to get b_p_key to these functions? +- Do profiling on sha256 code to find obvious bottlenecks. +- Do profiling on crypt code to find obvious bottlenecks. +- Use off_t instead of long for bytes in a buffer. (James Vega, 2010 May 22, + update next day) - Include conceal patch? http://vince.negri.googlepages.com/ http://vim.wikia.com/wiki/Patch_to_conceal_parts_of_lines @@ -1150,42 +1165,17 @@ Needs some work: Needs some more testing. Update 2010 Apr 20, patch by Andy Kittner, May 16 - Easier/standard way to disable default plugins. -8 Persistent undo: store undo in a file. Patch by Jordan Lewis, 2009 Feb - 20. Repost 2009 Nov 16. - Get tar file from: http://repo.or.cz/w/vim_extended.git/tree/feat/persistent-undo - -> disable by default and add remark that it's new and may fail. - Testing remarks by Christian Brabandt, 2010 May 1: - - doesn't work well with symlinks (Jordan will look into it) - - old undo files tend to pile up - - :rundo should output a message (Jordan will fix this) - Bugs / fixes: - - Undo file should be stored with the original file by default, the undo - directory doesn't handle remote files or directory renames. - Use same mechanism as for swap files? But only with one file name. - - Read coladd depending on FEAT_VIRTUALEDIT, should always read/write it - - invoke u_wundo() inside buf_write() - - invoke u_rundo() inside readfile() - - Document that ":wundo" and ":rundo" should only be used in autocommands. - - unserialize_pos() does not need a return value - - function comments go before the function, not inside - - u_get_undofile() changes its argument ffname - - make magic four bytes. - - errors need numbers "E000:" - - also put 'enc' in undo file. - - don't use timestamp, "touch file" or dir copy may change it and undo - still works. - Older ideas: - - Use timestamps, so that a version a certain time ago can be found and - info before some time/date can be flushed. 'undopersist' gives maximum - time to keep undo: "3h", "1d", "2w", "1y", etc. For the file use dot - and extension: ".filename.un~" (like swapfile but "un~" instead of - "swp"). - ":{range}source": source the lines from the current file. You can already yank lines and use :@" to execute them. Most of do_source() would not be used, need a new function. It's easy when not doing breakpoints or profiling. +Probably not now: +- Use timestamps for undo, so that a version a certain time ago can be found + and info before some time/date can be flushed. 'undopersist' gives maximum + time to keep undo: "3h", "1d", "2w", "1y", etc. Before (beta) release: - Add fixes for 7.2 to version7.txt +- Rename vim73 branch to default (hints: Xavier de Gaye, 2010 May 23) More patches: @@ -1292,7 +1282,6 @@ 7 When 'rightleft' is set, the search to left as well? See patch of Dec 26. (Nadim Shaikli) 8 Option to lock all used memory so that it doesn't get swapped to disk (uncrypted). Patch by Jason Holt, 2003 May 23. Uses mlock. -7 Support a stronger encryption. Jason Holt implemented AES (May 6 2003). 7 Add ! register, for shell commands. (patch from Grenie) 8 In the gzip plugin, also recognize *.gz.orig, *.gz.bak, etc. Like it's done for filetype detection. Patch from Walter Briscoe, 2003 Jul 1. @@ -4320,11 +4309,6 @@ 8 Add 'mouse' flag, which sets a behav - When mouse click after 'r' command, get character that was pointed to. -Crypt and security: -8 Also crypt the swapfile, each block separately. Change mf_write() and - mf_read(). How to get b_p_key to these functions? - - Argument list: 6 Add command to put all filenames from the tag files in the argument list. When given an argument, only use the files where that argument matches diff --git a/runtime/doc/undo.txt b/runtime/doc/undo.txt --- a/runtime/doc/undo.txt +++ b/runtime/doc/undo.txt @@ -12,7 +12,8 @@ 1. Undo and redo commands |undo-commands 2. Two ways of undo |undo-two-ways| 3. Undo blocks |undo-blocks| 4. Undo branches |undo-branches| -5. Remarks about undo |undo-remarks| +5. Undo persistence |undo-persistence| +6. Remarks about undo |undo-remarks| ============================================================================== 1. Undo and redo commands *undo-commands* @@ -22,7 +23,7 @@ u Undo [count] changes. {Vi: only one *:u* *:un* *:undo* :u[ndo] Undo one change. {Vi: only one level} - + *E830* :u[ndo] {N} Jump to after change number {N}. See |undo-branches| for the meaning of {N}. {not in Vi} @@ -109,6 +110,8 @@ change. To do the opposite, break a change into two undo blocks, in Insert mode use CTRL-G u. This is useful if you want an insert command to be undoable in parts. E.g., for each sentence. |i_CTRL-G_u| +Setting the value of 'undolevels' also breaks undo. Even when the new value +is equal to the old value. ============================================================================== 4. Undo branches *undo-branches* *undo-tree* @@ -201,7 +204,88 @@ Note that using "u" and CTRL-R will not while repeating "g-" and "g+" does. ============================================================================== -5. Remarks about undo *undo-remarks* +5. Undo persistence *undo-persistence* *persistent-undo* + +When unloading a buffer Vim normally destroys the tree of undos created for +that buffer. By setting the 'undofile' option, Vim will automatically save +your undo history when you write a file and restore undo history when you edit +the file again. + +The 'undofile' option is checked after writing a file, before the BufWritePost +autocommands. If you want to control what files to write undo information +for, you can use a BufWritePre autocommand: > + au BufWritePre /tmp/* setlocal noundofile + +Vim saves undo trees in a separate undo file, one for each edited file, using +a simple scheme that maps filesystem paths directly to undo files. Vim will +detect if an undo file is no longer synchronized with the file it was written +for (with a hash of the file contents) and ignore it when the file was changed +after the undo file was written, to prevent corruption. + +Undo files are normally saved in the same directory as the file. This can be +changed with the 'undodir' option. + +You can also save and restore undo histories by using ":wundo" and ":rundo" +respectively: + *:wundo* *:rundo* +:wundo[!] {file} + Write undo history to {file}. + When {file} exists and it does not look like an undo file + (the magic number at the start of the file is wrong), then + this fails, unless the ! was added. + If it exists and does look like an undo file it is + overwritten. + {not in Vi} + +:rundo {file} Read undo history from {file}. + {not in Vi} + +You can use these in autocommands to explicitly specify the name of the +history file. E.g.: > + + au BufReadPost * rundo %:h/UNDO/%:t + au BufWritePost * wundo %:h/UNDO/%:t + +You should keep 'undofile' off, otherwise you end up with two undo files for +every write. +Note: I did not verify this always works! + +Note that while reading/writing files and 'undofile' is set most errors will +be silent, unless 'verbose' is set. With :wundo and :rundo you will get more +error messages, e.g., when the file cannot be read or written. + +NOTE: undo files are never deleted by Vim. You need to delete them yourself. + +Reading an existing undo file may fail for several reasons: +*E822* It cannot be opened, because the file permissions don't allow it. +*E823* The magic number at the start of the file doesn't match. This usually + means it is not an undo file. +*E824* The version number of the undo file indicates that it's written by a + newer version of Vim. You need that newer version to open it. Don't + write the buffer if you want to keep the undo info in the file. +"Undo file contents changed" + The file text differs from when the undo file was written. This means + the undo file cannot be used, it would corrupt the text. +*E825* *E826* The undo file does not contain valid contents and cannot be + used. +*E827* The magic number at the end of the file was not found. This usually + means the file was truncated. + +Writing an undo file may fail for these reasons: +*E828* The file to be written cannot be created. Perhaps you do not have + write permissions in the directory. +"Will not overwrite with undo file, cannot read" + A file exists with the name of the undo file to be written, but it + cannot be read. You may want to delete this file or rename it. +"Will not overwrite, this is not an undo file" + A file exists with the name of the undo file to be written, but it + does not start with the right magic number. You may want to delete + this file or rename it. +*E829* An error occurred while writing the undo file. You may want to try + again. + +============================================================================== +6. Remarks about undo *undo-remarks* The number of changes that are remembered is set with the 'undolevels' option. If it is zero, the Vi-compatible way is always used. If it is negative no diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -359,6 +359,7 @@ m *+ole* Win32 GUI only: |ole-interfac N *+path_extra* Up/downwards search in 'path' and 'tags' m *+perl* Perl interface |perl| m *+perl/dyn* Perl interface |perl-dynamic| |/dyn| +H *+persistent_undo* Persistent undo |undo-persistence| *+postscript* |:hardcopy| writes a PostScript file N *+printer* |:hardcopy| command H *+profile* |:profile| command diff --git a/runtime/doc/version7.txt b/runtime/doc/version7.txt --- a/runtime/doc/version7.txt +++ b/runtime/doc/version7.txt @@ -7170,6 +7170,10 @@ the buffer is marked as modified. Added *added-7.3* ----- +Persistent undo: store undo information in a file. Can undo to before when +the file was read, also for unloaded buffers. |undo-persistence| +(partly by Jordan Lewis) + Added the 'relativenumber' option. (Markus Heidelberg) Support for Blowfish encryption. Added the 'cryptmethod' option. diff --git a/runtime/optwin.vim b/runtime/optwin.vim --- a/runtime/optwin.vim +++ b/runtime/optwin.vim @@ -1032,6 +1032,10 @@ if has("vertsplit") call append("$", "cmdwinheight\theight of the command-line window") call OptionG("cwh", &cwh) endif +call append("$", "undofile\tautomatically save and restore undo history") +call BinOptionG("udf", &udf) +call append("$", "undodir\tlist of directories for undo files") +call OptionG("udir", &udir) call Header("executing external commands") diff --git a/runtime/tutor/tutor.it b/runtime/tutor/tutor.it --- a/runtime/tutor/tutor.it +++ b/runtime/tutor/tutor.it @@ -288,7 +288,7 @@ NOTA: Se batti solo il movimento mentre sei in Modalità Normale, senza 2. Batti d2w per cancellare le due parole MAIUSCOLE - 3. Ripeti i passi 1 e 2 con un contatore diverso per cancellare la parole + 3. Ripeti i passi 1 e 2 con un contatore diverso per cancellare le parole MAIUSCOLE consecutive con un solo comando ---> questa ABC DE linea FGHI JK LMN OP di parole è Q RS TUV ora ripulita. diff --git a/runtime/tutor/tutor.it.utf-8 b/runtime/tutor/tutor.it.utf-8 --- a/runtime/tutor/tutor.it.utf-8 +++ b/runtime/tutor/tutor.it.utf-8 @@ -288,7 +288,7 @@ NOTA: Se batti solo il movimento mentre sei in Modalità Normale, senza 2. Batti d2w per cancellare le due parole MAIUSCOLE - 3. Ripeti i passi 1 e 2 con un contatore diverso per cancellare la parole + 3. Ripeti i passi 1 e 2 con un contatore diverso per cancellare le parole MAIUSCOLE consecutive con un solo comando ---> questa ABC DE linea FGHI JK LMN OP di parole è Q RS TUV ora ripulita. diff --git a/src/buffer.c b/src/buffer.c --- a/src/buffer.c +++ b/src/buffer.c @@ -61,8 +61,9 @@ static void buf_delete_signs __ARGS((buf #endif /* - * Open current buffer, that is: open the memfile and read the file into memory - * return FAIL for failure, OK otherwise + * Open current buffer, that is: open the memfile and read the file into + * memory. + * Return FAIL for failure, OK otherwise. */ int open_buffer(read_stdin, eap) diff --git a/src/eval.c b/src/eval.c --- a/src/eval.c +++ b/src/eval.c @@ -11869,6 +11869,9 @@ f_has(argvars, rettv) "perl", #endif #endif +#ifdef FEAT_PERSISTENT_UNDO + "persistent_undo", +#endif #ifdef FEAT_PYTHON #ifndef DYNAMIC_PYTHON "python", diff --git a/src/ex_cmds.h b/src/ex_cmds.h --- a/src/ex_cmds.h +++ b/src/ex_cmds.h @@ -773,6 +773,8 @@ EX(CMD_rubydo, "rubydo", ex_rubydo, RANGE|DFLALL|EXTRA|NEEDARG|CMDWIN), EX(CMD_rubyfile, "rubyfile", ex_rubyfile, RANGE|FILE1|NEEDARG|CMDWIN), +EX(CMD_rundo, "rundo", ex_rundo, + NEEDARG|EXTRA|XFILE), EX(CMD_rviminfo, "rviminfo", ex_viminfo, BANG|FILE1|TRLBAR|CMDWIN), EX(CMD_substitute, "substitute", do_sub, @@ -1061,6 +1063,8 @@ EX(CMD_wqall, "wqall", do_wqall, BANG|FILE1|ARGOPT|DFLALL|TRLBAR), EX(CMD_wsverb, "wsverb", ex_wsverb, EXTRA|NOTADR|NEEDARG), +EX(CMD_wundo, "wundo", ex_wundo, + BANG|NEEDARG|EXTRA|XFILE), EX(CMD_wviminfo, "wviminfo", ex_viminfo, BANG|FILE1|TRLBAR|CMDWIN), EX(CMD_xit, "xit", ex_exit, diff --git a/src/ex_docmd.c b/src/ex_docmd.c --- a/src/ex_docmd.c +++ b/src/ex_docmd.c @@ -243,6 +243,10 @@ static void ex_popup __ARGS((exarg_T *ea # define ex_spellinfo ex_ni # define ex_spellrepall ex_ni #endif +#ifndef FEAT_PERSISTENT_UNDO +# define ex_rundo ex_ni +# define ex_wundo ex_ni +#endif #ifndef FEAT_MZSCHEME # define ex_mzscheme ex_script_ni # define ex_mzfile ex_ni @@ -298,6 +302,10 @@ static void ex_join __ARGS((exarg_T *eap static void ex_at __ARGS((exarg_T *eap)); static void ex_bang __ARGS((exarg_T *eap)); static void ex_undo __ARGS((exarg_T *eap)); +#ifdef FEAT_PERSISTENT_UNDO +static void ex_wundo __ARGS((exarg_T *eap)); +static void ex_rundo __ARGS((exarg_T *eap)); +#endif static void ex_redo __ARGS((exarg_T *eap)); static void ex_later __ARGS((exarg_T *eap)); static void ex_redir __ARGS((exarg_T *eap)); @@ -8452,6 +8460,28 @@ ex_undo(eap) u_undo(1); } +#ifdef FEAT_PERSISTENT_UNDO + void +ex_wundo(eap) + exarg_T *eap; +{ + char_u hash[UNDO_HASH_SIZE]; + + u_compute_hash(hash); + u_write_undo(eap->arg, eap->forceit, curbuf, hash); +} + + void +ex_rundo(eap) + exarg_T *eap; +{ + char_u hash[UNDO_HASH_SIZE]; + + u_compute_hash(hash); + u_read_undo(eap->arg, hash); +} +#endif + /* * ":redo". */ diff --git a/src/feature.h b/src/feature.h --- a/src/feature.h +++ b/src/feature.h @@ -1275,3 +1275,11 @@ || defined(FEAT_BIG) # define FEAT_AUTOCHDIR #endif + +/* + * +persistent_undo 'undofile', 'undodir' options, :wundo and :rundo, and + * implementation. + */ +#ifdef FEAT_NORMAL +# define FEAT_PERSISTENT_UNDO +#endif diff --git a/src/fileio.c b/src/fileio.c --- a/src/fileio.c +++ b/src/fileio.c @@ -253,6 +253,10 @@ readfile(fname, sfname, from, lines_to_s char_u *cryptkey = NULL; int did_ask_for_key = FALSE; #endif +#ifdef FEAT_PERSISTENT_UNDO + context_sha256_T sha_ctx; + int read_undo_file = FALSE; +#endif int split = 0; /* number of split lines */ #define UNKNOWN 0x0fffffff /* file size is unknown */ linenr_T linecnt; @@ -1178,6 +1182,12 @@ retry: #ifdef FEAT_MBYTE conv_restlen = 0; #endif +#ifdef FEAT_PERSISTENT_UNDO + read_undo_file = (newfile && curbuf->b_ffname != NULL && curbuf->b_p_udf + && !filtering && !read_stdin && !read_buffer); + if (read_undo_file) + sha256_start(&sha_ctx); +#endif } while (!error && !got_int) @@ -2133,6 +2143,10 @@ rewind_retry: error = TRUE; break; } +#ifdef FEAT_PERSISTENT_UNDO + if (read_undo_file) + sha256_update(&sha_ctx, line_start, len); +#endif ++lnum; if (--read_count == 0) { @@ -2197,6 +2211,10 @@ rewind_retry: error = TRUE; break; } +#ifdef FEAT_PERSISTENT_UNDO + if (read_undo_file) + sha256_update(&sha_ctx, line_start, len); +#endif ++lnum; if (--read_count == 0) { @@ -2237,11 +2255,17 @@ failed: if (set_options) curbuf->b_p_eol = FALSE; *ptr = NUL; - if (ml_append(lnum, line_start, - (colnr_T)(ptr - line_start + 1), newfile) == FAIL) + len = (colnr_T)(ptr - line_start + 1); + if (ml_append(lnum, line_start, len, newfile) == FAIL) error = TRUE; else + { +#ifdef FEAT_PERSISTENT_UNDO + if (read_undo_file) + sha256_update(&sha_ctx, line_start, len); +#endif read_no_eol_lnum = ++lnum; + } } if (set_options) @@ -2555,6 +2579,19 @@ failed: */ write_no_eol_lnum = read_no_eol_lnum; +#ifdef FEAT_PERSISTENT_UNDO + /* + * When opening a new file locate undo info and read it. + */ + if (read_undo_file) + { + char_u hash[UNDO_HASH_SIZE]; + + sha256_finish(&sha_ctx, hash); + u_read_undo(NULL, hash); + } +#endif + #ifdef FEAT_AUTOCMD if (!read_stdin && !read_buffer) { @@ -3038,6 +3075,10 @@ buf_write(buf, fname, sfname, start, end vim_acl_T acl = NULL; /* ACL copied from original file to backup or new file */ #endif +#ifdef FEAT_PERSISTENT_UNDO + int write_undo_file = FALSE; + context_sha256_T sha_ctx; +#endif if (fname == NULL || *fname == NUL) /* safety check */ return FAIL; @@ -4344,6 +4385,14 @@ restore_backup: write_info.bw_start_lnum = start; #endif +#ifdef FEAT_PERSISTENT_UNDO + write_undo_file = (buf->b_p_udf && overwriting && !append + && !filtering && reset_changed); + if (write_undo_file) + /* Prepare for computing the hash value of the text. */ + sha256_start(&sha_ctx); +#endif + write_info.bw_len = bufsize; #ifdef HAS_BW_FLAGS write_info.bw_flags = wb_flags; @@ -4358,6 +4407,10 @@ restore_backup: * Keep it fast! */ ptr = ml_get_buf(buf, lnum, FALSE) - 1; +#ifdef FEAT_PERSISTENT_UNDO + if (write_undo_file) + sha256_update(&sha_ctx, ptr + 1, STRLEN(ptr + 1) + 1); +#endif while ((c = *++ptr) != NUL) { if (c == NL) @@ -4886,6 +4939,20 @@ nofail: } msg_scroll = msg_save; +#ifdef FEAT_PERSISTENT_UNDO + /* + * When writing the whole file and 'undofile' is set, also write the undo + * file. + */ + if (retval == OK && write_undo_file) + { + char_u hash[UNDO_HASH_SIZE]; + + sha256_finish(&sha_ctx, hash); + u_write_undo(NULL, FALSE, buf, hash); + } +#endif + #ifdef FEAT_AUTOCMD #ifdef FEAT_EVAL if (!should_abort(retval)) diff --git a/src/memline.c b/src/memline.c --- a/src/memline.c +++ b/src/memline.c @@ -245,9 +245,6 @@ static char_u *make_percent_swname __ARG #ifdef FEAT_BYTEOFF static void ml_updatechunk __ARGS((buf_T *buf, long line, long len, int updtype)); #endif -#ifdef HAVE_READLINK -static int resolve_symlink __ARGS((char_u *fname, char_u *buf)); -#endif /* * Open a new memline for "buf". @@ -3559,7 +3556,7 @@ ml_lineadd(buf, count) } } -#ifdef HAVE_READLINK +#if defined(HAVE_READLINK) || defined(PROTO) /* * Resolve a symlink in the last component of a file name. * Note that f_resolve() does it for every part of the path, we don't do that @@ -3567,7 +3564,7 @@ ml_lineadd(buf, count) * If it worked returns OK and the resolved link in "buf[MAXPATHL]". * Otherwise returns FAIL. */ - static int + int resolve_symlink(fname, buf) char_u *fname; char_u *buf; @@ -3862,7 +3859,7 @@ do_swapexists(buf, fname) * Returns the name in allocated memory or NULL. * * Note: If BASENAMELEN is not correct, you will get error messages for - * not being able to open the swapfile + * not being able to open the swap or undo file * Note: May trigger SwapExists autocmd, pointers may change! */ static char_u * @@ -3886,29 +3883,29 @@ findswapname(buf, dirp, old_fname) # define CREATE_DUMMY_FILE FILE *dummyfd = NULL; -/* - * If we start editing a new file, e.g. "test.doc", which resides on an MSDOS - * compatible filesystem, it is possible that the file "test.doc.swp" which we - * create will be exactly the same file. To avoid this problem we temporarily - * create "test.doc". - * Don't do this when the check below for a 8.3 file name is used. - */ + /* + * If we start editing a new file, e.g. "test.doc", which resides on an + * MSDOS compatible filesystem, it is possible that the file + * "test.doc.swp" which we create will be exactly the same file. To avoid + * this problem we temporarily create "test.doc". Don't do this when the + * check below for a 8.3 file name is used. + */ if (!(buf->b_p_sn || buf->b_shortname) && buf->b_fname != NULL && mch_getperm(buf->b_fname) < 0) dummyfd = mch_fopen((char *)buf->b_fname, "w"); #endif -/* - * Isolate a directory name from *dirp and put it in dir_name. - * First allocate some memory to put the directory name in. - */ + /* + * Isolate a directory name from *dirp and put it in dir_name. + * First allocate some memory to put the directory name in. + */ dir_name = alloc((unsigned)STRLEN(*dirp) + 1); if (dir_name != NULL) (void)copy_option_part(dirp, dir_name, 31000, ","); -/* - * we try different names until we find one that does not exist yet - */ + /* + * we try different names until we find one that does not exist yet + */ if (dir_name == NULL) /* out of memory */ fname = NULL; else diff --git a/src/option.c b/src/option.c --- a/src/option.c +++ b/src/option.c @@ -177,6 +177,9 @@ #define PV_TS OPT_BUF(BV_TS) #define PV_TW OPT_BUF(BV_TW) #define PV_TX OPT_BUF(BV_TX) +#ifdef FEAT_PERSISTENT_UNDO +# define PV_UDF OPT_BUF(BV_UDF) +#endif #define PV_WM OPT_BUF(BV_WM) /* @@ -362,6 +365,9 @@ static char_u *p_spl; static long p_ts; static long p_tw; static int p_tx; +#ifdef FEAT_PERSISTENT_UNDO +static int p_udf; +#endif static long p_wm; #ifdef FEAT_KEYMAP static char_u *p_keymap; @@ -2586,6 +2592,22 @@ static struct vimoption {"ttytype", "tty", P_STRING|P_EXPAND|P_NODEFAULT|P_NO_MKRC|P_VI_DEF|P_RALL, (char_u *)&T_NAME, PV_NONE, {(char_u *)"", (char_u *)0L} SCRIPTID_INIT}, + {"undodir", "udir", P_STRING|P_EXPAND|P_COMMA|P_NODUP|P_SECURE|P_VI_DEF, +#ifdef FEAT_PERSISTENT_UNDO + (char_u *)&p_udir, PV_NONE, + {(char_u *)".", (char_u *)0L} +#else + (char_u *)NULL, PV_NONE, + {(char_u *)0L, (char_u *)0L} +#endif + SCRIPTID_INIT}, + {"undofile", "udf", P_BOOL|P_VI_DEF|P_VIM, +#ifdef FEAT_PERSISTENT_UNDO + (char_u *)&p_udf, PV_UDF, +#else + (char_u *)NULL, PV_NONE, +#endif + {(char_u *)FALSE, (char_u *)0L} SCRIPTID_INIT}, {"undolevels", "ul", P_NUM|P_VI_DEF, (char_u *)&p_ul, PV_NONE, { @@ -9404,6 +9426,9 @@ get_varp(p) case PV_TS: return (char_u *)&(curbuf->b_p_ts); case PV_TW: return (char_u *)&(curbuf->b_p_tw); case PV_TX: return (char_u *)&(curbuf->b_p_tx); +#ifdef FEAT_PERSISTENT_UNDO + case PV_UDF: return (char_u *)&(curbuf->b_p_udf); +#endif case PV_WM: return (char_u *)&(curbuf->b_p_wm); #ifdef FEAT_KEYMAP case PV_KMAP: return (char_u *)&(curbuf->b_p_keymap); @@ -9774,6 +9799,9 @@ buf_copy_options(buf, flags) #if defined(FEAT_BEVAL) && defined(FEAT_EVAL) buf->b_p_bexpr = empty_option; #endif +#ifdef FEAT_PERSISTENT_UNDO + buf->b_p_udf = p_udf; +#endif /* * Don't copy the options set by ex_help(), use the saved values, diff --git a/src/option.h b/src/option.h --- a/src/option.h +++ b/src/option.h @@ -815,6 +815,7 @@ static char *(p_ttym_values[]) = {"xterm # define TTYM_JSBTERM 0x10 # define TTYM_PTERM 0x20 #endif +EXTERN char_u *p_udir; /* 'undodir' */ EXTERN long p_ul; /* 'undolevels' */ EXTERN long p_uc; /* 'updatecount' */ EXTERN long p_ut; /* 'updatetime' */ @@ -1004,6 +1005,7 @@ enum , BV_TS , BV_TW , BV_TX + , BV_UDF , BV_WM , BV_COUNT /* must be the last one */ }; diff --git a/src/os_mac.h b/src/os_mac.h --- a/src/os_mac.h +++ b/src/os_mac.h @@ -204,7 +204,7 @@ #endif #ifndef DFLT_VDIR -# define DFLT_VDIR "$VIM/vimfiles/view" /* default for 'viewdir' */ +# define DFLT_VDIR "$VIM/vimfiles/view" /* default for 'viewdir' */ #endif #define DFLT_ERRORFILE "errors.err" diff --git a/src/proto/memline.pro b/src/proto/memline.pro --- a/src/proto/memline.pro +++ b/src/proto/memline.pro @@ -25,6 +25,7 @@ int ml_delete __ARGS((linenr_T lnum, int void ml_setmarked __ARGS((linenr_T lnum)); linenr_T ml_firstmarked __ARGS((void)); void ml_clearmarked __ARGS((void)); +int resolve_symlink __ARGS((char_u *fname, char_u *buf)); char_u *makeswapname __ARGS((char_u *fname, char_u *ffname, buf_T *buf, char_u *dir_name)); char_u *get_file_in_dir __ARGS((char_u *fname, char_u *dname)); void ml_setflags __ARGS((buf_T *buf)); diff --git a/src/proto/sha256.pro b/src/proto/sha256.pro --- a/src/proto/sha256.pro +++ b/src/proto/sha256.pro @@ -1,4 +1,7 @@ /* sha256.c */ +void sha256_start __ARGS((context_sha256_T *ctx)); +void sha256_update __ARGS((context_sha256_T *ctx, char_u *input, uint32_t length)); +void sha256_finish __ARGS((context_sha256_T *ctx, char_u digest[32])); char_u *sha256_key __ARGS((char_u *buf)); int sha256_self_test __ARGS((void)); void sha2_seed __ARGS((char_u header[], int header_len)); diff --git a/src/proto/spell.pro b/src/proto/spell.pro --- a/src/proto/spell.pro +++ b/src/proto/spell.pro @@ -2,11 +2,14 @@ int spell_check __ARGS((win_T *wp, char_u *ptr, hlf_T *attrp, int *capcol, int docount)); int spell_move_to __ARGS((win_T *wp, int dir, int allwords, int curline, hlf_T *attrp)); void spell_cat_line __ARGS((char_u *buf, char_u *line, int maxlen)); +int get2c __ARGS((FILE *fd)); +int get3c __ARGS((FILE *fd)); +int get4c __ARGS((FILE *fd)); char_u *did_set_spelllang __ARGS((buf_T *buf)); void spell_free_all __ARGS((void)); void spell_reload __ARGS((void)); int spell_check_msm __ARGS((void)); -void put_bytes __ARGS((FILE *fd, long_u nr, int len)); +int put_bytes __ARGS((FILE *fd, long_u nr, int len)); void ex_mkspell __ARGS((exarg_T *eap)); void ex_spell __ARGS((exarg_T *eap)); void spell_add_word __ARGS((char_u *word, int len, int bad, int idx, int undo)); diff --git a/src/proto/undo.pro b/src/proto/undo.pro --- a/src/proto/undo.pro +++ b/src/proto/undo.pro @@ -5,6 +5,9 @@ int u_savesub __ARGS((linenr_T lnum)); int u_inssub __ARGS((linenr_T lnum)); int u_savedel __ARGS((linenr_T lnum, long nlines)); int undo_allowed __ARGS((void)); +void u_compute_hash __ARGS((char_u *hash)); +void u_read_undo __ARGS((char_u *name, char_u *hash)); +void u_write_undo __ARGS((char_u *name, int forceit, buf_T *buf, char_u *hash)); void u_undo __ARGS((int count)); void u_redo __ARGS((int count)); void undo_time __ARGS((long step, int sec, int absolute)); diff --git a/src/sha256.c b/src/sha256.c --- a/src/sha256.c +++ b/src/sha256.c @@ -20,18 +20,9 @@ #include "vim.h" -#ifdef FEAT_CRYPT +#if defined(FEAT_CRYPT) || defined(FEAT_PERSISTENT_UNDO) -typedef struct { - UINT32_T total[2]; - UINT32_T state[8]; - char_u buffer[64]; -} context_sha256_T; - -static void sha256_starts __ARGS((context_sha256_T *ctx)); static void sha256_process __ARGS((context_sha256_T *ctx, char_u data[64])); -static void sha256_update __ARGS((context_sha256_T *ctx, char_u *input, UINT32_T length)); -static void sha256_finish __ARGS((context_sha256_T *ctx, char_u digest[32])); static char_u *sha256_bytes __ARGS((char_u *buf, int buflen)); static unsigned int get_some_time __ARGS((void)); @@ -52,8 +43,8 @@ static unsigned int get_some_time __ARGS (b)[(i) + 3] = (char_u)((n) ); \ } - static void -sha256_starts(ctx) + void +sha256_start(ctx) context_sha256_T *ctx; { ctx->total[0] = 0; @@ -203,7 +194,7 @@ sha256_process(ctx, data) ctx->state[7] += H; } - static void + void sha256_update(ctx, input, length) context_sha256_T *ctx; char_u *input; @@ -250,7 +241,7 @@ static char_u sha256_padding[64] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - static void + void sha256_finish(ctx, digest) context_sha256_T *ctx; char_u digest[32]; @@ -296,7 +287,7 @@ sha256_bytes(buf, buflen) sha256_self_test(); - sha256_starts(&ctx); + sha256_start(&ctx); sha256_update(&ctx, buf, buflen); sha256_finish(&ctx, sha256sum); for (j = 0; j < 32; j++) @@ -368,7 +359,7 @@ sha256_self_test() } else { - sha256_starts(&ctx); + sha256_start(&ctx); memset(buf, 'a', 1000); for (j = 0; j < 1000; j++) sha256_update(&ctx, (char_u *)buf, 1000); @@ -416,7 +407,7 @@ sha2_seed(header, header_len) for (i = 0; i < (int)sizeof(random_data) - 1; i++) random_data[i] = (char_u)((get_some_time() ^ rand()) & 0xff); - sha256_starts(&ctx); + sha256_start(&ctx); sha256_update(&ctx, (char_u *)random_data, sizeof(random_data)); sha256_finish(&ctx, sha256sum); @@ -424,4 +415,4 @@ sha2_seed(header, header_len) header[i] = sha256sum[i % sizeof(sha256sum)]; } -#endif /* FEAT_CRYPT */ +#endif /* FEAT_CRYPT || FEAT_PERSISTENT_UNDO */ diff --git a/src/spell.c b/src/spell.c --- a/src/spell.c +++ b/src/spell.c @@ -854,9 +854,6 @@ static char_u *spell_enc __ARGS((void)); static void int_wordlist_spl __ARGS((char_u *fname)); static void spell_load_cb __ARGS((char_u *fname, void *cookie)); static slang_T *spell_load_file __ARGS((char_u *fname, char_u *lang, slang_T *old_lp, int silent)); -static int get2c __ARGS((FILE *fd)); -static int get3c __ARGS((FILE *fd)); -static int get4c __ARGS((FILE *fd)); static time_t get8c __ARGS((FILE *fd)); static char_u *read_cnt_string __ARGS((FILE *fd, int cnt_bytes, int *lenp)); static char_u *read_string __ARGS((FILE *fd, int cnt)); @@ -2988,7 +2985,7 @@ endOK: /* * Read 2 bytes from "fd" and turn them into an int, MSB first. */ - static int + int get2c(fd) FILE *fd; { @@ -3002,7 +2999,7 @@ get2c(fd) /* * Read 3 bytes from "fd" and turn them into an int, MSB first. */ - static int + int get3c(fd) FILE *fd; { @@ -3017,7 +3014,7 @@ get3c(fd) /* * Read 4 bytes from "fd" and turn them into an int, MSB first. */ - static int + int get4c(fd) FILE *fd; { @@ -8018,7 +8015,7 @@ node_equal(n1, n2) /* * Write a number to file "fd", MSB first, in "len" bytes. */ - void + int put_bytes(fd, nr, len) FILE *fd; long_u nr; @@ -8027,7 +8024,9 @@ put_bytes(fd, nr, len) int i; for (i = len - 1; i >= 0; --i) - putc((int)(nr >> (i * 8)), fd); + if (putc((int)(nr >> (i * 8)), fd) == EOF) + return FAIL; + return OK; } #ifdef _MSC_VER diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -1465,6 +1465,9 @@ struct file_buffer char_u *b_p_dict; /* 'dictionary' local value */ char_u *b_p_tsr; /* 'thesaurus' local value */ #endif +#ifdef FEAT_PERSISTENT_UNDO + int b_p_udf; /* 'undofile' */ +#endif /* end of buffer options */ @@ -2392,3 +2395,9 @@ typedef struct #define CPT_KIND 2 /* "kind" */ #define CPT_INFO 3 /* "info" */ #define CPT_COUNT 4 /* Number of entries */ + +typedef struct { + UINT32_T total[2]; + UINT32_T state[8]; + char_u buffer[64]; +} context_sha256_T; diff --git a/src/testdir/Makefile b/src/testdir/Makefile --- a/src/testdir/Makefile +++ b/src/testdir/Makefile @@ -69,7 +69,7 @@ test1.out: test1.in fi \ else echo $* NO OUTPUT >>test.log; \ fi" - #-rm -rf X* test.ok viminfo + -rm -rf X* test.ok viminfo test49.out: test49.vim diff --git a/src/testdir/test61.in b/src/testdir/test61.in --- a/src/testdir/test61.in +++ b/src/testdir/test61.in @@ -50,6 +50,53 @@ obbbbu:.w >>test.out obbbb:set ul=100 :undojoin occccu:.w >>test.out +:" +:" Test 'undofile': first a simple one-line change. +:set nocp ul=100 undofile +:e! Xtestfile +ggdGithis is one line:set ul=100 +:s/one/ONE/ +:set ul=100 +:w +:bwipe! +:e Xtestfile +u:.w >>test.out +:" +:" Test 'undofile', change in original file fails check +:set noundofile +:e! Xtestfile +:s/line/Line/ +:w +:set undofile +:bwipe! +:e Xtestfile +u:.w >>test.out +:" +:" Test 'undofile', add 10 lines, delete 6 lines, undo 3 +:set undofile +ggdGione +two +three +four +five +six +seven +eight +nine +ten:set ul=100 +3Gdd:set ul=100 +dd:set ul=100 +dd:set ul=100 +dd:set ul=100 +dd:set ul=100 +dd:set ul=100 +:w +:bwipe! +:e Xtestfile +uuu:w >>test.out +:" +:" Rename the undo file so that it gets cleaned up. +:call rename(".Xtestfile.un~", "Xtestundo") :qa! ENDTEST diff --git a/src/testdir/test61.ok b/src/testdir/test61.ok --- a/src/testdir/test61.ok +++ b/src/testdir/test61.ok @@ -22,3 +22,12 @@ 123456 123456abc aaaa aaaa +this is one line +this is ONE Line +one +two +six +seven +eight +nine +ten diff --git a/src/undo.c b/src/undo.c --- a/src/undo.c +++ b/src/undo.c @@ -99,6 +99,14 @@ static void u_freeheader __ARGS((buf_T * static void u_freebranch __ARGS((buf_T *buf, u_header_T *uhp, u_header_T **uhpp)); static void u_freeentries __ARGS((buf_T *buf, u_header_T *uhp, u_header_T **uhpp)); static void u_freeentry __ARGS((u_entry_T *, long)); +#ifdef FEAT_PERSISTENT_UNDO +static void unserialize_pos __ARGS((pos_T *pos, FILE *fp)); +static void unserialize_visualinfo __ARGS((visualinfo_T *info, FILE *fp)); +static char_u *u_get_undo_file_name __ARGS((char_u *, int reading)); +static int serialize_uep __ARGS((u_entry_T *uep, FILE *fp)); +static void serialize_pos __ARGS((pos_T pos, FILE *fp)); +static void serialize_visualinfo __ARGS((visualinfo_T info, FILE *fp)); +#endif #ifdef U_USE_MALLOC # define U_FREE_LINE(ptr) vim_free(ptr) @@ -119,6 +127,8 @@ static long u_newcount, u_oldcount; */ static int undo_undoes = FALSE; +static int lastmark = 0; + #ifdef U_DEBUG /* * Check the undo structures for being valid. Print a warning when something @@ -652,6 +662,795 @@ nomem: return FAIL; } +#ifdef FEAT_PERSISTENT_UNDO + +# define UF_START_MAGIC 0xfeac /* magic at start of undofile */ +# define UF_HEADER_MAGIC 0x5fd0 /* magic at start of header */ +# define UF_END_MAGIC 0xe7aa /* magic after last header */ +# define UF_VERSION 1 /* 2-byte undofile version number */ + +/* + * Compute the hash for the current buffer text into hash[UNDO_HASH_SIZE]. + */ + void +u_compute_hash(hash) + char_u *hash; +{ + context_sha256_T ctx; + linenr_T lnum; + char_u *p; + + sha256_start(&ctx); + for (lnum = 1; lnum < curbuf->b_ml.ml_line_count; ++lnum) + { + p = ml_get(lnum); + sha256_update(&ctx, p, STRLEN(p) + 1); + } + sha256_finish(&ctx, hash); +} + +/* + * Unserialize the pos_T at the current position in fp. + */ + static void +unserialize_pos(pos, fp) + pos_T *pos; + FILE *fp; +{ + pos->lnum = get4c(fp); + pos->col = get4c(fp); +#ifdef FEAT_VIRTUALEDIT + pos->coladd = get4c(fp); +#else + (void)get4c(fp); +#endif +} + +/* + * Unserialize the visualinfo_T at the current position in fp. + */ + static void +unserialize_visualinfo(info, fp) + visualinfo_T *info; + FILE *fp; +{ + unserialize_pos(&info->vi_start, fp); + unserialize_pos(&info->vi_end, fp); + info->vi_mode = get4c(fp); + info->vi_curswant = get4c(fp); +} + +/* + * Return an allocated string of the full path of the target undofile. + * When "reading" is TRUE find the file to read, go over all directories in + * 'undodir'. + * When "reading" is FALSE use the first name where the directory exists. + */ + static char_u * +u_get_undo_file_name(buf_ffname, reading) + char_u *buf_ffname; + int reading; +{ + char_u *dirp; + char_u dir_name[IOSIZE + 1]; + char_u *munged_name = NULL; + char_u *undo_file_name = NULL; + int dir_len; + char_u *p; + struct stat st; + char_u *ffname = buf_ffname; +#ifdef HAVE_READLINK + char_u fname_buf[MAXPATHL]; +#endif + + if (ffname == NULL) + return NULL; + +#ifdef HAVE_READLINK + /* Expand symlink in the file name, so that we put the undo file with the + * actual file instead of with the symlink. */ + if (resolve_symlink(ffname, fname_buf) == OK) + ffname = fname_buf; +#endif + + /* Loop over 'undodir'. When reading find the first file that exists. + * When not reading use the first directory that exists or ".". */ + dirp = p_udir; + while (*dirp != NUL) + { + dir_len = copy_option_part(&dirp, dir_name, IOSIZE, ","); + if (dir_len == 1 && dir_name[0] == '.') + { + /* Use same directory as the ffname, + * "dir/name" -> "dir/.name.un~" */ + undo_file_name = vim_strnsave(ffname, STRLEN(ffname) + 5); + if (undo_file_name == NULL) + break; + p = gettail(undo_file_name); + mch_memmove(p + 1, p, STRLEN(p) + 1); + *p = '.'; + STRCAT(p, ".un~"); + } + else + { + dir_name[dir_len] = NUL; + if (mch_isdir(dir_name)) + { + if (munged_name == NULL) + { + munged_name = vim_strsave(ffname); + if (munged_name == NULL) + return NULL; + for (p = munged_name; *p != NUL; mb_ptr_adv(p)) + if (vim_ispathsep(*p)) + *p = '%'; + } + undo_file_name = concat_fnames(dir_name, munged_name, TRUE); + } + } + + /* When reading check if the file exists. */ + if (undo_file_name != NULL && (!reading + || mch_stat((char *)undo_file_name, &st) >= 0)) + break; + vim_free(undo_file_name); + undo_file_name = NULL; + } + + vim_free(munged_name); + return undo_file_name; +} + +/* + * Load the undo tree from an undo file. + * If "name" is not NULL use it as the undo file name. This also means being + * a bit more verbose. + * Otherwise use curbuf->b_ffname to generate the undo file name. + * "hash[UNDO_HASH_SIZE]" must be the hash value of the buffer text. + */ + void +u_read_undo(name, hash) + char_u *name; + char_u *hash; +{ + char_u *file_name; + FILE *fp; + long magic, version, str_len; + char_u *line_ptr = NULL; + linenr_T line_lnum; + colnr_T line_colnr; + linenr_T line_count; + int uep_len; + int line_len; + int num_head; + long old_header_seq, new_header_seq, cur_header_seq; + long seq_last, seq_cur; + short old_idx = -1, new_idx = -1, cur_idx = -1; + long num_read_uhps = 0; + time_t seq_time; + int i, j; + int c; + short found_first_uep = 0; + char_u **array; + char_u *line; + u_entry_T *uep, *last_uep, *nuep; + u_header_T *uhp; + u_header_T **uhp_table = NULL; + char_u read_hash[UNDO_HASH_SIZE]; + + if (name == NULL) + { + file_name = u_get_undo_file_name(curbuf->b_ffname, TRUE); + if (file_name == NULL) + return; + } + else + file_name = name; + + if (p_verbose > 0) + smsg((char_u *)_("Reading undo file: %s"), file_name); + fp = mch_fopen((char *)file_name, "r"); + if (fp == NULL) + { + if (name != NULL || p_verbose > 0) + EMSG2(_("E822: Cannot open undo file for reading: %s"), file_name); + goto error; + } + + /* Begin overall file information */ + magic = get2c(fp); + if (magic != UF_START_MAGIC) + { + EMSG2(_("E823: Corrupted undo file: %s"), file_name); + goto error; + } + version = get2c(fp); + if (version != UF_VERSION) + { + EMSG2(_("E824: Incompatible undo file: %s"), file_name); + goto error; + } + + fread(read_hash, UNDO_HASH_SIZE, 1, fp); + line_count = (linenr_T)get4c(fp); + if (memcmp(hash, read_hash, UNDO_HASH_SIZE) != 0 + || line_count != curbuf->b_ml.ml_line_count) + { + if (p_verbose > 0 || name != NULL) + { + verbose_enter(); + give_warning((char_u *)_("Undo file contents changed"), TRUE); + verbose_leave(); + } + goto error; + } + + /* Begin undo data for U */ + str_len = get4c(fp); + if (str_len < 0) + goto error; + else if (str_len > 0) + { + if ((line_ptr = U_ALLOC_LINE(str_len)) == NULL) + goto error; + for (i = 0; i < str_len; i++) + line_ptr[i] = (char_u)getc(fp); + line_ptr[i] = NUL; + } + line_lnum = (linenr_T)get4c(fp); + line_colnr = (colnr_T)get4c(fp); + + /* Begin general undo data */ + old_header_seq = get4c(fp); + new_header_seq = get4c(fp); + cur_header_seq = get4c(fp); + num_head = get4c(fp); + seq_last = get4c(fp); + seq_cur = get4c(fp); + seq_time = get4c(fp); + + /* uhp_table will store the freshly created undo headers we allocate + * until we insert them into curbuf. The table remains sorted by the + * sequence numbers of the headers. */ + uhp_table = (u_header_T **)U_ALLOC_LINE(num_head * sizeof(u_header_T *)); + if (uhp_table == NULL) + goto error; + vim_memset(uhp_table, 0, num_head * sizeof(u_header_T *)); + + c = get2c(fp); + while (c == UF_HEADER_MAGIC) + { + found_first_uep = 0; + uhp = (u_header_T *)U_ALLOC_LINE((unsigned)sizeof(u_header_T)); + if (uhp == NULL) + goto error; + vim_memset(uhp, 0, sizeof(u_header_T)); + /* We're not actually trying to store pointers here. We're just storing + * IDs so we can swizzle them into pointers later - hence the type + * cast. */ + uhp->uh_next = (u_header_T *)(long)get4c(fp); + uhp->uh_prev = (u_header_T *)(long)get4c(fp); + uhp->uh_alt_next = (u_header_T *)(long)get4c(fp); + uhp->uh_alt_prev = (u_header_T *)(long)get4c(fp); + uhp->uh_seq = get4c(fp); + if (uhp->uh_seq <= 0) + { + EMSG2(_("E825: Undo file corruption: invalid uh_seq.: %s"), + file_name); + U_FREE_LINE(uhp); + goto error; + } + uhp->uh_walk = 0; + unserialize_pos(&uhp->uh_cursor, fp); +#ifdef FEAT_VIRTUALEDIT + uhp->uh_cursor_vcol = get4c(fp); +#else + (void)get4c(fp); +#endif + uhp->uh_flags = get2c(fp); + for (i = 0; i < NMARKS; ++i) + unserialize_pos(&uhp->uh_namedm[i], fp); +#ifdef FEAT_VISUAL + unserialize_visualinfo(&uhp->uh_visual, fp); +#else + { + visualinfo_T info; + unserialize_visualinfo(&info, fp); + } +#endif + uhp->uh_time = get4c(fp); + + /* Unserialize uep list. The first 4 bytes is the length of the + * entire uep in bytes minus the length of the strings within. + * -1 is a sentinel value meaning no more ueps.*/ + last_uep = NULL; + while ((uep_len = get4c(fp)) != -1) + { + uep = (u_entry_T *)U_ALLOC_LINE((unsigned)sizeof(u_entry_T)); + vim_memset(uep, 0, sizeof(u_entry_T)); + if (uep == NULL) + goto error; + uep->ue_top = get4c(fp); + uep->ue_bot = get4c(fp); + uep->ue_lcount = get4c(fp); + uep->ue_size = get4c(fp); + uep->ue_next = NULL; + array = (char_u **)U_ALLOC_LINE( + (unsigned)(sizeof(char_u *) * uep->ue_size)); + for (i = 0; i < uep->ue_size; i++) + { + line_len = get4c(fp); + /* U_ALLOC_LINE provides an extra byte for the NUL terminator.*/ + line = (char_u *)U_ALLOC_LINE( + (unsigned) (sizeof(char_u) * line_len)); + if (line == NULL) + goto error; + for (j = 0; j < line_len; j++) + { + line[j] = getc(fp); + } + line[j] = '\0'; + array[i] = line; + } + uep->ue_array = array; + if (found_first_uep == 0) + { + uhp->uh_entry = uep; + found_first_uep = 1; + } + else + { + last_uep->ue_next = uep; + } + last_uep = uep; + } + + /* Insertion sort the uhp into the table by its uh_seq. This is + * required because, while the number of uhps is limited to + * num_heads, and the uh_seq order is monotonic with respect to + * creation time, the starting uh_seq can be > 0 if any undolevel + * culling was done at undofile write time, and there can be uh_seq + * gaps in the uhps. + */ + for (i = num_read_uhps - 1; i >= -1; i--) + { + /* if i == -1, we've hit the leftmost side of the table, so insert + * at uhp_table[0]. */ + if (i == -1 || uhp->uh_seq > uhp_table[i]->uh_seq) + { + /* If we've had to move from the rightmost side of the table, + * we have to shift everything to the right by one spot. */ + if (i < num_read_uhps - 1) + { + memmove(uhp_table + i + 2, uhp_table + i + 1, + (num_read_uhps - i) * sizeof(u_header_T *)); + } + uhp_table[i + 1] = uhp; + break; + } + else if (uhp->uh_seq == uhp_table[i]->uh_seq) + { + EMSG2(_("E826 Undo file corruption: duplicate uh_seq: %s"), + file_name); + goto error; + } + } + num_read_uhps++; + c = get2c(fp); + } + + if (c != UF_END_MAGIC) + { + EMSG2(_("E827: Undo file corruption; no end marker: %s"), file_name); + goto error; + } + + /* We've organized all of the uhps into a table sorted by uh_seq. Now we + * iterate through the table and swizzle each sequence number we've + * stored in uh_foo into a pointer corresponding to the header with that + * sequence number. Then free curbuf's old undo structure, give curbuf + * the updated {old,new,cur}head pointers, and then free the table. */ + for (i = 0; i < num_head; i++) + { + uhp = uhp_table[i]; + if (uhp == NULL) + continue; + for (j = 0; j < num_head; j++) + { + if (uhp_table[j] == NULL) + continue; + if (uhp_table[j]->uh_seq == (long)uhp->uh_next) + uhp->uh_next = uhp_table[j]; + if (uhp_table[j]->uh_seq == (long)uhp->uh_prev) + uhp->uh_prev = uhp_table[j]; + if (uhp_table[j]->uh_seq == (long)uhp->uh_alt_next) + uhp->uh_alt_next = uhp_table[j]; + if (uhp_table[j]->uh_seq == (long)uhp->uh_alt_prev) + uhp->uh_alt_prev = uhp_table[j]; + } + if (old_header_seq > 0 && old_idx < 0 && uhp->uh_seq == old_header_seq) + old_idx = i; + if (new_header_seq > 0 && new_idx < 0 && uhp->uh_seq == new_header_seq) + new_idx = i; + if (cur_header_seq > 0 && cur_idx < 0 && uhp->uh_seq == cur_header_seq) + cur_idx = i; + } + u_blockfree(curbuf); + curbuf->b_u_oldhead = old_idx < 0 ? 0 : uhp_table[old_idx]; + curbuf->b_u_newhead = new_idx < 0 ? 0 : uhp_table[new_idx]; + curbuf->b_u_curhead = cur_idx < 0 ? 0 : uhp_table[cur_idx]; + curbuf->b_u_line_ptr = line_ptr; + curbuf->b_u_line_lnum = line_lnum; + curbuf->b_u_line_colnr = line_colnr; + curbuf->b_u_numhead = num_head; + curbuf->b_u_seq_last = seq_last; + curbuf->b_u_seq_cur = seq_cur; + curbuf->b_u_seq_time = seq_time; + U_FREE_LINE(uhp_table); +#ifdef U_DEBUG + u_check(TRUE); +#endif + if (name != NULL) + smsg((char_u *)_("Finished reading undo file %s"), file_name); + goto theend; + +error: + if (line_ptr != NULL) + U_FREE_LINE(line_ptr); + if (uhp_table != NULL) + { + for (i = 0; i < num_head; i++) + { + if (uhp_table[i] != NULL) + { + uep = uhp_table[i]->uh_entry; + while (uep != NULL) + { + nuep = uep->ue_next; + u_freeentry(uep, uep->ue_size); + uep = nuep; + } + U_FREE_LINE(uhp_table[i]); + } + } + U_FREE_LINE(uhp_table); + } + +theend: + if (fp != NULL) + fclose(fp); + if (file_name != name) + vim_free(file_name); + return; +} + +/* + * Serialize "uep" to "fp". + */ + static int +serialize_uep(uep, fp) + u_entry_T *uep; + FILE *fp; +{ + int i; + int uep_len; + int *entry_lens; + + if (uep->ue_size > 0) + entry_lens = (int *)alloc(uep->ue_size * sizeof(int)); + + /* Define uep_len to be the size of the entire uep minus the size of its + * component strings, in bytes. The sizes of the component strings + * are written before each individual string. + * We have 4 entries each of 4 bytes, plus ue_size * 4 bytes + * of string size information. */ + + uep_len = uep->ue_size * 4; + /* Collect sizing information for later serialization. */ + for (i = 0; i < uep->ue_size; i++) + { + entry_lens[i] = (int)STRLEN(uep->ue_array[i]); + uep_len += entry_lens[i]; + } + put_bytes(fp, (long_u)uep_len, 4); + put_bytes(fp, (long_u)uep->ue_top, 4); + put_bytes(fp, (long_u)uep->ue_bot, 4); + put_bytes(fp, (long_u)uep->ue_lcount, 4); + put_bytes(fp, (long_u)uep->ue_size, 4); + for (i = 0; i < uep->ue_size; i++) + { + if (put_bytes(fp, (long_u)entry_lens[i], 4) == FAIL) + return FAIL; + fprintf(fp, "%s", uep->ue_array[i]); + } + if (uep->ue_size > 0) + vim_free(entry_lens); + return OK; +} + +/* + * Serialize "pos" to "fp". + */ + static void +serialize_pos(pos, fp) + pos_T pos; + FILE *fp; +{ + put_bytes(fp, (long_u)pos.lnum, 4); + put_bytes(fp, (long_u)pos.col, 4); +#ifdef FEAT_VIRTUALEDIT + put_bytes(fp, (long_u)pos.coladd, 4); +#else + put_bytes(fp, (long_u)0, 4); +#endif +} + +/* + * Serialize "info" to "fp". + */ + static void +serialize_visualinfo(info, fp) + visualinfo_T info; + FILE *fp; +{ + serialize_pos(info.vi_start, fp); + serialize_pos(info.vi_end, fp); + put_bytes(fp, (long_u)info.vi_mode, 4); + put_bytes(fp, (long_u)info.vi_curswant, 4); +} + +static char_u e_not_open[] = N_("E828: Cannot open undo file for writing: %s"); + +/* + * Write the undo tree in an undo file. + * When "name" is not NULL, use it as the name of the undo file. + * Otherwise use buf->b_ffname to generate the undo file name. + * "buf" must never be null, buf->b_ffname is used to obtain the original file + * permissions. + * "forceit" is TRUE for ":wundo!", FALSE otherwise. + * "hash[UNDO_HASH_SIZE]" must be the hash value of the buffer text. + */ + void +u_write_undo(name, forceit, buf, hash) + char_u *name; + int forceit; + buf_T *buf; + char_u *hash; +{ + u_header_T *uhp; + u_entry_T *uep; + char_u *file_name; + int str_len, i, uep_len, mark; + int fd; + FILE *fp = NULL; + int perm; + int write_ok = FALSE; +#ifdef UNIX + struct stat st_old; + struct stat st_new; +#endif + + if (name == NULL) + { + file_name = u_get_undo_file_name(buf->b_ffname, FALSE); + if (file_name == NULL) + return; + } + else + file_name = name; + +#ifdef UNIX + if (mch_stat((char *)buf->b_ffname, &st_old) >= 0) + perm = st_old.st_mode; + else + perm = 0600; +#else + perm = mch_getperm(buf->b_ffname); + if (perm < 0) + perm = 0600; +#endif + /* set file protection same as original file, but strip s-bit */ + perm = perm & 0777; + + /* If the undo file exists, verify that it actually is an undo file, and + * delete it. */ + if (mch_getperm(file_name) >= 0) + { + if (name == NULL || !forceit) + { + /* Check we can read it and it's an undo file. */ + fd = mch_open((char *)file_name, O_RDONLY|O_EXTRA, 0); + if (fd < 0) + { + if (name != NULL || p_verbose > 0) + smsg((char_u *)_("Will not overwrite with undo file, cannot read: %s"), + file_name); + goto theend; + } + else + { + char_u buf[2]; + + vim_read(fd, buf, 2); + close(fd); + if ((buf[0] << 8) + buf[1] != UF_START_MAGIC) + { + if (name != NULL || p_verbose > 0) + smsg((char_u *)_("Will not overwrite, this is not an undo file: %s"), + file_name); + goto theend; + } + } + } + mch_remove(file_name); + } + + fd = mch_open((char *)file_name, + O_CREAT|O_EXTRA|O_WRONLY|O_EXCL|O_NOFOLLOW, perm); + (void)mch_setperm(file_name, perm); + if (fd < 0) + { + EMSG2(_(e_not_open), file_name); + goto theend; + } + if (p_verbose > 0) + smsg((char_u *)_("Writing undo file: %s"), file_name); + +#ifdef UNIX + /* + * Try to set the group of the undo file same as the original file. If + * this fails, set the protection bits for the group same as the + * protection bits for others. + */ + if (mch_stat((char *)file_name, &st_new) >= 0 + && st_new.st_gid != st_old.st_gid +# ifdef HAVE_FCHOWN /* sequent-ptx lacks fchown() */ + && fchown(fd, (uid_t)-1, st_old.st_gid) != 0 +# endif + ) + mch_setperm(file_name, (perm & 0707) | ((perm & 07) << 3)); +# ifdef HAVE_SELINUX + mch_copy_sec(buf->b_ffname, file_name); +# endif +#endif + + fp = fdopen(fd, "w"); + if (fp == NULL) + { + EMSG2(_(e_not_open), file_name); + close(fd); + mch_remove(file_name); + goto theend; + } + + /* Start writing, first overall file information */ + put_bytes(fp, (long_u)UF_START_MAGIC, 2); + put_bytes(fp, (long_u)UF_VERSION, 2); + + /* Write a hash of the buffer text, so that we can verify it is still the + * same when reading the buffer text. */ + if (fwrite(hash, (size_t)UNDO_HASH_SIZE, (size_t)1, fp) != 1) + goto write_error; + put_bytes(fp, (long_u)buf->b_ml.ml_line_count, 4); + + /* Begin undo data for U */ + str_len = buf->b_u_line_ptr != NULL ? STRLEN(buf->b_u_line_ptr) : 0; + put_bytes(fp, (long_u)str_len, 4); + if (str_len > 0 && fwrite(buf->b_u_line_ptr, (size_t)str_len, + (size_t)1, fp) != 1) + goto write_error; + + put_bytes(fp, (long_u)buf->b_u_line_lnum, 4); + put_bytes(fp, (long_u)buf->b_u_line_colnr, 4); + + /* Begin general undo data */ + uhp = buf->b_u_oldhead; + put_bytes(fp, (long_u)(uhp != NULL ? uhp->uh_seq : 0), 4); + + uhp = buf->b_u_newhead; + put_bytes(fp, (long_u)(uhp != NULL ? uhp->uh_seq : 0), 4); + + uhp = buf->b_u_curhead; + put_bytes(fp, (long_u)(uhp != NULL ? uhp->uh_seq : 0), 4); + + put_bytes(fp, (long_u)buf->b_u_numhead, 4); + put_bytes(fp, (long_u)buf->b_u_seq_last, 4); + put_bytes(fp, (long_u)buf->b_u_seq_cur, 4); + put_bytes(fp, (long_u)buf->b_u_seq_time, 4); + + /* Iteratively serialize UHPs and their UEPs from the top down. */ + mark = ++lastmark; + uhp = buf->b_u_oldhead; + while (uhp != NULL) + { + /* Serialize current UHP if we haven't seen it */ + if (uhp->uh_walk != mark) + { + if (put_bytes(fp, (long_u)UF_HEADER_MAGIC, 2) == FAIL) + goto write_error; + + put_bytes(fp, (long_u)((uhp->uh_next != NULL) + ? uhp->uh_next->uh_seq : 0), 4); + put_bytes(fp, (long_u)((uhp->uh_prev != NULL) + ? uhp->uh_prev->uh_seq : 0), 4); + put_bytes(fp, (long_u)((uhp->uh_alt_next != NULL) + ? uhp->uh_alt_next->uh_seq : 0), 4); + put_bytes(fp, (long_u)((uhp->uh_alt_prev != NULL) + ? uhp->uh_alt_prev->uh_seq : 0), 4); + put_bytes(fp, uhp->uh_seq, 4); + serialize_pos(uhp->uh_cursor, fp); +#ifdef FEAT_VIRTUALEDIT + put_bytes(fp, (long_u)uhp->uh_cursor_vcol, 4); +#else + put_bytes(fp, (long_u)0, 4); +#endif + put_bytes(fp, (long_u)uhp->uh_flags, 2); + /* Assume NMARKS will stay the same. */ + for (i = 0; i < NMARKS; ++i) + { + serialize_pos(uhp->uh_namedm[i], fp); + } +#ifdef FEAT_VISUAL + serialize_visualinfo(uhp->uh_visual, fp); +#endif + put_bytes(fp, (long_u)uhp->uh_time, 4); + + uep = uhp->uh_entry; + while (uep != NULL) + { + if (serialize_uep(uep, fp) == FAIL) + goto write_error; + uep = uep->ue_next; + } + /* Sentinel value: no more ueps */ + uep_len = -1; + put_bytes(fp, (long_u)uep_len, 4); + uhp->uh_walk = mark; + } + + /* Now walk through the tree - algorithm from undo_time */ + if (uhp->uh_prev != NULL && uhp->uh_prev->uh_walk != mark) + uhp = uhp->uh_prev; + else if (uhp->uh_alt_next != NULL && uhp->uh_alt_next->uh_walk != mark) + uhp = uhp->uh_alt_next; + else if (uhp->uh_next != NULL && uhp->uh_alt_prev == NULL + && uhp->uh_next->uh_walk != mark) + uhp = uhp->uh_next; + else if (uhp->uh_alt_prev != NULL) + uhp = uhp->uh_alt_prev; + else + uhp = uhp->uh_next; + } + + if (put_bytes(fp, (long_u)UF_END_MAGIC, 2) == OK) + write_ok = TRUE; + +write_error: + fclose(fp); + if (!write_ok) + EMSG2(_("E829: write error in undo file: %s"), file_name); + +#if defined(MACOS_CLASSIC) || defined(WIN3264) + (void)mch_copy_file_attribute(buf->b_ffname, file_name); +#endif +#ifdef HAVE_ACL + { + vim_acl_T acl; + + /* For systems that support ACL: get the ACL from the original file. */ + acl = mch_get_acl(buf->b_ffname); + mch_set_acl(file_name, acl); + } +#endif + +theend: + if (file_name != name) + vim_free(file_name); +} + +#endif /* FEAT_PERSISTENT_UNDO */ + + /* * If 'cpoptions' contains 'u': Undo the previous undo or redo (vi compatible). * If 'cpoptions' does not contain 'u': Always undo. @@ -757,8 +1556,6 @@ u_doit(startcount) u_undo_end(undo_undoes, FALSE); } -static int lastmark = 0; - /* * Undo or redo over the timeline. * When "step" is negative go back in time, otherwise goes forward in time. @@ -927,7 +1724,7 @@ undo_time(step, sec, absolute) if (absolute) { - EMSGN(_("Undo number %ld not found"), step); + EMSGN(_("E830: Undo number %ld not found"), step); return; } diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -426,6 +426,11 @@ static char *(features[]) = #else "-perl", #endif +#ifdef FEAT_PERSISTENT_UNDO + "+persistent_undo", +#else + "-persistent_undo", +#endif #ifdef FEAT_PRINTER # ifdef FEAT_POSTSCRIPT "+postscript", diff --git a/src/vim.h b/src/vim.h --- a/src/vim.h +++ b/src/vim.h @@ -1398,6 +1398,9 @@ typedef enum */ #define MAXMAPLEN 50 +/* Size in bytes of the hash used in the undo file. */ +#define UNDO_HASH_SIZE 32 + #ifdef HAVE_FCNTL_H # include #endif