Patch 8.0.1523
Problem:    Cannot write and read terminal screendumps.
Solution:   Add term_dumpwrite(), term_dumpread() and term_dumpdiff().
            Also add assert_equalfile().
Files:      src/terminal.c, src/proto/terminal.pro, src/evalfunc.c,
            src/normal.c, src/eval.c, src/proto/eval.pro,
            runtime/doc/eval.txt, src/testdir/test_assert.vim


*** ../vim-8.0.1522/src/terminal.c      2018-02-16 20:01:00.234123812 +0100
--- src/terminal.c      2018-02-18 22:06:27.431911770 +0100
***************
*** 47,52 ****
--- 47,55 ----
   * - Redirecting output does not work on MS-Windows, 
Test_terminal_redir_file()
   *   is disabled.
   * - cursor blinks in terminal on widows with a timer. (xtal8, #2142)
+  * - What to store in a session file?  Shell at the prompt would be OK to
+  *   restore, but others may not.  Open the window and let the user start the
+  *   command?  Also see #2650.
   * - When closing gvim with an active terminal buffer, the dialog suggests
   *   saving the buffer.  Should say something else. (Manas Thakur, #2215)
   *   Also: #2223
***************
*** 55,63 ****
   * - Adding WinBar to terminal window doesn't display, text isn't shifted 
down.
   * - MS-Windows GUI: still need to type a key after shell exits?  #1924
   * - After executing a shell command the status line isn't redraw.
-  * - What to store in a session file?  Shell at the prompt would be OK to
-  *   restore, but others may not.  Open the window and let the user start the
-  *   command?
   * - implement term_setsize()
   * - add test for giving error for invalid 'termsize' value.
   * - support minimal size when 'termsize' is "rows*cols".
--- 58,63 ----
***************
*** 99,105 ****
  typedef struct {
    VTermScreenCellAttrs        attrs;
    char                        width;
!   VTermColor          fg, bg;
  } cellattr_T;
  
  typedef struct sb_line_S {
--- 99,106 ----
  typedef struct {
    VTermScreenCellAttrs        attrs;
    char                        width;
!   VTermColor          fg;
!   VTermColor          bg;
  } cellattr_T;
  
  typedef struct sb_line_S {
***************
*** 153,158 ****
--- 154,162 ----
      int               tl_scrollback_scrolled;
      cellattr_T        tl_default_color;
  
+     linenr_T  tl_top_diff_rows;   /* rows of top diff file or zero */
+     linenr_T  tl_bot_diff_rows;   /* rows of bottom diff file */
+ 
      VTermPos  tl_cursor_pos;
      int               tl_cursor_visible;
      int               tl_cursor_blink;
***************
*** 283,293 ****
  }
  
  /*
   * Start a terminal window and return its buffer.
   * Returns NULL when failed.
   */
      static buf_T *
! term_start(typval_T *argvar, jobopt_T *opt, int forceit)
  {
      exarg_T   split_ea;
      win_T     *old_curwin = curwin;
--- 287,320 ----
  }
  
  /*
+  * Close a terminal buffer (and its window).  Used when creating the terminal
+  * fails.
+  */
+     static void
+ term_close_buffer(buf_T *buf, buf_T *old_curbuf)
+ {
+     free_terminal(buf);
+     if (old_curbuf != NULL)
+     {
+       --curbuf->b_nwindows;
+       curbuf = old_curbuf;
+       curwin->w_buffer = curbuf;
+       ++curbuf->b_nwindows;
+     }
+ 
+     /* Wiping out the buffer will also close the window and call
+      * free_terminal(). */
+     do_buffer(DOBUF_WIPE, DOBUF_FIRST, FORWARD, buf->b_fnum, TRUE);
+ }
+ 
+ /*
   * Start a terminal window and return its buffer.
+  * When "without_job" is TRUE only create the buffer, b_term and open the
+  * window.
   * Returns NULL when failed.
   */
      static buf_T *
! term_start(typval_T *argvar, jobopt_T *opt, int without_job, int forceit)
  {
      exarg_T   split_ea;
      win_T     *old_curwin = curwin;
***************
*** 454,459 ****
--- 481,489 ----
      set_term_and_win_size(term);
      setup_job_options(opt, term->tl_rows, term->tl_cols);
  
+     if (without_job)
+       return curbuf;
+ 
      /* System dependent: setup the vterm and maybe start the job in it. */
      if (argvar->v_type == VAR_STRING
            && argvar->vval.v_string != NULL
***************
*** 492,511 ****
      }
      else
      {
!       buf_T *buf = curbuf;
! 
!       free_terminal(curbuf);
!       if (old_curbuf != NULL)
!       {
!           --curbuf->b_nwindows;
!           curbuf = old_curbuf;
!           curwin->w_buffer = curbuf;
!           ++curbuf->b_nwindows;
!       }
! 
!       /* Wiping out the buffer will also close the window and call
!        * free_terminal(). */
!       do_buffer(DOBUF_WIPE, DOBUF_FIRST, FORWARD, buf->b_fnum, TRUE);
        return NULL;
      }
      return newbuf;
--- 522,528 ----
      }
      else
      {
!       term_close_buffer(curbuf, old_curbuf);
        return NULL;
      }
      return newbuf;
***************
*** 597,603 ****
      argvar[0].v_type = VAR_STRING;
      argvar[0].vval.v_string = cmd;
      argvar[1].v_type = VAR_UNKNOWN;
!     term_start(argvar, &opt, eap->forceit);
      vim_free(tofree);
      vim_free(opt.jo_eof_chars);
  }
--- 614,620 ----
      argvar[0].v_type = VAR_STRING;
      argvar[0].vval.v_string = cmd;
      argvar[1].v_type = VAR_UNKNOWN;
!     term_start(argvar, &opt, FALSE, eap->forceit);
      vim_free(tofree);
      vim_free(opt.jo_eof_chars);
  }
***************
*** 1035,1040 ****
--- 1052,1087 ----
        && a->bg.blue == b->bg.blue;
  }
  
