feat(editor): continue blockquote markers on Enter
Enter at the end of a `> ` line now starts the next line with `> `, mirroring list continuation. Nested `> > ` keeps its depth, and Enter on an empty quote drops the marker to exit. Generalises `list_marker` to `continuation_marker`.
This commit is contained in:
@@ -101,18 +101,19 @@ impl Editor {
|
||||
true
|
||||
}
|
||||
|
||||
/// Enter in Insert mode, with Markdown list continuation. At the END of a
|
||||
/// list line (`- `/`* `/`+ ` or `N. `), start the next item automatically —
|
||||
/// same bullet, or the next number — preserving indentation. Enter on an
|
||||
/// otherwise-empty item strips the marker instead (exits the list). Anywhere
|
||||
/// else (mid-line, or a non-list line) it's a plain newline.
|
||||
/// Enter in Insert mode, with Markdown list and blockquote continuation. At the
|
||||
/// END of a list line (`- `/`* `/`+ ` or `N. `) or a blockquote (`> `, nested
|
||||
/// depth preserved), start the next line automatically — same bullet, the next
|
||||
/// number, or the same quote depth — preserving indentation. Enter on an
|
||||
/// otherwise-empty item/quote strips the marker instead (exits it). Anywhere
|
||||
/// else (mid-line, or a plain line) it's a plain newline.
|
||||
pub(crate) fn insert_newline(&mut self) {
|
||||
let le = self.line_end(self.caret);
|
||||
if self.caret == le {
|
||||
let ls = self.line_start(self.caret);
|
||||
if let Some((next, cur_len, content_empty)) = list_marker(&self.text[ls..le]) {
|
||||
if let Some((next, cur_len, content_empty)) = continuation_marker(&self.text[ls..le]) {
|
||||
if content_empty {
|
||||
// Empty item: drop the marker, leaving a blank line.
|
||||
// Empty item/quote: drop the marker, leaving a blank line.
|
||||
self.text.replace_range(ls..ls + cur_len, "");
|
||||
self.caret = ls;
|
||||
} else {
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Parse a Markdown list marker at the start of `line`. Returns
|
||||
/// Parse an auto-continued Markdown line prefix at the start of `line` — a list
|
||||
/// item (`- `/`* `/`+ ` or `N. `) or a blockquote (`> `, possibly nested). Returns
|
||||
/// `(next_marker, current_marker_len, content_empty)` where `next_marker` is what
|
||||
/// the following item should start with (same bullet, or the incremented number,
|
||||
/// preserving indentation), `current_marker_len` is the byte length of this
|
||||
/// line's marker prefix, and `content_empty` is whether anything follows it.
|
||||
/// Returns `None` when the line isn't a list item. ASCII throughout (leading
|
||||
/// spaces, bullets, digits, `. ` are all single-byte).
|
||||
pub(crate) fn list_marker(line: &str) -> Option<(String, usize, bool)> {
|
||||
/// the following line should start with (same bullet, the incremented number, or
|
||||
/// the same quote depth, preserving indentation), `current_marker_len` is the byte
|
||||
/// length of this line's marker prefix, and `content_empty` is whether anything
|
||||
/// follows it. Returns `None` when the line has no such prefix. ASCII throughout
|
||||
/// (leading spaces, bullets, digits, `. `, `> ` are all single-byte).
|
||||
pub(crate) fn continuation_marker(line: &str) -> Option<(String, usize, bool)> {
|
||||
let indent = line.len() - line.trim_start_matches(' ').len();
|
||||
let rest = &line[indent..];
|
||||
for bullet in ["- ", "* ", "+ "] {
|
||||
@@ -27,6 +28,25 @@ pub(crate) fn list_marker(line: &str) -> Option<(String, usize, bool)> {
|
||||
let n: usize = rest[..digits].parse().unwrap_or(0);
|
||||
return Some((format!("{}{}. ", &line[..indent], n + 1), cur_len, content_empty));
|
||||
}
|
||||
// Blockquote: a run of `>` markers, each with an optional trailing space,
|
||||
// continued at the same depth (`> > text` → `> > `). A bare `>` normalizes to
|
||||
// `> `. A nested list inside the quote isn't preserved — it degrades to `> `.
|
||||
if rest.starts_with('>') {
|
||||
let bytes = rest.as_bytes();
|
||||
let mut j = 0;
|
||||
let mut depth = 0;
|
||||
while j < bytes.len() && bytes[j] == b'>' {
|
||||
depth += 1;
|
||||
j += 1;
|
||||
if j < bytes.len() && bytes[j] == b' ' {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
let cur_len = indent + j;
|
||||
let content_empty = line[cur_len..].trim().is_empty();
|
||||
let next = format!("{}{}", &line[..indent], "> ".repeat(depth));
|
||||
return Some((next, cur_len, content_empty));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,32 @@ fn enter_splits_the_line() {
|
||||
assert_eq!(e.caret, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_in_a_blockquote_continues_the_marker() {
|
||||
let mut e = typed("> quote");
|
||||
e.handle(Key::Enter);
|
||||
e.handle(Key::Char('m'));
|
||||
assert_eq!(e.text, "> quote\n> m");
|
||||
assert_eq!(e.caret, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_on_an_empty_blockquote_exits_the_quote() {
|
||||
let mut e = typed("> quote");
|
||||
e.handle(Key::Enter); // -> "> quote\n> "
|
||||
e.handle(Key::Enter); // empty quote: drop the "> ", leaving a blank line
|
||||
assert_eq!(e.text, "> quote\n");
|
||||
assert_eq!(e.caret, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_in_a_nested_blockquote_keeps_the_depth() {
|
||||
let mut e = typed("> > deep");
|
||||
e.handle(Key::Enter);
|
||||
e.handle(Key::Char('x'));
|
||||
assert_eq!(e.text, "> > deep\n> > x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_enters_normal_and_steps_onto_last_char() {
|
||||
let mut e = typed("abc");
|
||||
|
||||
Reference in New Issue
Block a user