To: vim_dev@googlegroups.com Subject: Patch 8.0.1523 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ 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 *)_("") : 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 -- Bram@Moolenaar.net -- 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 ///