changeset 13298:a88c5e12b860 v8.0.1523

patch 8.0.1523: cannot write and read terminal screendumps commit https://github.com/vim/vim/commit/d96ff165113ce5fe62107add590997660e3d4802 Author: Bram Moolenaar <Bram@vim.org> Date: Sun Feb 18 22:13:29 2018 +0100 patch 8.0.1523: cannot write and read terminal screendumps Problem: Cannot write and read terminal screendumps. Solution: Add term_dumpwrite(), term_dumpread() and term_dumpdiff(). Also add assert_equalfile().
author Christian Brabandt <cb@256bit.org>
date Sun, 18 Feb 2018 22:15:06 +0100
parents 46d884aa4d75
children 1b2b8b5e2181
files runtime/doc/eval.txt src/eval.c src/evalfunc.c src/normal.c src/proto/eval.pro src/proto/terminal.pro src/terminal.c src/testdir/test_assert.vim src/version.c
diffstat 9 files changed, 1017 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -1,4 +1,4 @@
-*eval.txt*	For Vim version 8.0.  Last change: 2018 Feb 10
+*eval.txt*	For Vim version 8.0.  Last change: 2018 Feb 18
 
 
 		  VIM REFERENCE MANUAL	  by Bram Moolenaar
@@ -2020,6 +2020,8 @@ argv()				List	the argument list
 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
@@ -2410,6 +2412,12 @@ 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
@@ -2590,6 +2598,14 @@ assert_equal({expected}, {actual} [, {ms
 <		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|.
@@ -4587,8 +4603,7 @@ getftype({fname})					*getftype()*
 		"file" are returned.  On MS-Windows a symbolic link to a
 		directory returns "dir" instead of "link".
 
-							*getjumplist()*
-getjumplist([{winnr} [, {tabnr}]])
+getjumplist([{winnr} [, {tabnr}]])			*getjumplist()*
 		Returns the |jumplist| for the specified window.
 
 		Without arguments use the current window.
@@ -8135,6 +8150,53 @@ tempname()					*tempname()* *temp-file-n
 		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.
--- a/src/eval.c
+++ b/src/eval.c
@@ -8834,6 +8834,73 @@ assert_equal_common(typval_T *argvars, a
 }
 
     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;
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -46,6 +46,7 @@ static void f_arglistid(typval_T *argvar
 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,6 +488,7 @@ static struct fst
 #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,6 +849,9 @@ static struct fst
 #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,6 +1302,15 @@ f_assert_equal(typval_T *argvars, typval
 }
 
 /*
+ * "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
--- a/src/normal.c
+++ b/src/normal.c
@@ -7474,6 +7474,11 @@ v_visop(cmdarg_T *cap)
     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')
--- a/src/proto/eval.pro
+++ b/src/proto/eval.pro
@@ -122,6 +122,7 @@ void reset_v_option_vars(void);
 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);
--- a/src/proto/terminal.pro
+++ b/src/proto/terminal.pro
@@ -21,6 +21,10 @@ int term_get_attr(buf_T *buf, linenr_T l
 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);
--- a/src/terminal.c
+++ b/src/terminal.c
@@ -47,6 +47,9 @@
  * - 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,9 +58,6 @@
  * - 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".
@@ -99,7 +99,8 @@
 typedef struct {
   VTermScreenCellAttrs	attrs;
   char			width;
-  VTermColor		fg, bg;
+  VTermColor		fg;
+  VTermColor		bg;
 } cellattr_T;
 
 typedef struct sb_line_S {
@@ -153,6 +154,9 @@ struct terminal_S {
     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,11 +287,34 @@ setup_job_options(jobopt_T *opt, int row
 }
 
 /*
+ * 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 forceit)
+term_start(typval_T *argvar, jobopt_T *opt, int without_job, int forceit)
 {
     exarg_T	split_ea;
     win_T	*old_curwin = curwin;
@@ -454,6 +481,9 @@ term_start(typval_T *argvar, jobopt_T *o
     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,20 +522,7 @@ term_start(typval_T *argvar, jobopt_T *o
     }
     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);
+	term_close_buffer(curbuf, old_curbuf);
 	return NULL;
     }
     return newbuf;
@@ -597,7 +614,7 @@ ex_terminal(exarg_T *eap)
     argvar[0].v_type = VAR_STRING;
     argvar[0].vval.v_string = cmd;
     argvar[1].v_type = VAR_UNKNOWN;
-    term_start(argvar, &opt, eap->forceit);
+    term_start(argvar, &opt, FALSE, eap->forceit);
     vim_free(tofree);
     vim_free(opt.jo_eof_chars);
 }
@@ -1035,6 +1052,36 @@ equal_celattr(cellattr_T *a, cellattr_T 
 	&& 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,18 +1126,8 @@ move_terminal_to_buffer(term_T *term)
 	    {
 		/* 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;
-
+		if (add_empty_scrollback(term, &fill_attr, 0) == OK)
 		    add_scrollback_line_to_buffer(term, (char_u *)"", 0);
-		}
 	    }
 
 	    if (len == 0)
@@ -1849,10 +1886,10 @@ color2index(VTermColor *color, int fg, i
 }
 
 /*
- * Convert the attributes of a vterm cell into an attribute index.
+ * Convert Vterm attributes to highlight flags.
  */
     static int
-cell2attr(VTermScreenCellAttrs cellattrs, VTermColor cellfg, VTermColor cellbg)
+vtermAttr2hl(VTermScreenCellAttrs cellattrs)
 {
     int attr = 0;
 
@@ -1866,6 +1903,35 @@ cell2attr(VTermScreenCellAttrs cellattrs
 	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,6 +2851,730 @@ term_get_buf(typval_T *argvars)
     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,7 +4020,7 @@ f_term_start(typval_T *argvars, typval_T
 
     if (opt.jo_vertical)
 	cmdmod.split = WSP_VERT;
-    buf = term_start(&argvars[0], &opt, FALSE);
+    buf = term_start(&argvars[0], &opt, FALSE, FALSE);
 
     if (buf != NULL && buf->b_term != NULL)
 	rettv->vval.v_number = buf->b_fnum;
--- a/src/testdir/test_assert.vim
+++ b/src/testdir/test_assert.vim
@@ -25,6 +25,41 @@ func Test_assert_equal()
   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)
--- a/src/version.c
+++ b/src/version.c
@@ -772,6 +772,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1523,
+/**/
     1522,
 /**/
     1521,