diff options
Diffstat (limited to 'src/nvim/shada.c')
-rw-r--r-- | src/nvim/shada.c | 3454 |
1 files changed, 3454 insertions, 0 deletions
diff --git a/src/nvim/shada.c b/src/nvim/shada.c new file mode 100644 index 0000000000..73aaa311b4 --- /dev/null +++ b/src/nvim/shada.c @@ -0,0 +1,3454 @@ +#include <stddef.h> +#include <stdbool.h> +#include <string.h> +#include <stdint.h> +#include <inttypes.h> +#include <errno.h> +#include <assert.h> + +#include <msgpack.h> + +#include "nvim/os/os.h" +#include "nvim/os/time.h" +#include "nvim/vim.h" +#include "nvim/ascii.h" +#include "nvim/shada.h" +#include "nvim/message.h" +#include "nvim/globals.h" +#include "nvim/macros.h" +#include "nvim/memory.h" +#include "nvim/mark.h" +#include "nvim/ops.h" +#include "nvim/garray.h" +#include "nvim/option.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/globals.h" +#include "nvim/buffer.h" +#include "nvim/misc2.h" +#include "nvim/ex_getln.h" +#include "nvim/search.h" +#include "nvim/eval.h" +#include "nvim/eval_defs.h" +#include "nvim/version.h" +#include "nvim/path.h" + +#define buflist_nr2name(...) ((char *) buflist_nr2name(__VA_ARGS__)) +#define copy_option_part(src, dest, ...) \ + ((char *) copy_option_part((char_u **) src, (char_u *) dest, __VA_ARGS__)) +#define find_viminfo_parameter(...) \ + ((const char *) find_viminfo_parameter(__VA_ARGS__)) +#define emsg2(a, b) emsg2((char_u *) a, (char_u *) b) +#define emsg3(a, b, c) emsg3((char_u *) a, (char_u *) b, (char_u *) c) +#define emsgn(a, ...) emsgn((char_u *) a, __VA_ARGS__) +#define home_replace_save(a, b) \ + ((char *)home_replace_save(a, (char_u *)b)) + +/// Possible ShaDa entry types +/// +/// @warning Enum values are part of the API and must not be altered. +/// +/// All values that are not in enum are ignored. +typedef enum { + kSDItemUnknown = -1, ///< Unknown item. + kSDItemMissing = 0, ///< Missing value. Should never appear in a file. + kSDItemHeader = 1, ///< Header. Present for debugging purposes. + kSDItemSearchPattern = 2, ///< Last search pattern (*not* history item). + ///< Comes from user searches (e.g. when typing + ///< "/pat") or :substitute command calls. + kSDItemSubString = 3, ///< Last substitute replacement string. + kSDItemHistoryEntry = 4, ///< History item. + kSDItemRegister = 5, ///< Register. + kSDItemVariable = 6, ///< Global variable. + kSDItemGlobalMark = 7, ///< Global mark definition. + kSDItemJump = 8, ///< Item from jump list. + kSDItemBufferList = 9, ///< Buffer list. + kSDItemLocalMark = 10, ///< Buffer-local mark. +#define SHADA_LAST_ENTRY ((uint64_t) kSDItemLocalMark) +} ShadaEntryType; + +/// Structure defining a single ShaDa file entry +typedef struct { + ShadaEntryType type; + Timestamp timestamp; + union { + Dictionary header; + struct shada_filemark { + char name; + pos_T mark; + char *fname; + Dictionary *additional_data; + } filemark; + struct search_pattern { + bool magic; + bool smartcase; + bool has_line_offset; + bool place_cursor_at_end; + int64_t offset; + bool is_last_used; + bool is_substitute_pattern; + char *pat; + Dictionary *additional_data; + } search_pattern; + struct history_item { + uint8_t histtype; + char *string; + Array *additional_elements; + } history_item; + struct reg { + char name; + uint8_t type; + char **contents; + size_t contents_size; + size_t width; + Dictionary *additional_data; + } reg; + struct global_var { + char *name; + Object value; + Array *additional_elements; + } global_var; + struct { + uint64_t type; + char *contents; + size_t size; + } unknown_item; + struct sub_string { + char *sub; + Array *additional_elements; + } sub_string; + Array buffer_list; + } data; +} ShadaEntry; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "shada.c.generated.h" +#endif + +/// Msgpack callback for writing to FILE* +static int msgpack_fbuffer_write(void *data, const char *buf, size_t len) +{ + return (fwrite(buf, len, 1, (FILE *) data) == 1) ? 0 : -1; +} + +/// Check whether writing to shada file was disabled with -i NONE +/// +/// @return true if it was disabled, false otherwise. +static bool shada_disabled(void) + FUNC_ATTR_PURE +{ + return used_shada_file != NULL && STRCMP(used_shada_file, "NONE") == 0; +} + +/// Read ShaDa file +/// +/// @param[in] file File to read or NULL to use default name. +/// @param[in] flags Flags, see kShaDa enum values in shada.h. +/// +/// @return FAIL if reading failed for some reason and OK otherwise. +int shada_read_file(const char *const file, const int flags) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + FILE *fp; + + if (shada_disabled()) { + return FAIL; + } + + char *const fname = shada_filename(file); + fp = mch_fopen(fname, READBIN); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Reading viminfo file \"%s\"%s%s%s"), + fname, + (flags & kShaDaWantInfo) ? _(" info") : "", + (flags & kShaDaWantMarks) ? _(" marks") : "", + (flags & kShaDaGetOldfiles) ? _(" oldfiles") : "", + fp == NULL ? _(" FAILED") : ""); + verbose_leave(); + } + + xfree(fname); + if (fp == NULL) { + return FAIL; + } + + shada_read(fp, flags); + + fclose(fp); + return OK; +} + +/// Read data from ShaDa file +/// +/// @param[in] fp File to read from. +/// @param[in] flags What to read. +static void shada_read(FILE *const fp, const int flags) + FUNC_ATTR_NONNULL_ALL +{ + ShadaEntry cur_entry; + buf_T *local_mark_prev_buf = NULL; + char *local_mark_prev_fname = NULL; + size_t local_mark_prev_fname_len = 0; + while (shada_read_next_item(fp, &cur_entry) == NOTDONE) { + switch (cur_entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + break; + } + case kSDItemHeader: { + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemSearchPattern: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + (cur_entry.data.search_pattern.is_substitute_pattern + ? &set_substitute_pattern + : &set_search_pattern)((SearchPattern) { + .magic = cur_entry.data.search_pattern.magic, + .no_scs = !cur_entry.data.search_pattern.smartcase, + .off = { + .line = cur_entry.data.search_pattern.has_line_offset, + .end = cur_entry.data.search_pattern.place_cursor_at_end, + .off = cur_entry.data.search_pattern.offset, + }, + .pat = (char_u *) cur_entry.data.search_pattern.pat, + .additional_data = cur_entry.data.search_pattern.additional_data, + .timestamp = cur_entry.timestamp, + }); + if (cur_entry.data.search_pattern.is_last_used) { + set_last_used_pattern( + cur_entry.data.search_pattern.is_substitute_pattern); + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemSubString: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + sub_set_replacement((SubReplacementString) { + .sub = cur_entry.data.sub_string.sub, + .timestamp = cur_entry.timestamp, + .additional_elements = cur_entry.data.sub_string.additional_elements, + }); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemHistoryEntry: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + // FIXME + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemRegister: { + if (!(flags & kShaDaWantInfo)) { + shada_free_shada_entry(&cur_entry); + break; + } + if (cur_entry.data.reg.type != MCHAR + && cur_entry.data.reg.type != MLINE + && cur_entry.data.reg.type != MBLOCK) { + shada_free_shada_entry(&cur_entry); + break; + } + register_set(cur_entry.data.reg.name, (yankreg_T) { + .y_array = (char_u **) cur_entry.data.reg.contents, + .y_size = (linenr_T) cur_entry.data.reg.contents_size, + .y_type = cur_entry.data.reg.type, + .y_width = (colnr_T) cur_entry.data.reg.width, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.reg.additional_data, + }); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemVariable: { + if (!(flags & kShaDaWantInfo) || find_viminfo_parameter('!') == NULL) { + shada_free_shada_entry(&cur_entry); + break; + } + typval_T vartv; + Error err; + if (!object_to_vim(cur_entry.data.global_var.value, &vartv, &err)) { + if (err.set) { + emsg3("Error while reading ShaDa file: " + "failed to read value for variable %s: %s", + cur_entry.data.global_var.name, err.msg); + } + break; + } + var_set_global(cur_entry.data.global_var.name, vartv); + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemGlobalMark: { + if (!(flags & kShaDaWantMarks) || get_viminfo_parameter('f') == 0) { + shada_free_shada_entry(&cur_entry); + break; + } + mark_set_global(cur_entry.data.filemark.name, (xfmark_T) { + .fname = (char_u *) cur_entry.data.filemark.fname, + .fmark = { + .mark = cur_entry.data.filemark.mark, + .fnum = 0, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }, + }, !(flags & kShaDaForceit)); + break; + } + case kSDItemJump: { + // FIXME + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemBufferList: { + // FIXME + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemLocalMark: { + if (!(flags & kShaDaWantMarks)) { + shada_free_shada_entry(&cur_entry); + break; + } + buf_T *buf = NULL; + if (local_mark_prev_fname != NULL + && strncmp(local_mark_prev_fname, + cur_entry.data.filemark.fname, + local_mark_prev_fname_len) == 0) { + buf = local_mark_prev_buf; + } else { + FOR_ALL_BUFFERS(bp) { + if (bp->b_ffname != NULL && bp != local_mark_prev_buf) { + if (fnamecmp(cur_entry.data.filemark.fname, bp->b_ffname) == 0) { + buf = bp; + break; + } + } + } + xfree(local_mark_prev_fname); + local_mark_prev_buf = buf; + local_mark_prev_fname_len = strlen(cur_entry.data.filemark.fname); + local_mark_prev_fname = xmemdupz(cur_entry.data.filemark.fname, + local_mark_prev_fname_len); + } + if (buf == NULL) { + break; + } + mark_set_local( + cur_entry.data.filemark.name, buf, + (fmark_T) { + .mark = cur_entry.data.filemark.mark, + .fnum = 0, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }, !(flags & kShaDaForceit)); + free(cur_entry.data.filemark.fname); + break; + } + } + } + xfree(local_mark_prev_fname); + FOR_ALL_BUFFERS(buf) { + fmarks_check_names(buf); + } +} + +/// Get the ShaDa 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. +/// +/// @param[in] file Forced file name or NULL. +/// +/// @return An allocated string containing shada file name. +static char *shada_filename(const char *file) + FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (file == NULL || *file == NUL) { + if (used_shada_file != NULL) { + file = used_shada_file; + } else if ((file = find_viminfo_parameter('n')) == NULL || *file == NUL) { +#ifdef SHADA_FILE2 + // don't use $HOME when not defined (turned into "c:/"!). + if (os_getenv((char_u *)"HOME") == NULL) { + // don't use $VIM when not available. + expand_env((char_u *)"$VIM", NameBuff, MAXPATHL); + if (STRCMP("$VIM", NameBuff) != 0) { // $VIM was expanded + file = SHADA_FILE2; + } else { + file = SHADA_FILE; + } + } else { +#endif + file = SHADA_FILE; +#ifdef SHADA_FILE2 + } +#endif + // XXX It used to be one level lower, so that whatever is in + // `used_shada_file` was expanded. I intentionally moved it here + // because various expansions must have already be done by the shell. + // If shell is not performing them then they should be done in main.c + // where arguments are parsed, *not here*. + expand_env((char_u *)file, &(NameBuff[0]), MAXPATHL); + file = (const char *) &(NameBuff[0]); + } + } + return xstrdup(file); +} + +#define PACK_STATIC_STR(s) \ + do { \ + msgpack_pack_str(spacker, sizeof(s) - 1); \ + msgpack_pack_str_body(spacker, s, sizeof(s) - 1); \ + } while (0) + +/// Write single ShaDa entry +/// +/// @param[in] packer Packer used to write entry. +/// @param[in] entry Entry written. +/// @param[in] max_kbyte Maximum size of an item in KiB. Zero means no +/// restrictions. +static void shada_pack_entry(msgpack_packer *const packer, + const ShadaEntry entry, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL +{ + if (entry.type == kSDItemMissing) { + return; + } + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer *spacker = msgpack_packer_new(&sbuf, &msgpack_sbuffer_write); + switch (entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + msgpack_pack_uint64(packer, (uint64_t) entry.data.unknown_item.size); + packer->callback(packer->data, entry.data.unknown_item.contents, + (unsigned) entry.data.unknown_item.size); + break; + } + case kSDItemHistoryEntry: { + const size_t arr_size = 2 + ( + entry.data.history_item.additional_elements == NULL + ? 0 + : entry.data.history_item.additional_elements->size); + msgpack_pack_array(spacker, arr_size); + msgpack_pack_uint8(spacker, entry.data.history_item.histtype); + msgpack_rpc_from_string(cstr_as_string(entry.data.history_item.string), + spacker); + for (size_t i = 0; i < arr_size - 2; i++) { + msgpack_rpc_from_object( + entry.data.history_item.additional_elements->items[i], spacker); + } + break; + } + case kSDItemVariable: { + const size_t arr_size = 2 + ( + entry.data.global_var.additional_elements == NULL + ? 0 + : entry.data.global_var.additional_elements->size); + msgpack_pack_array(spacker, arr_size); + msgpack_rpc_from_string(cstr_as_string(entry.data.global_var.name), + spacker); + msgpack_rpc_from_object(entry.data.global_var.value, spacker); + for (size_t i = 0; i < arr_size - 2; i++) { + msgpack_rpc_from_object( + entry.data.global_var.additional_elements->items[i], spacker); + } + break; + } + case kSDItemSubString: { + const size_t arr_size = 1 + ( + entry.data.sub_string.additional_elements == NULL + ? 0 + : entry.data.sub_string.additional_elements->size); + msgpack_pack_array(spacker, arr_size); + msgpack_rpc_from_string(cstr_as_string(entry.data.sub_string.sub), + spacker); + for (size_t i = 0; i < arr_size - 1; i++) { + msgpack_rpc_from_object( + entry.data.sub_string.additional_elements->items[i], spacker); + } + break; + } + case kSDItemSearchPattern: { + const size_t map_size = (size_t) ( + 1 // Search pattern is always present + // Following items default to true: + + !entry.data.search_pattern.magic + + !entry.data.search_pattern.is_last_used + // Following items default to false: + + entry.data.search_pattern.smartcase + + entry.data.search_pattern.has_line_offset + + entry.data.search_pattern.place_cursor_at_end + + entry.data.search_pattern.is_substitute_pattern + // offset defaults to zero: + + (entry.data.search_pattern.offset != 0) + // finally, additional data: + + (entry.data.search_pattern.additional_data + ? entry.data.search_pattern.additional_data->size + : 0) + ); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR("pat"); + msgpack_rpc_from_string(cstr_as_string(entry.data.search_pattern.pat), + spacker); +#define PACK_BOOL(name, attr, nondef_value) \ + do { \ + if (entry.data.search_pattern.attr == nondef_value) { \ + PACK_STATIC_STR(name); \ + msgpack_pack_##nondef_value(spacker); \ + } \ + } while (0) + PACK_BOOL("magic", magic, false); + PACK_BOOL("islast", is_last_used, false); + PACK_BOOL("smartcase", smartcase, true); + PACK_BOOL("lineoff", has_line_offset, true); + PACK_BOOL("curatend", place_cursor_at_end, true); + PACK_BOOL("sub", is_substitute_pattern, true); + if (entry.data.search_pattern.offset) { + PACK_STATIC_STR("off"); + msgpack_pack_int64(spacker, entry.data.search_pattern.offset); + } +#undef PACK_BOOL + if (entry.data.search_pattern.additional_data != NULL) { + for (size_t i = 0; i < entry.data.search_pattern.additional_data->size; + i++) { + msgpack_rpc_from_string( + entry.data.search_pattern.additional_data->items[i].key, spacker); + msgpack_rpc_from_object( + entry.data.search_pattern.additional_data->items[i].value, + spacker); + } + } + break; + } + case kSDItemGlobalMark: + case kSDItemLocalMark: + case kSDItemJump: { + const size_t map_size = (size_t) ( + 1 // File name + // Line: defaults to 1 + + (entry.data.filemark.mark.lnum != 1) + // Column: defaults to zero: + + (entry.data.filemark.mark.col != 0) + // Mark name: defaults to '"' + + (entry.type != kSDItemJump + && entry.data.filemark.name != '"') + // Additional entries, if any: + + (entry.data.filemark.additional_data == NULL + ? 0 + : entry.data.filemark.additional_data->size) + ); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR("file"); + msgpack_rpc_from_string(cstr_as_string(entry.data.filemark.fname), + spacker); + if (entry.data.filemark.mark.lnum != 1) { + PACK_STATIC_STR("line"); + msgpack_pack_long(spacker, entry.data.filemark.mark.lnum); + } + if (entry.data.filemark.mark.col != 0) { + PACK_STATIC_STR("col"); + msgpack_pack_long(spacker, entry.data.filemark.mark.col); + } + if (entry.data.filemark.name != '"' && entry.type != kSDItemJump) { + PACK_STATIC_STR("name"); + msgpack_pack_uint8(spacker, (uint8_t) entry.data.filemark.name); + } + if (entry.data.filemark.additional_data != NULL) { + for (size_t i = 0; i < entry.data.filemark.additional_data->size; + i++) { + msgpack_rpc_from_string( + entry.data.filemark.additional_data->items[i].key, spacker); + msgpack_rpc_from_object( + entry.data.filemark.additional_data->items[i].value, spacker); + } + } + break; + } + case kSDItemRegister: { + const size_t map_size = (size_t) ( + 2 // Register contents and name + // Register type: defaults to MCHAR + + (entry.data.reg.type != MCHAR) + // Register width: defaults to zero + + (entry.data.reg.width != 0) + // Additional entries, if any: + + (entry.data.reg.additional_data == NULL + ? 0 + : entry.data.reg.additional_data->size) + ); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR("contents"); + msgpack_pack_array(spacker, entry.data.reg.contents_size); + for (size_t i = 0; i < entry.data.reg.contents_size; i++) { + msgpack_rpc_from_string(cstr_as_string(entry.data.reg.contents[i]), + spacker); + } + PACK_STATIC_STR("name"); + msgpack_pack_char(spacker, entry.data.reg.name); + if (entry.data.reg.type != MCHAR) { + PACK_STATIC_STR("type"); + msgpack_pack_uint8(spacker, entry.data.reg.type); + } + if (entry.data.reg.width != 0) { + PACK_STATIC_STR("width"); + msgpack_pack_uint64(spacker, (uint64_t) entry.data.reg.width); + } + if (entry.data.reg.additional_data != NULL) { + for (size_t i = 0; i < entry.data.reg.additional_data->size; + i++) { + msgpack_rpc_from_string(entry.data.reg.additional_data->items[i].key, + spacker); + msgpack_rpc_from_object( + entry.data.reg.additional_data->items[i].value, spacker); + } + } + break; + } + case kSDItemBufferList: { + msgpack_rpc_from_array(entry.data.buffer_list, spacker); + break; + } + case kSDItemHeader: { + msgpack_rpc_from_dictionary(entry.data.header, spacker); + break; + } + } + if (!max_kbyte || sbuf.size <= max_kbyte * 1024) { + if (entry.type == kSDItemUnknown) { + msgpack_pack_uint64(packer, (uint64_t) entry.data.unknown_item.type); + } else { + msgpack_pack_uint64(packer, (uint64_t) entry.type); + } + msgpack_pack_uint64(packer, (uint64_t) entry.timestamp); + if (sbuf.size > 0) { + msgpack_pack_uint64(packer, (uint64_t) sbuf.size); + packer->callback(packer->data, sbuf.data, (unsigned) sbuf.size); + } + } + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); +} + +/// Write ShaDa file +/// +/// @param[in] newfp File pointer to write to. Must not be NULL. +/// @param[in] oldfp Pointer to the previous ShaDa file. If it is not NULL +/// then contents of this file will be merged with current +/// NeoVim runtime. +static void shada_write(FILE *const newfp, FILE *const oldfp) + FUNC_ATTR_NONNULL_ARG(1) +{ + int max_kbyte_i = get_viminfo_parameter('s'); + if (max_kbyte_i < 0) { + max_kbyte_i = 10; + } + if (max_kbyte_i == 0) { + return; + } + const size_t max_kbyte = (size_t) max_kbyte_i; + + msgpack_packer *packer = msgpack_packer_new(newfp, &msgpack_fbuffer_write); + + // First write values that do not require merging + // 1. Header + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemHeader, + .timestamp = os_time(), + .data = { + .header = { + .size = 3, + .capacity = 3, + .items = ((KeyValuePair []) { + { STATIC_CSTR_AS_STRING("version"), + STRING_OBJ(cstr_as_string(longVersion)) }, + { STATIC_CSTR_AS_STRING("max_kbyte"), + INTEGER_OBJ((Integer) max_kbyte) }, + { STATIC_CSTR_AS_STRING("pid"), + INTEGER_OBJ((Integer) os_get_pid()) }, + }), + } + } + }, 0); + fflush(newfp); + + // 2. Buffer list + if (find_viminfo_parameter('%') != NULL) { + msgpack_pack_uint64(packer, (uint64_t) kSDItemBufferList); + msgpack_pack_uint64(packer, (uint64_t) os_time()); + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer *spacker = msgpack_packer_new(&sbuf, &msgpack_sbuffer_write); + + size_t buf_count = 0; + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL) { + buf_count++; + } + } + msgpack_pack_array(packer, buf_count); + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL) { + continue; + } + msgpack_pack_map(packer, 3); + PACK_STATIC_STR("fname"); + msgpack_rpc_from_string(cstr_as_string((char *) buf->b_ffname), packer); + PACK_STATIC_STR("lnum"); + msgpack_pack_uint64(packer, (uint64_t) buf->b_last_cursor.mark.lnum); + PACK_STATIC_STR("col"); + msgpack_pack_uint64(packer, (uint64_t) buf->b_last_cursor.mark.col); + } + msgpack_pack_uint64(packer, (uint64_t) sbuf.size); + packer->callback(packer->data, sbuf.data, (unsigned) sbuf.size); + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); + } + + // 3. Jump list + const void *jump_iter = NULL; + do { + xfmark_T fm; + jump_iter = mark_jumplist_iter(jump_iter, curwin, &fm); + char *fname = (fm.fmark.fnum == 0 + ? (fm.fname == NULL + ? NULL + : xstrdup((char *) fm.fname)) + : buflist_nr2name(fm.fmark.fnum, true, false)); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemJump, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .name = NUL, + .mark = fm.fmark.mark, + .fname = fname, + .additional_data = fm.fmark.additional_data, + } + } + }, max_kbyte); + xfree(fname); + } while (jump_iter != NULL); + + // FIXME No merging currently + + // 4. History + const void *hist_iters[HIST_COUNT] = {NULL, NULL, NULL, NULL, NULL}; + for (uint8_t i = 0; i < HIST_COUNT; i++) { + do { + histentry_T cur_hist; + hist_iters[i] = hist_iter(hist_iters[i], i, false, &cur_hist); + if (cur_hist.hisstr == NULL) { + break; + } + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemHistoryEntry, + .timestamp = cur_hist.timestamp, + .data = { + .history_item = { + .histtype = i, + .string = (char *) cur_hist.hisstr, + .additional_elements = cur_hist.additional_elements, + } + } + }, max_kbyte); + } while (hist_iters[i] != NULL); + } + + // 5. Search patterns + SearchPattern pat; + get_search_pattern(&pat); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemSearchPattern, + .timestamp = pat.timestamp, + .data = { + .search_pattern = { + .magic = pat.magic, + .smartcase = !pat.no_scs, + .has_line_offset = pat.off.line, + .place_cursor_at_end = pat.off.end, + .offset = pat.off.off, + .is_last_used = search_was_last_used(), + .is_substitute_pattern = false, + .pat = (char *) pat.pat, + .additional_data = pat.additional_data, + } + } + }, max_kbyte); + get_substitute_pattern(&pat); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemSearchPattern, + .timestamp = pat.timestamp, + .data = { + .search_pattern = { + .magic = pat.magic, + .smartcase = !pat.no_scs, + .has_line_offset = false, + .place_cursor_at_end = false, + .offset = 0, + .is_last_used = !search_was_last_used(), + .is_substitute_pattern = true, + .pat = (char *) pat.pat, + .additional_data = pat.additional_data, + } + } + }, max_kbyte); + + // 6. Substitute string + SubReplacementString sub; + sub_get_replacement(&sub); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemSubString, + .timestamp = sub.timestamp, + .data = { + .sub_string = { + .sub = (char *) sub.sub, + .additional_elements = sub.additional_elements, + } + } + }, max_kbyte); + + // 7. Global marks + if (get_viminfo_parameter('f') != 0) { + ShadaEntry *const global_marks = list_global_marks(); + for (ShadaEntry *mark = global_marks; mark->type != kSDItemMissing; mark++) { + shada_pack_entry(packer, *mark, max_kbyte); + } + xfree(global_marks); + } + + // 8. Buffer marks + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL) { + continue; + } + ShadaEntry *const buffer_marks = list_buffer_marks(buf); + for (ShadaEntry *mark = buffer_marks; mark->type != kSDItemMissing; + mark++) { + shada_pack_entry(packer, *mark, max_kbyte); + } + xfree(buffer_marks); + } + // FIXME: Copy previous marks, up to num_marked_files + // size_t num_marked_files = get_viminfo_parameter('\''); + + // 9. Registers + int max_num_lines_i = get_viminfo_parameter('<'); + if (max_num_lines_i < 0) { + max_num_lines_i = get_viminfo_parameter('"'); + } + if (max_num_lines_i != 0) { + const size_t max_num_lines = (max_num_lines_i < 0 + ? 0 + : (size_t) max_num_lines_i); + ShadaEntry *const registers = list_registers(max_num_lines); + for (ShadaEntry *reg = registers; reg->type != kSDItemMissing; reg++) { + shada_pack_entry(packer, *reg, max_kbyte); + } + xfree(registers); + } + + // 10. Variables + if (find_viminfo_parameter('!') != NULL) { + const void *var_iter = NULL; + const Timestamp cur_timestamp = os_time(); + do { + typval_T vartv; + const char *name; + var_iter = var_shada_iter(var_iter, &name, &vartv); + if (var_iter == NULL && vartv.v_type == VAR_UNKNOWN) { + break; + } + Object obj = vim_to_object(&vartv); + shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemVariable, + .timestamp = cur_timestamp, + .data = { + .global_var = { + .name = (char *) name, + .value = obj, + .additional_elements = NULL, + } + } + }, max_kbyte); + api_free_object(obj); + clear_tv(&vartv); + } while (var_iter != NULL); + } + + msgpack_packer_free(packer); +} + +#undef PACK_STATIC_STR + +/// Write ShaDa file to a given location +/// +/// @param[in] fname File to write to. If it is NULL or empty then default +/// location is used. +/// @param[in] nomerge If true then old file is ignored. +/// +/// @return OK if writing was successfull, FAIL otherwise. +int shada_write_file(const char *const file, const bool nomerge) +{ + char *const fname = shada_filename(file); + FILE *wfp = mch_fopen(fname, WRITEBIN); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Writing viminfo file \"%s\""), fname); + verbose_leave(); + } + + xfree(fname); + if (wfp == NULL) { + return FAIL; + } + + shada_write(wfp, NULL); + + fclose(wfp); + return OK; +} + +/// Read marks information from ShaDa file +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_marks(void) +{ + return shada_read_file(NULL, kShaDaWantMarks); +} + +/// Read all information from ShaDa file +/// +/// @param[in] fname File to write to. If it is NULL or empty then default +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_everything(const char *const fname, const bool forceit) +{ + return shada_read_file(fname, + kShaDaWantInfo|kShaDaWantMarks|kShaDaGetOldfiles + |(forceit?kShaDaForceit:0)); +} + +static void shada_free_shada_entry(ShadaEntry *const entry) +{ + if (entry == NULL) { + return; + } + switch (entry->type) { + case kSDItemMissing: { + break; + } + case kSDItemUnknown: { + xfree(entry->data.unknown_item.contents); + break; + } + case kSDItemHeader: { + api_free_dictionary(entry->data.header); + break; + } + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + if (entry->data.filemark.additional_data != NULL) { + api_free_dictionary(*entry->data.filemark.additional_data); + xfree(entry->data.filemark.additional_data); + } + xfree(entry->data.filemark.fname); + break; + } + case kSDItemSearchPattern: { + if (entry->data.search_pattern.additional_data != NULL) { + api_free_dictionary(*entry->data.search_pattern.additional_data); + xfree(entry->data.search_pattern.additional_data); + } + xfree(entry->data.search_pattern.pat); + break; + } + case kSDItemRegister: { + if (entry->data.reg.additional_data != NULL) { + api_free_dictionary(*entry->data.reg.additional_data); + xfree(entry->data.reg.additional_data); + } + for (size_t i = 0; i < entry->data.reg.contents_size; i++) { + xfree(entry->data.reg.contents[i]); + } + xfree(entry->data.reg.contents); + break; + } + case kSDItemHistoryEntry: { + if (entry->data.history_item.additional_elements != NULL) { + api_free_array(*entry->data.history_item.additional_elements); + xfree(entry->data.history_item.additional_elements); + } + xfree(entry->data.history_item.string); + break; + } + case kSDItemVariable: { + if (entry->data.global_var.additional_elements != NULL) { + api_free_array(*entry->data.global_var.additional_elements); + xfree(entry->data.global_var.additional_elements); + } + xfree(entry->data.global_var.name); + api_free_object(entry->data.global_var.value); + break; + } + case kSDItemSubString: { + if (entry->data.sub_string.additional_elements != NULL) { + api_free_array(*entry->data.sub_string.additional_elements); + xfree(entry->data.sub_string.additional_elements); + } + xfree(entry->data.sub_string.sub); + break; + } + case kSDItemBufferList: { + api_free_array(entry->data.buffer_list); + break; + } + } +} + +/// Read next unsigned integer from file +/// +/// Errors out if the result is not an unsigned integer. +/// +/// Unlike msgpack own function this one works with `FILE *` and reads *exactly* +/// as much bytes as needed, making it possible to avoid both maintaining own +/// buffer and calling `fseek`. +/// +/// One byte from file stream is always consumed, even if it is not correct. +/// +/// @param[in] fp File to read from. +/// @param[out] result Location where result is saved. +/// +/// @return OK if read was successfull, FAIL if it was not. +static int msgpack_read_uint64(FILE *const fp, const int first_char, + uint64_t *const result) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const long fpos = ftell(fp) - 1; + + if (first_char == EOF) { + if (ferror(fp)) { + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + } else if (feof(fp)) { + emsgn("Error while reading ShaDa file: " + "expected positive integer at position %" PRId64, + (int64_t) fpos); + } + return FAIL; + } + + if (~first_char & 0x80) { + // Positive fixnum + *result = (uint64_t) ((uint8_t) first_char); + } else { + size_t length = 0; + switch (first_char) { + case 0xCC: { // uint8 + length = 1; + break; + } + case 0xCD: { // uint16 + length = 2; + break; + } + case 0xCE: { // uint32 + length = 4; + break; + } + case 0xCF: { // uint64 + length = 8; + break; + } + default: { + emsgn("Error while reading ShaDa file: " + "expected positive integer at position %" PRId64, + (int64_t) fpos); + return FAIL; + } + } + uint8_t buf[8]; + size_t read_bytes = fread((char *) &(buf[0]), 1, length, fp); + if (ferror(fp)) { + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + return FAIL; + } else if (read_bytes != length) { + emsgn("Error while reading ShaDa file: " + "not enough bytes for positive integer at position %" PRId64, + (int64_t) fpos); + return FAIL; + } + // TODO(ZyX-I): Just cast if current platform is big-endian. + *result = 0; + for (size_t i = length; i; i--) { + *result |= ((uint64_t) buf[i - 1]) << ((length - i) * 8); + } + } + return OK; +} + +/// Iterate over shada file contents +/// +/// @param[in] fp Pointer to the opened ShaDa file. +/// @param[out] entry Address where next entry contents will be saved. +/// +/// @return NOTDONE if entry was read correctly, FAIL if there were errors and +/// OK at EOF. +static int shada_read_next_item(FILE *const fp, ShadaEntry *const entry) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + entry->type = kSDItemMissing; + if (feof(fp)) { + return OK; + } + + // First: manually unpack type, timestamp and length. + // This is needed to avoid both seeking and having to maintain a buffer. + uint64_t type_u64; + uint64_t timestamp_u64; + uint64_t length_u64; + + const long initial_fpos = ftell(fp); + const int first_char = fgetc(fp); + if (first_char == EOF && feof(fp)) { + return OK; + } + + if (msgpack_read_uint64(fp, first_char, &type_u64) != OK + || msgpack_read_uint64(fp, fgetc(fp), ×tamp_u64) != OK + || msgpack_read_uint64(fp, fgetc(fp), &length_u64) != OK) { + return FAIL; + } + if (type_u64 == 0) { + emsgn("Error while reading ShaDa file: " + "entry at position %" PRId64 "has invalid zero type", + (int64_t) initial_fpos); + return FAIL; + } + + const size_t length = (size_t) length_u64; + entry->timestamp = (Timestamp) timestamp_u64; + if (type_u64 > SHADA_LAST_ENTRY) { + entry->data.unknown_item.size = length; + char *contents = xmalloc(length); + entry->data.unknown_item.contents = contents; + entry->data.unknown_item.type = type_u64; + size_t read_bytes = fread(contents, 1, length, fp); + if (ferror(fp)) { + free(entry->data.unknown_item.contents); + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + return FAIL; + } else if (read_bytes != length) { + free(entry->data.unknown_item.contents); + emsgn("Error while reading ShaDa file: " + "last entry specified that it occupies %" PRId64 " bytes, " + "but file ended earlier", + (int64_t) length); + return FAIL; + } + entry->type = kSDItemUnknown; + return NOTDONE; + } + + msgpack_unpacked unpacked; + msgpack_unpacked_init(&unpacked); + msgpack_unpacker *unpacker = msgpack_unpacker_new(length); + if (unpacker == NULL || + !msgpack_unpacker_reserve_buffer(unpacker, length)) { + EMSG(e_outofmem); + goto shada_read_next_item_error; + } + msgpack_unpack_return result; + size_t read_bytes = 0; + + const long fpos = ftell(fp); + read_bytes = fread(msgpack_unpacker_buffer(unpacker), 1, length, fp); + if (ferror(fp)) { + emsg2("System error while reading ShaDa file: %s", + strerror(errno)); + goto shada_read_next_item_error; + } else if (read_bytes != length) { + emsgn("Error while reading ShaDa file: " + "last entry specified that it occupies %" PRId64 " bytes, " + "but file ended earlier", + (int64_t) fpos); + goto shada_read_next_item_error; + } + msgpack_unpacker_buffer_consumed(unpacker, read_bytes); + + result = msgpack_unpacker_next(unpacker, &unpacked); + if (result != MSGPACK_UNPACK_SUCCESS) { + if (result == MSGPACK_UNPACK_NOMEM_ERROR) { + EMSG(e_outofmem); + goto shada_read_next_item_error; + } + if (result == MSGPACK_UNPACK_PARSE_ERROR) { + EMSG("Failed to parse ShaDa file"); + goto shada_read_next_item_error; + } + } +#define CHECK_KEY(key, expected) \ + (key.via.str.size == sizeof(expected) - 1 \ + && STRNCMP(key.via.str.ptr, expected, sizeof(expected) - 1) == 0) +#define ID(s) s +#define BINDUP(b) xmemdupz(b.ptr, b.size) +#define TOINT(s) ((int) (s)) +#define TOLONG(s) ((long) (s)) +#define TOCHAR(s) ((char) (s)) +#define TOU8(s) ((uint8_t) (s)) +#define TOSIZE(s) ((size_t) (s)) +#define CHECKED_ENTRY(condition, error_desc, entry_name, obj, tgt, attr, \ + proc) \ + do { \ + if (!(condition)) { \ + emsgn("Error while reading ShaDa file: " \ + entry_name " entry at position %" PRId64 " " \ + error_desc, \ + (int64_t) fpos); \ + ga_clear(&ad_ga); \ + goto shada_read_next_item_error; \ + } \ + tgt = proc(obj.via.attr); \ + } while (0) +#define CHECK_KEY_IS_STR(entry_name) \ + do { \ + if (unpacked.data.via.map.ptr[i].key.type != MSGPACK_OBJECT_STR) { \ + emsgn("Error while reading ShaDa file: " \ + entry_name " entry at position %" PRId64 " " \ + "has key which is not a string", \ + (int64_t) fpos); \ + emsgn("It is %" PRId64 " instead", \ + unpacked.data.via.map.ptr[i].key.type ); \ + ga_clear(&ad_ga); \ + goto shada_read_next_item_error; \ + } \ + } while (0) +#define CHECKED_KEY(entry_name, name, error_desc, tgt, condition, attr, proc) \ + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, name)) { \ + CHECKED_ENTRY( \ + condition, "has " name " key value " error_desc, \ + entry_name, unpacked.data.via.map.ptr[i].val, \ + tgt, attr, proc); \ + } +#define TYPED_KEY(entry_name, name, type_name, tgt, objtype, attr, proc) \ + CHECKED_KEY( \ + entry_name, name, " which is not " type_name, tgt, \ + unpacked.data.via.map.ptr[i].val.type == MSGPACK_OBJECT_##objtype, \ + attr, proc) +#define BOOLEAN_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a boolean", tgt, BOOLEAN, boolean, ID) +#define STRING_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a binary", tgt, BIN, bin, BINDUP) +#define INT_KEY(entry_name, name, tgt, proc) \ + CHECKED_KEY( \ + entry_name, name, " which is not an integer", tgt, \ + (unpacked.data.via.map.ptr[i].val.type \ + == MSGPACK_OBJECT_POSITIVE_INTEGER \ + || unpacked.data.via.map.ptr[i].val.type \ + == MSGPACK_OBJECT_NEGATIVE_INTEGER), \ + i64, proc) +#define INTEGER_KEY(entry_name, name, tgt) \ + INT_KEY(entry_name, name, tgt, TOINT) +#define LONG_KEY(entry_name, name, tgt) \ + INT_KEY(entry_name, name, tgt, TOLONG) +#define ADDITIONAL_KEY \ + { \ + ga_grow(&ad_ga, 1); \ + memcpy(((char *)ad_ga.ga_data) + ((size_t) ad_ga.ga_len \ + * sizeof(*unpacked.data.via.map.ptr)),\ + unpacked.data.via.map.ptr + i, \ + sizeof(*unpacked.data.via.map.ptr)); \ + ad_ga.ga_len++; \ + } + switch ((ShadaEntryType) type_u64) { + case kSDItemHeader: { + if (!msgpack_rpc_to_dictionary(&(unpacked.data), &(entry->data.header))) { + emsgn("Error while reading ShaDa file: " + "header entry at position %" PRId64 " is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + break; + } + case kSDItemSearchPattern: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgn("Error while reading ShaDa file: " + "search pattern entry at position %" PRId64 " " + "is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.search_pattern = (struct search_pattern) { + .magic = true, + .smartcase = false, + .has_line_offset = false, + .place_cursor_at_end = false, + .offset = 0, + .is_last_used = true, + .is_substitute_pattern = false, + .pat = NULL, + .additional_data = NULL, + }; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("search pattern"); + BOOLEAN_KEY("search pattern", "magic", entry->data.search_pattern.magic) + else BOOLEAN_KEY("search pattern", "smartcase", + entry->data.search_pattern.smartcase) + else BOOLEAN_KEY("search pattern", "lineoff", + entry->data.search_pattern.has_line_offset) + else BOOLEAN_KEY("search pattern", "curatend", + entry->data.search_pattern.place_cursor_at_end) + else BOOLEAN_KEY("search pattern", "islast", + entry->data.search_pattern.is_last_used) + else BOOLEAN_KEY("search pattern", "sub", + entry->data.search_pattern.is_substitute_pattern) + else INTEGER_KEY("search pattern", "off", + entry->data.search_pattern.offset) + else STRING_KEY("search pattern", "pat", entry->data.search_pattern.pat) + else ADDITIONAL_KEY + } + if (entry->data.search_pattern.pat == NULL) { + emsgn("Error while reading ShaDa file: " + "search pattern entry at position %" PRId64 " " + "has no pattern", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (ad_ga.ga_len) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_MAP, + .via = { + .map = { + .size = (uint32_t) ad_ga.ga_len, + .ptr = ad_ga.ga_data, + } + } + }; + entry->data.search_pattern.additional_data = + xmalloc(sizeof(Dictionary)); + if (!msgpack_rpc_to_dictionary( + &obj, entry->data.search_pattern.additional_data)) { + emsgu("Error while reading ShaDa file: " + "search pattern entry at position %" PRIu64 " " + "cannot be converted to a Dictionary", + (uint64_t) initial_fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + ga_clear(&ad_ga); + break; + } + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgn("Error while reading ShaDa file: " + "mark entry at position %" PRId64 " " + "is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.filemark = (struct shada_filemark) { + .name = '"', + .mark = (pos_T) {1, 0, 0}, + .fname = NULL, + .additional_data = NULL, + }; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("mark"); + CHECKED_KEY( + "mark", "name", " which is not an unsigned integer", + entry->data.filemark.name, + (type_u64 != kSDItemJump + && unpacked.data.via.map.ptr[i].val.type + == MSGPACK_OBJECT_POSITIVE_INTEGER), + u64, TOCHAR) + else LONG_KEY("mark", "line", entry->data.filemark.mark.lnum) + else INTEGER_KEY("mark", "col", entry->data.filemark.mark.col) + else STRING_KEY("mark", "file", entry->data.filemark.fname) + else ADDITIONAL_KEY + } + if (entry->data.filemark.mark.lnum == 0) { + emsgn("Error while reading ShaDa file: " + "mark entry at position %" PRId64 " " + "is missing line number", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (ad_ga.ga_len) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_MAP, + .via = { + .map = { + .size = (uint32_t) ad_ga.ga_len, + .ptr = ad_ga.ga_data, + } + } + }; + entry->data.filemark.additional_data = xmalloc(sizeof(Dictionary)); + if (!msgpack_rpc_to_dictionary( + &obj, entry->data.filemark.additional_data)) { + emsgu("Error while reading ShaDa file: " + "mark entry at position %" PRIu64 " " + "cannot be converted to a Dictionary", + (uint64_t) initial_fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + ga_clear(&ad_ga); + break; + } + case kSDItemRegister: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "is not a dictionary", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.reg = (struct reg) { + .name = NUL, + .type = MCHAR, + .contents = NULL, + .contents_size = 0, + .width = 0, + .additional_data = NULL, + }; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("register"); + TYPED_KEY("register", "type", "an unsigned integer", + entry->data.reg.type, POSITIVE_INTEGER, u64, TOU8) + else TYPED_KEY("register", "name", "an unsigned integer", + entry->data.reg.name, POSITIVE_INTEGER, u64, TOCHAR) + else TYPED_KEY("register", "width", "an unsigned integer", + entry->data.reg.width, POSITIVE_INTEGER, u64, TOSIZE) + else if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, "contents")) { + if (unpacked.data.via.map.ptr[i].val.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has contents key with non-array value", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (unpacked.data.via.map.ptr[i].val.via.array.size == 0) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has contents key with empty array", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + const msgpack_object_array arr = + unpacked.data.via.map.ptr[i].val.via.array; + for (size_t i = 0; i < arr.size; i++) { + if (arr.ptr[i].type != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has contents array with non-string value", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + entry->data.reg.contents_size = arr.size; + entry->data.reg.contents = xmalloc(arr.size * sizeof(char *)); + for (size_t i = 0; i < arr.size; i++) { + entry->data.reg.contents[i] = xmemdupz(arr.ptr[i].via.bin.ptr, + arr.ptr[i].via.bin.size); + } + } else ADDITIONAL_KEY + } + if (entry->data.reg.contents == NULL) { + emsgn("Error while reading ShaDa file: " + "register entry at position %" PRId64 " " + "has missing contents array", + (int64_t) fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + if (ad_ga.ga_len) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_MAP, + .via = { + .map = { + .size = (uint32_t) ad_ga.ga_len, + .ptr = ad_ga.ga_data, + } + } + }; + entry->data.reg.additional_data = xmalloc(sizeof(Dictionary)); + if (!msgpack_rpc_to_dictionary( + &obj, entry->data.reg.additional_data)) { + emsgu("Error while reading ShaDa file: " + "register entry at position %" PRIu64 " " + "cannot be converted to a Dictionary", + (uint64_t) initial_fpos); + ga_clear(&ad_ga); + goto shada_read_next_item_error; + } + } + ga_clear(&ad_ga); + break; + } + case kSDItemHistoryEntry: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.history_item = (struct history_item) { + .histtype = 0, + .string = NULL, + .additional_elements = NULL + }; + if (unpacked.data.via.array.size < 2) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "does not have enough elements", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type + != MSGPACK_OBJECT_POSITIVE_INTEGER) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "has wrong history type type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type + != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "history entry at position %" PRId64 " " + "has wrong history string type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.history_item.histtype = + (uint8_t) unpacked.data.via.array.ptr[0].via.u64; + entry->data.history_item.string = + xmemdupz(unpacked.data.via.array.ptr[1].via.bin.ptr, + unpacked.data.via.array.ptr[1].via.bin.size); + if (unpacked.data.via.array.size > 2) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_ARRAY, + .via = { + .array = { + .size = unpacked.data.via.array.size - 2, + .ptr = unpacked.data.via.array.ptr + 2, + } + } + }; + entry->data.history_item.additional_elements = xmalloc(sizeof(Array)); + if (!msgpack_rpc_to_array( + &obj, entry->data.history_item.additional_elements)) { + emsgu("Error while reading ShaDa file: " + "history entry at position %" PRIu64 " " + "cannot be converted to an Array", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + break; + } + case kSDItemVariable: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.global_var = (struct global_var) { + .name = NULL, + .value = { + .type = kObjectTypeNil, + }, + .additional_elements = NULL + }; + if (unpacked.data.via.array.size < 2) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "does not have enough elements", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "has wrong variable name type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_NIL + || unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_EXT) { + emsgn("Error while reading ShaDa file: " + "variable entry at position %" PRId64 " " + "has wrong variable value type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.global_var.name = + xmemdupz(unpacked.data.via.array.ptr[0].via.bin.ptr, + unpacked.data.via.array.ptr[0].via.bin.size); + if (!msgpack_rpc_to_object(&(unpacked.data.via.array.ptr[1]), + &(entry->data.global_var.value))) { + emsgu("Error while reading ShaDa file: " + "variable entry at position %" PRIu64 " " + "has value that cannot be converted to the object", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size > 2) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_ARRAY, + .via = { + .array = { + .size = unpacked.data.via.array.size - 2, + .ptr = unpacked.data.via.array.ptr + 2, + } + } + }; + entry->data.global_var.additional_elements = xmalloc(sizeof(Array)); + if (!msgpack_rpc_to_array( + &obj, entry->data.global_var.additional_elements)) { + emsgu("Error while reading ShaDa file: " + "variable entry at position %" PRIu64 " " + "cannot be converted to an Array", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + break; + } + case kSDItemSubString: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgn("Error while reading ShaDa file: " + "sub string entry at position %" PRId64 " " + "is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.sub_string = (struct sub_string) { + .sub = NULL, + .additional_elements = NULL + }; + if (unpacked.data.via.array.size < 1) { + emsgn("Error while reading ShaDa file: " + "sub string entry at position %" PRId64 " " + "does not have enough elements", + (int64_t) fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgn("Error while reading ShaDa file: " + "sub string entry at position %" PRId64 " " + "has wrong sub string type", + (int64_t) fpos); + goto shada_read_next_item_error; + } + entry->data.sub_string.sub = + xmemdupz(unpacked.data.via.array.ptr[0].via.bin.ptr, + unpacked.data.via.array.ptr[0].via.bin.size); + if (unpacked.data.via.array.size > 1) { + msgpack_object obj = { + .type = MSGPACK_OBJECT_ARRAY, + .via = { + .array = { + .size = unpacked.data.via.array.size - 1, + .ptr = unpacked.data.via.array.ptr + 1, + } + } + }; + entry->data.sub_string.additional_elements = xmalloc(sizeof(Array)); + if (!msgpack_rpc_to_array( + &obj, entry->data.sub_string.additional_elements)) { + emsgu("Error while reading ShaDa file: " + "sub string entry at position %" PRIu64 " " + "cannot be converted to an Array", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + break; + } + case kSDItemBufferList: { + if (!msgpack_rpc_to_array(&(unpacked.data), &(entry->data.buffer_list))) { + emsgn("Error while reading ShaDa file: " + "buffer list entry at position %" PRId64 " is not an array", + (int64_t) fpos); + goto shada_read_next_item_error; + } + break; + } + case kSDItemMissing: + case kSDItemUnknown: { + emsgu("Error while reading ShaDa file: " + "there is an item at position %" PRIu64 " " + "that must not be there: Missing and Unknown items are " + "for internal uses only", + (uint64_t) initial_fpos); + goto shada_read_next_item_error; + } + } + entry->type = (ShadaEntryType) type_u64; + goto shada_read_next_item_end; +#undef CHECK_KEY +#undef BOOLEAN_KEY +#undef ADDITIONAL_KEY +#undef ID +#undef BINDUP +#undef TOCHAR +#undef TOINT +#undef TOLONG +#undef TYPED_KEY +#undef INT_KEY +#undef INTEGER_KEY +#undef LONG_KEY +#undef TOU8 +#undef TOSIZE +shada_read_next_item_error: + msgpack_unpacked_destroy(&unpacked); + msgpack_unpacker_free(unpacker); + return FAIL; +shada_read_next_item_end: + msgpack_unpacked_destroy(&unpacked); + msgpack_unpacker_free(unpacker); + return NOTDONE; +} + +/// Return a list with all global marks set in current NeoVim instance +/// +/// List is not sorted. +/// +/// @warning Listed marks must be used before any buffer- or mark-editing +/// function is run. +/// +/// @return Array of ShadaEntry values, last one has type kSDItemMissing. +/// +/// @warning Resulting ShadaEntry values must not be freed. Returned +/// array must be freed with `xfree()`. +static ShadaEntry *list_global_marks(void) +{ + const void *iter = NULL; + const size_t nummarks = mark_global_amount(); + ShadaEntry *const ret = xmalloc(sizeof(ShadaEntry) * (nummarks + 1)); + ShadaEntry *cur = ret; + if (nummarks) { + do { + cur->type = kSDItemGlobalMark; + xfmark_T cur_fm; + iter = mark_global_iter(iter, &(cur->data.filemark.name), &cur_fm); + cur->data.filemark.mark = cur_fm.fmark.mark; + cur->data.filemark.additional_data = cur_fm.fmark.additional_data; + cur->timestamp = cur_fm.fmark.timestamp; + if (cur_fm.fmark.fnum == 0) { + cur->data.filemark.fname = (char *) (cur_fm.fname == NULL + ? NULL + : cur_fm.fname); + } else { + const buf_T *const buf = buflist_findnr(cur_fm.fmark.fnum); + if (buf == NULL) { + continue; + } else { + cur->data.filemark.fname = (char *) buf->b_ffname; + } + } + if (cur->data.filemark.fname != NULL) { + if (shada_removable(cur->data.filemark.fname)) { + continue; + } else { + cur++; + } + } + } while(iter != NULL); + } + cur->type = kSDItemMissing; + return ret; +} + +/// Return a list with all buffer marks set in some buffer +/// +/// List is not sorted. +/// +/// @warning Listed marks must be used before any buffer- or mark-editing +/// function is run. +/// +/// @param[in] buf Buffer for which marks are listed. +/// +/// @return Array of ShadaEntry values, last one has type kSDItemMissing. +/// +/// @warning Resulting ShadaEntry values must not be freed. Returned +/// array must be freed with `xfree()`. +static ShadaEntry *list_buffer_marks(const buf_T *const buf) +{ + const char *const fname = (char *) buf->b_ffname; + const void *iter = NULL; + const size_t nummarks = mark_buffer_amount(buf); + ShadaEntry *const ret = xmalloc(sizeof(ShadaEntry) * (nummarks + 1)); + ShadaEntry *cur = ret; + if (nummarks) { + do { + cur->type = kSDItemLocalMark; + fmark_T cur_fm; + iter = mark_buffer_iter(iter, buf, &(cur->data.filemark.name), &cur_fm); + cur->data.filemark.mark = cur_fm.mark; + cur->data.filemark.fname = (char *) fname; + cur->data.filemark.additional_data = cur_fm.additional_data; + cur->timestamp = cur_fm.timestamp; + if (cur->data.filemark.mark.lnum != 0) { + cur++; + } + } while(iter != NULL); + } + cur->type = kSDItemMissing; + return ret; +} + +/// Check whether "name" is on removable media (according to 'viminfo') +/// +/// @param[in] name Checked name. +/// +/// @return True if it is, false otherwise. +bool shada_removable(const char *name) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + char *p; + char part[51]; + int retval = FALSE; + size_t n; + + char *new_name = home_replace_save(NULL, name); + for (p = (char *) p_viminfo; *p; ) { + (void) copy_option_part(&p, part, 51, ", "); + if (part[0] == 'r') { + n = STRLEN(part + 1); + if (STRNICMP(part + 1, new_name, n) == 0) { + retval = TRUE; + break; + } + } + } + xfree(new_name); + return retval; +} + +/// Return a list with all registers and their contents +/// +/// List is not sorted. +/// +/// @warning Listed registers must be used before any register-editing function +/// is run. +/// +/// @param[in] max_num_lines Maximum number of lines in the register. If it is +/// zero then all registers are listed. +/// +/// @return Array of ShadaEntry values, last one has type kSDItemMissing. +/// +/// @warning Resulting ShadaEntry values must not be freed. Returned +/// array must be freed with `xfree()`. +static ShadaEntry *list_registers(const size_t max_num_lines) +{ + const void *iter = NULL; + const size_t numregs = op_register_amount(); + ShadaEntry *const ret = xmalloc(sizeof(ShadaEntry) * (numregs + 1)); + ShadaEntry *cur = ret; + if (numregs) { + do { + cur->type = kSDItemRegister; + yankreg_T cur_reg; + iter = op_register_iter(iter, &(cur->data.reg.name), &cur_reg); + if (max_num_lines && (size_t) cur_reg.y_size > max_num_lines) { + continue; + } + cur->data.reg.contents = (char **) cur_reg.y_array; + cur->data.reg.type = (uint8_t) cur_reg.y_type; + cur->data.reg.contents_size = (size_t) cur_reg.y_size; + if (cur_reg.y_type == MBLOCK) { + cur->data.reg.width = (size_t) cur_reg.y_width; + } else { + cur->data.reg.width = 0; + } + cur->data.reg.additional_data = cur_reg.additional_data; + cur->timestamp = cur_reg.timestamp; + cur++; + } while(iter != NULL); + } + cur->type = kSDItemMissing; + return ret; +} + + +#if 0 + +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); +} + +/* + * Write all the named marks for all buffers. + * Return the number of buffers for which marks have been written. + */ +int write_viminfo_marks(FILE *fp_out) +{ + /* + * Set b_last_cursor for the all buffers that have a window. + */ + FOR_ALL_TAB_WINDOWS(tp, win) { + set_last_cursor(win); + } + + fputs(_("\n# History of marks within files (newest to oldest):\n"), fp_out); + int count = 0; + FOR_ALL_BUFFERS(buf) { + /* + * Only write something if buffer has been loaded and at least one + * mark is set. + */ + if (buf->b_marks_read) { + bool is_mark_set = true; + if (buf->b_last_cursor.lnum == 0) { + is_mark_set = false; + for (int i = 0; i < NMARKS; i++) { + if (buf->b_namedm[i].lnum != 0) { + is_mark_set = true; + break; + } + } + } + if (is_mark_set && buf->b_ffname != NULL + && buf->b_ffname[0] != NUL && !removable(buf->b_ffname)) { + home_replace(NULL, buf->b_ffname, IObuff, IOSIZE, TRUE); + fprintf(fp_out, "\n> "); + viminfo_writestring(fp_out, IObuff); + write_one_mark(fp_out, '"', &buf->b_last_cursor); + write_one_mark(fp_out, '^', &buf->b_last_insert); + write_one_mark(fp_out, '.', &buf->b_last_change); + /* changelist positions are stored oldest first */ + for (int i = 0; i < buf->b_changelistlen; ++i) { + write_one_mark(fp_out, '+', &buf->b_changelist[i]); + } + for (int i = 0; i < NMARKS; i++) { + write_one_mark(fp_out, 'a' + i, &buf->b_namedm[i]); + } + count++; + } + } + } + + return count; +} + +static void write_one_mark(FILE *fp_out, int c, pos_T *pos) +{ + if (pos->lnum != 0) + fprintf(fp_out, "\t%c\t%" PRId64 "\t%d\n", c, + (int64_t)pos->lnum, (int)pos->col); +} + +/* + * Handle marks in the viminfo file: + * fp_out != NULL: copy marks for buffers not in buffer list + * fp_out == NULL && (flags & VIF_WANT_MARKS): read marks for curbuf only + * fp_out == NULL && (flags & VIF_GET_OLDFILES | VIF_FORCEIT): fill v:oldfiles + */ +void copy_viminfo_marks(vir_T *virp, FILE *fp_out, int count, int eof, int flags) +{ + char_u *line = virp->vir_line; + buf_T *buf; + int num_marked_files; + int load_marks; + int copy_marks_out; + char_u *str; + int i; + char_u *p; + char_u *name_buf; + pos_T pos; + list_T *list = NULL; + + name_buf = xmalloc(LSIZE); + *name_buf = NUL; + + if (fp_out == NULL && (flags & (VIF_GET_OLDFILES | VIF_FORCEIT))) { + list = list_alloc(); + set_vim_var_list(VV_OLDFILES, list); + } + + num_marked_files = get_viminfo_parameter('\''); + while (!eof && (count < num_marked_files || fp_out == NULL)) { + if (line[0] != '>') { + if (line[0] != '\n' && line[0] != '\r' && line[0] != '#') { + if (viminfo_error("E576: ", _("Missing '>'"), line)) + break; /* too many errors, return now */ + } + eof = vim_fgets(line, LSIZE, virp->vir_fd); + continue; /* Skip this dud line */ + } + + /* + * Handle long line and translate escaped characters. + * Find file name, set str to start. + * Ignore leading and trailing white space. + */ + str = skipwhite(line + 1); + str = viminfo_readstring(virp, (int)(str - virp->vir_line), FALSE); + p = str + STRLEN(str); + while (p != str && (*p == NUL || ascii_isspace(*p))) + p--; + if (*p) + p++; + *p = NUL; + + if (list != NULL) + list_append_string(list, str, -1); + + /* + * If fp_out == NULL, load marks for current buffer. + * If fp_out != NULL, copy marks for buffers not in buflist. + */ + load_marks = copy_marks_out = FALSE; + if (fp_out == NULL) { + if ((flags & VIF_WANT_MARKS) && curbuf->b_ffname != NULL) { + if (*name_buf == NUL) /* only need to do this once */ + home_replace(NULL, curbuf->b_ffname, name_buf, LSIZE, TRUE); + if (fnamecmp(str, name_buf) == 0) + load_marks = TRUE; + } + } else { /* fp_out != NULL */ + /* This is slow if there are many buffers!! */ + buf = NULL; + FOR_ALL_BUFFERS(bp) { + if (bp->b_ffname != NULL) { + home_replace(NULL, bp->b_ffname, name_buf, LSIZE, TRUE); + if (fnamecmp(str, name_buf) == 0) { + buf = bp; + break; + } + } + } + + /* + * copy marks if the buffer has not been loaded + */ + if (buf == NULL || !buf->b_marks_read) { + copy_marks_out = TRUE; + fputs("\n> ", fp_out); + viminfo_writestring(fp_out, str); + count++; + } + } + free(str); + + pos.coladd = 0; + while (!(eof = viminfo_readline(virp)) && line[0] == TAB) { + if (load_marks) { + if (line[1] != NUL) { + int64_t lnum_64; + unsigned int u; + sscanf((char *)line + 2, "%" SCNd64 "%u", &lnum_64, &u); + // safely downcast to linenr_T (long); remove when linenr_T refactored + assert(lnum_64 <= LONG_MAX); + pos.lnum = (linenr_T)lnum_64; + assert(u <= INT_MAX); + pos.col = (colnr_T)u; + switch (line[1]) { + case '"': curbuf->b_last_cursor = pos; break; + case '^': curbuf->b_last_insert = pos; break; + case '.': curbuf->b_last_change = pos; break; + case '+': + /* changelist positions are stored oldest + * first */ + if (curbuf->b_changelistlen == JUMPLISTSIZE) + /* list is full, remove oldest entry */ + memmove(curbuf->b_changelist, + curbuf->b_changelist + 1, + sizeof(pos_T) * (JUMPLISTSIZE - 1)); + else + ++curbuf->b_changelistlen; + curbuf->b_changelist[ + curbuf->b_changelistlen - 1] = pos; + break; + default: if ((i = line[1] - 'a') >= 0 && i < NMARKS) + curbuf->b_namedm[i] = pos; + } + } + } else if (copy_marks_out) + fputs((char *)line, fp_out); + } + if (load_marks) { + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_buffer == curbuf) + wp->w_changelistidx = curbuf->b_changelistlen; + } + break; + } + } + free(name_buf); +} + +int read_viminfo_filemark(vir_T *virp, int force) +{ + char_u *str; + xfmark_T *fm; + int i; + + /* We only get here if line[0] == '\'' or '-'. + * Illegal mark names are ignored (for future expansion). */ + str = virp->vir_line + 1; + if ( + *str <= 127 && + ((*virp->vir_line == '\'' && (ascii_isdigit(*str) || isupper(*str))) + || (*virp->vir_line == '-' && *str == '\''))) { + if (*str == '\'') { + /* If the jumplist isn't full insert fmark as oldest entry */ + if (curwin->w_jumplistlen == JUMPLISTSIZE) + fm = NULL; + else { + for (i = curwin->w_jumplistlen; i > 0; --i) + curwin->w_jumplist[i] = curwin->w_jumplist[i - 1]; + ++curwin->w_jumplistidx; + ++curwin->w_jumplistlen; + fm = &curwin->w_jumplist[0]; + fm->fmark.mark.lnum = 0; + fm->fname = NULL; + } + } else if (ascii_isdigit(*str)) + fm = &namedfm[*str - '0' + NMARKS]; + else { // is uppercase + assert(*str >= 'A' && *str <= 'Z'); + fm = &namedfm[*str - 'A']; + } + if (fm != NULL && (fm->fmark.mark.lnum == 0 || force)) { + str = skipwhite(str + 1); + fm->fmark.mark.lnum = getdigits_long(&str); + str = skipwhite(str); + fm->fmark.mark.col = getdigits_int(&str); + fm->fmark.mark.coladd = 0; + fm->fmark.fnum = 0; + str = skipwhite(str); + free(fm->fname); + fm->fname = viminfo_readstring(virp, (int)(str - virp->vir_line), + FALSE); + } + } + return vim_fgets(virp->vir_line, LSIZE, virp->vir_fd); +} + +void write_viminfo_filemarks(FILE *fp) +{ + int i; + char_u *name; + buf_T *buf; + xfmark_T *fm; + + if (get_viminfo_parameter('f') == 0) + return; + + fputs(_("\n# File marks:\n"), fp); + + /* + * Find a mark that is the same file and position as the cursor. + * That one, or else the last one is deleted. + * Move '0 to '1, '1 to '2, etc. until the matching one or '9 + * Set '0 mark to current cursor position. + */ + if (curbuf->b_ffname != NULL && !removable(curbuf->b_ffname)) { + name = buflist_nr2name(curbuf->b_fnum, TRUE, FALSE); + for (i = NMARKS; i < NMARKS + EXTRA_MARKS - 1; ++i) + if (namedfm[i].fmark.mark.lnum == curwin->w_cursor.lnum + && (namedfm[i].fname == NULL + ? namedfm[i].fmark.fnum == curbuf->b_fnum + : (name != NULL + && STRCMP(name, namedfm[i].fname) == 0))) + break; + free(name); + + free(namedfm[i].fname); + for (; i > NMARKS; --i) + namedfm[i] = namedfm[i - 1]; + namedfm[NMARKS].fmark.mark = curwin->w_cursor; + namedfm[NMARKS].fmark.fnum = curbuf->b_fnum; + namedfm[NMARKS].fname = NULL; + } + + /* Write the filemarks '0 - '9 and 'A - 'Z */ + for (i = 0; i < NMARKS + EXTRA_MARKS; i++) + write_one_filemark(fp, &namedfm[i], '\'', + i < NMARKS ? i + 'A' : i - NMARKS + '0'); + + /* Write the jumplist with -' */ + fputs(_("\n# Jumplist (newest first):\n"), fp); + setpcmark(); /* add current cursor position */ + cleanup_jumplist(); + for (fm = &curwin->w_jumplist[curwin->w_jumplistlen - 1]; + fm >= &curwin->w_jumplist[0]; --fm) { + if (fm->fmark.fnum == 0 + || ((buf = buflist_findnr(fm->fmark.fnum)) != NULL + && !removable(buf->b_ffname))) + write_one_filemark(fp, fm, '-', '\''); + } +} + +int read_viminfo_search_pattern(vir_T *virp, int force) +{ + char_u *lp; + int idx = -1; + int magic = FALSE; + int no_scs = FALSE; + int off_line = FALSE; + int off_end = 0; + long off = 0; + int setlast = FALSE; + static int hlsearch_on = FALSE; + char_u *val; + + /* + * Old line types: + * "/pat", "&pat": search/subst. pat + * "~/pat", "~&pat": last used search/subst. pat + * New line types: + * "~h", "~H": hlsearch highlighting off/on + * "~<magic><smartcase><line><end><off><last><which>pat" + * <magic>: 'm' off, 'M' on + * <smartcase>: 's' off, 'S' on + * <line>: 'L' line offset, 'l' char offset + * <end>: 'E' from end, 'e' from start + * <off>: decimal, offset + * <last>: '~' last used pattern + * <which>: '/' search pat, '&' subst. pat + */ + lp = virp->vir_line; + if (lp[0] == '~' && (lp[1] == 'm' || lp[1] == 'M')) { /* new line type */ + if (lp[1] == 'M') /* magic on */ + magic = TRUE; + if (lp[2] == 's') + no_scs = TRUE; + if (lp[3] == 'L') + off_line = TRUE; + if (lp[4] == 'E') + off_end = SEARCH_END; + lp += 5; + off = getdigits_long(&lp); + } + if (lp[0] == '~') { /* use this pattern for last-used pattern */ + setlast = TRUE; + lp++; + } + if (lp[0] == '/') + idx = RE_SEARCH; + else if (lp[0] == '&') + idx = RE_SUBST; + else if (lp[0] == 'h') /* ~h: 'hlsearch' highlighting off */ + hlsearch_on = FALSE; + else if (lp[0] == 'H') /* ~H: 'hlsearch' highlighting on */ + hlsearch_on = TRUE; + if (idx >= 0) { + if (force || spats[idx].pat == NULL) { + val = viminfo_readstring(virp, (int)(lp - virp->vir_line + 1), TRUE); + set_last_search_pat(val, idx, magic, setlast); + xfree(val); + spats[idx].no_scs = no_scs; + spats[idx].off.line = off_line; + spats[idx].off.end = off_end; + spats[idx].off.off = off; + if (setlast) { + SET_NO_HLSEARCH(!hlsearch_on); + } + } + } + return viminfo_readline(virp); +} + +void write_viminfo_search_pattern(FILE *fp) +{ + if (get_viminfo_parameter('/') != 0) { + fprintf(fp, "\n# hlsearch on (H) or off (h):\n~%c", + (no_hlsearch || find_viminfo_parameter('h') != NULL) ? 'h' : 'H'); + wvsp_one(fp, RE_SEARCH, "", '/'); + wvsp_one(fp, RE_SUBST, _("Substitute "), '&'); + } +} + +static void +wvsp_one ( + FILE *fp, /* file to write to */ + int idx, /* spats[] index */ + char *s, /* search pat */ + int sc /* dir char */ +) +{ + if (spats[idx].pat != NULL) { + fprintf(fp, _("\n# Last %sSearch Pattern:\n~"), s); + /* off.dir is not stored, it's reset to forward */ + fprintf(fp, "%c%c%c%c%" PRId64 "%s%c", + spats[idx].magic ? 'M' : 'm', /* magic */ + spats[idx].no_scs ? 's' : 'S', /* smartcase */ + spats[idx].off.line ? 'L' : 'l', /* line offset */ + spats[idx].off.end ? 'E' : 'e', /* offset from end */ + (int64_t)spats[idx].off.off, /* offset */ + last_idx == idx ? "~" : "", /* last used pat */ + sc); + viminfo_writestring(fp, spats[idx].pat); + } +} + +/* + * Prepare for reading the history from the viminfo file. + * This allocates history arrays to store the read history lines. + */ +void prepare_viminfo_history(int asklen, int writing) +{ + int i; + int num; + + init_history(); + viminfo_add_at_front = (asklen != 0 && !writing); + if (asklen > hislen) + asklen = hislen; + + for (int type = 0; type < HIST_COUNT; ++type) { + /* Count the number of empty spaces in the history list. Entries read + * from viminfo previously are also considered empty. If there are + * more spaces available than we request, then fill them up. */ + for (i = 0, num = 0; i < hislen; i++) + if (history[type][i].hisstr == NULL || history[type][i].viminfo) + num++; + int len = asklen; + if (num > len) + len = num; + if (len <= 0) + viminfo_history[type] = NULL; + else + viminfo_history[type] = xmalloc(len * sizeof(char_u *)); + if (viminfo_history[type] == NULL) + len = 0; + viminfo_hislen[type] = len; + viminfo_hisidx[type] = 0; + } +} + +/* + * Accept a line from the viminfo, store it in the history array when it's + * new. + */ +int read_viminfo_history(vir_T *virp, int writing) +{ + int type; + char_u *val; + + type = hist_char2type(virp->vir_line[0]); + if (viminfo_hisidx[type] < viminfo_hislen[type]) { + val = viminfo_readstring(virp, 1, TRUE); + if (val != NULL && *val != NUL) { + int sep = (*val == ' ' ? NUL : *val); + + if (!in_history(type, val + (type == HIST_SEARCH), + viminfo_add_at_front, sep, writing)) { + /* Need to re-allocate to append the separator byte. */ + size_t len = STRLEN(val); + char_u *p = xmalloc(len + 2); + if (type == HIST_SEARCH) { + /* Search entry: Move the separator from the first + * column to after the NUL. */ + memmove(p, val + 1, len); + p[len] = sep; + } else { + /* Not a search entry: No separator in the viminfo + * file, add a NUL separator. */ + memmove(p, val, len + 1); + p[len + 1] = NUL; + } + viminfo_history[type][viminfo_hisidx[type]++] = p; + } + } + xfree(val); + } + return viminfo_readline(virp); +} + +/* + * Finish reading history lines from viminfo. Not used when writing viminfo. + */ +void finish_viminfo_history(void) +{ + int idx; + int i; + int type; + + for (type = 0; type < HIST_COUNT; ++type) { + if (history[type] == NULL) + continue; + idx = hisidx[type] + viminfo_hisidx[type]; + if (idx >= hislen) + idx -= hislen; + else if (idx < 0) + idx = hislen - 1; + if (viminfo_add_at_front) + hisidx[type] = idx; + else { + if (hisidx[type] == -1) + hisidx[type] = hislen - 1; + do { + if (history[type][idx].hisstr != NULL + || history[type][idx].viminfo) + break; + if (++idx == hislen) + idx = 0; + } while (idx != hisidx[type]); + if (idx != hisidx[type] && --idx < 0) + idx = hislen - 1; + } + for (i = 0; i < viminfo_hisidx[type]; i++) { + xfree(history[type][idx].hisstr); + history[type][idx].hisstr = viminfo_history[type][i]; + history[type][idx].viminfo = TRUE; + if (--idx < 0) + idx = hislen - 1; + } + idx += 1; + idx %= hislen; + for (i = 0; i < viminfo_hisidx[type]; i++) { + history[type][idx++].hisnum = ++hisnum[type]; + idx %= hislen; + } + xfree(viminfo_history[type]); + viminfo_history[type] = NULL; + viminfo_hisidx[type] = 0; + } +} + +/* + * Write history to viminfo file in "fp". + * When "merge" is TRUE merge history lines with a previously read viminfo + * file, data is in viminfo_history[]. + * When "merge" is FALSE just write all history lines. Used for ":wviminfo!". + */ +void write_viminfo_history(FILE *fp, int merge) +{ + int i; + int type; + int num_saved; + char_u *p; + int c; + int round; + + init_history(); + if (hislen == 0) + return; + for (type = 0; type < HIST_COUNT; ++type) { + num_saved = get_viminfo_parameter(hist_type2char(type, FALSE)); + if (num_saved == 0) + continue; + if (num_saved < 0) /* Use default */ + num_saved = hislen; + fprintf(fp, _("\n# %s History (newest to oldest):\n"), + type == HIST_CMD ? _("Command Line") : + type == HIST_SEARCH ? _("Search String") : + type == HIST_EXPR ? _("Expression") : + _("Input Line")); + if (num_saved > hislen) + num_saved = hislen; + + /* + * Merge typed and viminfo history: + * round 1: history of typed commands. + * round 2: history from recently read viminfo. + */ + for (round = 1; round <= 2; ++round) { + if (round == 1) + /* start at newest entry, somewhere in the list */ + i = hisidx[type]; + else if (viminfo_hisidx[type] > 0) + /* start at newest entry, first in the list */ + i = 0; + else + /* empty list */ + i = -1; + if (i >= 0) + while (num_saved > 0 + && !(round == 2 && i >= viminfo_hisidx[type])) { + p = round == 1 ? history[type][i].hisstr + : viminfo_history[type] == NULL ? NULL + : viminfo_history[type][i]; + if (p != NULL && (round == 2 + || !merge + || !history[type][i].viminfo)) { + --num_saved; + fputc(hist_type2char(type, TRUE), fp); + /* For the search history: put the separator in the + * second column; use a space if there isn't one. */ + if (type == HIST_SEARCH) { + c = p[STRLEN(p) + 1]; + putc(c == NUL ? ' ' : c, fp); + } + viminfo_writestring(fp, p); + } + if (round == 1) { + /* Decrement index, loop around and stop when back at + * the start. */ + if (--i < 0) + i = hislen - 1; + if (i == hisidx[type]) + break; + } else { + /* Increment index. Stop at the end in the while. */ + ++i; + } + } + } + for (i = 0; i < viminfo_hisidx[type]; ++i) + if (viminfo_history[type] != NULL) + xfree(viminfo_history[type][i]); + xfree(viminfo_history[type]); + viminfo_history[type] = NULL; + viminfo_hisidx[type] = 0; + } +} + +int read_viminfo_register(vir_T *virp, int force) +{ + int eof; + int do_it = TRUE; + int size; + int limit; + int set_prev = FALSE; + char_u *str; + char_u **array = NULL; + + /* We only get here (hopefully) if line[0] == '"' */ + str = virp->vir_line + 1; + + /* If the line starts with "" this is the y_previous register. */ + if (*str == '"') { + set_prev = TRUE; + str++; + } + + if (!ASCII_ISALNUM(*str) && *str != '-') { + if (viminfo_error("E577: ", _("Illegal register name"), virp->vir_line)) + return TRUE; /* too many errors, pretend end-of-file */ + do_it = FALSE; + } + yankreg_T *reg = get_yank_register(*str++, YREG_PUT); + if (!force && reg->y_array != NULL) + do_it = FALSE; + + if (*str == '@') { + /* "x@: register x used for @@ */ + if (force || execreg_lastc == NUL) + execreg_lastc = str[-1]; + } + + size = 0; + limit = 100; /* Optimized for registers containing <= 100 lines */ + if (do_it) { + if (set_prev) { + y_previous = reg; + } + + free_register(reg); + array = xmalloc(limit * sizeof(char_u *)); + + str = skipwhite(skiptowhite(str)); + if (STRNCMP(str, "CHAR", 4) == 0) { + reg->y_type = MCHAR; + } else if (STRNCMP(str, "BLOCK", 5) == 0) { + reg->y_type = MBLOCK; + } else { + reg->y_type = MLINE; + } + /* get the block width; if it's missing we get a zero, which is OK */ + str = skipwhite(skiptowhite(str)); + reg->y_width = getdigits_int(&str); + } + + while (!(eof = viminfo_readline(virp)) + && (virp->vir_line[0] == TAB || virp->vir_line[0] == '<')) { + if (do_it) { + if (size >= limit) { + limit *= 2; + array = xrealloc(array, limit * sizeof(char_u *)); + } + array[size++] = viminfo_readstring(virp, 1, TRUE); + } + } + + if (do_it) { + if (size == 0) { + xfree(array); + } else if (size < limit) { + reg->y_array = xrealloc(array, size * sizeof(char_u *)); + } else { + reg->y_array = array; + } + reg->y_size = size; + } + return eof; +} + +void write_viminfo_registers(FILE *fp) +{ + int i, j; + char_u *type; + char_u c; + int num_lines; + int max_num_lines; + int max_kbyte; + long len; + + fputs(_("\n# Registers:\n"), fp); + + /* Get '<' value, use old '"' value if '<' is not found. */ + max_num_lines = get_viminfo_parameter('<'); + if (max_num_lines < 0) + max_num_lines = get_viminfo_parameter('"'); + if (max_num_lines == 0) + return; + max_kbyte = get_viminfo_parameter('s'); + if (max_kbyte == 0) + return; + + // don't include clipboard registers '*'/'+' + for (i = 0; i < NUM_SAVED_REGISTERS; i++) { + if (y_regs[i].y_array == NULL) + continue; + + /* Skip empty registers. */ + num_lines = y_regs[i].y_size; + if (num_lines == 0 + || (num_lines == 1 && y_regs[i].y_type == MCHAR + && *y_regs[i].y_array[0] == NUL)) + continue; + + if (max_kbyte > 0) { + /* Skip register if there is more text than the maximum size. */ + len = 0; + for (j = 0; j < num_lines; j++) + len += (long)STRLEN(y_regs[i].y_array[j]) + 1L; + if (len > (long)max_kbyte * 1024L) + continue; + } + + switch (y_regs[i].y_type) { + case MLINE: + type = (char_u *)"LINE"; + break; + case MCHAR: + type = (char_u *)"CHAR"; + break; + case MBLOCK: + type = (char_u *)"BLOCK"; + break; + default: + sprintf((char *)IObuff, _("E574: Unknown register type %d"), + y_regs[i].y_type); + emsg(IObuff); + type = (char_u *)"LINE"; + break; + } + if (y_previous == &y_regs[i]) + fprintf(fp, "\""); + c = get_register_name(i); + fprintf(fp, "\"%c", c); + if (c == execreg_lastc) + fprintf(fp, "@"); + fprintf(fp, "\t%s\t%d\n", type, + (int)y_regs[i].y_width + ); + + /* If max_num_lines < 0, then we save ALL the lines in the register */ + if (max_num_lines > 0 && num_lines > max_num_lines) + num_lines = max_num_lines; + for (j = 0; j < num_lines; j++) { + putc('\t', fp); + viminfo_writestring(fp, y_regs[i].y_array[j]); + } + } +} + +/* + * Restore global vars that start with a capital from the viminfo file + */ +int read_viminfo_varlist(vir_T *virp, int writing) +{ + char_u *tab; + int type = VAR_NUMBER; + typval_T tv; + + if (!writing && (find_viminfo_parameter('!') != NULL)) { + tab = vim_strchr(virp->vir_line + 1, '\t'); + if (tab != NULL) { + *tab++ = NUL; /* isolate the variable name */ + switch (*tab) { + case 'S': type = VAR_STRING; break; + case 'F': type = VAR_FLOAT; break; + case 'D': type = VAR_DICT; break; + case 'L': type = VAR_LIST; break; + } + + tab = vim_strchr(tab, '\t'); + if (tab != NULL) { + tv.v_type = type; + if (type == VAR_STRING || type == VAR_DICT || type == VAR_LIST) + tv.vval.v_string = viminfo_readstring(virp, + (int)(tab - virp->vir_line + 1), TRUE); + else if (type == VAR_FLOAT) + (void)string2float(tab + 1, &tv.vval.v_float); + else + tv.vval.v_number = atol((char *)tab + 1); + if (type == VAR_DICT || type == VAR_LIST) { + typval_T *etv = eval_expr(tv.vval.v_string, NULL); + + if (etv == NULL) + /* Failed to parse back the dict or list, use it as a + * string. */ + tv.v_type = VAR_STRING; + else { + free(tv.vval.v_string); + tv = *etv; + free(etv); + } + } + + set_var(virp->vir_line + 1, &tv, FALSE); + + if (tv.v_type == VAR_STRING) + free(tv.vval.v_string); + else if (tv.v_type == VAR_DICT || tv.v_type == VAR_LIST) + clear_tv(&tv); + } + } + } + + return viminfo_readline(virp); +} + +/* + * Write global vars that start with a capital to the viminfo file + */ +void write_viminfo_varlist(FILE *fp) +{ + hashitem_T *hi; + dictitem_T *this_var; + int todo; + char *s; + char_u *p; + char_u *tofree; + char_u numbuf[NUMBUFLEN]; + + if (find_viminfo_parameter('!') == NULL) + return; + + fputs(_("\n# global variables:\n"), fp); + + todo = (int)globvarht.ht_used; + for (hi = globvarht.ht_array; todo > 0; ++hi) { + if (!HASHITEM_EMPTY(hi)) { + --todo; + this_var = HI2DI(hi); + if (var_flavour(this_var->di_key) == VAR_FLAVOUR_VIMINFO) { + switch (this_var->di_tv.v_type) { + case VAR_STRING: s = "STR"; break; + case VAR_NUMBER: s = "NUM"; break; + case VAR_FLOAT: s = "FLO"; break; + case VAR_DICT: s = "DIC"; break; + case VAR_LIST: s = "LIS"; break; + default: continue; + } + fprintf(fp, "!%s\t%s\t", this_var->di_key, s); + p = echo_string(&this_var->di_tv, &tofree, numbuf, 0); + if (p != NULL) + viminfo_writestring(fp, p); + free(tofree); + } + } + } +} + +int read_viminfo_bufferlist(vir_T *virp, int writing) +{ + char_u *tab; + linenr_T lnum; + colnr_T col; + buf_T *buf; + char_u *sfname; + char_u *xline; + + /* Handle long line and escaped characters. */ + xline = viminfo_readstring(virp, 1, FALSE); + + /* don't read in if there are files on the command-line or if writing: */ + if (xline != NULL && !writing && ARGCOUNT == 0 + && find_viminfo_parameter('%') != NULL) { + /* Format is: <fname> Tab <lnum> Tab <col>. + * Watch out for a Tab in the file name, work from the end. */ + lnum = 0; + col = 0; + tab = vim_strrchr(xline, '\t'); + if (tab != NULL) { + *tab++ = '\0'; + col = (colnr_T)atoi((char *)tab); + tab = vim_strrchr(xline, '\t'); + if (tab != NULL) { + *tab++ = '\0'; + lnum = atol((char *)tab); + } + } + + /* Expand "~/" in the file name at "line + 1" to a full path. + * Then try shortening it by comparing with the current directory */ + expand_env(xline, NameBuff, MAXPATHL); + sfname = path_shorten_fname_if_possible(NameBuff); + + buf = buflist_new(NameBuff, sfname, (linenr_T)0, BLN_LISTED); + if (buf != NULL) { /* just in case... */ + buf->b_last_cursor.lnum = lnum; + buf->b_last_cursor.col = col; + buflist_setfpos(buf, curwin, lnum, col, FALSE); + } + } + xfree(xline); + + return viminfo_readline(virp); +} + +void write_viminfo_bufferlist(FILE *fp) +{ + char_u *line; + int max_buffers; + + if (find_viminfo_parameter('%') == NULL) + return; + + /* Without a number -1 is returned: do all buffers. */ + max_buffers = get_viminfo_parameter('%'); + + /* Allocate room for the file name, lnum and col. */ +#define LINE_BUF_LEN (MAXPATHL + 40) + line = xmalloc(LINE_BUF_LEN); + + FOR_ALL_TAB_WINDOWS(tp, win) { + set_last_cursor(win); + } + + fputs(_("\n# Buffer list:\n"), fp); + FOR_ALL_BUFFERS(buf) { + if (buf->b_fname == NULL + || !buf->b_p_bl + || bt_quickfix(buf) + || removable(buf->b_ffname)) + continue; + + if (max_buffers-- == 0) + break; + putc('%', fp); + home_replace(NULL, buf->b_ffname, line, MAXPATHL, TRUE); + vim_snprintf_add((char *)line, LINE_BUF_LEN, "\t%" PRId64 "\t%d", + (int64_t)buf->b_last_cursor.lnum, + buf->b_last_cursor.col); + viminfo_writestring(fp, line); + } + xfree(line); +} + +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); + } +} + +/* + * Structure used for reading from the viminfo file. + */ +typedef struct { + char_u *vir_line; /* text of the current line */ + FILE *vir_fd; /* file descriptor */ + vimconv_T vir_conv; /* encoding conversion */ +} vir_T; +#endif |