From dfd75b52f64dab10a31853392b63f6e5b06a40f1 Mon Sep 17 00:00:00 2001 From: kellito Date: Sun, 5 Jul 2026 19:31:33 +0300 Subject: [PATCH] =?UTF-8?q?tlc:=20Sprint=203=20C18=20=E2=80=94=20format=20?= =?UTF-8?q?paragraph=20preserves=20code=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../tui/tlc/source/src/editor/format.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/local/recipes/tui/tlc/source/src/editor/format.rs b/local/recipes/tui/tlc/source/src/editor/format.rs index 4c53b3aaa1..10c24f0a0f 100644 --- a/local/recipes/tui/tlc/source/src/editor/format.rs +++ b/local/recipes/tui/tlc/source/src/editor/format.rs @@ -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 <