+ /*
+  * Add an empty scrollback line to "term".  When "lnum" is not zero, add the
+  * line at this position.  Otherwise at the end.
+  */
+     static int
+ add_empty_scrollback(term_T *term, cellattr_T *fill_attr, int lnum)
+ {
+     if (ga_grow(&term->tl_scrollback, 1) == OK)
+     {
+       sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data
+                                     + term->tl_scrollback.ga_len;
+ 
+       if (lnum > 0)
+       {
+           int i;
+ 
+           for (i = 0; i < term->tl_scrollback.ga_len - lnum; ++i)
+           {
+               *line = *(line - 1);
+               --line;
+           }
+       }
+       line->sb_cols = 0;
+       line->sb_cells = NULL;
+       line->sb_fill_attr = *fill_attr;
+       ++term->tl_scrollback.ga_len;
+       return OK;
+     }
+     return FALSE;
+ }
  
  /*
   * Add the current lines of the terminal to scrollback and to the buffer.
***************
*** 1079,1096 ****
            {
                /* Line was skipped, add an empty line. */
                --lines_skipped;
!               if (ga_grow(&term->tl_scrollback, 1) == OK)
!               {
!                   sb_line_T *line = (sb_line_T *)term->tl_scrollback.ga_data
!                                                 + term->tl_scrollback.ga_len;
! 
!                   line->sb_cols = 0;
!                   line->sb_cells = NULL;
!                   line->sb_fill_attr = fill_attr;
!                   ++term->tl_scrollback.ga_len;
! 
                    add_scrollback_line_to_buffer(term, (char_u *)"", 0);
-               }
            }
  
            if (len == 0)
--- 1126,1133 ----
            {
                /* Line was skipped, add an empty line. */
                --lines_skipped;
!               if (add_empty_scrollback(term, &fill_attr, 0) == OK)
                    add_scrollback_line_to_buffer(term, (char_u *)"", 0);
            }
  
            if (len == 0)
***************
*** 1849,1858 ****
  }
  
  /*
!  * Convert the attributes of a vterm cell into an attribute index.
   */
      static int
! cell2attr(VTermScreenCellAttrs cellattrs, VTermColor cellfg, VTermColor 
cellbg)
  {
      int attr = 0;
  
--- 1886,1895 ----
  }
  
  /*
!  * Convert Vterm attributes to highlight flags.
   */
      static int
! vtermAttr2hl(VTermScreenCellAttrs cellattrs)
  {
      int attr = 0;
  
***************
*** 1866,1871 ****
--- 1903,1937 ----
        attr |= HL_STRIKETHROUGH;
      if (cellattrs.reverse)
        attr |= HL_INVERSE;
+     return attr;
+ }
+ 
+ /*
+  * Store Vterm attributes in "cell" from highlight flags.
+  */
+     static void
+ hl2vtermAttr(int attr, cellattr_T *cell)
+ {
+     vim_memset(&cell->attrs, 0, sizeof(VTermScreenCellAttrs));
+     if (attr & HL_BOLD)
+       cell->attrs.bold = 1;
+     if (attr & HL_UNDERLINE)
+       cell->attrs.underline = 1;
+     if (attr & HL_ITALIC)
+       cell->attrs.italic = 1;
+     if (attr & HL_STRIKETHROUGH)
+       cell->attrs.strike = 1;
+     if (attr & HL_INVERSE)
+       cell->attrs.reverse = 1;
+ }
+ 
+ /*
+  * Convert the attributes of a vterm cell into an attribute index.
+  */
+     static int
+ cell2attr(VTermScreenCellAttrs cellattrs, VTermColor cellfg, VTermColor 
cellbg)
+ {
+     int attr = vtermAttr2hl(cellattrs);
  
  #ifdef FEAT_GUI
      if (gui.in_use)
***************
*** 2785,2790 ****
--- 2851,3580 ----
      return buf;
  }
  
