diff --git a/src/editor/edit.c b/src/editor/edit.c index e4237472b2..10f197d48d 100644 --- a/src/editor/edit.c +++ b/src/editor/edit.c @@ -882,6 +882,9 @@ edit_cursor_to_eol (WEdit *edit) off_t b; b = edit_buffer_get_current_eol (&edit->buffer); + // do not place the cursor after the "\r" of a "\r\n" line break + if (b > 0 && edit_buffer_is_crlf (&edit->buffer, b - 1)) + b--; edit_cursor_move (edit, b - edit->buffer.curs1); edit->search_start = edit->buffer.curs1; edit->prev_col = edit_get_col (edit); @@ -1379,7 +1382,9 @@ edit_group_undo (WEdit *edit) static void edit_delete_to_line_end (WEdit *edit) { - while (edit_buffer_get_current_byte (&edit->buffer) != '\n' && edit->buffer.curs2 != 0) + // delete up to (but not including) the line break, which may be "\n" or "\r\n" + while (edit->buffer.curs2 != 0 && !edit_buffer_is_crlf (&edit->buffer, edit->buffer.curs1) + && edit_buffer_get_current_byte (&edit->buffer) != '\n') edit_delete (edit, TRUE); } @@ -1454,16 +1459,74 @@ edit_auto_indent (WEdit *edit) } /* --------------------------------------------------------------------------------------------- */ +/** + * Check whether a line break ("\n" or "\r\n") ends exactly at the specified position, + * i.e. whether the line before that position is empty. + */ + +static inline gboolean +edit_line_break_ends_at (const WEdit *edit, off_t p) +{ + if (p >= 1 && edit_buffer_get_byte (&edit->buffer, p - 1) == '\n') + return TRUE; + + return edit_buffer_is_crlf (&edit->buffer, p - 2); +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Insert a line break at the cursor inheriting the type of the line break + * of the current line ("\r\n" or "\n"). If the current line has no line + * break (the last line of the file), use the line break of the previous line. + */ static inline void -edit_double_newline (WEdit *edit) +edit_insert_line_break (WEdit *edit) { + const off_t eol = edit_buffer_get_current_eol (&edit->buffer); + gboolean crlf; + + if (edit_buffer_is_crlf (&edit->buffer, eol - 1)) + crlf = TRUE; + else if (eol == edit->buffer.size) + { + const off_t bol = edit_buffer_get_current_bol (&edit->buffer); + + crlf = (bol > 0) && edit_buffer_is_crlf (&edit->buffer, bol - 2); + } + else + crlf = FALSE; + + // if the character right before the cursor is already a "\r" (e.g. the + // cursor is between the "\r" and the "\n" of a "\r\n" line break), + // inserting another "\r" would create a duplicate, so insert only "\n" + if (crlf && edit_buffer_get_byte (&edit->buffer, edit->buffer.curs1 - 1) == '\r') + crlf = FALSE; + + if (crlf) + edit_insert (edit, '\r'); + edit_insert (edit, '\n'); +} + +/* --------------------------------------------------------------------------------------------- */ + +static inline void +edit_double_newline (WEdit *edit) +{ + const off_t pos = edit->buffer.curs1; + + edit_insert_line_break (edit); + + // do not add a second line break if the next char or the previous line + // is already a line break, i.e. there is already a blank line if (edit_buffer_get_current_byte (&edit->buffer) == '\n' - || edit_buffer_get_byte (&edit->buffer, edit->buffer.curs1 - 2) == '\n') + || edit_buffer_is_crlf (&edit->buffer, edit->buffer.curs1) + || edit_line_break_ends_at (edit, pos)) return; + edit->force |= REDRAW_PAGE; - edit_insert (edit, '\n'); + edit_insert_line_break (edit); } /* --------------------------------------------------------------------------------------------- */ @@ -1530,14 +1593,14 @@ check_and_wrap_line (WEdit *edit) c = edit_buffer_get_byte (&edit->buffer, curs); if (c == '\n' || curs <= 0) { - edit_insert (edit, '\n'); + edit_insert_line_break (edit); return; } if (whitespace (c)) { off_t current = edit->buffer.curs1; edit_cursor_move (edit, curs - edit->buffer.curs1 + 1); - edit_insert (edit, '\n'); + edit_insert_line_break (edit); edit_cursor_move (edit, current - edit->buffer.curs1 + 1); return; } @@ -1898,99 +1961,63 @@ edit_get_write_filter (const vfs_path_t *write_name_vpath, const vfs_path_t *fil off_t edit_write_stream (WEdit *edit, FILE *f) { - long i; + const off_t size = edit->buffer.size; + off_t i; if (edit->lb == LB_ASIS) { - for (i = 0; i < edit->buffer.size; i++) + for (i = 0; i < size; i++) if (fputc (edit_buffer_get_byte (&edit->buffer, i), f) < 0) break; return i; } // change line breaks - for (i = 0; i < edit->buffer.size; i++) + for (i = 0; i < size; i++) { - unsigned char c; + const unsigned char c = edit_buffer_get_byte (&edit->buffer, i); - c = edit_buffer_get_byte (&edit->buffer, i); - if (!(c == '\n' || c == '\r')) + if (c != '\n' && c != '\r') { // not line break if (fputc (c, f) < 0) return i; + continue; } - else - { // (c == '\n' || c == '\r') - unsigned char c1; - - c1 = edit_buffer_get_byte (&edit->buffer, i + 1); // next char - - switch (edit->lb) - { - case LB_UNIX: // replace "\r\n" or '\r' to '\n' - // put one line break unconditionally - if (fputc ('\n', f) < 0) - return i; - - i++; // 2 chars are processed - if (c == '\r' && c1 == '\n') - // Windows line break; go to the next char - break; - - if (c == '\r' && c1 == '\r') - { - // two Macintosh line breaks; put second line break - if (fputc ('\n', f) < 0) - return i; - break; - } + // c is a line break; c1 is the char after it (-1 if there is none) + const int c1 = (i + 1 < size) ? edit_buffer_get_byte (&edit->buffer, i + 1) : -1; - if (fputc (c1, f) < 0) - return i; - break; - - case LB_WIN: // replace '\n' or '\r' to "\r\n" - // put one line break unconditionally - if (fputc ('\r', f) < 0 || fputc ('\n', f) < 0) - return i; - - if (c == '\r' && c1 == '\n') - // Windows line break; go to the next char - i++; - break; - - case LB_MAC: // replace "\r\n" or '\n' to '\r' - // put one line break unconditionally - if (fputc ('\r', f) < 0) - return i; - - i++; // 2 chars are processed - - if (c == '\r' && c1 == '\n') - // Windows line break; go to the next char - break; + switch (edit->lb) + { + case LB_WIN: // replace "\r\n", "\r" or "\n" to "\r\n" + if (fputc ('\r', f) < 0 || fputc ('\n', f) < 0) + return i; + if (c == '\r' && c1 == '\n') + // Windows line break; skip the second char + i++; + break; - if (c == '\n' && c1 == '\n') - { - // two Windows line breaks; put second line break - if (fputc ('\r', f) < 0) - return i; - break; - } + case LB_MAC: // replace "\r\n", "\r" or "\n" to "\r" + if (fputc ('\r', f) < 0) + return i; + if (c == '\r' && c1 == '\n') + // Windows line break; skip the second char + i++; + break; - if (fputc (c1, f) < 0) - return i; - break; - case LB_ASIS: // default without changes - default: - break; - } + case LB_UNIX: // replace "\r\n", "\r" or "\n" to "\n" + default: + if (fputc ('\n', f) < 0) + return i; + if (c == '\r' && c1 == '\n') + // Windows line break; skip the second char + i++; + break; } } - return edit->buffer.size; + return size; } /* --------------------------------------------------------------------------------------------- */ @@ -2665,6 +2692,10 @@ edit_delete (WEdit *edit, gboolean byte_delete) char_length = 1; } + // a "\r\n" line break is deleted as a single unit + if (edit_buffer_is_crlf (&edit->buffer, edit->buffer.curs1)) + char_length = 2; + if (edit->mark2 != edit->mark1) edit_push_markers (edit); @@ -2710,6 +2741,7 @@ edit_backspace (WEdit *edit, gboolean byte_delete) int p = 0; int char_length = 1; int i; + gboolean deleted_nl = FALSE; if (edit->buffer.curs1 == 0) return 0; @@ -2724,6 +2756,10 @@ edit_backspace (WEdit *edit, gboolean byte_delete) char_length = 1; } + // a "\r\n" line break is deleted as a single unit + if (edit_buffer_is_crlf (&edit->buffer, edit->buffer.curs1 - 2)) + char_length = 2; + for (i = 1; i <= char_length; i++) { if (edit->mark1 >= edit->buffer.curs1) @@ -2738,10 +2774,13 @@ edit_backspace (WEdit *edit, gboolean byte_delete) p = edit_buffer_backspace (&edit->buffer); + if (p == '\n') + deleted_nl = TRUE; + edit_push_undo_action (edit, p); } edit_modification (edit); - if (p == '\n') + if (deleted_nl) { book_mark_dec (edit, edit->buffer.curs_line); edit->buffer.curs_line--; @@ -2752,7 +2791,7 @@ edit_backspace (WEdit *edit, gboolean byte_delete) if (edit->buffer.curs1 < edit->start_display) { edit->start_display--; - if (p == '\n') + if (deleted_nl) edit->start_line--; } @@ -2812,6 +2851,10 @@ edit_move_forward3 (const WEdit *edit, off_t current, long cols, off_t upto) { off_t p, q; long col; + gboolean crlf_hidden; + + // a "\r" of a "\r\n" line break is hidden only in a pure Windows file + crlf_hidden = (edit_buffer_get_line_breaks (&edit->buffer) == LB_WIN); if (upto != 0) { @@ -2855,11 +2898,17 @@ edit_move_forward3 (const WEdit *edit, off_t current, long cols, off_t upto) c = convert_to_display_c (c); if (c == '\n') - return (upto != 0 ? (off_t) col : p); + { + // a CRLF line break ends the line before the "\r", not after it + return (upto != 0) ? (off_t) col + : (edit_buffer_is_crlf (&edit->buffer, p - 1) ? p - 1 : p); + } + // a hidden "\r" of a "\r\n" line break (a pure Windows file) occupies no column + if (c == '\r' && crlf_hidden && edit_buffer_is_crlf (&edit->buffer, p)) + continue; if (c == '\t') col += TAB_SIZE - col % TAB_SIZE; else if ((c < 32 || c == 127) && (orig_c == c || (!mc_global.utf8_display && !edit->utf8))) - // '\r' is shown as ^M, so we must advance 2 characters // Caret notation for control characters col += 2; else @@ -3261,15 +3310,17 @@ void edit_delete_line (WEdit *edit) { /* - * Delete right part of the line. + * Delete right part of the line, up to (but not including) the line + * break. The line break may be "\n" or "\r\n". * Note that edit_buffer_get_byte() returns '\n' when byte position is * beyond EOF. */ - while (edit_buffer_get_current_byte (&edit->buffer) != '\n') + while (!edit_buffer_is_crlf (&edit->buffer, edit->buffer.curs1) + && edit_buffer_get_current_byte (&edit->buffer) != '\n') (void) edit_delete (edit, TRUE); /* - * Delete '\n' char. + * Delete the line break ("\n" or "\r\n"). * Note that edit_delete() will not corrupt anything if called while * cursor position is EOF. */ @@ -3652,13 +3703,13 @@ edit_execute_cmd (WEdit *edit, long command, int char_for_insertion) } else { - edit_insert (edit, '\n'); + edit_insert_line_break (edit); if (edit_options.return_does_auto_indent && !bracketed_pasting_in_progress) edit_auto_indent (edit); } break; case CK_Return: - edit_insert (edit, '\n'); + edit_insert_line_break (edit); break; case CK_MarkColumnPageUp: diff --git a/src/editor/editbuffer.c b/src/editor/editbuffer.c index 451369092a..8dff699c0f 100644 --- a/src/editor/editbuffer.c +++ b/src/editor/editbuffer.c @@ -154,6 +154,9 @@ edit_buffer_init (edit_buffer_t *buf, off_t size) buf->size = size; buf->lines = 0; + + buf->lb_detected = LB_ASIS; + buf->lb_dirty = TRUE; } /* --------------------------------------------------------------------------------------------- */ @@ -506,6 +509,9 @@ edit_buffer_insert (edit_buffer_t *buf, int c) // update file length buf->size++; + + if (c == '\r' || c == '\n') + buf->lb_dirty = TRUE; } /* --------------------------------------------------------------------------------------------- */ @@ -538,6 +544,9 @@ edit_buffer_insert_ahead (edit_buffer_t *buf, int c) // update file length buf->size++; + + if (c == '\r' || c == '\n') + buf->lb_dirty = TRUE; } /* --------------------------------------------------------------------------------------------- */ @@ -577,6 +586,9 @@ edit_buffer_delete (edit_buffer_t *buf) // update file length buf->size--; + if (c == '\r' || c == '\n') + buf->lb_dirty = TRUE; + return c; } @@ -617,6 +629,9 @@ edit_buffer_backspace (edit_buffer_t *buf) // update file length buf->size--; + if (c == '\r' || c == '\n') + buf->lb_dirty = TRUE; + return c; } @@ -790,6 +805,9 @@ edit_buffer_read_file (edit_buffer_t *buf, int fd, off_t size, } } + // the loaded content may contain line breaks + buf->lb_dirty = TRUE; + return ret; } @@ -895,3 +913,125 @@ edit_buffer_calc_percent (const edit_buffer_t *buf, off_t offset) } /* --------------------------------------------------------------------------------------------- */ +/** + * Get the start offset of the trailing whitespace (spaces and tabs) run of the + * line that begins at the given offset. If the line has no trailing whitespace + * the end of the line content is returned, so no character of the line is at or + * after the result. + * + * @param buf editor buffer + * @param bol offset of the first character of the line + * + * @return start offset of the trailing whitespace run (or end of line content) + */ + +off_t +edit_buffer_trailing_ws_start (const edit_buffer_t *buf, off_t bol) +{ + off_t tws, eol; + + eol = edit_buffer_get_eol (buf, bol); + + // a CRLF line break's content ends at the "\r", not at the "\n" + if (eol > bol && edit_buffer_is_crlf (buf, eol - 1)) + eol--; + + for (tws = eol; tws > bol; tws--) + { + int c; + + c = edit_buffer_get_byte (buf, tws - 1); + if (c != ' ' && c != '\t') + break; + } + + return tws; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Detect the line break type used in the buffer content. + * + * @param buf editor buffer + * + * @return LB_WIN if all line breaks are "\r\n"; LB_UNIX if all line breaks + * are "\n" (including buffers without any line breaks); LB_MAC if + * all line breaks are "\r"; LB_ASIS if the buffer uses a mixture of + * different line breaks (such a file can only be saved as-is) + */ + +LineBreaks +edit_buffer_detect_line_breaks (const edit_buffer_t *buf) +{ + off_t crlf = 0; // "\r\n" line breaks + off_t cr = 0; // "\r" line breaks not followed by "\n" + off_t lf = 0; // "\n" line breaks not preceded by "\r" + off_t i; + + for (i = 0; i < buf->size; i++) + { + if (edit_buffer_get_byte (buf, i) == '\r') + { + if (edit_buffer_is_crlf (buf, i)) + crlf++; + else + cr++; + } + else if (edit_buffer_get_byte (buf, i) == '\n' + && (i == 0 || edit_buffer_get_byte (buf, i - 1) != '\r')) + lf++; + } + + if (crlf > 0) + { + if (cr > 0 || lf > 0) + return LB_ASIS; // mixture of line breaks + return LB_WIN; + } + + if (cr > 0) + { + if (lf > 0) + return LB_ASIS; // mixture of line breaks + return LB_MAC; + } + + return LB_UNIX; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Recompute the cached line break type if the line breaks have changed. + * Cheap (O(1)) unless a line break was inserted or deleted since the last + * recomputation. + * + * @param buf pointer to editor buffer + */ + +void +edit_buffer_refresh_line_breaks (edit_buffer_t *buf) +{ + if (buf->lb_dirty) + { + buf->lb_detected = edit_buffer_detect_line_breaks (buf); + buf->lb_dirty = FALSE; + } +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Get the cached line break type of the buffer content. + * Call edit_buffer_refresh_line_breaks() first if the result must be current. + * + * @param buf pointer to editor buffer + * + * @return cached detect_line_breaks() result + */ + +LineBreaks +edit_buffer_get_line_breaks (const edit_buffer_t *buf) +{ + return buf->lb_detected; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/editor/editbuffer.h b/src/editor/editbuffer.h index 686af3e3e5..be1f8e93c5 100644 --- a/src/editor/editbuffer.h +++ b/src/editor/editbuffer.h @@ -20,6 +20,9 @@ typedef struct edit_buffer_struct off_t size; // file size long lines; // total lines in the file long curs_line; // line number of the cursor. + + LineBreaks lb_detected; // cached detect_line_breaks() result + gboolean lb_dirty; // line breaks have changed, need detect_line_breaks() } edit_buffer_t; typedef struct edit_buffer_read_file_status_msg_struct @@ -63,6 +66,11 @@ off_t edit_buffer_write_file (edit_buffer_t *buf, int fd); int edit_buffer_calc_percent (const edit_buffer_t *buf, off_t offset); +off_t edit_buffer_trailing_ws_start (const edit_buffer_t *buf, off_t bol); +LineBreaks edit_buffer_detect_line_breaks (const edit_buffer_t *buf); +void edit_buffer_refresh_line_breaks (edit_buffer_t *buf); +LineBreaks edit_buffer_get_line_breaks (const edit_buffer_t *buf); + /*** inline functions ****************************************************************************/ static inline int @@ -109,6 +117,25 @@ edit_buffer_get_current_eol (const edit_buffer_t *buf) return edit_buffer_get_eol (buf, buf->curs1); } +/* --------------------------------------------------------------------------------------------- */ +/** + * Check whether the buffer contains a Windows ("\r\n") line break + * starting at the specified position. + * + * @param buf editor buffer + * @param byte_index position of the potential "\r" character + * + * @return TRUE if the two bytes at byte_index and byte_index + 1 are "\r" and "\n" + */ + +static inline gboolean +edit_buffer_is_crlf (const edit_buffer_t *buf, off_t byte_index) +{ + return (byte_index >= 0 && byte_index + 1 < buf->size + && edit_buffer_get_byte (buf, byte_index) == '\r' + && edit_buffer_get_byte (buf, byte_index + 1) == '\n'); +} + /* --------------------------------------------------------------------------------------------- */ #endif diff --git a/src/editor/editdraw.c b/src/editor/editdraw.c index bde7e18047..a6e9fc6b87 100644 --- a/src/editor/editdraw.c +++ b/src/editor/editdraw.c @@ -142,22 +142,61 @@ format_character_code (WEdit *edit) #undef CHAR_CODE_BUF_SIZE +/* --------------------------------------------------------------------------------------------- */ +/** + * Get the cached line break type of the buffer content. + * The result is recomputed only when line breaks have changed. + */ + +static inline LineBreaks +edit_get_detected_line_breaks (WEdit *edit) +{ + edit_buffer_refresh_line_breaks (&edit->buffer); + return edit_buffer_get_line_breaks (&edit->buffer); +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Get the status line character describing the line break type + * of the buffer content: U - Unix ("\n"), W - Windows ("\r\n"), + * M - Macintosh ("\r"), - - mixture of line breaks or no line breaks. + */ + +static inline char +edit_line_breaks_status_char (WEdit *edit) +{ + switch (edit_get_detected_line_breaks (edit)) + { + case LB_WIN: + return 'W'; + case LB_MAC: + return 'M'; + case LB_UNIX: + return 'U'; + case LB_ASIS: + default: + return '-'; + } +} + /* --------------------------------------------------------------------------------------------- */ static inline void status_string (WEdit *edit, char *s, int w) { char *character_code; + const char lb = edit_line_breaks_status_char (edit); character_code = format_character_code (edit); // The field lengths just prevent the status line from shortening too much if (edit_options.simple_statusbar) - g_snprintf (s, w, "%c%c%c%c %3ld %5ld/%ld %6ld/%ld [%s] %s", + g_snprintf (s, w, "%c%c%c%c%c %3ld %5ld/%ld %6ld/%ld [%s] %s", edit->mark1 != edit->mark2 ? (edit->column_highlight ? 'C' : 'B') : '-', // edit->modified != 0 ? 'M' : '-', // macro_index < 0 ? '-' : 'R', // edit->overwrite == 0 ? '-' : 'O', // + lb, // edit->curs_col + edit->over_col, // edit->buffer.curs_line + 1, // edit->buffer.lines + 1, // @@ -167,11 +206,12 @@ status_string (WEdit *edit, char *s, int w) mc_global.source_codepage >= 0 ? get_codepage_id (mc_global.source_codepage) : ""); else - g_snprintf (s, w, "[%c%c%c%c] %2ld L:[%3ld+%2ld %3ld/%3ld] *(%-4ld/%4ldb) [%s] %s", + g_snprintf (s, w, "[%c%c%c%c%c] %2ld L:[%3ld+%2ld %3ld/%3ld] *(%-4ld/%4ldb) [%s] %s", edit->mark1 != edit->mark2 ? (edit->column_highlight ? 'C' : 'B') : '-', // edit->modified != 0 ? 'M' : '-', // macro_index < 0 ? '-' : 'R', // edit->overwrite == 0 ? '-' : 'O', // + lb, // edit->curs_col + edit->over_col, // edit->start_line + 1, // edit->curs_row, // @@ -288,13 +328,13 @@ edit_status_window (WEdit *edit) tty_getyx (&y, &x); x -= w->rect.x; x += 4; - if (x + 6 <= cols - 2 - 6) + if (x + 7 <= cols - 2 - 6) { edit_move (x, 0); - tty_printf ("[%c%c%c%c]", + tty_printf ("[%c%c%c%c%c]", edit->mark1 != edit->mark2 ? (edit->column_highlight ? 'C' : 'B') : '-', edit->modified != 0 ? 'M' : '-', macro_index < 0 ? '-' : 'R', - edit->overwrite == 0 ? '-' : 'O'); + edit->overwrite == 0 ? '-' : 'O', edit_line_breaks_status_char (edit)); } if (cols > 30) @@ -497,11 +537,16 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co int col, start_col_real; int abn_style; int book_mark = 0; + gboolean crlf_hidden; char line_stat[LINE_STATE_WIDTH + 1] = "\0"; if (row > w->rect.lines - 1 - EDIT_TEXT_VERTICAL_OFFSET - 2 * (edit->fullscreen != 0 ? 0 : 1)) return; + // in a pure Windows file the "\r" of a "\r\n" line break is a line break + // (hidden); in any other file it is shown as "^M" + crlf_hidden = (edit_buffer_get_line_breaks (&edit->buffer) == LB_WIN); + if (book_mark_query_color (edit, edit->start_line + row, EDITOR_BOOKMARK_COLOR)) book_mark = EDITOR_BOOKMARK_COLOR; else if (book_mark_query_color (edit, edit->start_line + row, EDITOR_BOOKMARK_FOUND_COLOR)) @@ -554,14 +599,7 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co off_t tws = 0; if (edit_options.visible_tws && tty_use_colors ()) - for (tws = edit_buffer_get_eol (&edit->buffer, b); tws > b; tws--) - { - unsigned int c; - - c = edit_buffer_get_byte (&edit->buffer, tws - 1); - if (!whitespace (c)) - break; - } + tws = edit_buffer_trailing_ws_start (&edit->buffer, b); while (col <= end_col - edit->start_col) { @@ -603,6 +641,13 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co else c = edit_buffer_get_byte (&edit->buffer, q); + // the "\r" of a hidden "\r\n" line break is not shown + if (c == '\r' && crlf_hidden && edit_buffer_is_crlf (&edit->buffer, q)) + { + q++; + continue; + } + // we don't use bg for mc - fg contains both if (book_mark != 0) p->style |= book_mark << 16; @@ -817,6 +862,10 @@ render_edit_text (WEdit *edit, long start_row, long start_column, long end_row, int y1, x1, y2, x2; int last_line, last_column; + // make sure the cached line break type (used to decide whether a "\r" is + // shown as "^M" or hidden) is up to date before drawing + edit_buffer_refresh_line_breaks (&edit->buffer); + // draw only visible region last_line = wh->rect.y + wh->rect.lines - 1; diff --git a/src/editor/editsearch.c b/src/editor/editsearch.c index 63b4b376c4..4aad6b0dc6 100644 --- a/src/editor/editsearch.c +++ b/src/editor/editsearch.c @@ -155,6 +155,10 @@ edit_dialog_search_show (WEdit *edit) /** * Get EOL symbol for searching. * + * Line breaks in the buffer are either "\n" or "\r\n"; in both cases the + * line is terminated by "\n" (a "\r" only precedes it, it never terminates a + * line by itself). Therefore the search EOL symbol is always "\n". + * * @param edit editor object * @return EOL symbol */ @@ -162,13 +166,8 @@ edit_dialog_search_show (WEdit *edit) static inline char edit_search_get_current_end_line_char (const WEdit *edit) { - switch (edit->lb) - { - case LB_MAC: - return '\r'; - default: - return '\n'; - } + (void) edit; + return '\n'; } /* --------------------------------------------------------------------------------------------- */ diff --git a/src/editor/editwidget.h b/src/editor/editwidget.h index 38c948aee5..913566e43f 100644 --- a/src/editor/editwidget.h +++ b/src/editor/editwidget.h @@ -154,7 +154,7 @@ struct WEdit gboolean is_case_insensitive; // selects language case sensitivity // line break - LineBreaks lb; + LineBreaks lb; // line break conversion mode on save (set in Save As dialog) }; /*** global variables defined in .c file *********************************************************/ diff --git a/src/editor/format.c b/src/editor/format.c index b012f6163c..892089ca4b 100644 --- a/src/editor/format.c +++ b/src/editor/format.c @@ -482,6 +482,13 @@ format_paragraph (WEdit *edit, gboolean force) p = begin_paragraph (edit, force, &lines); q = end_paragraph (edit, force); + + // Formatting assumes "\n" line breaks: a "\r\n" line break would be mangled + // (the "\r" leaks into the reformatted text), so leave CRLF paragraphs as is. + for (off_t i = p; i + 1 <= q; i++) + if (edit_buffer_is_crlf (&edit->buffer, i)) + return; + indent = test_indent (edit, p, q); t = get_paragraph (&edit->buffer, p, q, indent != 0); diff --git a/tests/src/editor/Makefile.am b/tests/src/editor/Makefile.am index 81f3d9ae37..2bf3d3b4a2 100644 --- a/tests/src/editor/Makefile.am +++ b/tests/src/editor/Makefile.am @@ -19,6 +19,7 @@ EXTRA_DIST = edit_complete_word_cmd_test_data.txt.in TESTS = \ edit_complete_word_cmd \ edit_insert_column_of_text \ + edit_line_breaks \ edit_replace_cmd check_PROGRAMS = $(TESTS) @@ -29,6 +30,9 @@ edit_complete_word_cmd_SOURCES = \ edit_insert_column_of_text_SOURCES = \ edit_insert_column_of_text.c +edit_line_breaks_SOURCES = \ + edit_line_breaks.c + edit_replace_cmd_SOURCES = \ edit_replace_cmd.c diff --git a/tests/src/editor/edit_line_breaks.c b/tests/src/editor/edit_line_breaks.c new file mode 100644 index 0000000000..9b5e27d275 --- /dev/null +++ b/tests/src/editor/edit_line_breaks.c @@ -0,0 +1,876 @@ +/* + src/editor - tests for line break handling: CRLF/LF/CR detection, + atomic "\r\n" editing, line break inheritance and write conversion + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/src/editor" + +#include "tests/mctest.h" + +#include + +#include "lib/charsets.h" +#include "lib/keybind.h" +#include "lib/vfs/path.h" +#include "lib/vfs/vfs.h" +#include "src/selcodepage.h" +#include "src/vfs/local/local.c" + +#include "src/editor/edit-impl.h" +#include "src/editor/editmacros.h" // edit_load_macro_cmd() +#include "src/editor/editsearch.h" // edit_search_update_callback() +#include "src/editor/editwidget.h" + +static WGroup owner; +static WEdit *test_edit; + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +void +message (int flags, const char *title, const char *text, ...) +{ + (void) flags; + (void) title; + (void) text; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +void +status_msg_init (status_msg_t *sm, const char *title, double delay, status_msg_cb init_cb, + status_msg_update_cb update_cb, status_msg_cb deinit_cb) +{ + (void) sm; + (void) title; + (void) delay; + (void) init_cb; + (void) update_cb; + (void) deinit_cb; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +void +status_msg_deinit (status_msg_t *sm) +{ + (void) sm; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +mc_search_cbret_t +edit_search_update_callback (const void *user_data, off_t char_offset) +{ + (void) user_data; + (void) char_offset; + + return MC_SEARCH_CB_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +void +edit_load_syntax (WEdit *edit, GPtrArray *pnames, const char *type) +{ + (void) edit; + (void) pnames; + (void) type; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +int +edit_get_syntax_color (WEdit *edit, off_t byte_index) +{ + (void) edit; + (void) byte_index; + + return 0; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +gboolean +edit_load_macro_cmd (WEdit *edit) +{ + (void) edit; + + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ + WRect r; + + str_init_strings (NULL); + + vfs_init (); + vfs_init_localfs (); + vfs_setup_work_dir (); + + mc_global.sysconfig_dir = (char *) TEST_SHARE_DIR; + load_codepages_list (); + + edit_options.filesize_threshold = (char *) "64M"; + edit_options.return_does_auto_indent = FALSE; + edit_options.save_position = FALSE; + + rect_init (&r, 0, 0, 24, 80); + test_edit = edit_init (NULL, &r, NULL); + memset (&owner, 0, sizeof (owner)); + group_add_widget (&owner, WIDGET (test_edit)); + + mc_global.source_codepage = 0; + mc_global.display_codepage = 0; + cp_source = "ASCII"; + cp_display = "ASCII"; + + do_set_codepage (0); + edit_set_codeset (test_edit); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + edit_clean (test_edit); + group_remove_widget (test_edit); + g_free (test_edit); + + free_codepages_list (); + vfs_shut (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +// the line counters are maintained by edit_insert()/edit_delete() in the real +// session; refresh them after the raw buffer manipulations +static void +test_refresh_lines (void) +{ + long lines = 1; + long curs_line = 0; + + for (off_t i = 0; i < test_edit->buffer.size; i++) + { + if (edit_buffer_get_byte (&test_edit->buffer, i) == '\n') + { + lines++; + if (i < test_edit->buffer.curs1) + curs_line++; + } + } + + test_edit->buffer.lines = lines; + test_edit->buffer.curs_line = curs_line; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +test_load_text (const char *text) +{ + for (; *text != '\0'; text++) + edit_buffer_insert (&test_edit->buffer, (unsigned char) *text); + test_refresh_lines (); +} + +/* --------------------------------------------------------------------------------------------- */ + +// move the cursor to the absolute buffer position +static void +test_cursor_to (off_t pos) +{ + edit_cursor_move (test_edit, pos - test_edit->buffer.curs1); + test_refresh_lines (); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +test_check (const char *expected) +{ + GString *actual; + + actual = g_string_new (""); + + for (off_t i = 0; i < test_edit->buffer.size; i++) + g_string_append_c (actual, edit_buffer_get_byte (&test_edit->buffer, i)); + + mctest_assert_str_eq (actual->str, expected); + g_string_free (actual, TRUE); +} + +/* --------------------------------------------------------------------------------------------- */ +/* edit_buffer_detect_line_breaks() */ + +static const struct test_detect_ds +{ + const char *in; + LineBreaks expected; +} test_detect_ds[] = { + // empty buffer + { "", LB_UNIX }, + { "abc", LB_UNIX }, + // all "\n" + { "a\nb\n", LB_UNIX }, + { "a\nb", LB_UNIX }, + // all "\r\n" + { "a\r\nb\r\n", LB_WIN }, + { "a\r\nb", LB_WIN }, + // all "\r" + { "a\rb\r", LB_MAC }, + { "a\rb", LB_MAC }, + // mixture of line breaks + { "a\r\nb\n", LB_ASIS }, + { "a\nb\r\n", LB_ASIS }, + { "a\rb\n", LB_ASIS }, + { "a\nb\rc", LB_ASIS }, + { "a\r\nb\rc", LB_ASIS }, +}; + +/* @Test(dataSource = "test_detect_ds") */ +START_PARAMETRIZED_TEST (test_detect, test_detect_ds) +{ + // given + test_load_text (data->in); + + // when + const LineBreaks lb = edit_buffer_detect_line_breaks (&test_edit->buffer); + + // then + ck_assert_int_eq (lb, data->expected); +} +END_PARAMETRIZED_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_buffer_is_crlf() */ + +static const struct test_is_crlf_ds +{ + const char *in; + off_t index; + gboolean expected; +} test_is_crlf_ds[] = { + { "a\r\nb", 1, TRUE }, // "\r" of the "\r\n" pair + { "a\r\nb", 0, FALSE }, // plain char + { "a\r\nb", 2, FALSE }, // "\n" of the "\r\n" pair + { "a\r\nb", 3, FALSE }, // out of range + { "a\r\nb", -1, FALSE }, // negative index + { "a\r", 1, FALSE }, // trailing "\r", out of range + { "\r\n", 0, TRUE }, // + { "a\rb\n", 1, FALSE }, // standalone "\r" + { "a\rb\n", 3, FALSE }, // standalone "\n" +}; + +/* @Test(dataSource = "test_is_crlf_ds") */ +START_PARAMETRIZED_TEST (test_is_crlf, test_is_crlf_ds) +{ + // given + test_load_text (data->in); + + // when + const gboolean is_crlf = edit_buffer_is_crlf (&test_edit->buffer, data->index); + + // then + ck_assert_int_eq (is_crlf, data->expected); +} +END_PARAMETRIZED_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_buffer_trailing_ws_start() */ + +static const struct test_trailing_ws_ds +{ + const char *in; + off_t bol; + off_t expected; +} test_trailing_ws_ds[] = { + // LF lines + { "ab \n", 0, 2 }, // trailing spaces + { "ab\n", 0, 2 }, // no trailing spaces + { " \n", 0, 0 }, // the line is all spaces + // CRLF lines: the "\r" is part of the line break, so the content ends at the + // "\r" (the same in a pure Windows file and in a mixed file) + { "ab \r\n", 0, 2 }, // trailing spaces + { "ab\r\n", 0, 2 }, // no trailing spaces + { " \r\n", 0, 0 }, // the line is all spaces + { "a\t \r\n", 0, 1 }, // trailing tab + space + { "xx\nab \r\n", 3, 5 }, // second line, trailing spaces + // last line without a line break + { "ab ", 0, 2 }, +}; + +/* @Test(dataSource = "test_trailing_ws_ds") */ +START_PARAMETRIZED_TEST (test_trailing_ws_start, test_trailing_ws_ds) +{ + // given + test_load_text (data->in); + + // when + const off_t tws = edit_buffer_trailing_ws_start (&test_edit->buffer, data->bol); + + // then + ck_assert_int_eq (tws, data->expected); +} +END_PARAMETRIZED_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_write_stream() */ + +static const struct test_write_ds +{ + const char *in; + LineBreaks lb; + const char *out; +} test_write_ds[] = { + { "a\r\nb\r\n", LB_ASIS, "a\r\nb\r\n" }, // as-is + { "a\r\nb\r\n", LB_UNIX, "a\nb\n" }, // "\r\n" -> "\n" + { "a\r\nb\r\n", LB_WIN, "a\r\nb\r\n" }, // "\r\n" -> "\r\n" + { "a\r\nb\r\n", LB_MAC, "a\rb\r" }, // "\r\n" -> "\r" + { "a\nb\n", LB_WIN, "a\r\nb\r\n" }, // "\n" -> "\r\n" + { "a\nb\n", LB_MAC, "a\rb\r" }, // "\n" -> "\r" + { "a\nb", LB_WIN, "a\r\nb" }, // no trailing line break + { "a\r\nb", LB_UNIX, "a\nb" }, // no trailing line break + { "abc\n", LB_UNIX, "abc\n" }, // no extra "\n" is appended (regression) + { "abc\r\n", LB_UNIX, "abc\n" }, + { "abc\r\n", LB_WIN, "abc\r\n" }, + { "\n", LB_WIN, "\r\n" }, // line break at the beginning + { "a\r\n\r\nb", LB_UNIX, "a\n\nb" }, // blank line + { "a\r\r\nb", LB_UNIX, "a\n\nb" }, // standalone "\r" before "\r\n" + { "a\r\nb\r\nc", LB_MAC, "a\rb\rc" }, // last line without line break + { "a\rb", LB_UNIX, "a\nb" }, // standalone "\r" is a line break too + { "abc", LB_WIN, "abc" }, // no line breaks +}; + +/* @Test(dataSource = "test_write_ds") */ +START_PARAMETRIZED_TEST (test_write_stream, test_write_ds) +{ + char *mem = NULL; + size_t mem_size = 0; + FILE *f; + off_t written; + + // given + test_load_text (data->in); + test_edit->lb = data->lb; + + // when + f = open_memstream (&mem, &mem_size); + written = edit_write_stream (test_edit, f); + fclose (f); + + // then + ck_assert_int_eq (written, (off_t) strlen (data->in)); + mctest_assert_str_eq (mem, data->out); + free (mem); +} +END_PARAMETRIZED_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* CK_Enter: a new line break inherits the type of the current line break */ + +START_TEST (test_enter_inherits_crlf) +{ + // given: cursor is at the end of the first line + test_load_text ("line1\r\nline2\r\n"); + test_cursor_to (5); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("line1\r\n\r\nline2\r\n"); +} +END_TEST + +START_TEST (test_enter_inherits_lf) +{ + // given: cursor is at the end of the first line + test_load_text ("line1\nline2\n"); + test_cursor_to (5); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("line1\n\nline2\n"); +} +END_TEST + +// the last line has no line break of its own: use the previous line's line break +START_TEST (test_enter_last_line_inherits_crlf) +{ + // given: cursor is at the end of the last line + test_load_text ("a\r\nb\r\nc"); + test_cursor_to (7); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("a\r\nb\r\nc\r\n"); +} +END_TEST + +START_TEST (test_enter_last_line_inherits_lf) +{ + // given: cursor is at the end of the last line + test_load_text ("a\nb\nc"); + test_cursor_to (5); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("a\nb\nc\n"); +} +END_TEST + +START_TEST (test_enter_empty_buffer) +{ + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("\n"); +} +END_TEST + +/* with auto indent enabled the line break must stay of the file's type */ +START_TEST (test_enter_auto_indent_crlf) +{ + // given: indented line, cursor at the end of the second line + edit_options.return_does_auto_indent = TRUE; + test_load_text (" a\r\n b\r\n"); + test_cursor_to (8); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then: the new line gets the indent of the previous line, line break stays CRLF + test_check (" a\r\n b\r\n \r\n"); +} +END_TEST + +/* with auto paragraph formatting enabled pressing Enter must keep the file's line breaks */ +START_TEST (test_enter_auto_para_format_crlf) +{ + // given: cursor at the end of the second line + edit_options.auto_para_formatting = TRUE; + test_load_text ("aaaa\r\nbbbb\r\n"); + test_cursor_to (10); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("aaaa\r\nbbbb\r\n\r\n"); +} +END_TEST + +/* formatting a CRLF paragraph must not leak "\r" into the text or change line breaks */ +START_TEST (test_format_paragraph_crlf) +{ + // given: a CRLF paragraph, cursor on a non-blank line + test_load_text ("aaaa bbbb\r\ncccc dddd\r\n"); + test_cursor_to (11); + + // when + format_paragraph (test_edit, FALSE); + + // then: the paragraph is left unchanged (line breaks intact, no stray "\r") + test_check ("aaaa bbbb\r\ncccc dddd\r\n"); +} +END_TEST + +START_TEST (test_enter_auto_indent_crlf_empty_prev_line) +{ + // given: cursor is on an empty line + edit_options.return_does_auto_indent = TRUE; + test_load_text ("line1\r\n\r\nline3\r\n"); + test_cursor_to (7); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then + test_check ("line1\r\n\r\n\r\nline3\r\n"); +} +END_TEST + +START_TEST (test_enter_auto_indent_crlf_last_line) +{ + // given: cursor is at the end of the last (indented) line + edit_options.return_does_auto_indent = TRUE; + test_load_text (" a\r\n b\r\n c"); + test_cursor_to (13); + + // when + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then: the new line gets the indent of the previous line, line break stays CRLF + test_check (" a\r\n b\r\n c\r\n "); +} +END_TEST + +/* moving down onto a shorter CRLF line must not leave the cursor inside the "\r\n" pair */ +START_TEST (test_down_then_enter_crlf) +{ + // given: auto-indent on; a non-empty line, an empty line, a non-empty line; all CRLF + edit_options.return_does_auto_indent = TRUE; + test_load_text ("xxxx\r\n\r\nyyyy\r\n"); + test_cursor_to (0); + + // when: go to the end of the first line, move down onto the empty line, press Enter + edit_execute_cmd (test_edit, CK_End, -1); + edit_execute_cmd (test_edit, CK_Down, -1); + + // then: the cursor is at the end of the empty line, before its "\r" (not after it) + ck_assert_int_eq (test_edit->buffer.curs1, 6); + + edit_execute_cmd (test_edit, CK_Enter, -1); + + // a new CRLF line is added; no LF line break is introduced + test_check ("xxxx\r\n\r\n\r\nyyyy\r\n"); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* CK_End: the cursor stops before the "\r" of a "\r\n" line break */ + +START_TEST (test_end_stops_before_cr) +{ + // given: cursor is at the begin of the first line + test_load_text ("ab\r\ncd"); + test_cursor_to (0); + + // when + edit_execute_cmd (test_edit, CK_End, -1); + + // then + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +START_TEST (test_end_stops_at_lf) +{ + // given: cursor is at the begin of the first line + test_load_text ("ab\ncd"); + test_cursor_to (0); + + // when + edit_execute_cmd (test_edit, CK_End, -1); + + // then + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_delete()/edit_backspace(): a "\r\n" line break is an atomic unit */ + +START_TEST (test_delete_crlf_atomic) +{ + // given: cursor is on the "\r" of a "\r\n" line break + test_load_text ("ab\r\ncd"); + test_cursor_to (2); + + // when + edit_delete (test_edit, TRUE); + + // then + test_check ("abcd"); + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +START_TEST (test_backspace_crlf_atomic) +{ + // given: cursor is after the "\n" of a "\r\n" line break + test_load_text ("ab\r\ncd"); + test_cursor_to (4); + + // when + edit_backspace (test_edit, TRUE); + + // then + test_check ("abcd"); + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +// a standalone "\r" (not followed by "\n") is not a line break: delete one byte only +START_TEST (test_delete_standalone_cr) +{ + // given: cursor is on the standalone "\r" + test_load_text ("ab\rcd"); + test_cursor_to (2); + + // when + edit_delete (test_edit, TRUE); + + // then + test_check ("abcd"); + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +START_TEST (test_backspace_standalone_cr) +{ + // given: cursor is after the standalone "\r" + test_load_text ("ab\rcd"); + test_cursor_to (3); + + // when + edit_backspace (test_edit, TRUE); + + // then + test_check ("abcd"); + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* CK_DeleteToEnd: stop before the "\r" of a "\r\n" line break */ + +START_TEST (test_delete_to_line_end_crlf) +{ + // given: cursor is at the begin of the first line + test_load_text ("ab\r\ncd"); + test_cursor_to (0); + + // when + edit_execute_cmd (test_edit, CK_DeleteToEnd, -1); + + // then: the "\r\n" line break itself is preserved + test_check ("\r\ncd"); +} +END_TEST + +START_TEST (test_delete_to_line_end_lf) +{ + // given: cursor is at the begin of the first line + test_load_text ("ab\ncd"); + test_cursor_to (0); + + // when + edit_execute_cmd (test_edit, CK_DeleteToEnd, -1); + + // then + test_check ("\ncd"); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* In a file with a mixture of line breaks the "\r" of a "\r\n" pair is NOT hidden: it is shown + * as "^M". Nevertheless a "\r\n" pair is always a single (atomic) line break, exactly as in a pure + * Windows file: the cursor stops before the "\r", Delete/Backspace remove the pair as a unit, and + * the content of the line ends at the "\r". The test buffers below are mixed (contain both "\r\n" + * and "\n"), so they are LB_ASIS; they verify the atomic behaviour on a non-pure-Windows file. */ + +// CK_End: the cursor stops before the "\r" (the line break), even in a mixed file +START_TEST (test_end_stops_before_cr_mixed) +{ + // given: a mixed file, cursor is at the begin of the CRLF line + test_load_text ("ab\r\ncd\n"); + test_cursor_to (0); + + // when + edit_execute_cmd (test_edit, CK_End, -1); + + // then: the cursor is before the "\r" (offset 2), not after it + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +// edit_delete(): the "\r\n" pair is deleted as a single unit, even in a mixed file +START_TEST (test_delete_crlf_atomic_mixed) +{ + // given: a mixed file, cursor is before the "\r" of a "\r\n" line break + test_load_text ("ab\r\ncd\n"); + test_cursor_to (2); + + // when + edit_delete (test_edit, TRUE); + + // then: the whole "\r\n" pair is gone, the lines are joined + test_check ("abcd\n"); + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +// edit_backspace(): the "\r\n" pair is deleted as a single unit, even in a mixed file +START_TEST (test_backspace_crlf_atomic_mixed) +{ + // given: a mixed file, cursor is at the begin of the line after a "\r\n" line break + test_load_text ("ab\r\ncd\n"); + test_cursor_to (4); + + // when + edit_backspace (test_edit, TRUE); + + // then: the whole "\r\n" pair is gone, the lines are joined + test_check ("abcd\n"); + ck_assert_int_eq (test_edit->buffer.curs1, 2); +} +END_TEST + +// CK_DeleteToEnd: stops before the "\r\n" line break, which is preserved +START_TEST (test_delete_to_line_end_crlf_mixed) +{ + // given: a mixed file, cursor is at the begin of the CRLF line + test_load_text ("ab\r\ncd\n"); + test_cursor_to (0); + + // when + edit_execute_cmd (test_edit, CK_DeleteToEnd, -1); + + // then: "ab" is deleted, the "\r\n" line break is preserved + test_check ("\r\ncd\n"); +} +END_TEST + +// CK_Enter after CK_End on a CRLF line: a blank CRLF line is added, with no +// duplicate "\r" (no "^M^M"), the cursor is at the begin of the blank line +START_TEST (test_enter_after_end_crlf_mixed) +{ + // given: a mixed file, cursor is at the begin of the CRLF line + test_load_text ("ab\r\ncd\n"); + test_cursor_to (0); + + // when: move to the end of the line, then press Enter + edit_execute_cmd (test_edit, CK_End, -1); + edit_execute_cmd (test_edit, CK_Enter, -1); + + // then: a blank CRLF line is added (the line break is inherited), no duplicate + // "\r" is introduced, the cursor is at the begin of the blank line + test_check ("ab\r\n\r\ncd\n"); + ck_assert_int_eq (test_edit->buffer.curs1, 4); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* loading a file: the buffer keeps the raw content, detection works on the loaded text */ + +START_TEST (test_load_crlf_file) +{ + const char *path = "/tmp/mc-test-line-breaks.crlf"; + WEdit *edit; + WRect r; + edit_arg_t arg; + vfs_path_t *vpath; + FILE *f; + GString *actual; + + // given: a file with "\r\n" line breaks + f = fopen (path, "wb"); + mctest_assert_not_null (f); + fputs ("line1\r\nline2\r\nline3\r\n", f); + fclose (f); + + vpath = vfs_path_from_str (path); + arg.file_vpath = vpath; + arg.line_number = 0; + rect_init (&r, 0, 0, 24, 80); + edit = edit_init (NULL, &r, &arg); + + // then + mctest_assert_not_null (edit); + + actual = g_string_new (""); + for (off_t i = 0; i < edit->buffer.size; i++) + g_string_append_c (actual, edit_buffer_get_byte (&edit->buffer, i)); + mctest_assert_str_eq (actual->str, "line1\r\nline2\r\nline3\r\n"); + g_string_free (actual, TRUE); + + ck_assert_int_eq (edit->lb, LB_ASIS); + ck_assert_int_eq (edit_buffer_detect_line_breaks (&edit->buffer), LB_WIN); + + // cleanup + edit_clean (edit); + g_free (edit); + vfs_path_free (vpath, TRUE); + unlink (path); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + + // Add new tests here: *************** + mctest_add_parameterized_test (tc_core, test_detect, test_detect_ds); + mctest_add_parameterized_test (tc_core, test_is_crlf, test_is_crlf_ds); + mctest_add_parameterized_test (tc_core, test_trailing_ws_start, test_trailing_ws_ds); + mctest_add_parameterized_test (tc_core, test_write_stream, test_write_ds); + tcase_add_test (tc_core, test_enter_inherits_crlf); + tcase_add_test (tc_core, test_enter_inherits_lf); + tcase_add_test (tc_core, test_enter_last_line_inherits_crlf); + tcase_add_test (tc_core, test_enter_last_line_inherits_lf); + tcase_add_test (tc_core, test_enter_empty_buffer); + tcase_add_test (tc_core, test_enter_auto_indent_crlf); + tcase_add_test (tc_core, test_enter_auto_para_format_crlf); + tcase_add_test (tc_core, test_format_paragraph_crlf); + tcase_add_test (tc_core, test_enter_auto_indent_crlf_empty_prev_line); + tcase_add_test (tc_core, test_enter_auto_indent_crlf_last_line); + tcase_add_test (tc_core, test_down_then_enter_crlf); + tcase_add_test (tc_core, test_end_stops_before_cr); + tcase_add_test (tc_core, test_end_stops_at_lf); + tcase_add_test (tc_core, test_delete_crlf_atomic); + tcase_add_test (tc_core, test_backspace_crlf_atomic); + tcase_add_test (tc_core, test_delete_standalone_cr); + tcase_add_test (tc_core, test_backspace_standalone_cr); + tcase_add_test (tc_core, test_delete_to_line_end_crlf); + tcase_add_test (tc_core, test_delete_to_line_end_lf); + tcase_add_test (tc_core, test_end_stops_before_cr_mixed); + tcase_add_test (tc_core, test_delete_crlf_atomic_mixed); + tcase_add_test (tc_core, test_backspace_crlf_atomic_mixed); + tcase_add_test (tc_core, test_delete_to_line_end_crlf_mixed); + tcase_add_test (tc_core, test_enter_after_end_crlf_mixed); + tcase_add_test (tc_core, test_load_crlf_file); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */ \ No newline at end of file