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 <