+     static int
+ same_color(VTermColor *a, VTermColor *b)
+ {
+     return a->red == b->red
+       && a->green == b->green
+       && a->blue == b->blue
+       && a->ansi_index == b->ansi_index;
+ }
+ 
+     static void
+ dump_term_color(FILE *fd, VTermColor *color)
+ {
+     fprintf(fd, "%02x%02x%02x%d",
+           (int)color->red, (int)color->green, (int)color->blue,
+           (int)color->ansi_index);
+ }
+ 
+ /*
+  * "term_dumpwrite(buf, filename, max-height, max-width)" function
+  *
+  * Each screen cell in full is:
+  *    |{characters}+{attributes}#{fg-color}{color-idx}#{bg-color}{color-idx}
+  * {characters} is a space for an empty cell
+  * For a double-width character "+" is changed to "*" and the next cell is
+  * skipped.
+  * {attributes} is the decimal value of HL_BOLD + HL_UNDERLINE, etc.
+  *                      when "&" use the same as the previous cell.
+  * {fg-color} is hex RGB, when "&" use the same as the previous cell.
+  * {bg-color} is hex RGB, when "&" use the same as the previous cell.
+  * {color-idx} is a number from 0 to 255
+  *
+  * Screen cell with same width, attributes and color as the previous one:
+  *    |{characters}
+  *
+  * To use the color of the previous cell, use "&" instead of {color}-{idx}.
+  *
+  * Repeating the previous screen cell:
+  *    @{count}
+  */
+     void
+ f_term_dumpwrite(typval_T *argvars, typval_T *rettv UNUSED)
+ {
+     buf_T     *buf = term_get_buf(argvars);
+     term_T    *term;
+     char_u    *fname;
+     int               max_height = 99999;
+     int               max_width = 99999;
+     stat_T    st;
+     FILE      *fd;
+     VTermPos  pos;
+     VTermScreen *screen;
+     VTermScreenCell prev_cell;
+ 
+     if (check_restricted() || check_secure())
+       return;
+     if (buf == NULL)
+       return;
+     term = buf->b_term;
+ 
+     fname = get_tv_string_chk(&argvars[1]);
+     if (fname == NULL)
+       return;
+     if (mch_stat((char *)fname, &st) >= 0)
+     {
+       EMSG2(_("E953: File exists: %s"), fname);
+       return;
+     }
+ 
+     if (argvars[2].v_type != VAR_UNKNOWN)
+     {
+       max_height = get_tv_number(&argvars[2]);
+       if (argvars[3].v_type != VAR_UNKNOWN)
+           max_width = get_tv_number(&argvars[3]);
+     }
+ 
+     if (*fname == NUL || (fd = mch_fopen((char *)fname, WRITEBIN)) == NULL)
+     {
+       EMSG2(_(e_notcreate), *fname == NUL ? (char_u *)_("<empty>") : fname);
+       return;
+     }
+ 
+     vim_memset(&prev_cell, 0, sizeof(prev_cell));
+ 
+     screen = vterm_obtain_screen(term->tl_vterm);
+     for (pos.row = 0; pos.row < max_height && pos.row < term->tl_rows;
+                                                                    ++pos.row)
+     {
+       int             repeat = 0;
+ 
+       for (pos.col = 0; pos.col < max_width && pos.col < term->tl_cols;
+                                                                    ++pos.col)
+       {
+           VTermScreenCell cell;
+           int             same_attr;
+           int             same_chars = TRUE;
+           int             i;
+ 
+           if (vterm_screen_get_cell(screen, pos, &cell) == 0)
+               vim_memset(&cell, 0, sizeof(cell));
+ 
+           for (i = 0; i < VTERM_MAX_CHARS_PER_CELL; ++i)
+           {
+               if (cell.chars[i] != prev_cell.chars[i])
+                   same_chars = FALSE;
+               if (cell.chars[i] == NUL || prev_cell.chars[i] == NUL)
+                   break;
+           }
+           same_attr = vtermAttr2hl(cell.attrs)
+                                              == vtermAttr2hl(prev_cell.attrs)
+                       && same_color(&cell.fg, &prev_cell.fg)
+                       && same_color(&cell.bg, &prev_cell.bg);
+           if (same_chars && cell.width == prev_cell.width && same_attr)
+           {
+               ++repeat;
+           }
+           else
+           {
+               if (repeat > 0)
+               {
+                   fprintf(fd, "@%d", repeat);
+                   repeat = 0;
+               }
+               fputs("|", fd);
+ 
+               if (cell.chars[0] == NUL)
+                   fputs(" ", fd);
+               else
+               {
+                   char_u      charbuf[10];
+                   int         len;
+ 
+                   for (i = 0; i < VTERM_MAX_CHARS_PER_CELL
+                                                 && cell.chars[i] != NUL; ++i)
+                   {
+                       len = utf_char2bytes(cell.chars[0], charbuf);
+                       fwrite(charbuf, len, 1, fd);
+                   }
+               }
+ 
+               /* When only the characters differ we don't write anything, the
+                * following "|", "@" or NL will indicate using the same
+                * attributes. */
+               if (cell.width != prev_cell.width || !same_attr)
+               {
+                   if (cell.width == 2)
+                   {
+                       fputs("*", fd);
+                       ++pos.col;
+                   }
+                   else
+                       fputs("+", fd);
+ 
+                   if (same_attr)
+                   {
+                       fputs("&", fd);
+                   }
+                   else
+                   {
+                       fprintf(fd, "%d", vtermAttr2hl(cell.attrs));
+                       if (same_color(&cell.fg, &prev_cell.fg))
+                           fputs("&", fd);
+                       else
+                       {
+                           fputs("#", fd);
+                           dump_term_color(fd, &cell.fg);
+                       }
+                       if (same_color(&cell.bg, &prev_cell.bg))
+                           fputs("&", fd);
+                       else
+                       {
+                           fputs("#", fd);
+                           dump_term_color(fd, &cell.bg);
+                       }
+                   }
+               }
+ 
+               prev_cell = cell;
+           }
+       }
+       if (repeat > 0)
+           fprintf(fd, "@%d", repeat);
+       fputs("\n", fd);
+     }
+ 
+     fclose(fd);
+ }
+ 
+ /*
+  * Called when a dump is corrupted.  Put a breakpoint here when debugging.
+  */
+     static void
+ dump_is_corrupt(garray_T *gap)
+ {
+     ga_concat(gap, (char_u *)"CORRUPT");
+ }
+ 
+     static void
+ append_cell(garray_T *gap, cellattr_T *cell)
+ {
+     if (ga_grow(gap, 1) == OK)
+     {
+       *(((cellattr_T *)gap->ga_data) + gap->ga_len) = *cell;
+       ++gap->ga_len;
+     }
+ }
+ 
+ /*
+  * Read the dump file from "fd" and append lines to the current buffer.
+  * Return the cell width of the longest line.
+  */
+     static int
+ read_dump_file(FILE *fd)
+ {
+     int                   c;
+     garray_T      ga_text;
+     garray_T      ga_cell;
+     char_u        *prev_char = NULL;
+     int                   attr = 0;
+     cellattr_T            cell;
+     term_T        *term = curbuf->b_term;
+     int                   max_cells = 0;
+ 
+     ga_init2(&ga_text, 1, 90);
+     ga_init2(&ga_cell, sizeof(cellattr_T), 90);
+     vim_memset(&cell, 0, sizeof(cell));
+ 
+     c = fgetc(fd);
+     for (;;)
+     {
+       if (c == EOF)
+           break;
+       if (c == '\n')
+       {
+           /* End of a line: append it to the buffer. */
+           if (ga_text.ga_data == NULL)
+               dump_is_corrupt(&ga_text);
+           if (ga_grow(&term->tl_scrollback, 1) == OK)
+           {
+               sb_line_T   *line = (sb_line_T *)term->tl_scrollback.ga_data
+                                                 + term->tl_scrollback.ga_len;
+ 
+               if (max_cells < ga_cell.ga_len)
+                   max_cells = ga_cell.ga_len;
+               line->sb_cols = ga_cell.ga_len;
+               line->sb_cells = ga_cell.ga_data;
+               line->sb_fill_attr = term->tl_default_color;
+               ++term->tl_scrollback.ga_len;
+               ga_init(&ga_cell);
+ 
+               ga_append(&ga_text, NUL);
+               ml_append(curbuf->b_ml.ml_line_count, ga_text.ga_data,
+                                                       ga_text.ga_len, FALSE);
+           }
+           else
+               ga_clear(&ga_cell);
+           ga_text.ga_len = 0;
+ 
+           c = fgetc(fd);
+       }
+       else if (c == '|')
+       {
+           int prev_len = ga_text.ga_len;
+ 
+           /* normal character(s) followed by "+", "*", "|", "@" or NL */
+           c = fgetc(fd);
+           if (c != EOF)
+               ga_append(&ga_text, c);
+           for (;;)
+           {
+               c = fgetc(fd);
+               if (c == '+' || c == '*' || c == '|' || c == '@'
+                                                     || c == EOF || c == '\n')
+                   break;
+               ga_append(&ga_text, c);
+           }
+ 
+           /* save the character for repeating it */
+           vim_free(prev_char);
+           if (ga_text.ga_data != NULL)
+               prev_char = vim_strnsave(((char_u *)ga_text.ga_data) + prev_len,
+                                                   ga_text.ga_len - prev_len);
+ 
+           if (c == '@' || c == '|' || c == '\n')
+           {
+               /* use all attributes from previous cell */
+           }
+           else if (c == '+' || c == '*')
+           {
+               int is_bg;
+ 
+               cell.width = c == '+' ? 1 : 2;
+ 
+               c = fgetc(fd);
+               if (c == '&')
+               {
+                   /* use same attr as previous cell */
+                   c = fgetc(fd);
+               }
+               else if (isdigit(c))
+               {
+                   /* get the decimal attribute */
+                   attr = 0;
+                   while (isdigit(c))
+                   {
+                       attr = attr * 10 + (c - '0');
+                       c = fgetc(fd);
+                   }
+                   hl2vtermAttr(attr, &cell);
+               }
+               else
+                   dump_is_corrupt(&ga_text);
+ 
+               /* is_bg == 0: fg, is_bg == 1: bg */
+               for (is_bg = 0; is_bg <= 1; ++is_bg)
+               {
+                   if (c == '&')
+                   {
+                       /* use same color as previous cell */
+                       c = fgetc(fd);
+                   }
+                   else if (c == '#')
+                   {
+                       int red, green, blue, index = 0;
+ 
+                       c = fgetc(fd);
+                       red = hex2nr(c);
+                       c = fgetc(fd);
+                       red = (red << 4) + hex2nr(c);
+                       c = fgetc(fd);
+                       green = hex2nr(c);
+                       c = fgetc(fd);
+                       green = (green << 4) + hex2nr(c);
+                       c = fgetc(fd);
+                       blue = hex2nr(c);
+                       c = fgetc(fd);
+                       blue = (blue << 4) + hex2nr(c);
+                       c = fgetc(fd);
+                       if (!isdigit(c))
+                           dump_is_corrupt(&ga_text);
+                       while (isdigit(c))
+                       {
+                           index = index * 10 + (c - '0');
+                           c = fgetc(fd);
+                       }
+ 
+                       if (is_bg)
+                       {
+                           cell.bg.red = red;
+                           cell.bg.green = green;
+                           cell.bg.blue = blue;
+                           cell.bg.ansi_index = index;
+                       }
+                       else
+                       {
+                           cell.fg.red = red;
+                           cell.fg.green = green;
+                           cell.fg.blue = blue;
+                           cell.fg.ansi_index = index;
+                       }
+                   }
+                   else
+                       dump_is_corrupt(&ga_text);
+               }
+           }
+           else
+               dump_is_corrupt(&ga_text);
+ 
+           append_cell(&ga_cell, &cell);
+       }
+       else if (c == '@')
+       {
+           if (prev_char == NULL)
+               dump_is_corrupt(&ga_text);
+           else
+           {
+               int count = 0;
+ 
+               /* repeat previous character, get the count */
+               for (;;)
+               {
+                   c = fgetc(fd);
+                   if (!isdigit(c))
+                       break;
+                   count = count * 10 + (c - '0');
+               }
+ 
+               while (count-- > 0)
+               {
+                   ga_concat(&ga_text, prev_char);
+                   append_cell(&ga_cell, &cell);
+               }
+           }
+       }
+       else
+       {
+           dump_is_corrupt(&ga_text);
+           c = fgetc(fd);
+       }
+     }
+ 
+     if (ga_text.ga_len > 0)
+     {
+       /* trailing characters after last NL */
+       dump_is_corrupt(&ga_text);
+       ga_append(&ga_text, NUL);
+       ml_append(curbuf->b_ml.ml_line_count, ga_text.ga_data,
+                                                       ga_text.ga_len, FALSE);
+     }
+ 
+     ga_clear(&ga_text);
+     vim_free(prev_char);
+ 
+     return max_cells;
+ }
+ 
+ /*
+  * Common for "term_dumpdiff()" and "term_dumpload()".
+  */
+     static void
+ term_load_dump(typval_T *argvars, typval_T *rettv, int do_diff)
+ {
+     jobopt_T  opt;
+     buf_T     *buf;
+     char_u    buf1[NUMBUFLEN];
+     char_u    buf2[NUMBUFLEN];
+     char_u    *fname1;
+     char_u    *fname2;
+     FILE      *fd1;
+     FILE      *fd2;
+     char_u    *textline = NULL;
+ 
+     /* First open the files.  If this fails bail out. */
+     fname1 = get_tv_string_buf_chk(&argvars[0], buf1);
+     if (do_diff)
+       fname2 = get_tv_string_buf_chk(&argvars[1], buf2);
+     if (fname1 == NULL || (do_diff && fname2 == NULL))
+     {
+       EMSG(_(e_invarg));
+       return;
+     }
+     fd1 = mch_fopen((char *)fname1, READBIN);
+     if (fd1 == NULL)
+     {
+       EMSG2(_(e_notread), fname1);
+       return;
+     }
+     if (do_diff)
+     {
+       fd2 = mch_fopen((char *)fname2, READBIN);
+       if (fd2 == NULL)
+       {
+           fclose(fd1);
+           EMSG2(_(e_notread), fname2);
+           return;
+       }
+     }
+ 
+     init_job_options(&opt);
+     /* TODO: use the {options} argument */
+ 
+     /* TODO: use the file name arguments for the buffer name */
+     opt.jo_term_name = (char_u *)"dump diff";
+ 
+     buf = term_start(&argvars[0], &opt, TRUE, FALSE);
+     if (buf != NULL && buf->b_term != NULL)
+     {
+       int             i;
+       linenr_T        bot_lnum;
+       linenr_T        lnum;
+       term_T          *term = buf->b_term;
+       int             width;
+       int             width2;
+ 
+       rettv->vval.v_number = buf->b_fnum;
+ 
+       /* read the files, fill the buffer with the diff */
+       width = read_dump_file(fd1);
+ 
+       /* Delete the empty line that was in the empty buffer. */
+       ml_delete(1, FALSE);
+ 
+       /* For term_dumpload() we are done here. */
+       if (!do_diff)
+           goto theend;
+ 
+       term->tl_top_diff_rows = curbuf->b_ml.ml_line_count;
+ 
+       textline = alloc(width + 1);
+       if (textline == NULL)
+           goto theend;
+       for (i = 0; i < width; ++i)
+           textline[i] = '=';
+       textline[width] = NUL;
+       if (add_empty_scrollback(term, &term->tl_default_color, 0) == OK)
+           ml_append(curbuf->b_ml.ml_line_count, textline, 0, FALSE);
+       if (add_empty_scrollback(term, &term->tl_default_color, 0) == OK)
+           ml_append(curbuf->b_ml.ml_line_count, textline, 0, FALSE);
+ 
+       bot_lnum = curbuf->b_ml.ml_line_count;
+       width2 = read_dump_file(fd2);
+       if (width2 > width)
+       {
+           vim_free(textline);
+           textline = alloc(width2 + 1);
+           if (textline == NULL)
+               goto theend;
+           width = width2;
+           textline[width] = NUL;
+       }
+       term->tl_bot_diff_rows = curbuf->b_ml.ml_line_count - bot_lnum;
+ 
+       for (lnum = 1; lnum <= term->tl_top_diff_rows; ++lnum)
+       {
+           if (lnum + bot_lnum > curbuf->b_ml.ml_line_count)
+           {
+               /* bottom part has fewer rows, fill with "-" */
+               for (i = 0; i < width; ++i)
+                   textline[i] = '-';
+           }
+           else
+           {
+               char_u *line1;
+               char_u *line2;
+               char_u *p1;
+               char_u *p2;
+               int     col;
+               sb_line_T   *sb_line = (sb_line_T *)term->tl_scrollback.ga_data;
+               cellattr_T *cellattr1 = (sb_line + lnum - 1)->sb_cells;
+               cellattr_T *cellattr2 = (sb_line + lnum + bot_lnum - 1)
+                                                                   ->sb_cells;
+ 
+               /* Make a copy, getting the second line will invalidate it. */
+               line1 = vim_strsave(ml_get(lnum));
+               if (line1 == NULL)
+                   break;
+               p1 = line1;
+ 
+               line2 = ml_get(lnum + bot_lnum);
+               p2 = line2;
+               for (col = 0; col < width && *p1 != NUL && *p2 != NUL; ++col)
+               {
+                   int len1 = utfc_ptr2len(p1);
+                   int len2 = utfc_ptr2len(p2);
+ 
+                   textline[col] = ' ';
+                   if (len1 != len2 || STRNCMP(p1, p2, len1) != 0)
+                       textline[col] = 'X';
+                   else if (cellattr1 != NULL && cellattr2 != NULL)
+                   {
+                       if ((cellattr1 + col)->width
+                                                  != (cellattr2 + col)->width)
+                           textline[col] = 'w';
+                       else if (!same_color(&(cellattr1 + col)->fg,
+                                                  &(cellattr2 + col)->fg))
+                           textline[col] = 'f';
+                       else if (!same_color(&(cellattr1 + col)->bg,
+                                                  &(cellattr2 + col)->bg))
+                           textline[col] = 'b';
+                       else if (vtermAttr2hl((cellattr1 + col)->attrs)
+                                  != vtermAttr2hl(((cellattr2 + col)->attrs)))
+                           textline[col] = 'a';
+                   }
+                   p1 += len1;
+                   p2 += len2;
+                   /* TODO: handle different width */
+               }
+               vim_free(line1);
+ 
+               while (col < width)
+               {
+                   if (*p1 == NUL && *p2 == NUL)
+                       textline[col] = '?';
+                   else if (*p1 == NUL)
+                   {
+                       textline[col] = '+';
+                       p2 += utfc_ptr2len(p2);
+                   }
+                   else
+                   {
+                       textline[col] = '-';
+                       p1 += utfc_ptr2len(p1);
+                   }
+                   ++col;
+               }
+           }
+           if (add_empty_scrollback(term, &term->tl_default_color,
+                                                term->tl_top_diff_rows) == OK)
+               ml_append(term->tl_top_diff_rows + lnum, textline, 0, FALSE);
+           ++bot_lnum;
+       }
+ 
+       while (lnum + bot_lnum <= curbuf->b_ml.ml_line_count)
+       {
+           /* bottom part has more rows, fill with "+" */
+           for (i = 0; i < width; ++i)
+               textline[i] = '+';
+           if (add_empty_scrollback(term, &term->tl_default_color,
+                                                term->tl_top_diff_rows) == OK)
+               ml_append(term->tl_top_diff_rows + lnum, textline, 0, FALSE);
+           ++lnum;
+           ++bot_lnum;
+       }
+ 
+       term->tl_cols = width;
+     }
+ 
+ theend:
+     vim_free(textline);
+     fclose(fd1);
+     if (do_diff)
+       fclose(fd2);
+ }
+ 
+ /*
+  * If the current buffer shows the output of term_dumpdiff(), swap the top and
+  * bottom files.
+  * Return FAIL when this is not possible.
+  */
+     int
+ term_swap_diff()
+ {
+     term_T    *term = curbuf->b_term;
+     linenr_T  line_count;
+     linenr_T  top_rows;
+     linenr_T  bot_rows;
+     linenr_T  bot_start;
+     linenr_T  lnum;
+     char_u    *p;
+     sb_line_T *sb_line;
+ 
+     if (term == NULL
+           || !term_is_finished(curbuf)
+           || term->tl_top_diff_rows == 0
+           || term->tl_scrollback.ga_len == 0)
+       return FAIL;
+ 
+     line_count = curbuf->b_ml.ml_line_count;
+     top_rows = term->tl_top_diff_rows;
+     bot_rows = term->tl_bot_diff_rows;
+     bot_start = line_count - bot_rows;
+     sb_line = (sb_line_T *)term->tl_scrollback.ga_data;
+ 
+     /* move lines from top to above the bottom part */
+     for (lnum = 1; lnum <= top_rows; ++lnum)
+     {
+       p = vim_strsave(ml_get(1));
+       if (p == NULL)
+           return OK;
+       ml_append(bot_start, p, 0, FALSE);
+       ml_delete(1, FALSE);
+       vim_free(p);
+     }
+ 
+     /* move lines from bottom to the top */
+     for (lnum = 1; lnum <= bot_rows; ++lnum)
+     {
+       p = vim_strsave(ml_get(bot_start + lnum));
+       if (p == NULL)
+           return OK;
+       ml_delete(bot_start + lnum, FALSE);
+       ml_append(lnum - 1, p, 0, FALSE);
+       vim_free(p);
+     }
+ 
+     if (top_rows == bot_rows)
+     {
+       /* rows counts are equal, can swap cell properties */
+       for (lnum = 0; lnum < top_rows; ++lnum)
+       {
+           sb_line_T   temp;
+ 
+           temp = *(sb_line + lnum);
+           *(sb_line + lnum) = *(sb_line + bot_start + lnum);
+           *(sb_line + bot_start + lnum) = temp;
+       }
+     }
+     else
+     {
+       size_t          size = sizeof(sb_line_T) * term->tl_scrollback.ga_len;
+       sb_line_T       *temp = (sb_line_T *)alloc((int)size);
+ 
+       /* need to copy cell properties into temp memory */
+       if (temp != NULL)
+       {
+           mch_memmove(temp, term->tl_scrollback.ga_data, size);
+           mch_memmove(term->tl_scrollback.ga_data,
+                   temp + bot_start,
+                   sizeof(sb_line_T) * bot_rows);
+           mch_memmove((sb_line_T *)term->tl_scrollback.ga_data + bot_rows,
+                   temp + top_rows,
+                   sizeof(sb_line_T) * (line_count - top_rows - bot_rows));
+           mch_memmove((sb_line_T *)term->tl_scrollback.ga_data
+                                                      + line_count - top_rows,
+                   temp,
+                   sizeof(sb_line_T) * top_rows);
+           vim_free(temp);
+       }
+     }
+ 
+     term->tl_top_diff_rows = bot_rows;
+     term->tl_bot_diff_rows = top_rows;
+ 
+     update_screen(NOT_VALID);
+     return OK;
+ }
+ 
+ /*
+  * "term_dumpdiff(filename, filename, options)" function
+  */
+     void
+ f_term_dumpdiff(typval_T *argvars, typval_T *rettv)
+ {
+     term_load_dump(argvars, rettv, TRUE);
+ }
+ 
+ /*
+  * "term_dumpload(filename, options)" function
+  */
+     void
+ f_term_dumpload(typval_T *argvars, typval_T *rettv)
+ {
+     term_load_dump(argvars, rettv, FALSE);
+ }
+ 
  /*
   * "term_getaltscreen(buf)" function
   */
