Skip to content

Commit

Permalink
fix: Code highlighting with word wrapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Dustin Blackman committed Dec 29, 2023
1 parent d3eefcb commit d9f1787
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 175 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,5 @@ profile-flamegraph = '''set -e
'''

top = '''
top -pid $(ps aux | grep 'target\/debug\/oatmeal' | awk '{print $2}')
top -pid $(ps aux | grep 'target\/release\/oatmeal' | awk '{print $2}')
'''
2 changes: 1 addition & 1 deletion src/application/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn is_line_width_sufficient(line_width: u16) -> bool {
.max()
.unwrap();

let bubble_style = Bubble::style_confg();
let bubble_style = Bubble::style_config();
let min_width =
(author_lengths + bubble_style.bubble_padding + bubble_style.border_elements_length) as i32;
let trimmed_line_width =
Expand Down
31 changes: 0 additions & 31 deletions src/domain/models/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,37 +44,6 @@ impl Message {
self.text += &text.replace('\t', " ");
}

pub fn as_string_lines(&self, line_max_width: usize) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();

for full_line in self.text.split('\n') {
// TODO may not need this.
if full_line.trim().is_empty() {
lines.push(" ".to_string());
continue;
}

let mut char_count = 0;
let mut current_lines: Vec<&str> = vec![];

for word in full_line.split(' ') {
if word.len() + char_count + 1 > line_max_width {
lines.push(current_lines.join(" ").trim_end().to_string());
current_lines = vec![word];
char_count = word.len() + 1;
} else {
current_lines.push(word);
char_count += word.len() + 1;
}
}
if !current_lines.is_empty() {
lines.push(current_lines.join(" ").trim_end().to_string());
}
}

return lines;
}

