From 829c4445a19b053a35d0bb02640a77363408751c Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Thu, 2 Oct 2025 17:16:06 -0700 Subject: [PATCH] textwrap: don't split words at punctuation by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default `textwrap::WordSeparator::UnicodeBreakProperties` provides sensible line breaking for (e.g.) emojis and CJK text. Unfortunately, it also considers punctuation like `/` to be an appropriate location for line breaks. This is fine for normal text, but leads to very bad behavior when attempting to wrap error messages. Here, a file path is broken across multiple lines (with box drawing characters added in between parts of the path as well), making it impossible to copy-paste the path out of the error message: ``` Error: × Failed to read Buck2 event log from `buck2 build //aaa/aaaa` via /var/folders/z5/fclwwdms3r1gq4k4p3pkvvc00000gn/ │ T/.tmpBgvlUI/buck-log.jsonl.gz ╰─▶ failed to open file `/var/folders/z5/fclwwdms3r1gq4k4p3pkvvc00000gn/T/.tmpBgvlUI/buck-log.jsonl.gz`: No such file or directory (os error 2) ``` In the future, we may want to write our own line break algorithm that breaks between CJK codepoints and emojis but not at punctuation like slashes. For now, I believe it will be better to break lines at ASCII spaces only. Similar changes are made for some other settings: - The default for `break_words` has been changed to `false` for similar reasons. - The default `textwrap::WordSplitter` has been changed to not split words at existing hyphens, to prevent splits like `--foo-bar` into `--foo-` and `bar`. --- src/handlers/graphical.rs | 87 ++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 37b6bf8..7d62e31 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -1,6 +1,8 @@ use std::fmt::{self, Write}; use owo_colors::{OwoColorize, Style, StyledList}; +use textwrap::WordSeparator; +use textwrap::WordSplitter; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; @@ -61,7 +63,7 @@ impl GraphicalReportHandler { tab_width: 4, with_cause_chain: true, wrap_lines: true, - break_words: true, + break_words: false, with_primary_span_start: true, word_separator: None, word_splitter: None, @@ -83,7 +85,7 @@ impl GraphicalReportHandler { wrap_lines: true, with_cause_chain: true, with_primary_span_start: true, - break_words: true, + break_words: false, word_separator: None, word_splitter: None, highlighter: MietteHighlighter::default(), @@ -262,16 +264,16 @@ impl GraphicalReportHandler { if let Some(footer) = &self.footer { writeln!(f)?; let width = self.termwidth.saturating_sub(2); - let mut opts = textwrap::Options::new(width) + let opts = textwrap::Options::new(width) .initial_indent(" ") .subsequent_indent(" ") - .break_words(self.break_words); - if let Some(word_separator) = self.word_separator { - opts = opts.word_separator(word_separator); - } - if let Some(word_splitter) = self.word_splitter.clone() { - opts = opts.word_splitter(word_splitter); - } + .break_words(self.break_words) + .word_separator(self.word_separator.unwrap_or(WordSeparator::AsciiSpace)) + .word_splitter( + self.word_splitter + .clone() + .unwrap_or(WordSplitter::NoHyphenation), + ); writeln!(f, "{}", self.wrap(footer, opts))?; } @@ -340,16 +342,16 @@ impl GraphicalReportHandler { let initial_indent = format!(" {} ", severity_icon.style(severity_style)); let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); let width = self.termwidth.saturating_sub(2); - let mut opts = textwrap::Options::new(width) + let opts = textwrap::Options::new(width) .initial_indent(&initial_indent) .subsequent_indent(&rest_indent) - .break_words(self.break_words); - if let Some(word_separator) = self.word_separator { - opts = opts.word_separator(word_separator); - } - if let Some(word_splitter) = self.word_splitter.clone() { - opts = opts.word_splitter(word_splitter); - } + .break_words(self.break_words) + .word_separator(self.word_separator.unwrap_or(WordSeparator::AsciiSpace)) + .word_splitter( + self.word_splitter + .clone() + .unwrap_or(WordSplitter::NoHyphenation), + ); writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?; @@ -386,16 +388,16 @@ impl GraphicalReportHandler { ) .style(severity_style) .to_string(); - let mut opts = textwrap::Options::new(width) + let opts = textwrap::Options::new(width) .initial_indent(&initial_indent) .subsequent_indent(&rest_indent) - .break_words(self.break_words); - if let Some(word_separator) = self.word_separator { - opts = opts.word_separator(word_separator); - } - if let Some(word_splitter) = self.word_splitter.clone() { - opts = opts.word_splitter(word_splitter); - } + .break_words(self.break_words) + .word_separator(self.word_separator.unwrap_or(WordSeparator::AsciiSpace)) + .word_splitter( + self.word_splitter + .clone() + .unwrap_or(WordSplitter::NoHyphenation), + ); match error { ErrorKind::Diagnostic(diag) => { @@ -428,17 +430,16 @@ impl GraphicalReportHandler { if let Some(help) = diagnostic.help() { let width = self.termwidth.saturating_sub(2); let initial_indent = " help: ".style(self.theme.styles.help).to_string(); - let mut opts = textwrap::Options::new(width) + let opts = textwrap::Options::new(width) .initial_indent(&initial_indent) .subsequent_indent(" ") - .break_words(self.break_words); - if let Some(word_separator) = self.word_separator { - opts = opts.word_separator(word_separator); - } - if let Some(word_splitter) = self.word_splitter.clone() { - opts = opts.word_splitter(word_splitter); - } - + .break_words(self.break_words) + .word_separator(self.word_separator.unwrap_or(WordSeparator::AsciiSpace)) + .word_splitter( + self.word_splitter + .clone() + .unwrap_or(WordSplitter::NoHyphenation), + ); writeln!(f, "{}", self.wrap(&help.to_string(), opts))?; } Ok(()) @@ -489,16 +490,16 @@ impl GraphicalReportHandler { .style(severity_style) .to_string(); - let mut opts = textwrap::Options::new(width) + let opts = textwrap::Options::new(width) .initial_indent(&initial_indent) .subsequent_indent(&rest_indent) - .break_words(self.break_words); - if let Some(word_separator) = self.word_separator { - opts = opts.word_separator(word_separator); - } - if let Some(word_splitter) = self.word_splitter.clone() { - opts = opts.word_splitter(word_splitter); - } + .break_words(self.break_words) + .word_separator(self.word_separator.unwrap_or(WordSeparator::AsciiSpace)) + .word_splitter( + self.word_splitter + .clone() + .unwrap_or(WordSplitter::NoHyphenation), + ); let mut inner = String::new();