tlc: Sprint 3 C18 — format paragraph preserves code blocks
Previously reformat_paragraph_at() would join indented lines
together, destroying intentional indentation (Python code,
shell heredocs, ASCII art, markdown sub-blocks).
Now reformat_paragraph_at() detects code blocks (paragraphs
where every non-blank line begins with whitespace) and
returns the text unchanged. The detection is intentionally
conservative — false positives leave the paragraph alone
rather than destroying user-written indentation.
format.rs:
- New is_code_block(paragraph) helper
- reformat_paragraph_at() calls is_code_block(); if true,
returns text unchanged (preserving indentation)
Tests (6 new in editor::format::tests):
- is_code_block_detects_indented_paragraph
- is_code_block_rejects_unindented_paragraph
- is_code_block_ignores_blank_lines_in_paragraph
- reformat_paragraph_skips_code_block
- reformat_paragraph_skips_shell_heredoc
- reformat_paragraph_handles_mixed_code_and_prose
Total: 1289 passing (was 1283; +6 new).
This commit is contained in:
@@ -242,6 +242,11 @@ pub fn paragraph_range(text: &str, cursor: usize) -> (usize, usize) {
|
||||
/// terminates the paragraph) is preserved verbatim. If the cursor
|
||||
/// is on a blank line, `text` is returned unchanged.
|
||||
///
|
||||
/// Code blocks (paragraphs where every non-blank line begins with
|
||||
/// whitespace) are detected and left unchanged — formatting them
|
||||
/// would destroy intentional indentation (e.g. Python code,
|
||||
/// indented shell scripts, ASCII art, markdown sub-blocks).
|
||||
///
|
||||
/// This is a pure string function — it does not touch the
|
||||
/// [`Buffer`] or its undo stack. The caller is responsible for
|
||||
/// recording the pre-edit state if undo is desired.
|
||||
@@ -253,6 +258,13 @@ pub fn reformat_paragraph_at(text: &str, cursor: usize, width: usize) -> String
|
||||
return text.to_string();
|
||||
}
|
||||
let paragraph = &text[para_start..para_end];
|
||||
if is_code_block(paragraph) {
|
||||
// Don't touch indented paragraphs — formatting them would
|
||||
// collapse the leading whitespace that the user wrote
|
||||
// intentionally (Python indentation, markdown sub-blocks,
|
||||
// shell heredocs, etc).
|
||||
return text.to_string();
|
||||
}
|
||||
let para_with_newline_end = text[para_end..]
|
||||
.find('\n')
|
||||
.map(|n| para_end + n + 1)
|
||||
@@ -267,6 +279,27 @@ pub fn reformat_paragraph_at(text: &str, cursor: usize, width: usize) -> String
|
||||
out
|
||||
}
|
||||
|
||||
/// True if `paragraph` looks like an indented code block: every
|
||||
/// non-blank line begins with at least one whitespace character.
|
||||
/// The heuristic intentionally errs on the side of preservation
|
||||
/// (false positives leave the paragraph alone rather than
|
||||
/// destroying indentation).
|
||||
#[must_use]
|
||||
pub fn is_code_block(paragraph: &str) -> bool {
|
||||
let mut has_content = false;
|
||||
for line in paragraph.split('\n') {
|
||||
if line.chars().all(|c| c.is_whitespace()) {
|
||||
continue;
|
||||
}
|
||||
has_content = true;
|
||||
let first = line.chars().next().expect("non-blank line has a char");
|
||||
if !first.is_whitespace() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
has_content
|
||||
}
|
||||
|
||||
/// Compute the leading whitespace of a line — the maximal run of
|
||||
/// `' '` and `'\t'` characters at the start of `line`.
|
||||
///
|
||||
@@ -621,4 +654,55 @@ mod tests {
|
||||
fn reformat_paragraph_uses_default_width_constant() {
|
||||
assert_eq!(DEFAULT_WRAP_WIDTH, 72);
|
||||
}
|
||||
|
||||
// --- code-block detection (C18) ---
|
||||
|
||||
#[test]
|
||||
fn is_code_block_detects_indented_paragraph() {
|
||||
// Every non-blank line starts with whitespace → code block.
|
||||
assert!(is_code_block(" if x:"));
|
||||
assert!(is_code_block(" if x:\n y\n z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_code_block_rejects_unindented_paragraph() {
|
||||
// First line not indented → not a code block.
|
||||
assert!(!is_code_block("foo bar baz"));
|
||||
assert!(!is_code_block("foo\nbar\nbaz"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_code_block_ignores_blank_lines_in_paragraph() {
|
||||
// Blank lines (all-whitespace) inside a paragraph are
|
||||
// skipped — the remaining content's indentation decides.
|
||||
assert!(is_code_block(" foo\n\n bar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reformat_paragraph_skips_code_block() {
|
||||
// Indented Python-ish paragraph must not be reformatted.
|
||||
let text = " if x:\n y\n";
|
||||
let out = reformat_paragraph_at(text, 0, 72);
|
||||
assert_eq!(out, text, "code block should be preserved verbatim");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reformat_paragraph_skips_shell_heredoc() {
|
||||
// Multi-line indented shell snippet.
|
||||
let text = " cat <<EOF\n hello\n EOF\n";
|
||||
let out = reformat_paragraph_at(text, 0, 72);
|
||||
assert_eq!(out, text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reformat_paragraph_handles_mixed_code_and_prose() {
|
||||
// The first paragraph (unindented) is reformatted; the
|
||||
// second (indented code block) is preserved.
|
||||
let text = "the quick brown fox\njumps over\n\n def f():\n return 1\n";
|
||||
let out = reformat_paragraph_at(text, 0, 30);
|
||||
assert!(out.contains("def f():\n return 1"), "code preserved");
|
||||
assert!(out.contains("the quick brown fox"), "prose still present");
|
||||
// The prose should have been joined into one wrapped line.
|
||||
assert!(out.contains("the quick brown fox jumps"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user