pub fn codeblocks(&self) -> Vec<String> {
let mut codeblocks: Vec<String> = vec![];
let mut current_codeblock: Vec<&str> = vec![];
Expand Down
15 changes: 3 additions & 12 deletions src/domain/models/message_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,6 @@ fn it_executes_append_with_tabs() {
assert_eq!(msg.text, "Hi there! It's me!");
}

#[test]
fn it_executes_as_string_lines() {
let msg = Message::new(Author::Oatmeal, "Hi there! This is a really long line that pushes the boundaries of 50 characters across the screen, resulting in the line being wrapped. Cool right?");
let lines = msg.as_string_lines(50).join("\n");
insta::assert_snapshot!(lines, @r###"
Hi there! This is a really long line that pushes
the boundaries of 50 characters across the
screen, resulting in the line being wrapped. Cool
right?
"###);
}

#[test]
fn it_executes_codeblocks() {
let msg = Message::new(Author::Oatmeal, codeblock_fixture());
Expand All @@ -87,6 +75,9 @@ fn it_executes_codeblocks() {
"###);

insta::assert_snapshot!(codeblocks[1], @r###"
// Hello World.
// This is a really long line that pushes the boundaries of 50 characters across the screen, resulting in a code comment block where the line is wrapped to the next line. Cool right?
function printNumbers() {
let numbers = [];
for (let i = 0; i <= 10; i++) {
Expand Down
186 changes: 104 additions & 82 deletions src/domain/services/bubble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ impl<'a> Bubble<'_> {
};
}

pub fn style_confg() -> BubbleConfig {
pub fn style_config() -> BubbleConfig {
return BubbleConfig {
// Unicode character border + padding.
bubble_padding: 8,
Expand All @@ -84,109 +84,152 @@ impl<'a> Bubble<'_> {
let mut in_codeblock = false;
let mut lines: Vec<Line> = vec![];

let (message_lines, max_line_length) = self.get_message_lines();
let max_line_length = self.get_max_line_length();

for line in message_lines {
let line_length = line.len();
let (mut spans, formatted_line_length) =
self.format_line(line.to_string(), max_line_length);
for line in self.message.text.lines() {
let mut spans = vec![];

if in_codeblock {
let highlighted_spans: Vec<Span> = highlight
.highlight_line(&line, &SYNTAX_SET)
.unwrap()
if line.trim().starts_with("```") {
let lang = line.trim().replace("```", "");
let syntax = Syntaxes::get(&lang);
if !in_codeblock {
highlight = HighlightLines::new(syntax, theme);
in_codeblock = true;

self.codeblock_counter += 1;
spans = vec![
Span::from(line.to_owned()),
Span::styled(
format!(" ({})", self.codeblock_counter),
Style {
fg: Some(Color::White),
..Style::default()
},
),
];
} else {
in_codeblock = false;
}
} else if in_codeblock {
// Highlighting doesn't work accurately unless each line is postfixed with '\n',
// especially when dealing with multi-line code comments.
let line_nl = format!("{line}\n");
let highlighted = highlight.highlight_line(&line_nl, &SYNTAX_SET).unwrap();

spans = highlighted
.iter()
.map(|segment| {
.enumerate()
.map(|(idx, segment)| {
let (style, content) = segment;
let mut text = content.to_string();
if idx == highlighted.len() - 1 {
text = text.trim_end().to_string();
}

return Span::styled(
content.to_string(),
text,
Style {
fg: Syntaxes::translate_colour(style.foreground),
..Style::default()
},
);
})
.collect();
}

spans = self
.format_spans(highlighted_spans, line_length, max_line_length)
.0;
if spans.is_empty() {
spans = vec![Span::styled(line.to_owned(), Style::default())];
}

if line.trim().starts_with("```") {
let lang = line.trim().replace("```", "");
let syntax = Syntaxes::get(&lang);
if !in_codeblock {
highlight = HighlightLines::new(syntax, theme);
in_codeblock = true;
let mut split_spans = vec![];
let mut line_char_count = 0;

self.codeblock_counter += 1;
spans = self
.format_spans(
vec![
Span::from(line),
Span::styled(
format!(" ({})", self.codeblock_counter),
Style {
fg: Some(Color::White),
..Style::default()
},
),
],
format!(" ({})", self.codeblock_counter).len() + line_length,
max_line_length,
)
.0;
} else {
in_codeblock = false;
for span in spans {
if span.content.len() + line_char_count <= max_line_length {
line_char_count += span.content.len();
split_spans.push(span);
continue;
}
}

let bubble_padding =
repeat_from_subtractions(" ", vec![self.window_max_width, formatted_line_length]);
let mut word_set: Vec<&str> = vec![];

for word in span.content.split(' ') {
if word.len() + line_char_count > max_line_length {
split_spans.push(Span::styled(word_set.join(" "), span.style));
lines.push(self.spans_to_line(split_spans, max_line_length));

if self.alignment == BubbleAlignment::Left {
spans.push(Span::from(bubble_padding));
lines.push(Line::from(spans));
} else {
let mut line_spans = vec![Span::from(bubble_padding)];
line_spans.extend(spans);
lines.push(Line::from(line_spans));
split_spans = vec![];
word_set = vec![];
line_char_count = 0;
}

word_set.push(word);
line_char_count += word.len() + 1;
}

split_spans.push(Span::styled(word_set.join(" "), span.style));
}

lines.push(self.spans_to_line(split_spans, max_line_length));
}

return self.wrap_lines_in_buddle(lines, max_line_length);
}

fn get_message_lines(&self) -> (Vec<String>, usize) {
fn spans_to_line(&self, mut spans: Vec<Span<'a>>, max_line_length: usize) -> Line<'a> {
let line_str_len: usize = spans.iter().map(|e| return e.content.len()).sum();
let fill = repeat_from_subtractions(" ", vec![max_line_length, line_str_len]);
let formatted_line_length =
line_str_len + fill.len() + Bubble::style_config().bubble_padding;

let mut wrapped_spans = vec![self.highlight_span("│ ".to_string())];
wrapped_spans.append(&mut spans);
wrapped_spans.push(self.highlight_span(format!("{fill} │")));

let outer_bubble_padding =
repeat_from_subtractions(" ", vec![self.window_max_width, formatted_line_length]);

if self.alignment == BubbleAlignment::Left {
wrapped_spans.push(Span::from(outer_bubble_padding));
return Line::from(wrapped_spans);
}

let mut line_spans = vec![Span::from(outer_bubble_padding)];
line_spans.extend(wrapped_spans);

return Line::from(line_spans);
}

fn get_max_line_length(&self) -> usize {
let style_config = Bubble::style_config();
// Add a minimum 4% of padding on the side.
let min_bubble_padding_length = ((self.window_max_width as f32
* Bubble::style_confg().outer_padding_percentage)
* style_config.outer_padding_percentage)
.ceil()) as usize;

// Border elements + minimum bubble padding.
let line_border_width =
Bubble::style_confg().border_elements_length + min_bubble_padding_length;
let line_border_width = style_config.border_elements_length + min_bubble_padding_length;

let message_lines = self
let mut max_line_length = self
.message
.as_string_lines(self.window_max_width - line_border_width);

let mut max_line_length = message_lines
.iter()
.text
.lines()
.map(|line| {
return line.len();
})
.max()
.unwrap();

if max_line_length > (self.window_max_width - line_border_width) {
max_line_length = self.window_max_width - line_border_width;
}

let username = &self.message.author.to_string();
if max_line_length < username.len() {
max_line_length = username.len();
}

return (message_lines, max_line_length);
return max_line_length;
}

fn wrap_lines_in_buddle(&self, lines: Vec<Line<'a>>, max_line_length: usize) -> Vec<Line<'a>> {
Expand All @@ -200,7 +243,7 @@ impl<'a> Bubble<'_> {
vec![
self.window_max_width,
max_line_length,
Bubble::style_confg().bubble_padding,
Bubble::style_config().bubble_padding,
],
);

Expand Down Expand Up @@ -231,27 +274,6 @@ impl<'a> Bubble<'_> {
}
}

fn format_spans(
&self,
mut spans: Vec<Span<'a>>,
line_str_len: usize,
max_line_length: usize,
) -> (Vec<Span<'a>>, usize) {
let fill = repeat_from_subtractions(" ", vec![max_line_length, line_str_len]);
// 8 is the unicode character border + padding.
let formatted_line_length = line_str_len + fill.len() + 8;

let mut spans_res = vec![self.highlight_span("│ ".to_string())];
spans_res.append(&mut spans);
spans_res.push(self.highlight_span(format!("{fill} │").to_string()));
return (spans_res, formatted_line_length);
}

fn format_line(&self, line: String, max_line_length: usize) -> (Vec<Span<'a>>, usize) {
let line_len = line.len();
return self.format_spans(vec![Span::from(line)], line_len, max_line_length);
}

fn highlight_span(&self, text: String) -> Span<'a> {
if self.message.message_type() == MessageType::Error {
return Span::styled(
Expand Down
2 changes: 1 addition & 1 deletion src/domain/services/bubble_list_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ fn it_returns_correct_length() -> Result<()> {
let mut bubble_list = BubbleList::new(theme);
bubble_list.set_messages(&messages, 50);

assert_eq!(bubble_list.len(), 43);
assert_eq!(bubble_list.len(), 50);
return Ok(());
}
14 changes: 7 additions & 7 deletions src/domain/services/bubble_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ fn it_creates_author_oatmeal_text() -> Result<()> {
fn it_creates_author_oatmeal_text_long() -> Result<()> {
let lines_str = create_lines(Author::Oatmeal, BubbleAlignment::Left, 0, "Hi there! This is a really long line that pushes the boundaries of 50 characters across the screen, resulting in a bubble where the line is wrapped to the next line. Cool right?")?;
insta::assert_snapshot!(lines_str, @r###"
╭Oatmeal────────────────────────────────────
│ Hi there! This is a really long line that
│ pushes the boundaries of 50 characters
│ across the screen, resulting in a bubble
│ where the line is wrapped to the next
line. Cool right?
╰───────────────────────────────────────────
╭Oatmeal──────────────────────────────────────╮
│ Hi there! This is a really long line that
│ pushes the boundaries of 50 characters
│ across the screen, resulting in a bubble
│ where the line is wrapped to the next line. │
│ Cool right?
╰─────────────────────────────────────────────╯
"###);

return Ok(());
Expand Down
6 changes: 6 additions & 0 deletions src/domain/services/code_blocks_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ fn it_provides_first_second_codeblock() {
}
}
// Hello World.
// This is a really long line that pushes the boundaries of 50 characters across the screen, resulting in a code comment block where the line is wrapped to the next line. Cool right?
function printNumbers() {
let numbers = [];
for (let i = 0; i <= 10; i++) {
Expand All @@ -81,6 +84,9 @@ fn it_provides_first_second_third_codeblock() {
}
}
// Hello World.
// This is a really long line that pushes the boundaries of 50 characters across the screen, resulting in a code comment block where the line is wrapped to the next line. Cool right?
function printNumbers() {
let numbers = [];
for (let i = 0; i <= 10; i++) {
Expand Down
Loading

0 comments on commit d9f1787

Please sign in to comment.