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:
kellito
2026-07-05 19:31:33 +03:00
parent eaf3c221ab
commit dfd75b52f6
@@ -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"));
}
}