@@ -1,12 +1,15 @@
1use helix_core::{
1use helix_core::{
2 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
2 comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
4+ get_line_ending, get_line_ending_of_str, line_end_char_index, str_is_line_ending,
3 match_brackets,
6 match_brackets,
4 movement::{self, Direction},
7 movement::{self, Direction},
5 object, pos_at_coords,
8 object, pos_at_coords,
6 regex::{self, Regex},
9 regex::{self, Regex},
7 register::{self, Register, Registers},
10 register::{self, Register, Registers},
8− search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection, SmallVec,
11+ search, selection, Change, ChangeSet, LineEnding, Position, Range, Rope, RopeGraphemes,
9− Tendril, Transaction,
12+ RopeSlice, Selection, SmallVec, Tendril, Transaction, DEFAULT_LINE_ENDING,
12use helix_view::{
15use helix_view::{
@@ -303,9 +306,8 @@ fn move_line_end(cx: &mut Context) {
303 let text = doc.text();
306 let text = doc.text();
304 let line = text.char_to_line(range.head);
307 let line = text.char_to_line(range.head);
306− // Line end is pos at the start of next line - 1
309+ let pos = line_end_char_index(&text.slice(..), line);
307− // subtract another 1 because the line ends with \n
310+
308− let pos = text.line_to_char(line + 1).saturating_sub(2);
309 Range::new(pos, pos)
311 Range::new(pos, pos)
@@ -452,12 +454,28 @@ where
452 let count = cx.count();
454 let count = cx.count();
454 // need to wait for next key
456 // need to wait for next key
457+ // TODO: should this be done by grapheme rather than char? For example,
458+ // we can't properly handle the line-ending CRLF case here in terms of char.
455 cx.on_next_key(move |cx, event| {
459 cx.on_next_key(move |cx, event| {
456 let ch = match event {
460 let ch = match event {
457 KeyEvent {
461 KeyEvent {
458 code: KeyCode::Enter,
462 code: KeyCode::Enter,
465+ // TODO: this isn't quite correct when CRLF is involved.
466+ // This hack will work in most cases, since documents don't
467+ // usually mix line endings. But we should fix it eventually
461 KeyEvent {
479 KeyEvent {
462 code: KeyCode::Char(ch),
480 code: KeyCode::Char(ch),
@@ -575,32 +593,37 @@ fn extend_first_nonwhitespace(cx: &mut Context) {
577fn replace(cx: &mut Context) {
595fn replace(cx: &mut Context) {
596+ let mut buf = [0u8; 4]; // To hold utf8 encoded char.
578 // need to wait for next key
598 // need to wait for next key
579 cx.on_next_key(move |cx, event| {
599 cx.on_next_key(move |cx, event| {
600+ let (view, doc) = current!(cx.editor);
580 let ch = match event {
601 let ch = match event {
581 KeyEvent {
602 KeyEvent {
582 code: KeyCode::Char(ch),
603 code: KeyCode::Char(ch),
584− } => Some(ch),
605+ } => Some(&ch.encode_utf8(&mut buf[..])[..]),
585 KeyEvent {
606 KeyEvent {
586 code: KeyCode::Enter,
607 code: KeyCode::Enter,
588− } => Some('\n'),
609+ } => Some(doc.line_ending.as_str()),
589 _ => None,
610 _ => None,
592 if let Some(ch) = ch {
613 if let Some(ch) = ch {
593− let (view, doc) = current!(cx.editor);
595 let transaction =
614 let transaction =
596 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
615 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
597 let max_to = doc.text().len_chars().saturating_sub(1);
616 let max_to = doc.text().len_chars().saturating_sub(1);
598 let to = std::cmp::min(max_to, range.to() + 1);
617 let to = std::cmp::min(max_to, range.to() + 1);
599− let text: String = doc
618+ let text: String = RopeGraphemes::new(doc.text().slice(range.from()..to))
600− .text()
619+ .map(|g| {
601− .slice(range.from()..to)
620+ let cow: Cow<str> = g.into();
602− .chars()
621+ if str_is_line_ending(&cow) {
603− .map(|c| if c == '\n' { '\n' } else { ch })
623+ } else {
604 .collect();
627 .collect();
606 (range.from(), to, Some(text.into()))
629 (range.from(), to, Some(text.into()))
@@ -725,9 +748,8 @@ fn extend_line_end(cx: &mut Context) {
725 let text = doc.text();
748 let text = doc.text();
726 let line = text.char_to_line(range.head);
749 let line = text.char_to_line(range.head);
728− // Line end is pos at the start of next line - 1
751+ let pos = line_end_char_index(&text.slice(..), line);
729− // subtract another 1 because the line ends with \n
752+
730− let pos = text.line_to_char(line + 1).saturating_sub(2);
731 Range::new(range.anchor, pos)
753 Range::new(range.anchor, pos)
@@ -783,7 +805,8 @@ fn split_selection_on_newline(cx: &mut Context) {
783 let text = doc.text().slice(..);
805 let text = doc.text().slice(..);
784 // only compile the regex once
806 // only compile the regex once
785 #[allow(clippy::trivial_regex)]
807 #[allow(clippy::trivial_regex)]
786− static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\n").unwrap());
808+ static REGEX: Lazy<Regex> =
809+ Lazy::new(|| Regex::new(r"\r\n|[\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}]").unwrap());
787 let selection = selection::split_on_matches(text, doc.selection(view.id), ®EX);
810 let selection = selection::split_on_matches(text, doc.selection(view.id), ®EX);
788 doc.set_selection(view.id, selection);
811 doc.set_selection(view.id, selection);
@@ -922,7 +945,13 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
922 // then delete
945 // then delete
923 let transaction =
946 let transaction =
924 Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
947 Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
948+ let alltext = doc.text();
949+ let line = alltext.char_to_line(range.head);
925− let max_to = doc.text().len_chars().saturating_sub(1);
950+ let max_to = doc.text().len_chars().saturating_sub(
951+ get_line_ending(&alltext.line(line))
952+ .map(|le| le.len_chars())
926 let to = std::cmp::min(max_to, range.to() + 1);
955 let to = std::cmp::min(max_to, range.to() + 1);
927 (range.from(), to, None)
956 (range.from(), to, None)
@@ -1003,7 +1032,7 @@ fn append_mode(cx: &mut Context) {
1003 if selection.iter().any(|range| range.head == end) {
1032 if selection.iter().any(|range| range.head == end) {
1004 let transaction = Transaction::change(
1033 let transaction = Transaction::change(
1005 doc.text(),
1034 doc.text(),
1006− std::array::IntoIter::new([(end, end, Some(Tendril::from_char('\n')))]),
1035+ std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
1008 doc.apply(&transaction, view.id);
1037 doc.apply(&transaction, view.id);
@@ -1131,6 +1160,45 @@ mod cmd {
1163+ /// Sets or reports the current document's line ending setting.
1164+ fn set_line_ending(editor: &mut Editor, args: &[&str], event: PromptEvent) {
1167+ // If no argument, report current line ending setting.
1168+ if args.is_empty() {
1169+ let line_ending = current!(editor).1.line_ending;
1170+ editor.set_status(match line_ending {
1171+ Crlf => "crlf".into(),
1172+ LF => "line feed".into(),
1173+ FF => "form feed".into(),
1174+ CR => "carriage return".into(),
1175+ Nel => "next line".into(),
1177+ // These should never be a document's default line ending.
1178+ VT | LS | PS => "error".into(),
1183+ // Attempt to parse argument as a line ending.
1184+ let line_ending = match args.get(0) {
1185+ // We check for CR first because it shares a common prefix with CRLF.
1186+ Some(arg) if "cr".starts_with(&arg.to_lowercase()) => Some(CR),
1187+ Some(arg) if "crlf".starts_with(&arg.to_lowercase()) => Some(Crlf),
1188+ Some(arg) if "lf".starts_with(&arg.to_lowercase()) => Some(LF),
1189+ Some(arg) if "ff".starts_with(&arg.to_lowercase()) => Some(FF),
1190+ Some(arg) if "nel".starts_with(&arg.to_lowercase()) => Some(Nel),
1194+ if let Some(le) = line_ending {
1195+ doc_mut!(editor).line_ending = le;
1197+ // Invalid argument.
1198+ editor.set_error(format!("invalid line ending '{}'", args[0],));
1134 fn earlier(editor: &mut Editor, args: &[&str], event: PromptEvent) {
1202 fn earlier(editor: &mut Editor, args: &[&str], event: PromptEvent) {
1135 let uk = match args.join(" ").parse::<helix_core::history::UndoKind>() {
1203 let uk = match args.join(" ").parse::<helix_core::history::UndoKind>() {
1136 Ok(uk) => uk,
1204 Ok(uk) => uk,
@@ -1274,7 +1342,11 @@ mod cmd {
1276 fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], _: PromptEvent) {
1344 fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], _: PromptEvent) {
1345+ let (_, doc) = current!(editor);
1277− let separator = args.first().copied().unwrap_or("\n");
1346+ let separator = args
1349+ .unwrap_or_else(|| doc.line_ending.as_str());
1278 yank_joined_to_clipboard_impl(editor, separator);
1350 yank_joined_to_clipboard_impl(editor, separator);
@@ -1359,6 +1431,13 @@ mod cmd {
1359 fun: set_indent_style,
1431 fun: set_indent_style,
1360 completer: None,
1432 completer: None,
1435+ name: "line-ending",
1437+ doc: "Set the document's default line ending. Options: crlf, lf, cr, ff, nel.",
1438+ fun: set_line_ending,
1362 TypableCommand {
1441 TypableCommand {
1363 name: "earlier",
1442 name: "earlier",
1364 alias: Some("ear"),
1443 alias: Some("ear"),
@@ -1683,8 +1762,7 @@ fn append_to_line(cx: &mut Context) {
1683 let selection = doc.selection(view.id).transform(|range| {
1762 let selection = doc.selection(view.id).transform(|range| {
1684 let text = doc.text();
1763 let text = doc.text();
1685 let line = text.char_to_line(range.head);
1764 let line = text.char_to_line(range.head);
1686− // we can't use line_to_char(line + 1) - 2 because the last line might not contain \n
1765+ let pos = line_end_char_index(&text.slice(..), line);
1687− let pos = (text.line_to_char(line) + text.line(line).len_chars()).saturating_sub(1);
1688 Range::new(pos, pos)
1766 Range::new(pos, pos)
1690 doc.set_selection(view.id, selection);
1768 doc.set_selection(view.id, selection);
@@ -1731,7 +1809,7 @@ fn open(cx: &mut Context, open: Open) {
1731 let indent = doc.indent_unit().repeat(indent_level);
1809 let indent = doc.indent_unit().repeat(indent_level);
1732 let indent_len = indent.len();
1810 let indent_len = indent.len();
1733 let mut text = String::with_capacity(1 + indent_len);
1811 let mut text = String::with_capacity(1 + indent_len);
1734− text.push('\n');
1812+ text.push_str(doc.line_ending.as_str());
1735 text.push_str(&indent);
1813 text.push_str(&indent);
1736 let text = text.repeat(count);
1814 let text = text.repeat(count);
@@ -2344,7 +2422,7 @@ pub mod insert {
2345 let indent = doc.indent_unit().repeat(indent_level);
2423 let indent = doc.indent_unit().repeat(indent_level);
2346 let mut text = String::with_capacity(1 + indent.len());
2424 let mut text = String::with_capacity(1 + indent.len());
2347− text.push('\n');
2425+ text.push_str(doc.line_ending.as_str());
2348 text.push_str(&indent);
2426 text.push_str(&indent);
2350 let head = pos + offs + text.chars().count();
2428 let head = pos + offs + text.chars().count();
@@ -2365,7 +2443,7 @@ pub mod insert {
2365 if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
2443 if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
2366 // another newline, indent the end bracket one level less
2444 // another newline, indent the end bracket one level less
2367 let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
2445 let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
2368− text.push('\n');
2446+ text.push_str(doc.line_ending.as_str());
2369 text.push_str(&indent);
2447 text.push_str(&indent);
@@ -2488,7 +2566,8 @@ fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) {
2490fn yank_joined_to_clipboard(cx: &mut Context) {
2568fn yank_joined_to_clipboard(cx: &mut Context) {
2569+ let line_ending = current!(cx.editor).1.line_ending;
2491− yank_joined_to_clipboard_impl(&mut cx.editor, "\n");
2570+ yank_joined_to_clipboard_impl(&mut cx.editor, line_ending.as_str());
2494fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) {
2573fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) {
@@ -2529,8 +2608,10 @@ fn paste_impl(
2529 .unwrap(),
2608 .unwrap(),
2532− // if any of values ends \n it's linewise paste
2611+ // if any of values ends with a line ending, it's linewise paste
2533− let linewise = values.iter().any(|value| value.ends_with('\n'));
2612+ let linewise = values
2614+ .any(|value| get_line_ending_of_str(value).is_some());
2535 let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
2616 let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);