***************
*** 3230,3236 ****
  
      if (opt.jo_vertical)
        cmdmod.split = WSP_VERT;
!     buf = term_start(&argvars[0], &opt, FALSE);
  
      if (buf != NULL && buf->b_term != NULL)
        rettv->vval.v_number = buf->b_fnum;
--- 4020,4026 ----
  
      if (opt.jo_vertical)
        cmdmod.split = WSP_VERT;
!     buf = term_start(&argvars[0], &opt, FALSE, FALSE);
  
      if (buf != NULL && buf->b_term != NULL)
        rettv->vval.v_number = buf->b_fnum;
*** ../vim-8.0.1522/src/proto/terminal.pro      2017-12-01 21:07:16.220989905 
+0100
--- src/proto/terminal.pro      2018-02-18 19:05:51.375862627 +0100
***************
*** 21,26 ****
--- 21,30 ----
  char_u *term_get_status_text(term_T *term);
  int set_ref_in_term(int copyID);
  void set_terminal_default_colors(int cterm_fg, int cterm_bg);
+ void f_term_dumpwrite(typval_T *argvars, typval_T *rettv);
+ int term_swap_diff(void);
+ void f_term_dumpdiff(typval_T *argvars, typval_T *rettv);
+ void f_term_dumpload(typval_T *argvars, typval_T *rettv);
  void f_term_getaltscreen(typval_T *argvars, typval_T *rettv);
  void f_term_getattr(typval_T *argvars, typval_T *rettv);
  void f_term_getcursor(typval_T *argvars, typval_T *rettv);
