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