neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

ex_session.c (35520B)


      1 // Functions for creating a session file, i.e. implementing:
      2 //   :mkexrc
      3 //   :mkvimrc
      4 //   :mkview
      5 //   :mksession
      6 
      7 #include <inttypes.h>
      8 #include <stdbool.h>
      9 #include <stdio.h>
     10 #include <string.h>
     11 
     12 #include "klib/kvec.h"
     13 #include "nvim/arglist.h"
     14 #include "nvim/arglist_defs.h"
     15 #include "nvim/ascii_defs.h"
     16 #include "nvim/autocmd.h"
     17 #include "nvim/autocmd_defs.h"
     18 #include "nvim/buffer.h"
     19 #include "nvim/buffer_defs.h"
     20 #include "nvim/errors.h"
     21 #include "nvim/eval.h"
     22 #include "nvim/eval/typval.h"
     23 #include "nvim/eval/typval_defs.h"
     24 #include "nvim/eval/vars.h"
     25 #include "nvim/ex_cmds_defs.h"
     26 #include "nvim/ex_docmd.h"
     27 #include "nvim/ex_getln.h"
     28 #include "nvim/ex_session.h"
     29 #include "nvim/file_search.h"
     30 #include "nvim/fileio.h"
     31 #include "nvim/fold.h"
     32 #include "nvim/garray_defs.h"
     33 #include "nvim/gettext_defs.h"
     34 #include "nvim/globals.h"
     35 #include "nvim/macros_defs.h"
     36 #include "nvim/mapping.h"
     37 #include "nvim/mbyte.h"
     38 #include "nvim/memory.h"
     39 #include "nvim/message.h"
     40 #include "nvim/option.h"
     41 #include "nvim/option_vars.h"
     42 #include "nvim/os/fs.h"
     43 #include "nvim/os/os.h"
     44 #include "nvim/os/os_defs.h"
     45 #include "nvim/path.h"
     46 #include "nvim/pos_defs.h"
     47 #include "nvim/runtime.h"
     48 #include "nvim/strings.h"
     49 #include "nvim/types_defs.h"
     50 #include "nvim/vim_defs.h"
     51 #include "nvim/window.h"
     52 
     53 #include "ex_session.c.generated.h"
     54 
     55 /// Whether ":lcd" or ":tcd" was produced for a session.
     56 static int did_lcd;
     57 
     58 #define PUTLINE_FAIL(s) \
     59  do { if (FAIL == put_line(fd, (s))) { return FAIL; } } while (0)
     60 
     61 static int put_view_curpos(FILE *fd, const win_T *wp, char *spaces)
     62 {
     63  int r;
     64 
     65  if (wp->w_curswant == MAXCOL) {
     66    r = fprintf(fd, "%snormal! $\n", spaces);
     67  } else {
     68    r = fprintf(fd, "%snormal! 0%d|\n", spaces, wp->w_virtcol + 1);
     69  }
     70  return r >= 0;
     71 }
     72 
     73 static int ses_winsizes(FILE *fd, bool restore_size, win_T *tab_firstwin)
     74 {
     75  if (restore_size && (ssop_flags & kOptSsopFlagWinsize)) {
     76    int n = 0;
     77    for (win_T *wp = tab_firstwin; wp != NULL; wp = wp->w_next) {
     78      if (!ses_do_win(wp)) {
     79        continue;
     80      }
     81      n++;
     82 
     83      // restore height when not full height
     84      if (wp->w_height + wp->w_hsep_height + wp->w_status_height < topframe->fr_height
     85          && (fprintf(fd,
     86                      "exe '%dresize ' . ((&lines * %" PRId64
     87                      " + %" PRId64 ") / %" PRId64 ")\n",
     88                      n, (int64_t)wp->w_height,
     89                      (int64_t)Rows / 2, (int64_t)Rows) < 0)) {
     90        return FAIL;
     91      }
     92 
     93      // restore width when not full width
     94      if (wp->w_width < Columns
     95          && (fprintf(fd,
     96                      "exe 'vert %dresize ' . ((&columns * %" PRId64
     97                      " + %" PRId64 ") / %" PRId64 ")\n",
     98                      n, (int64_t)wp->w_width, (int64_t)Columns / 2,
     99                      (int64_t)Columns) < 0)) {
    100        return FAIL;
    101      }
    102    }
    103  } else {
    104    // Just equalize window sizes.
    105    PUTLINE_FAIL("wincmd =");
    106  }
    107  return OK;
    108 }
    109 
    110 /// Write commands to "fd" to recursively create windows for frame "fr",
    111 /// horizontally and vertically split.
    112 /// After the commands the last window in the frame is the current window.
    113 ///
    114 /// @return  FAIL when writing the commands to "fd" fails.
    115 static int ses_win_rec(FILE *fd, frame_T *fr)
    116 {
    117  int count = 0;
    118 
    119  if (fr->fr_layout == FR_LEAF) {
    120    return OK;
    121  }
    122 
    123  // Find first frame that's not skipped and then create a window for
    124  // each following one (first frame is already there).
    125  frame_T *frc = ses_skipframe(fr->fr_child);
    126  if (frc != NULL) {
    127    while ((frc = ses_skipframe(frc->fr_next)) != NULL) {
    128      // Make window as big as possible so that we have lots of room
    129      // to split.
    130      if (fprintf(fd, "%s%s",
    131                  "wincmd _ | wincmd |\n",
    132                  (fr->fr_layout == FR_COL ? "split\n" : "vsplit\n")) < 0) {
    133        return FAIL;
    134      }
    135      count++;
    136    }
    137  }
    138 
    139  // Go back to the first window.
    140  if (count > 0 && (fprintf(fd, fr->fr_layout == FR_COL
    141                            ? "%dwincmd k\n" : "%dwincmd h\n", count) < 0)) {
    142    return FAIL;
    143  }
    144 
    145  // Recursively create frames/windows in each window of this column or row.
    146  frc = ses_skipframe(fr->fr_child);
    147  while (frc != NULL) {
    148    ses_win_rec(fd, frc);
    149    frc = ses_skipframe(frc->fr_next);
    150    // Go to next window.
    151    if (frc != NULL && put_line(fd, "wincmd w") == FAIL) {
    152      return FAIL;
    153    }
    154  }
    155 
    156  return OK;
    157 }
    158 
    159 /// Skip frames that don't contain windows we want to save in the Session.
    160 ///
    161 /// @return  NULL when there none.
    162 static frame_T *ses_skipframe(frame_T *fr)
    163 {
    164  frame_T *frc;
    165 
    166  FOR_ALL_FRAMES(frc, fr) {
    167    if (ses_do_frame(frc)) {
    168      break;
    169    }
    170  }
    171  return frc;
    172 }
    173 
    174 /// @return  true if frame "fr" has a window somewhere that we want to save in
    175 ///          the Session.
    176 static bool ses_do_frame(const frame_T *fr)
    177  FUNC_ATTR_NONNULL_ARG(1)
    178 {
    179  const frame_T *frc;
    180 
    181  if (fr->fr_layout == FR_LEAF) {
    182    return ses_do_win(fr->fr_win);
    183  }
    184  FOR_ALL_FRAMES(frc, fr->fr_child) {
    185    if (ses_do_frame(frc)) {
    186      return true;
    187    }
    188  }
    189  return false;
    190 }
    191 
    192 /// @return  non-zero if window "wp" is to be stored in the Session.
    193 static int ses_do_win(win_T *wp)
    194 {
    195  // Skip floating windows to avoid issues when restoring the Session. #18432
    196  if (wp->w_floating) {
    197    return false;
    198  }
    199  if (wp->w_buffer->b_fname == NULL
    200      // When 'buftype' is "nofile" can't restore the window contents.
    201      || (!wp->w_buffer->terminal && bt_nofilename(wp->w_buffer))) {
    202    return ssop_flags & kOptSsopFlagBlank;
    203  }
    204  if (bt_help(wp->w_buffer)) {
    205    return ssop_flags & kOptSsopFlagHelp;
    206  }
    207  if (bt_terminal(wp->w_buffer)) {
    208    return ssop_flags & kOptSsopFlagTerminal;
    209  }
    210  return true;
    211 }
    212 
    213 /// Writes an :argument list to the session file.
    214 ///
    215 /// @param fd
    216 /// @param cmd
    217 /// @param gap
    218 /// @param fullname  true: use full path name
    219 /// @param flagp
    220 ///
    221 /// @returns FAIL if writing fails.
    222 static int ses_arglist(FILE *fd, char *cmd, garray_T *gap, bool fullname, unsigned *flagp)
    223 {
    224  char *buf = NULL;
    225 
    226  if (fprintf(fd, "%s\n%s\n", cmd, "%argdel") < 0) {
    227    return FAIL;
    228  }
    229  for (int i = 0; i < gap->ga_len; i++) {
    230    // NULL file names are skipped (only happens when out of memory).
    231    char *s = alist_name(&((aentry_T *)gap->ga_data)[i]);
    232    if (s != NULL) {
    233      if (fullname) {
    234        buf = xmalloc(MAXPATHL);
    235        vim_FullName(s, buf, MAXPATHL, false);
    236        s = buf;
    237      }
    238      char *fname_esc = ses_escape_fname(s, flagp);
    239      if (fprintf(fd, "$argadd %s\n", fname_esc) < 0) {
    240        xfree(fname_esc);
    241        xfree(buf);
    242        return FAIL;
    243      }
    244      xfree(fname_esc);
    245      xfree(buf);
    246    }
    247  }
    248  return OK;
    249 }
    250 
    251 /// @return  the buffer name for `buf`.
    252 static char *ses_get_fname(buf_T *buf, const unsigned *flagp)
    253 {
    254  // Use the short file name if the current directory is known at the time
    255  // the session file will be sourced.
    256  // Don't do this for ":mkview", we don't know the current directory.
    257  // Don't do this after ":lcd", we don't keep track of what the current
    258  // directory is.
    259  if (buf->b_sfname != NULL
    260      && flagp == &ssop_flags
    261      && (ssop_flags & (kOptSsopFlagCurdir | kOptSsopFlagSesdir))
    262      && !p_acd
    263      && !did_lcd) {
    264    return buf->b_sfname;
    265  }
    266  return buf->b_ffname;
    267 }
    268 
    269 /// Write a buffer name to the session file.
    270 /// Also ends the line, if "add_eol" is true.
    271 ///
    272 /// @return  FAIL if writing fails.
    273 static int ses_fname(FILE *fd, buf_T *buf, unsigned *flagp, bool add_eol)
    274 {
    275  char *name = ses_get_fname(buf, flagp);
    276  if (ses_put_fname(fd, name, flagp) == FAIL
    277      || (add_eol && fprintf(fd, "\n") < 0)) {
    278    return FAIL;
    279  }
    280  return OK;
    281 }
    282 
    283 /// Escapes a filename for session writing.
    284 /// Takes care of "slash" flag in 'sessionoptions' and escapes special
    285 /// characters.
    286 ///
    287 /// @return  allocated string or NULL.
    288 static char *ses_escape_fname(char *name, unsigned *flagp)
    289 {
    290  char *p;
    291  char *sname = home_replace_save(NULL, name);
    292 
    293  // Always kOptSsopFlagSlash: change all backslashes to forward slashes.
    294  for (p = sname; *p != NUL; MB_PTR_ADV(p)) {
    295    if (*p == '\\') {
    296      *p = '/';
    297    }
    298  }
    299 
    300  // Escape special characters.
    301  p = vim_strsave_fnameescape(sname, VSE_NONE);
    302  xfree(sname);
    303  return p;
    304 }
    305 
    306 /// Write a file name to the session file.
    307 /// Takes care of the "slash" option in 'sessionoptions' and escapes special
    308 /// characters.
    309 ///
    310 /// @return  FAIL if writing fails.
    311 static int ses_put_fname(FILE *fd, char *name, unsigned *flagp)
    312 {
    313  char *p = ses_escape_fname(name, flagp);
    314  bool retval = fputs(p, fd) < 0 ? FAIL : OK;
    315  xfree(p);
    316  return retval;
    317 }
    318 
    319 /// Write commands to "fd" to restore the view of a window.
    320 /// Caller must make sure 'scrolloff' is zero.
    321 ///
    322 /// @param add_edit  add ":edit" command to view
    323 /// @param flagp  vop_flags or ssop_flags
    324 /// @param current_arg_idx  current argument index of the window, use -1 if unknown
    325 static int put_view(FILE *fd, win_T *wp, tabpage_T *tp, bool add_edit, unsigned *flagp,
    326                    int current_arg_idx)
    327 {
    328  int f;
    329  bool did_next = false;
    330 
    331  // Always restore cursor position for ":mksession".  For ":mkview" only
    332  // when 'viewoptions' contains "cursor".
    333  bool do_cursor = (flagp == &ssop_flags || *flagp & kOptSsopFlagCursor);
    334 
    335  // Local argument list.
    336  if (wp->w_alist == &global_alist) {
    337    PUTLINE_FAIL("argglobal");
    338  } else {
    339    if (ses_arglist(fd, "arglocal", &wp->w_alist->al_ga,
    340                    flagp == &vop_flags
    341                    || !(*flagp & kOptSsopFlagCurdir)
    342                    || tp->tp_localdir != NULL
    343                    || wp->w_localdir != NULL, flagp) == FAIL) {
    344      return FAIL;
    345    }
    346  }
    347 
    348  // Only when part of a session: restore the argument index.  Some
    349  // arguments may have been deleted, check if the index is valid.
    350  if (wp->w_arg_idx != current_arg_idx && wp->w_arg_idx < WARGCOUNT(wp)
    351      && flagp == &ssop_flags) {
    352    if (fprintf(fd, "%" PRId64 "argu\n", (int64_t)wp->w_arg_idx + 1) < 0) {
    353      return FAIL;
    354    }
    355    did_next = true;
    356  }
    357 
    358  // Edit the file.  Skip this when ":next" already did it.
    359  if (add_edit && (!did_next || wp->w_arg_idx_invalid)) {
    360    char *fname_esc = ses_escape_fname(ses_get_fname(wp->w_buffer, flagp), flagp);
    361    if (bt_help(wp->w_buffer)) {
    362      char *curtag = "";
    363 
    364      // A help buffer needs some options to be set.
    365      // First, create a new empty buffer with "buftype=help".
    366      // Then ":help" will re-use both the buffer and the window and set
    367      // the options, even when "options" is not in 'sessionoptions'.
    368      if (0 < wp->w_tagstackidx && wp->w_tagstackidx <= wp->w_tagstacklen) {
    369        curtag = wp->w_tagstack[wp->w_tagstackidx - 1].tagname;
    370      }
    371 
    372      if (put_line(fd, "enew | setl bt=help") == FAIL
    373          || fprintf(fd, "help %s", curtag) < 0 || put_eol(fd) == FAIL) {
    374        xfree(fname_esc);
    375        return FAIL;
    376      }
    377    } else if (wp->w_buffer->b_ffname != NULL
    378               && (!bt_nofilename(wp->w_buffer) || wp->w_buffer->terminal)) {
    379      // Load the file.
    380 
    381      // Editing a file in this buffer: use ":edit file".
    382      // This may have side effects! (e.g., compressed or network file).
    383      //
    384      // Note, if a buffer for that file already exists, use :badd to
    385      // edit that buffer, to not lose folding information (:edit resets
    386      // folds in other buffers)
    387      if (fprintf(fd,
    388                  "if bufexists(fnamemodify(\"%s\", \":p\")) | buffer %s | else | edit %s | endif\n"
    389                  // Fixup :terminal buffer name. #7836
    390                  "if &buftype ==# 'terminal'\n"
    391                  "  silent file %s\n"
    392                  "endif\n",
    393                  fname_esc,
    394                  fname_esc,
    395                  fname_esc,
    396                  fname_esc) < 0) {
    397        xfree(fname_esc);
    398        return FAIL;
    399      }
    400    } else {
    401      // No file in this buffer, just make it empty.
    402      PUTLINE_FAIL("enew");
    403      if (wp->w_buffer->b_ffname != NULL) {
    404        // The buffer does have a name, but it's not a file name.
    405        if (fprintf(fd, "file %s\n", fname_esc) < 0) {
    406          xfree(fname_esc);
    407          return FAIL;
    408        }
    409      }
    410      do_cursor = false;
    411    }
    412    xfree(fname_esc);
    413  }
    414 
    415  if (wp->w_alt_fnum) {
    416    buf_T *const alt = buflist_findnr(wp->w_alt_fnum);
    417 
    418    // Set the alternate file if the buffer is listed.
    419    if ((flagp == &ssop_flags) && alt != NULL && alt->b_fname != NULL
    420        && *alt->b_fname != NUL
    421        && alt->b_p_bl
    422        // do not set balt if buffer is terminal and "terminal" is not set in options
    423        && !(bt_terminal(alt) && !(ssop_flags & kOptSsopFlagTerminal))
    424        && (fputs("balt ", fd) < 0
    425            || ses_fname(fd, alt, flagp, true) == FAIL)) {
    426      return FAIL;
    427    }
    428  }
    429 
    430  // Local mappings and abbreviations.
    431  if ((*flagp & (kOptSsopFlagOptions | kOptSsopFlagLocaloptions))
    432      && makemap(fd, wp->w_buffer) == FAIL) {
    433    return FAIL;
    434  }
    435 
    436  // Local options.  Need to go to the window temporarily.
    437  // Store only local values when using ":mkview" and when ":mksession" is
    438  // used and 'sessionoptions' doesn't include "nvim/options".
    439  // Some folding options are always stored when "folds" is included,
    440  // otherwise the folds would not be restored correctly.
    441  win_T *save_curwin = curwin;
    442  curwin = wp;
    443  curbuf = curwin->w_buffer;
    444  if (*flagp & (kOptSsopFlagOptions | kOptSsopFlagLocaloptions)) {
    445    f = makeset(fd, OPT_LOCAL,
    446                flagp == &vop_flags || !(*flagp & kOptSsopFlagOptions));
    447  } else if (*flagp & kOptSsopFlagFolds) {
    448    f = makefoldset(fd);
    449  } else {
    450    f = OK;
    451  }
    452  curwin = save_curwin;
    453  curbuf = curwin->w_buffer;
    454  if (f == FAIL) {
    455    return FAIL;
    456  }
    457 
    458  // Save Folds when 'buftype' is empty and for help files.
    459  if ((*flagp & kOptSsopFlagFolds)
    460      && wp->w_buffer->b_ffname != NULL
    461      && (bt_normal(wp->w_buffer)
    462          || bt_help(wp->w_buffer))) {
    463    if (put_folds(fd, wp) == FAIL) {
    464      return FAIL;
    465    }
    466  }
    467 
    468  // Set the cursor after creating folds, since that moves the cursor.
    469  if (do_cursor) {
    470    // Restore the cursor line in the file and relatively in the
    471    // window.  Don't use "G", it changes the jumplist.
    472    if (wp->w_view_height <= 0) {
    473      if (fprintf(fd, "let s:l = %" PRIdLINENR "\n", wp->w_cursor.lnum) < 0) {
    474        return FAIL;
    475      }
    476    } else if (fprintf(fd,
    477                       "let s:l = %" PRIdLINENR " - ((%" PRIdLINENR
    478                       " * winheight(0) + %d) / %d)\n",
    479                       wp->w_cursor.lnum,
    480                       wp->w_cursor.lnum - wp->w_topline,
    481                       (wp->w_view_height / 2),
    482                       wp->w_view_height) < 0) {
    483      return FAIL;
    484    }
    485    if (fprintf(fd,
    486                "if s:l < 1 | let s:l = 1 | endif\n"
    487                "keepjumps exe s:l\n"
    488                "normal! zt\n"
    489                "keepjumps %" PRIdLINENR "\n",
    490                wp->w_cursor.lnum) < 0) {
    491      return FAIL;
    492    }
    493    // Restore the cursor column and left offset when not wrapping.
    494    if (wp->w_cursor.col == 0) {
    495      PUTLINE_FAIL("normal! 0");
    496    } else {
    497      if (!wp->w_p_wrap && wp->w_leftcol > 0 && wp->w_width > 0) {
    498        if (fprintf(fd,
    499                    "let s:c = %" PRId64 " - ((%" PRId64
    500                    " * winwidth(0) + %" PRId64 ") / %" PRId64 ")\n"
    501                    "if s:c > 0\n"
    502                    "  exe 'normal! ' . s:c . '|zs' . %" PRId64 " . '|'\n"
    503                    "else\n",
    504                    (int64_t)wp->w_virtcol + 1,
    505                    (int64_t)(wp->w_virtcol - wp->w_leftcol),
    506                    (int64_t)(wp->w_width / 2),
    507                    (int64_t)wp->w_width,
    508                    (int64_t)wp->w_virtcol + 1) < 0
    509            || put_view_curpos(fd, wp, "  ") == FAIL
    510            || put_line(fd, "endif") == FAIL) {
    511          return FAIL;
    512        }
    513      } else if (put_view_curpos(fd, wp, "") == FAIL) {
    514        return FAIL;
    515      }
    516    }
    517  }
    518 
    519  // Local directory, if the current flag is not view options or the "curdir"
    520  // option is included.
    521  if (wp->w_localdir != NULL
    522      && (flagp != &vop_flags || (*flagp & kOptSsopFlagCurdir))) {
    523    if (fputs("lcd ", fd) < 0
    524        || ses_put_fname(fd, wp->w_localdir, flagp) == FAIL
    525        || fprintf(fd, "\n") < 0) {
    526      return FAIL;
    527    }
    528    did_lcd = true;
    529  }
    530 
    531  return OK;
    532 }
    533 
    534 static int store_session_globals(FILE *fd)
    535 {
    536  TV_DICT_ITER(get_globvar_dict(), this_var, {
    537    if ((this_var->di_tv.v_type == VAR_NUMBER
    538         || this_var->di_tv.v_type == VAR_STRING)
    539        && var_flavour(this_var->di_key) == VAR_FLAVOUR_SESSION) {
    540      // Escape special characters with a backslash.  Turn a LF and
    541      // CR into \n and \r.
    542      char *const p = vim_strsave_escaped(tv_get_string(&this_var->di_tv), "\\\"\n\r");
    543      for (char *t = p; *t != NUL; t++) {
    544        if (*t == '\n') {
    545          *t = 'n';
    546        } else if (*t == '\r') {
    547          *t = 'r';
    548        }
    549      }
    550      if ((fprintf(fd, "let %s = %c%s%c",
    551                   this_var->di_key,
    552                   ((this_var->di_tv.v_type == VAR_STRING) ? '"' : ' '),
    553                   p,
    554                   ((this_var->di_tv.v_type == VAR_STRING) ? '"' : ' ')) < 0)
    555          || put_eol(fd) == FAIL) {
    556        xfree(p);
    557        return FAIL;
    558      }
    559      xfree(p);
    560    } else if (this_var->di_tv.v_type == VAR_FLOAT
    561               && var_flavour(this_var->di_key) == VAR_FLAVOUR_SESSION) {
    562      float_T f = this_var->di_tv.vval.v_float;
    563      int sign = ' ';
    564 
    565      if (f < 0) {
    566        f = -f;
    567        sign = '-';
    568      }
    569      if ((fprintf(fd, "let %s = %c%f", this_var->di_key, sign, f) < 0)
    570          || put_eol(fd) == FAIL) {
    571        return FAIL;
    572      }
    573    }
    574  });
    575  return OK;
    576 }
    577 
    578 /// Writes commands for restoring the current buffers, for :mksession.
    579 ///
    580 /// Legacy 'sessionoptions'/'viewoptions' flags kOptSsopFlagUnix, kOptSsopFlagSlash are
    581 /// always enabled.
    582 ///
    583 /// @param dirnow  Current directory name
    584 /// @param fd  File descriptor to write to
    585 ///
    586 /// @return FAIL on error, OK otherwise.
    587 static int makeopens(FILE *fd, char *dirnow)
    588 {
    589  bool only_save_windows = true;
    590  bool restore_size = true;
    591  win_T *edited_win = NULL;
    592  win_T *tab_firstwin;
    593  frame_T *tab_topframe;
    594  int cur_arg_idx = 0;
    595  int next_arg_idx = 0;
    596 
    597  if (ssop_flags & kOptSsopFlagBuffers) {
    598    only_save_windows = false;  // Save ALL buffers
    599  }
    600 
    601  // Begin by setting v:this_session, and then other sessionable variables.
    602  PUTLINE_FAIL("let v:this_session=expand(\"<sfile>:p\")");
    603 
    604  PUTLINE_FAIL("doautoall SessionLoadPre");
    605 
    606  if (ssop_flags & kOptSsopFlagGlobals) {
    607    if (store_session_globals(fd) == FAIL) {
    608      return FAIL;
    609    }
    610  }
    611 
    612  // Close all windows and tabs but one.
    613  PUTLINE_FAIL("silent only");
    614  if ((ssop_flags & kOptSsopFlagTabpages)
    615      && put_line(fd, "silent tabonly") == FAIL) {
    616    return FAIL;
    617  }
    618 
    619  // Now a :cd command to the session directory or the current directory
    620  if (ssop_flags & kOptSsopFlagSesdir) {
    621    PUTLINE_FAIL("exe \"cd \" . escape(expand(\"<sfile>:p:h\"), ' ')");
    622  } else if (ssop_flags & kOptSsopFlagCurdir) {
    623    char *sname = home_replace_save(NULL, globaldir != NULL ? globaldir : dirnow);
    624    char *fname_esc = ses_escape_fname(sname, &ssop_flags);
    625    if (fprintf(fd, "cd %s\n", fname_esc) < 0) {
    626      xfree(fname_esc);
    627      xfree(sname);
    628      return FAIL;
    629    }
    630    xfree(fname_esc);
    631    xfree(sname);
    632  }
    633 
    634  if (fprintf(fd,
    635              "%s",
    636              // If there is an empty, unnamed buffer we will wipe it out later.
    637              // Remember the buffer number.
    638              "if expand('%') == '' && !&modified && line('$') <= 1"
    639              " && getline(1) == ''\n"
    640              "  let s:wipebuf = bufnr('%')\n"
    641              "endif\n") < 0) {
    642    return FAIL;
    643  }
    644 
    645  // Save 'shortmess' if not storing options.
    646  if ((ssop_flags & kOptSsopFlagOptions) == 0) {
    647    PUTLINE_FAIL("let s:shortmess_save = &shortmess");
    648  }
    649 
    650  // Set 'shortmess' for the following.
    651  PUTLINE_FAIL("set shortmess+=aoO");
    652 
    653  // Now save the current files, current buffer first.
    654  // Put all buffers into the buffer list.
    655  // Do it very early to preserve buffer order after loading session (which
    656  // can be disrupted by prior `edit` or `tabedit` calls).
    657  FOR_ALL_BUFFERS(buf) {
    658    if (!(only_save_windows && buf->b_nwindows == 0)
    659        && !(buf->b_help && !(ssop_flags & kOptSsopFlagHelp))
    660        && !(bt_terminal(buf) && !(ssop_flags & kOptSsopFlagTerminal))
    661        && buf->b_fname != NULL
    662        && buf->b_p_bl) {
    663      if (fprintf(fd, "badd +%" PRId64 " ",
    664                  kv_size(buf->b_wininfo) == 0
    665                  ? 1 : (int64_t)kv_A(buf->b_wininfo, 0)->wi_mark.mark.lnum) < 0
    666          || ses_fname(fd, buf, &ssop_flags, true) == FAIL) {
    667        return FAIL;
    668      }
    669    }
    670  }
    671 
    672  // the global argument list
    673  if (ses_arglist(fd, "argglobal", &global_alist.al_ga,
    674                  !(ssop_flags & kOptSsopFlagCurdir), &ssop_flags) == FAIL) {
    675    return FAIL;
    676  }
    677 
    678  if (ssop_flags & kOptSsopFlagResize) {
    679    // Note: after the restore we still check it worked!
    680    if (fprintf(fd, "set lines=%" PRId64 " columns=%" PRId64 "\n",
    681                (int64_t)Rows, (int64_t)Columns) < 0) {
    682      return FAIL;
    683    }
    684  }
    685 
    686  bool restore_stal = false;
    687  // When there are two or more tabpages and 'showtabline' is 1 the tabline
    688  // will be displayed when creating the next tab.  That resizes the windows
    689  // in the first tab, which may cause problems.  Set 'showtabline' to 2
    690  // temporarily to avoid that.
    691  if (p_stal == 1 && first_tabpage->tp_next != NULL) {
    692    PUTLINE_FAIL("set stal=2");
    693    restore_stal = true;
    694  }
    695 
    696  if ((ssop_flags & kOptSsopFlagTabpages)) {
    697    // "tabpages" is in 'sessionoptions': Similar to ses_win_rec() below,
    698    // populate the tab pages first so later local options won't be copied
    699    // to the new tabs.
    700    FOR_ALL_TABS(tp) {
    701      // Use `bufhidden=wipe` to remove empty "placeholder" buffers once
    702      // they are not needed. This prevents creating extra buffers (see
    703      // cause of Vim patch 8.1.0829)
    704      if (tp->tp_next != NULL && put_line(fd, "tabnew +setlocal\\ bufhidden=wipe") == FAIL) {
    705        return FAIL;
    706      }
    707    }
    708 
    709    if (first_tabpage->tp_next != NULL && put_line(fd, "tabrewind") == FAIL) {
    710      return FAIL;
    711    }
    712  }
    713 
    714  // Assume "tabpages" is in 'sessionoptions'.  If not then we only do
    715  // "curtab" and bail out of the loop.
    716  bool restore_height_width = false;
    717  FOR_ALL_TABS(tp) {
    718    bool need_tabnext = false;
    719    int cnr = 1;
    720 
    721    // May repeat putting Windows for each tab, when "tabpages" is in
    722    // 'sessionoptions'.
    723    // Don't use goto_tabpage(), it may change directory and trigger
    724    // autocommands.
    725    if ((ssop_flags & kOptSsopFlagTabpages)) {
    726      if (tp == curtab) {
    727        tab_firstwin = firstwin;
    728        tab_topframe = topframe;
    729      } else {
    730        tab_firstwin = tp->tp_firstwin;
    731        tab_topframe = tp->tp_topframe;
    732      }
    733      if (tp != first_tabpage) {
    734        need_tabnext = true;
    735      }
    736    } else {
    737      tp = curtab;
    738      tab_firstwin = firstwin;
    739      tab_topframe = topframe;
    740    }
    741 
    742    // Before creating the window layout, try loading one file.  If this
    743    // is aborted we don't end up with a number of useless windows.
    744    // This may have side effects! (e.g., compressed or network file).
    745    for (win_T *wp = tab_firstwin; wp != NULL; wp = wp->w_next) {
    746      if (ses_do_win(wp)
    747          && wp->w_buffer->b_ffname != NULL
    748          && !bt_help(wp->w_buffer)
    749          && !bt_nofilename(wp->w_buffer)) {
    750        if (need_tabnext && put_line(fd, "tabnext") == FAIL) {
    751          return FAIL;
    752        }
    753        need_tabnext = false;
    754 
    755        if (fputs("edit ", fd) < 0
    756            || ses_fname(fd, wp->w_buffer, &ssop_flags, true) == FAIL) {
    757          return FAIL;
    758        }
    759        if (!wp->w_arg_idx_invalid) {
    760          edited_win = wp;
    761        }
    762        break;
    763      }
    764    }
    765 
    766    // If no file got edited create an empty tab page.
    767    if (need_tabnext && put_line(fd, "tabnext") == FAIL) {
    768      return FAIL;
    769    }
    770 
    771    if (tab_topframe->fr_layout != FR_LEAF) {
    772      // Save current window layout.
    773      PUTLINE_FAIL("let s:save_splitbelow = &splitbelow");
    774      PUTLINE_FAIL("let s:save_splitright = &splitright");
    775      PUTLINE_FAIL("set splitbelow splitright");
    776      if (ses_win_rec(fd, tab_topframe) == FAIL) {
    777        return FAIL;
    778      }
    779      PUTLINE_FAIL("let &splitbelow = s:save_splitbelow");
    780      PUTLINE_FAIL("let &splitright = s:save_splitright");
    781    }
    782 
    783    // Check if window sizes can be restored (no windows omitted).
    784    // Remember the window number of the current window after restoring.
    785    int nr = 0;
    786    for (win_T *wp = tab_firstwin; wp != NULL; wp = wp->w_next) {
    787      if (ses_do_win(wp)) {
    788        nr++;
    789      } else if (!wp->w_floating) {
    790        restore_size = false;
    791      }
    792      if (curwin == wp) {
    793        cnr = nr;
    794      }
    795    }
    796 
    797    if (tab_firstwin != NULL && tab_firstwin->w_next != NULL) {
    798      // Go to the first window.
    799      PUTLINE_FAIL("wincmd t");
    800 
    801      // If more than one window, see if sizes can be restored.
    802      // First set 'winheight' and 'winwidth' to 1 to avoid the windows
    803      // being resized when moving between windows.
    804      // Do this before restoring the view, so that the topline and the
    805      // cursor can be set.  This is done again below.
    806      // winminheight and winminwidth need to be set to avoid an error if
    807      // the user has set winheight or winwidth.
    808      PUTLINE_FAIL("let s:save_winminheight = &winminheight");
    809      PUTLINE_FAIL("let s:save_winminwidth = &winminwidth");
    810      if (fprintf(fd,
    811                  "set winminheight=0\n"
    812                  "set winheight=1\n"
    813                  "set winminwidth=0\n"
    814                  "set winwidth=1\n") < 0) {
    815        return FAIL;
    816      }
    817      restore_height_width = true;
    818    }
    819    if (nr > 1 && ses_winsizes(fd, restore_size, tab_firstwin) == FAIL) {
    820      return FAIL;
    821    }
    822 
    823    // Restore the tab-local working directory if specified
    824    // Do this before the windows, so that the window-local directory can
    825    // override the tab-local directory.
    826    if ((ssop_flags & kOptSsopFlagCurdir) && tp->tp_localdir != NULL) {
    827      if (fputs("tcd ", fd) < 0
    828          || ses_put_fname(fd, tp->tp_localdir, &ssop_flags) == FAIL
    829          || put_eol(fd) == FAIL) {
    830        return FAIL;
    831      }
    832      did_lcd = true;
    833    }
    834 
    835    // Restore the view of the window (options, file, cursor, etc.).
    836    for (win_T *wp = tab_firstwin; wp != NULL; wp = wp->w_next) {
    837      if (!ses_do_win(wp)) {
    838        continue;
    839      }
    840      if (put_view(fd, wp, tp, wp != edited_win, &ssop_flags, cur_arg_idx)
    841          == FAIL) {
    842        return FAIL;
    843      }
    844      if (nr > 1 && put_line(fd, "wincmd w") == FAIL) {
    845        return FAIL;
    846      }
    847      next_arg_idx = wp->w_arg_idx;
    848    }
    849 
    850    // The argument index in the first tab page is zero, need to set it in
    851    // each window.  For further tab pages it's the window where we do
    852    // "tabedit".
    853    cur_arg_idx = next_arg_idx;
    854 
    855    // Restore cursor to the current window if it's not the first one.
    856    if (cnr > 1 && (fprintf(fd, "%dwincmd w\n", cnr) < 0)) {
    857      return FAIL;
    858    }
    859 
    860    // Restore window sizes again after jumping around in windows, because
    861    // the current window has a minimum size while others may not.
    862    if (nr > 1 && ses_winsizes(fd, restore_size, tab_firstwin) == FAIL) {
    863      return FAIL;
    864    }
    865 
    866    // Don't continue in another tab page when doing only the current one
    867    // or when at the last tab page.
    868    if (!(ssop_flags & kOptSsopFlagTabpages)) {
    869      break;
    870    }
    871  }
    872 
    873  if (ssop_flags & kOptSsopFlagTabpages) {
    874    if (fprintf(fd, "tabnext %d\n", tabpage_index(curtab)) < 0) {
    875      return FAIL;
    876    }
    877  }
    878  if (restore_stal && put_line(fd, "set stal=1") == FAIL) {
    879    return FAIL;
    880  }
    881 
    882  // Wipe out an empty unnamed buffer we started in.
    883  if (fprintf(fd, "%s",
    884              "if exists('s:wipebuf') "
    885              "&& len(win_findbuf(s:wipebuf)) == 0 "
    886              "&& getbufvar(s:wipebuf, '&buftype') isnot# 'terminal'\n"
    887              "  silent exe 'bwipe ' . s:wipebuf\n"
    888              "endif\n"
    889              "unlet! s:wipebuf\n") < 0) {
    890    return FAIL;
    891  }
    892 
    893  // Re-apply 'winheight' and 'winwidth'.
    894  if (fprintf(fd, "set winheight=%" PRId64 " winwidth=%" PRId64 "\n",
    895              (int64_t)p_wh, (int64_t)p_wiw) < 0) {
    896    return FAIL;
    897  }
    898 
    899  // Restore 'shortmess'.
    900  if (ssop_flags & kOptSsopFlagOptions) {
    901    if (fprintf(fd, "set shortmess=%s\n", p_shm) < 0) {
    902      return FAIL;
    903    }
    904  } else {
    905    PUTLINE_FAIL("let &shortmess = s:shortmess_save");
    906  }
    907 
    908  if (restore_height_width) {
    909    // Restore 'winminheight' and 'winminwidth'.
    910    PUTLINE_FAIL("let &winminheight = s:save_winminheight");
    911    PUTLINE_FAIL("let &winminwidth = s:save_winminwidth");
    912  }
    913 
    914  // Lastly, execute the x.vim file if it exists.
    915  if (fprintf(fd, "%s",
    916              "let s:sx = expand(\"<sfile>:p:r\").\"x.vim\"\n"
    917              "if filereadable(s:sx)\n"
    918              "  exe \"source \" . fnameescape(s:sx)\n"
    919              "endif\n") < 0) {
    920    return FAIL;
    921  }
    922 
    923  return OK;
    924 }
    925 
    926 /// ":loadview [nr]"
    927 void ex_loadview(exarg_T *eap)
    928 {
    929  char *fname = get_view_file(*eap->arg);
    930  if (fname == NULL) {
    931    return;
    932  }
    933 
    934  if (do_source(fname, false, DOSO_NONE, NULL) == FAIL) {
    935    semsg(_(e_notopen), fname);
    936  }
    937  xfree(fname);
    938 }
    939 
    940 /// ":mkexrc", ":mkvimrc", ":mkview", ":mksession".
    941 ///
    942 /// Legacy 'sessionoptions'/'viewoptions' flags are always enabled:
    943 ///   - kOptSsopFlagUnix: line-endings are LF
    944 ///   - kOptSsopFlagSlash: filenames are written with "/" slash
    945 void ex_mkrc(exarg_T *eap)
    946 {
    947  bool view_session = false;  // :mkview, :mksession
    948  int using_vdir = false;  // using 'viewdir'?
    949  char *viewFile = NULL;
    950 
    951  if (eap->cmdidx == CMD_mksession || eap->cmdidx == CMD_mkview) {
    952    view_session = true;
    953  }
    954 
    955  // Use the short file name until ":lcd" is used.  We also don't use the
    956  // short file name when 'acd' is set, that is checked later.
    957  did_lcd = false;
    958 
    959  char *fname;
    960  // ":mkview" or ":mkview 9": generate file name with 'viewdir'
    961  if (eap->cmdidx == CMD_mkview
    962      && (*eap->arg == NUL
    963          || (ascii_isdigit(*eap->arg) && eap->arg[1] == NUL))) {
    964    eap->forceit = true;
    965    fname = get_view_file(*eap->arg);
    966    if (fname == NULL) {
    967      return;
    968    }
    969    viewFile = fname;
    970    using_vdir = true;
    971  } else if (*eap->arg != NUL) {
    972    fname = eap->arg;
    973  } else if (eap->cmdidx == CMD_mkvimrc) {
    974    fname = VIMRC_FILE;
    975  } else if (eap->cmdidx == CMD_mksession) {
    976    fname = SESSION_FILE;
    977  } else {
    978    fname = EXRC_FILE;
    979  }
    980 
    981  // When using 'viewdir' may have to create the directory.
    982  if (using_vdir && !os_isdir(p_vdir)) {
    983    vim_mkdir_emsg(p_vdir, 0755);
    984  }
    985 
    986  FILE *fd = open_exfile(fname, eap->forceit, WRITEBIN);
    987  if (fd != NULL) {
    988    bool failed = false;
    989    unsigned *flagp;
    990    if (eap->cmdidx == CMD_mkview) {
    991      flagp = &vop_flags;
    992    } else {
    993      flagp = &ssop_flags;
    994    }
    995 
    996    // Write the version command for :mkvimrc
    997    if (eap->cmdidx == CMD_mkvimrc) {
    998      put_line(fd, "version 6.0");
    999    }
   1000 
   1001    if (eap->cmdidx == CMD_mksession) {
   1002      if (put_line(fd, "let SessionLoad = 1") == FAIL) {
   1003        failed = true;
   1004      }
   1005    }
   1006 
   1007    if (!view_session || (eap->cmdidx == CMD_mksession
   1008                          && (*flagp & kOptSsopFlagOptions))) {
   1009      int flags = OPT_GLOBAL;
   1010 
   1011      if (eap->cmdidx == CMD_mksession && (*flagp & kOptSsopFlagSkiprtp)) {
   1012        flags |= OPT_SKIPRTP;
   1013      }
   1014      failed |= (makemap(fd, NULL) == FAIL
   1015                 || makeset(fd, flags, false) == FAIL);
   1016    }
   1017 
   1018    if (!failed && view_session) {
   1019      if (put_line(fd,
   1020                   "let s:so_save = &g:so | let s:siso_save = &g:siso"
   1021                   " | setg so=0 siso=0 | setl so=-1 siso=-1") == FAIL) {
   1022        failed = true;
   1023      }
   1024      if (eap->cmdidx == CMD_mksession) {
   1025        char *dirnow;  // current directory
   1026 
   1027        dirnow = xmalloc(MAXPATHL);
   1028 
   1029        // Change to session file's dir.
   1030        if (os_dirname(dirnow, MAXPATHL) == FAIL
   1031            || os_chdir(dirnow) != 0) {
   1032          *dirnow = NUL;
   1033        }
   1034        if (*dirnow != NUL && (ssop_flags & kOptSsopFlagSesdir)) {
   1035          if (vim_chdirfile(fname, kCdCauseOther) == OK) {
   1036            shorten_fnames(true);
   1037          }
   1038        } else if (*dirnow != NUL
   1039                   && (ssop_flags & kOptSsopFlagCurdir) && globaldir != NULL) {
   1040          if (os_chdir(globaldir) == 0) {
   1041            shorten_fnames(true);
   1042          }
   1043        }
   1044 
   1045        failed |= (makeopens(fd, dirnow) == FAIL);
   1046 
   1047        // restore original dir
   1048        if (*dirnow != NUL && ((ssop_flags & kOptSsopFlagSesdir)
   1049                               || ((ssop_flags & kOptSsopFlagCurdir) && globaldir !=
   1050                                   NULL))) {
   1051          if (os_chdir(dirnow) != 0) {
   1052            emsg(_(e_prev_dir));
   1053          }
   1054          shorten_fnames(true);
   1055        }
   1056        xfree(dirnow);
   1057      } else {
   1058        failed |= (put_view(fd, curwin, curtab, !using_vdir, flagp, -1) == FAIL);
   1059      }
   1060      if (fprintf(fd,
   1061                  "%s",
   1062                  "let &g:so = s:so_save | let &g:siso = s:siso_save\n")
   1063          < 0) {
   1064        failed = true;
   1065      }
   1066      if (p_hls && fprintf(fd, "%s", "set hlsearch\n") < 0) {
   1067        failed = true;
   1068      }
   1069      if (no_hlsearch && fprintf(fd, "%s", "nohlsearch\n") < 0) {
   1070        failed = true;
   1071      }
   1072      if (fprintf(fd, "%s", "doautoall SessionLoadPost\n") < 0) {
   1073        failed = true;
   1074      }
   1075      if (eap->cmdidx == CMD_mksession) {
   1076        if (fprintf(fd, "unlet SessionLoad\n") < 0) {
   1077          failed = true;
   1078        }
   1079      }
   1080    }
   1081    if (put_line(fd, "\" vim: set ft=vim :") == FAIL) {
   1082      failed = true;
   1083    }
   1084 
   1085    failed |= fclose(fd);
   1086 
   1087    if (failed) {
   1088      emsg(_(e_write));
   1089    } else if (eap->cmdidx == CMD_mksession) {
   1090      // successful session write - set v:this_session
   1091      char *const tbuf = xmalloc(MAXPATHL);
   1092      if (vim_FullName(fname, tbuf, MAXPATHL, false) == OK) {
   1093        set_vim_var_string(VV_THIS_SESSION, tbuf, -1);
   1094      }
   1095      xfree(tbuf);
   1096    }
   1097  }
   1098 
   1099  xfree(viewFile);
   1100 
   1101  apply_autocmds(EVENT_SESSIONWRITEPOST, NULL, NULL, false, curbuf);
   1102 }
   1103 
   1104 /// @return  the name of the view file for the current buffer.
   1105 static char *get_view_file(char c)
   1106 {
   1107  if (curbuf->b_ffname == NULL) {
   1108    emsg(_(e_noname));
   1109    return NULL;
   1110  }
   1111  char *sname = home_replace_save(NULL, curbuf->b_ffname);
   1112 
   1113  // We want a file name without separators, because we're not going to make
   1114  // a directory.
   1115  //    "normal" path separator   -> "=+"
   1116  //    "="                       -> "=="
   1117  //    ":" path separator        -> "=-"
   1118  size_t len = 0;
   1119  for (char *p = sname; *p; p++) {
   1120    if (*p == '=' || vim_ispathsep(*p)) {
   1121      len++;
   1122    }
   1123  }
   1124  char *retval = xmalloc(strlen(sname) + len + strlen(p_vdir) + 9);
   1125  STRCPY(retval, p_vdir);
   1126  add_pathsep(retval);
   1127  char *s = retval + strlen(retval);
   1128  for (char *p = sname; *p; p++) {
   1129    if (*p == '=') {
   1130      *s++ = '=';
   1131      *s++ = '=';
   1132    } else if (vim_ispathsep(*p)) {
   1133      *s++ = '=';
   1134 #if defined(BACKSLASH_IN_FILENAME)
   1135      *s++ = (*p == ':') ? '-' : '+';
   1136 #else
   1137      *s++ = '+';
   1138 #endif
   1139    } else {
   1140      *s++ = *p;
   1141    }
   1142  }
   1143  *s++ = '=';
   1144  *s++ = c;
   1145  xmemcpyz(s, S_LEN(".vim"));
   1146 
   1147  xfree(sname);
   1148  return retval;
   1149 }
   1150 
   1151 /// TODO(justinmk): remove this, not needed after 5ba3cecb68cd.
   1152 int put_eol(FILE *fd)
   1153 {
   1154  if (putc('\n', fd) < 0) {
   1155    return FAIL;
   1156  }
   1157  return OK;
   1158 }
   1159 
   1160 /// TODO(justinmk): remove this, not needed after 5ba3cecb68cd.
   1161 int put_line(FILE *fd, char *s)
   1162 {
   1163  if (fprintf(fd, "%s\n", s) < 0) {
   1164    return FAIL;
   1165  }
   1166  return OK;
   1167 }