*** ../vim-8.0.1522/src/evalfunc.c      2018-02-13 19:21:12.870210334 +0100
--- src/evalfunc.c      2018-02-18 21:00:25.124796442 +0100
***************
*** 46,51 ****
--- 46,52 ----
  static void f_argv(typval_T *argvars, typval_T *rettv);
  static void f_assert_beeps(typval_T *argvars, typval_T *rettv);
  static void f_assert_equal(typval_T *argvars, typval_T *rettv);
+ static void f_assert_equalfile(typval_T *argvars, typval_T *rettv);
  static void f_assert_exception(typval_T *argvars, typval_T *rettv);
  static void f_assert_fails(typval_T *argvars, typval_T *rettv);
  static void f_assert_false(typval_T *argvars, typval_T *rettv);
***************
*** 487,492 ****
--- 488,494 ----
  #endif
      {"assert_beeps",  1, 2, f_assert_beeps},
      {"assert_equal",  2, 3, f_assert_equal},
+     {"assert_equalfile", 2, 2, f_assert_equalfile},
      {"assert_exception", 1, 2, f_assert_exception},
      {"assert_fails",  1, 2, f_assert_fails},
      {"assert_false",  1, 2, f_assert_false},
***************
*** 847,852 ****
--- 849,857 ----
  #endif
      {"tempname",      0, 0, f_tempname},
  #ifdef FEAT_TERMINAL
