viminfo: First version of ShaDa file dumping

What works:

1. ShaDa file dumping: header, registers, jump list, history, search patterns,
   substitute strings, variables.
2. ShaDa file reading: registers, global marks, variables.

Most was not tested.

TODO:

1. Merging.
2. Reading history, local marks, jump and buffer lists.
3. Documentation update.
4. Converting some data from &encoding.
5. Safer variant of dumping viminfo (dump to temporary file then rename).
6. Removing old viminfo code (currently masked with `#if 0` in a ShaDa file for
   reference).
This commit is contained in:
ZyX
2015-04-25 18:47:31 +03:00
parent 0fdaab995e
commit 244dbe3a77
38 changed files with 4511 additions and 1848 deletions

View File

@@ -67,6 +67,9 @@
#include "nvim/os/os.h"
#include "nvim/os/shell.h"
#include "nvim/os/input.h"
#include "nvim/os/time.h"
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
/*
* Struct to hold the sign properties.
@@ -1391,550 +1394,6 @@ void append_redir(char_u *buf, int buflen, char_u *opt, char_u *fname)
(char *)opt, (char *)fname);
}
static int viminfo_errcnt;
static int no_viminfo(void)
{
/* "vim -i NONE" does not read or write a viminfo file */
return use_viminfo != NULL && STRCMP(use_viminfo, "NONE") == 0;
}
/*
* Report an error for reading a viminfo file.
* Count the number of errors. When there are more than 10, return TRUE.
*/
int viminfo_error(char *errnum, char *message, char_u *line)
{
vim_snprintf((char *)IObuff, IOSIZE, _("%sviminfo: %s in line: "),
errnum, message);
STRNCAT(IObuff, line, IOSIZE - STRLEN(IObuff) - 1);
if (IObuff[STRLEN(IObuff) - 1] == '\n')
IObuff[STRLEN(IObuff) - 1] = NUL;
emsg(IObuff);
if (++viminfo_errcnt >= 10) {
EMSG(_("E136: viminfo: Too many errors, skipping rest of file"));
return TRUE;
}
return FALSE;
}
/*
* read_viminfo() -- Read the viminfo file. Registers etc. which are already
* set are not over-written unless "flags" includes VIF_FORCEIT. -- webb
*/
int
read_viminfo (
char_u *file, /* file name or NULL to use default name */
int flags /* VIF_WANT_INFO et al. */
)
{
FILE *fp;
char_u *fname;
if (no_viminfo())
return FAIL;
fname = viminfo_filename(file); /* get file name in allocated buffer */
fp = mch_fopen((char *)fname, READBIN);
if (p_verbose > 0) {
verbose_enter();
smsg(_("Reading viminfo file \"%s\"%s%s%s"),
fname,
(flags & VIF_WANT_INFO) ? _(" info") : "",
(flags & VIF_WANT_MARKS) ? _(" marks") : "",
(flags & VIF_GET_OLDFILES) ? _(" oldfiles") : "",
fp == NULL ? _(" FAILED") : "");
verbose_leave();
}
xfree(fname);
if (fp == NULL)
return FAIL;
viminfo_errcnt = 0;
do_viminfo(fp, NULL, flags);
fclose(fp);
return OK;
}
/*
* Write the viminfo file. The old one is read in first so that effectively a
* merge of current info and old info is done. This allows multiple vims to
* run simultaneously, without losing any marks etc.
* If "forceit" is TRUE, then the old file is not read in, and only internal
* info is written to the file.
*/
void write_viminfo(char_u *file, int forceit)
{
char_u *fname;
FILE *fp_in = NULL; /* input viminfo file, if any */
FILE *fp_out = NULL; /* output viminfo file */
char_u *tempname = NULL; /* name of temp viminfo file */
char_u *wp;
#if defined(UNIX)
mode_t umask_save;
#endif
if (no_viminfo())
return;
fname = viminfo_filename(file); /* may set to default if NULL */
fp_in = mch_fopen((char *)fname, READBIN);
if (fp_in == NULL) {
/* if it does exist, but we can't read it, don't try writing */
if (os_file_exists(fname))
goto end;
#if defined(UNIX)
/*
* For Unix we create the .viminfo non-accessible for others,
* because it may contain text from non-accessible documents.
*/
umask_save = umask(077);
#endif
fp_out = mch_fopen((char *)fname, WRITEBIN);
#if defined(UNIX)
(void)umask(umask_save);
#endif
} else {
/*
* There is an existing viminfo file. Create a temporary file to
* write the new viminfo into, in the same directory as the
* existing viminfo file, which will be renamed later.
*/
#ifdef UNIX
/*
* For Unix we check the owner of the file. It's not very nice to
* overwrite a user's viminfo file after a "su root", with a
* viminfo file that the user can't read.
*/
FileInfo old_info; // FileInfo of existing viminfo file
if (os_fileinfo((char *)fname, &old_info)
&& getuid() != ROOT_UID
&& !(old_info.stat.st_uid == getuid()
? (old_info.stat.st_mode & 0200)
: (old_info.stat.st_gid == getgid()
? (old_info.stat.st_mode & 0020)
: (old_info.stat.st_mode & 0002)))) {
int tt = msg_didany;
/* avoid a wait_return for this message, it's annoying */
EMSG2(_("E137: Viminfo file is not writable: %s"), fname);
msg_didany = tt;
fclose(fp_in);
goto end;
}
#endif
// Make tempname
tempname = (char_u *)modname((char *)fname, ".tmp", FALSE);
if (tempname != NULL) {
/*
* Check if tempfile already exists. Never overwrite an
* existing file!
*/
if (os_file_exists(tempname)) {
/*
* Try another name. Change one character, just before
* the extension.
*/
wp = tempname + STRLEN(tempname) - 5;
if (wp < path_tail(tempname)) /* empty file name? */
wp = path_tail(tempname);
for (*wp = 'z'; os_file_exists(tempname); --*wp) {
/*
* They all exist? Must be something wrong! Don't
* write the viminfo file then.
*/
if (*wp == 'a') {
xfree(tempname);
tempname = NULL;
break;
}
}
}
}
if (tempname != NULL) {
int fd;
/* Use os_open() to be able to use O_NOFOLLOW and set file
* protection:
* Unix: same as original file, but strip s-bit. Reset umask to
* avoid it getting in the way.
* Others: r&w for user only. */
# ifdef UNIX
umask_save = umask(0);
fd = os_open((char *)tempname,
O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW,
(int)((old_info.stat.st_mode & 0777) | 0600));
(void)umask(umask_save);
# else
fd = os_open((char *)tempname,
O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW, 0600);
# endif
if (fd < 0)
fp_out = NULL;
else
fp_out = fdopen(fd, WRITEBIN);
/*
* If we can't create in the same directory, try creating a
* "normal" temp file.
*/
if (fp_out == NULL) {
xfree(tempname);
if ((tempname = vim_tempname()) != NULL)
fp_out = mch_fopen((char *)tempname, WRITEBIN);
}
#ifdef UNIX
/*
* Make sure the owner can read/write it. This only works for
* root.
*/
if (fp_out != NULL) {
os_fchown(fileno(fp_out), old_info.stat.st_uid, old_info.stat.st_gid);
}
#endif
}
}
/*
* Check if the new viminfo file can be written to.
*/
if (fp_out == NULL) {
EMSG2(_("E138: Can't write viminfo file %s!"),
(fp_in == NULL || tempname == NULL) ? fname : tempname);
if (fp_in != NULL)
fclose(fp_in);
goto end;
}
if (p_verbose > 0) {
verbose_enter();
smsg(_("Writing viminfo file \"%s\""), fname);
verbose_leave();
}
viminfo_errcnt = 0;
do_viminfo(fp_in, fp_out, forceit ? 0 : (VIF_WANT_INFO | VIF_WANT_MARKS));
fclose(fp_out); /* errors are ignored !? */
if (fp_in != NULL) {
fclose(fp_in);
/* In case of an error keep the original viminfo file. Otherwise
* rename the newly written file. Give an error if that fails. */
if (viminfo_errcnt == 0 && vim_rename(tempname, fname) == -1) {
viminfo_errcnt++;
EMSG2(_("E886: Can't rename viminfo file to %s!"), fname);
}
if (viminfo_errcnt > 0) {
os_remove((char *)tempname);
}
}
end:
xfree(fname);
xfree(tempname);
}
/*
* Get the viminfo file name to use.
* If "file" is given and not empty, use it (has already been expanded by
* cmdline functions).
* Otherwise use "-i file_name", value from 'viminfo' or the default, and
* expand environment variables.
* Returns an allocated string.
*/
static char_u *viminfo_filename(char_u *file)
{
if (file == NULL || *file == NUL) {
if (use_viminfo != NULL)
file = use_viminfo;
else if ((file = find_viminfo_parameter('n')) == NULL || *file == NUL) {
#ifdef VIMINFO_FILE2
// don't use $HOME when not defined (turned into "c:/"!).
if (!os_env_exists("HOME")) {
// don't use $VIM when not available.
expand_env((char_u *)"$VIM", NameBuff, MAXPATHL);
if (STRCMP("$VIM", NameBuff) != 0) /* $VIM was expanded */
file = (char_u *)VIMINFO_FILE2;
else
file = (char_u *)VIMINFO_FILE;
} else
#endif
file = (char_u *)VIMINFO_FILE;
}
expand_env(file, NameBuff, MAXPATHL);
file = NameBuff;
}
return vim_strsave(file);
}
/*
* do_viminfo() -- Should only be called from read_viminfo() & write_viminfo().
*/
static void do_viminfo(FILE *fp_in, FILE *fp_out, int flags)
{
int count = 0;
int eof = FALSE;
vir_T vir;
int merge = FALSE;
vir.vir_line = xmalloc(LSIZE);
vir.vir_fd = fp_in;
vir.vir_conv.vc_type = CONV_NONE;
if (fp_in != NULL) {
if (flags & VIF_WANT_INFO) {
eof = read_viminfo_up_to_marks(&vir,
flags & VIF_FORCEIT, fp_out != NULL);
merge = TRUE;
} else if (flags != 0)
/* Skip info, find start of marks */
while (!(eof = viminfo_readline(&vir))
&& vir.vir_line[0] != '>')
;
}
if (fp_out != NULL) {
/* Write the info: */
fprintf(fp_out, _("# This viminfo file was generated by Nvim %s.\n"),
mediumVersion);
fputs(_("# You may edit it if you're careful!\n\n"), fp_out);
fputs(_("# Value of 'encoding' when this file was written\n"), fp_out);
fprintf(fp_out, "*encoding=%s\n\n", p_enc);
write_viminfo_search_pattern(fp_out);
write_viminfo_sub_string(fp_out);
write_viminfo_history(fp_out, merge);
write_viminfo_registers(fp_out);
write_viminfo_varlist(fp_out);
write_viminfo_filemarks(fp_out);
write_viminfo_bufferlist(fp_out);
count = write_viminfo_marks(fp_out);
}
if (fp_in != NULL
&& (flags & (VIF_WANT_MARKS | VIF_GET_OLDFILES | VIF_FORCEIT)))
copy_viminfo_marks(&vir, fp_out, count, eof, flags);
xfree(vir.vir_line);
if (vir.vir_conv.vc_type != CONV_NONE)
convert_setup(&vir.vir_conv, NULL, NULL);
}
/*
* read_viminfo_up_to_marks() -- Only called from do_viminfo(). Reads in the
* first part of the viminfo file which contains everything but the marks that
* are local to a file. Returns TRUE when end-of-file is reached. -- webb
*/
static int read_viminfo_up_to_marks(vir_T *virp, int forceit, int writing)
{
int eof;
prepare_viminfo_history(forceit ? 9999 : 0, writing);
eof = viminfo_readline(virp);
while (!eof && virp->vir_line[0] != '>') {
switch (virp->vir_line[0]) {
/* Characters reserved for future expansion, ignored now */
case '+': /* "+40 /path/dir file", for running vim without args */
case '|': /* to be defined */
case '^': /* to be defined */
case '<': /* long line - ignored */
/* A comment or empty line. */
case NUL:
case '\r':
case '\n':
case '#':
eof = viminfo_readline(virp);
break;
case '*': /* "*encoding=value" */
eof = viminfo_encoding(virp);
break;
case '!': /* global variable */
eof = read_viminfo_varlist(virp, writing);
break;
case '%': /* entry for buffer list */
eof = read_viminfo_bufferlist(virp, writing);
break;
case '"':
eof = read_viminfo_register(virp, forceit);
break;
case '/': /* Search string */
case '&': /* Substitute search string */
case '~': /* Last search string, followed by '/' or '&' */
eof = read_viminfo_search_pattern(virp, forceit);
break;
case '$':
eof = read_viminfo_sub_string(virp, forceit);
break;
case ':':
case '?':
case '=':
case '@':
eof = read_viminfo_history(virp, writing);
break;
case '-':
case '\'':
eof = read_viminfo_filemark(virp, forceit);
break;
default:
if (viminfo_error("E575: ", _("Illegal starting char"),
virp->vir_line))
eof = TRUE;
else
eof = viminfo_readline(virp);
break;
}
}
/* Finish reading history items. */
if (!writing)
finish_viminfo_history();
/* Change file names to buffer numbers for fmarks. */
FOR_ALL_BUFFERS(buf) {
fmarks_check_names(buf);
}
return eof;
}
/*
* Compare the 'encoding' value in the viminfo file with the current value of
* 'encoding'. If different and the 'c' flag is in 'viminfo', setup for
* conversion of text with iconv() in viminfo_readstring().
*/
static int viminfo_encoding(vir_T *virp)
{
char_u *p;
int i;
if (get_viminfo_parameter('c') != 0) {
p = vim_strchr(virp->vir_line, '=');
if (p != NULL) {
/* remove trailing newline */
++p;
for (i = 0; vim_isprintc(p[i]); ++i)
;
p[i] = NUL;
convert_setup(&virp->vir_conv, p, p_enc);
}
}
return viminfo_readline(virp);
}
/*
* Read a line from the viminfo file.
* Returns TRUE for end-of-file;
*/
int viminfo_readline(vir_T *virp)
{
return vim_fgets(virp->vir_line, LSIZE, virp->vir_fd);
}
/*
* check string read from viminfo file
* remove '\n' at the end of the line
* - replace CTRL-V CTRL-V with CTRL-V
* - replace CTRL-V 'n' with '\n'
*
* Check for a long line as written by viminfo_writestring().
*
* Return the string in allocated memory.
*/
char_u *
viminfo_readstring (
vir_T *virp,
int off, /* offset for virp->vir_line */
int convert /* convert the string */
)
FUNC_ATTR_NONNULL_RET
{
char_u *retval;
char_u *s, *d;
if (virp->vir_line[off] == Ctrl_V && ascii_isdigit(virp->vir_line[off + 1])) {
ssize_t len = atol((char *)virp->vir_line + off + 1);
retval = xmalloc(len);
// TODO(philix): change type of vim_fgets() size argument to size_t
(void)vim_fgets(retval, (int)len, virp->vir_fd);
s = retval + 1; /* Skip the leading '<' */
} else {
retval = vim_strsave(virp->vir_line + off);
s = retval;
}
/* Change CTRL-V CTRL-V to CTRL-V and CTRL-V n to \n in-place. */
d = retval;
while (*s != NUL && *s != '\n') {
if (s[0] == Ctrl_V && s[1] != NUL) {
if (s[1] == 'n')
*d++ = '\n';
else
*d++ = Ctrl_V;
s += 2;
} else
*d++ = *s++;
}
*d = NUL;
if (convert && virp->vir_conv.vc_type != CONV_NONE && *retval != NUL) {
d = string_convert(&virp->vir_conv, retval, NULL);
if (d != NULL) {
xfree(retval);
retval = d;
}
}
return retval;
}
/*
* write string to viminfo file
* - replace CTRL-V with CTRL-V CTRL-V
* - replace '\n' with CTRL-V 'n'
* - add a '\n' at the end
*
* For a long line:
* - write " CTRL-V <length> \n " in first line
* - write " < <string> \n " in second line
*/
void viminfo_writestring(FILE *fd, char_u *p)
{
int c;
char_u *s;
int len = 0;
for (s = p; *s != NUL; ++s) {
if (*s == Ctrl_V || *s == '\n')
++len;
++len;
}
/* If the string will be too long, write its length and put it in the next
* line. Take into account that some room is needed for what comes before
* the string (e.g., variable name). Add something to the length for the
* '<', NL and trailing NUL. */
if (len > LSIZE / 2)
fprintf(fd, "\026%d\n<", len + 3);
while ((c = *p++) != NUL) {
if (c == Ctrl_V || c == '\n') {
putc(Ctrl_V, fd);
if (c == '\n')
c = 'n';
}
putc(c, fd);
}
putc('\n', fd);
}
void print_line_no_prefix(linenr_T lnum, int use_number, int list)
{
char_u numbuf[30];
@@ -3364,8 +2823,36 @@ int check_secure(void)
return FALSE;
}
static char_u *old_sub = NULL; /* previous substitute pattern */
static int global_need_beginline; /* call beginline() after ":g" */
/// Previous substitute replacement string
static SubReplacementString old_sub = {NULL, 0, NULL};
static int global_need_beginline; // call beginline() after ":g"
/// Get old substitute replacement string
///
/// @param[out] ret_sub Location where old string will be saved.
void sub_get_replacement(SubReplacementString *const ret_sub)
FUNC_ATTR_NONNULL_ALL
{
*ret_sub = old_sub;
}
/// Set substitute string and timestamp
///
/// @warning `sub` must be in allocated memory. It is not copied.
///
/// @param[in] sub New replacement string.
void sub_set_replacement(SubReplacementString sub)
{
xfree(old_sub.sub);
if (sub.additional_elements != old_sub.additional_elements) {
if (old_sub.additional_elements != NULL) {
api_free_array(*old_sub.additional_elements);
xfree(old_sub.additional_elements);
}
}
old_sub = sub;
}
/* do_sub()
*
@@ -3473,16 +2960,19 @@ void do_sub(exarg_T *eap)
}
if (!eap->skip) {
xfree(old_sub);
old_sub = vim_strsave(sub);
sub_set_replacement((SubReplacementString) {
.sub = xstrdup((char *) sub),
.timestamp = os_time(),
.additional_elements = NULL,
});
}
} else if (!eap->skip) { /* use previous pattern and substitution */
if (old_sub == NULL) { /* there is no previous command */
if (old_sub.sub == NULL) { /* there is no previous command */
EMSG(_(e_nopresub));
return;
}
pat = NULL; /* search_regcomp() will use previous pattern */
sub = old_sub;
sub = (char_u *) old_sub.sub;
/* Vi compatibility quirk: repeating with ":s" keeps the cursor in the
* last column after using "$". */
@@ -4501,27 +3991,10 @@ void global_exe(char_u *cmd)
msgmore(curbuf->b_ml.ml_line_count - old_lcount);
}
int read_viminfo_sub_string(vir_T *virp, int force)
{
if (force)
xfree(old_sub);
if (force || old_sub == NULL)
old_sub = viminfo_readstring(virp, 1, TRUE);
return viminfo_readline(virp);
}
void write_viminfo_sub_string(FILE *fp)
{
if (get_viminfo_parameter('/') != 0 && old_sub != NULL) {
fputs(_("\n# Last Substitute String:\n$"), fp);
viminfo_writestring(fp, old_sub);
}
}
#if defined(EXITFREE)
void free_old_sub(void)
{
xfree(old_sub);
sub_set_replacement((SubReplacementString) {NULL, 0, NULL});
}
#endif