+     {"term_dumpdiff", 2, 3, f_term_dumpdiff},
+     {"term_dumpload", 1, 2, f_term_dumpload},
+     {"term_dumpwrite",        2, 4, f_term_dumpwrite},
      {"term_getaltscreen", 1, 1, f_term_getaltscreen},
      {"term_getattr",  2, 2, f_term_getattr},
      {"term_getcursor",        1, 1, f_term_getcursor},
***************
*** 1297,1302 ****
--- 1302,1316 ----
  }
  
  /*
+  * "assert_equalfile(fname-one, fname-two)" function
+  */
+     static void
+ f_assert_equalfile(typval_T *argvars, typval_T *rettv UNUSED)
+ {
+     assert_equalfile(argvars);
+ }
+ 
+ /*
   * "assert_notequal(expected, actual[, msg])" function
   */
      static void
*** ../vim-8.0.1522/src/normal.c        2018-02-10 18:45:21.072822129 +0100
--- src/normal.c        2018-02-18 19:07:08.655340706 +0100
***************
*** 7474,7479 ****
--- 7474,7484 ----
      static void
  nv_subst(cmdarg_T *cap)
  {
+ #ifdef FEAT_TERMINAL
+     /* When showing output of term_dumpdiff() swap the top and botom. */
+     if (term_swap_diff() == OK)
+       return;
+ #endif
      if (VIsual_active)        /* "vs" and "vS" are the same as "vc" */
      {
        if (cap->cmdchar == 'S')
*** ../vim-8.0.1522/src/eval.c  2018-02-13 12:57:38.066977614 +0100
--- src/eval.c  2018-02-18 21:14:15.198659064 +0100
***************
*** 8834,8839 ****
--- 8834,8906 ----
  }
  
      void
+ assert_equalfile(typval_T *argvars)
+ {
+     char_u    buf1[NUMBUFLEN];
+     char_u    buf2[NUMBUFLEN];
+     char_u    *fname1 = get_tv_string_buf_chk(&argvars[0], buf1);
+     char_u    *fname2 = get_tv_string_buf_chk(&argvars[1], buf2);
+     garray_T  ga;
+     FILE      *fd1;
+     FILE      *fd2;
+ 
+     if (fname1 == NULL || fname2 == NULL)
+       return;
+ 
+     IObuff[0] = NUL;
+     fd1 = mch_fopen((char *)fname1, READBIN);
+     if (fd1 == NULL)
+     {
+       vim_snprintf((char *)IObuff, IOSIZE, (char *)e_notread, fname1);
+     }
+     else
+     {
+       fd2 = mch_fopen((char *)fname2, READBIN);
+       if (fd2 == NULL)
+       {
+           fclose(fd1);
+           vim_snprintf((char *)IObuff, IOSIZE, (char *)e_notread, fname2);
+       }
+       else
+       {
+           int c1, c2;
+           long count = 0;
+ 
+           for (;;)
+           {
+               c1 = fgetc(fd1);
+               c2 = fgetc(fd2);
+               if (c1 == EOF)
+               {
+                   if (c2 != EOF)
+                       STRCPY(IObuff, "first file is shorter");
+                   break;
+               }
+               else if (c2 == EOF)
+               {
+                   STRCPY(IObuff, "second file is shorter");
+                   break;
+               }
+               else if (c1 != c2)
+               {
+                   vim_snprintf((char *)IObuff, IOSIZE,
+                                             "difference at byte %ld", count);
+                   break;
+               }
+               ++count;
+           }
+       }
+     }
+     if (IObuff[0] != NUL)
+     {
+       prepare_assert_error(&ga);
+       ga_concat(&ga, IObuff);
+       assert_error(&ga);
+       ga_clear(&ga);
+     }
+ }
+ 
+     void
  assert_match_common(typval_T *argvars, assert_type_T atype)
  {
      garray_T  ga;
*** ../vim-8.0.1522/src/proto/eval.pro  2018-02-13 12:57:38.066977614 +0100
--- src/proto/eval.pro  2018-02-18 21:13:29.119000904 +0100
***************
*** 122,127 ****
--- 122,128 ----
  void prepare_assert_error(garray_T *gap);
  void assert_error(garray_T *gap);
  void assert_equal_common(typval_T *argvars, assert_type_T atype);
+ void assert_equalfile(typval_T *argvars);
  void assert_match_common(typval_T *argvars, assert_type_T atype);
  void assert_inrange(typval_T *argvars);
  void assert_bool(typval_T *argvars, int isTrue);
*** ../vim-8.0.1522/runtime/doc/eval.txt        2018-02-13 13:59:42.187667302 
+0100
--- runtime/doc/eval.txt        2018-02-18 22:11:55.773565394 +0100
***************
*** 2020,2025 ****
--- 2020,2027 ----
  assert_beeps({cmd})           none    assert {cmd} causes a beep
  assert_equal({exp}, {act} [, {msg}])
                                none    assert {exp} is equal to {act}
+ assert_equalfile({fname-one}, {fname-two})
+                               none    assert file contents is equal
  assert_exception({error} [, {msg}])
                                none    assert {error} is in v:exception
  assert_fails({cmd} [, {error}])       none    assert {cmd} fails
***************
*** 2402,2413 ****
  systemlist({expr} [, {input}])        List    output of shell command/filter 
{expr}
  tabpagebuflist([{arg}])               List    list of buffer numbers in tab 
page
  tabpagenr([{arg}])            Number  number of current or last tab page
! tabpagewinnr({tabarg}[, {arg}]) Number        number of current window in tab 
page
! taglist({expr}[, {filename}]) List    list of tags matching {expr}
  tagfiles()                    List    tags files used
  tan({expr})                   Float   tangent of {expr}
  tanh({expr})                  Float   hyperbolic tangent of {expr}
  tempname()                    String  name for a temporary file
  term_getaltscreen({buf})      Number  get the alternate screen flag
  term_getattr({attr}, {what})  Number  get the value of attribute {what}
  term_getcursor({buf})         List    get the cursor position of a terminal
--- 2406,2423 ----
  systemlist({expr} [, {input}])        List    output of shell command/filter 
{expr}
  tabpagebuflist([{arg}])               List    list of buffer numbers in tab 
page
  tabpagenr([{arg}])            Number  number of current or last tab page
! tabpagewinnr({tabarg} [, {arg}]) Number       number of current window in tab 
page
! taglist({expr} [, {filename}])        List    list of tags matching {expr}
  tagfiles()                    List    tags files used
  tan({expr})                   Float   tangent of {expr}
  tanh({expr})                  Float   hyperbolic tangent of {expr}
  tempname()                    String  name for a temporary file
+ term_dumpdiff({filename}, {filename} [, {options}])
+                               Number  display difference between two dumps
+ term_dumpload({filename} [, {options}])
+                               Number  displaying a screen dump
+ term_dumpwrite({buf}, {filename} [, {max-height} [, {max-width}]])
+                               none    dump terminal window contents
  term_getaltscreen({buf})      Number  get the alternate screen flag
  term_getattr({attr}, {what})  Number  get the value of attribute {what}
  term_getcursor({buf})         List    get the cursor position of a terminal
***************
*** 2417,2423 ****
  term_getsize({buf})           List    get the size of a terminal
  term_getstatus({buf})         String  get the status of a terminal
  term_gettitle({buf})          String  get the title of a terminal
! term_getttty({buf}, [{input}])        String  get the tty name of a terminal
  term_list()                   List    get the list of terminal buffers
  term_scrape({buf}, {row})     List    get row of a terminal screen
  term_sendkeys({buf}, {keys})  none    send keystrokes to a terminal
--- 2427,2433 ----
  term_getsize({buf})           List    get the size of a terminal
  term_getstatus({buf})         String  get the status of a terminal
  term_gettitle({buf})          String  get the title of a terminal
! term_gettty({buf}, [{input}]) String  get the tty name of a terminal
  term_list()                   List    get the list of terminal buffers
  term_scrape({buf}, {row})     List    get row of a terminal screen
  term_sendkeys({buf}, {keys})  none    send keystrokes to a terminal
***************
*** 2588,2593 ****
--- 2598,2611 ----
  <             Will result in a string to be added to |v:errors|:
        test.vim line 12: Expected 'foo' but got 'bar' ~
  
+                                                       *assert_equalfile()*
+ assert_equalfile({fname-one}, {fname-two})
+               When the files {fname-one} and {fname-two} do not contain
+               exactly the same text an error message is added to |v:errors|.
+               When {fname-one} or {fname-two} does not exist the error will
+               mention that.
+               Mainly useful with |terminal-diff|.
+ 
  assert_exception({error} [, {msg}])                   *assert_exception()*
                When v:exception does not contain the string {error} an error
                message is added to |v:errors|.
***************
*** 8091,8096 ****
--- 8150,8202 ----
                For MS-Windows forward slashes are used when the 'shellslash'
                option is set or when 'shellcmdflag' starts with '-'.
  
+                                                       *term_dumpdiff()*
+ term_dumpdiff({filename}, {filename} [, {options}])
+               Open a new window displaying the difference between the two
+               files.  The files must have been created with
+               |term_dumpwrite()|.
+               Returns the buffer number or zero when the diff fails.
+               Also see |terminal-diff|.
+               NOTE: this does not work with double-width characters yet.
+ 
+               The top part of the buffer contains the contents of the first
+               file, the bottom part of the buffer contains the contents of
+               the second file.  The middle part shows the differences.
+               The parts are separated by a line of dashes.
+ 
+               {options} are not implemented yet.
+ 
+               Each character in the middle part indicates a difference. If
+               there are multiple differences only the first in this list is
+               used:
+                       X       different character
+                       w       different width
+                       f       different foreground color
+                       b       different background color
+                       a       different attribute
+                       +       missing position in first file
+                       -       missing position in second file
+ 
+               Using the "s" key the top and bottom parts are swapped.  This
+               makes it easy to spot a difference.
+ 
+                                                       *term_dumpload()*
+ term_dumpload({filename} [, {options}])
+               Open a new window displaying the contents of {filename}
+               The file must have been created with |term_dumpwrite()|.
+               Returns the buffer number or zero when it fails.
+               Also see |terminal-diff|.
+ 
+               {options} are not implemented yet.
+ 
+                                                       *term_dumpwrite()*
+ term_dumpwrite({buf}, {filename} [, {max-height} [, {max-width}]])
+               Dump the contents of the terminal screen of {buf} in the file
+               {filename}.  This uses a format that can be used with
+               |term_dumpread()| and |term_dumpdiff()|.
+               If {filename} already exists an error is given. *E953*
+               Also see |terminal-diff|.
+ 
  term_getaltscreen({buf})                              *term_getaltscreen()*
                Returns 1 if the terminal of {buf} is using the alternate
                screen.
***************
*** 8109,8117 ****
  
  term_getcursor({buf})                                 *term_getcursor()*
                Get the cursor position of terminal {buf}. Returns a list with
!               two numbers and a dictionary: [rows, cols, dict].
  
!               "rows" and "cols" are one based, the first screen cell is row
                1, column 1.  This is the cursor position of the terminal
                itself, not of the Vim window.
  
--- 8215,8223 ----
  
  term_getcursor({buf})                                 *term_getcursor()*
                Get the cursor position of terminal {buf}. Returns a list with
!               two numbers and a dictionary: [row, col, dict].
  
!               "row" and "col" are one based, the first screen cell is row
                1, column 1.  This is the cursor position of the terminal
                itself, not of the Vim window.
  
*** ../vim-8.0.1522/src/testdir/test_assert.vim 2018-02-13 12:26:08.908247730 
+0100
--- src/testdir/test_assert.vim 2018-02-18 21:23:26.054581595 +0100
***************
*** 25,30 ****
--- 25,65 ----
    call remove(v:errors, 0)
  endfunc
  
+ func Test_assert_equalfile()
+   call assert_equalfile('abcabc', 'xyzxyz')
+   call assert_match("E485: Can't read file abcabc", v:errors[0])
+   call remove(v:errors, 0)
+ 
+   let goodtext = ["one", "two", "three"]
+   call writefile(goodtext, 'Xone')
+   call assert_equalfile('Xone', 'xyzxyz')
+   call assert_match("E485: Can't read file xyzxyz", v:errors[0])
+   call remove(v:errors, 0)
+ 
+   call writefile(goodtext, 'Xtwo')
+   call assert_equalfile('Xone', 'Xtwo')
+ 
+   call writefile([goodtext[0]], 'Xone')
+   call assert_equalfile('Xone', 'Xtwo')
+   call assert_match("first file is shorter", v:errors[0])
+   call remove(v:errors, 0)
+ 
+   call writefile(goodtext, 'Xone')
+   call writefile([goodtext[0]], 'Xtwo')
+   call assert_equalfile('Xone', 'Xtwo')
+   call assert_match("second file is shorter", v:errors[0])
+   call remove(v:errors, 0)
+ 
+   call writefile(['1234X89'], 'Xone')
+   call writefile(['1234Y89'], 'Xtwo')
+   call assert_equalfile('Xone', 'Xtwo')
+   call assert_match("difference at byte 4", v:errors[0])
+   call remove(v:errors, 0)
+ 
+   call delete('Xone')
+   call delete('Xtwo')
+ endfunc
+ 
  func Test_assert_notequal()
    let n = 4
    call assert_notequal('foo', n)
*** ../vim-8.0.1522/src/version.c       2018-02-17 20:35:24.430696008 +0100
--- src/version.c       2018-02-18 21:23:53.926382280 +0100
***************
*** 773,774 ****
--- 773,776 ----
  {   /* Add new patch number below this line */
+ /**/
+     1523,
  /**/

-- 
Save the plankton - eat a whale.

 /// Bram Moolenaar -- [email protected] -- http://www.Moolenaar.net   \\\
///        sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ \\\
\\\  an exciting new programming language -- http://www.Zimbu.org        ///
 \\\            help me help AIDS victims -- http://ICCF-Holland.org    ///

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/d/optout.

Raspunde prin e-mail lui