Skip to content

Commit 0aa0c2f

Browse files
committed
feat: wrap lines longer than the terminal width
Previously long lines would be truncated, now instead they are wrapped and a wrap indicator displayed at the front of each line, after the first one. The wrap indicator and collapse indicators are configurable.
1 parent d037dd5 commit 0aa0c2f

10 files changed

+211
-19
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ pretty_assertions = "1.4.0"
1414
temp-dir = "0.1.13"
1515
criterion = "0.5.1"
1616
insta = "1.42.2"
17-
unicode-width = "0.2.0"
1817

1918
[profile.release]
2019
strip = true
@@ -64,3 +63,4 @@ tree-sitter-elixir = "=0.1.1"
6463
regex = "1.11.1"
6564
strip-ansi-escapes = "0.2.1"
6665
unicode-segmentation = "1.12.0"
66+
unicode-width = "0.2.0"

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub struct StyleConfig {
4848
pub selection_line: StyleConfigEntry,
4949
pub selection_bar: SymbolStyleConfigEntry,
5050
pub selection_area: StyleConfigEntry,
51+
pub wrap_indicator: SymbolStyleConfigEntry,
52+
pub collapsed_indicator: SymbolStyleConfigEntry,
5153

5254
pub hash: StyleConfigEntry,
5355
pub branch: StyleConfigEntry,

src/default_config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ selection_line = { mods = "BOLD" }
6565
# You may want to set `selection_area.bg` to a nice background color.
6666
# Looks horrible with regular terminal colors, so is therefore not set.
6767
selection_area = {}
68+
wrap_indicator = { symbol = "", fg = "grey", mods = "DIM" }
69+
collapsed_indicator = { symbol = "", fg = "grey", mods = "DIM" }
6870

6971
hash = { fg = "yellow" }
7072
branch = { fg = "green" }

src/screen/mod.rs

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use crate::{config::Config, items::TargetData, Res};
44

55
use super::Item;
66
use std::{borrow::Cow, collections::HashSet, rc::Rc};
7+
use ratatui::widgets::{Paragraph, Wrap};
8+
use unicode_width::UnicodeWidthStr;
79

810
pub(crate) mod log;
911
pub(crate) mod show;
@@ -321,46 +323,92 @@ struct LineView<'a> {
321323
highlighted: bool,
322324
}
323325

326+
// Space for the cursor and the wrap indicator.
327+
const SCREEN_GUTTER_WIDTH: u16 = 2;
328+
324329
impl Widget for &Screen {
325330
fn render(self, area: Rect, buf: &mut Buffer) {
326331
let style = &self.config.style;
327-
328-
for (line_index, line) in self.line_views(area.as_size()).enumerate() {
332+
let mut line_index = 0;
333+
let wrap = Wrap { trim: false };
334+
335+
for line in self.line_views(area.as_size()) {
336+
let t = Text::from(line.display.clone());
337+
let width = buf.area.width - SCREEN_GUTTER_WIDTH;
338+
let remaining_y = area.height - line_index;
339+
let height = measure_paragraph_height(&t, wrap, SCREEN_GUTTER_WIDTH, width).min(remaining_y);
329340
let line_area = Rect {
330341
x: 0,
331-
y: line_index as u16,
332-
width: buf.area.width,
333-
height: 1,
342+
y: line_index,
343+
width,
344+
height,
334345
};
335346

336-
let indented_line_area = Rect { x: 1, ..line_area };
347+
let indented_line_area = Rect { x: SCREEN_GUTTER_WIDTH, ..line_area }.intersection(area);
337348

338349
if line.highlighted {
339350
buf.set_style(line_area, &style.selection_area);
340351

341352
if self.line_index[self.cursor] == line.item_index {
342-
buf.set_style(line_area, &style.selection_line);
353+
buf.set_style(indented_line_area, &style.selection_line);
343354
} else {
344-
buf[(0, line_index as u16)]
345-
.set_char(style.selection_bar.symbol)
346-
.set_style(&style.selection_bar);
355+
for i in 0..height {
356+
buf[(0, line_index + i)]
357+
.set_char(style.selection_bar.symbol)
358+
.set_style(&style.selection_bar);
359+
}
347360
}
348361
}
349362

350-
line.display.render(indented_line_area, buf);
351-
let overflow = line.display.width() > line_area.width as usize;
363+
let p = Paragraph::new(t).wrap(wrap);
364+
p.render(indented_line_area, buf);
365+
366+
for i in 1..height {
367+
buf[(1, line_index + i)]
368+
.set_char(style.wrap_indicator.symbol)
369+
.set_style(&style.wrap_indicator);
370+
}
352371

353-
if self.is_collapsed(line.item) && line.display.width() > 0 || overflow {
372+
if self.is_collapsed(line.item) && line.display.width() > 0 {
354373
let line_end =
355374
(indented_line_area.x + line.display.width() as u16).min(area.width - 1);
356-
buf[(line_end, line_index as u16)].set_char('…');
375+
buf[(line_end, line_index)]
376+
.set_char(style.collapsed_indicator.symbol)
377+
.set_style(&style.collapsed_indicator);
357378
}
358379

359380
if self.line_index[self.cursor] == line.item_index {
360-
buf[(0, line_index as u16)]
361-
.set_char(style.cursor.symbol)
362-
.set_style(&style.cursor);
381+
for i in 0..height {
382+
buf[(0, line_index + i as u16)]
383+
.set_char(style.cursor.symbol)
384+
.set_style(&style.cursor);
385+
}
363386
}
387+
388+
line_index += height;
364389
}
365390
}
366391
}
392+
393+
/// Measure each span in a `Text` and return the total height of the wrapped
394+
/// text.
395+
fn measure_paragraph_height(
396+
text: &Text,
397+
wrap: Wrap,
398+
offset: u16,
399+
max_width: u16,
400+
) -> u16 {
401+
let max_width = max_width.max(1);
402+
text.lines.iter().map(|line| {
403+
let mut width = 0;
404+
for span in &line.spans {
405+
let mut span_text = span.content.as_ref();
406+
if wrap.trim {
407+
span_text = span_text.trim_end_matches(|c: char| c == ' ' || c == '\t');
408+
}
409+
width += UnicodeWidthStr::width(span_text) as u16;
410+
}
411+
412+
((width + max_width - offset) / max_width).max(1)
413+
}).sum()
414+
}

src/tests/helpers/repo.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ pub fn commit(dir: &Path, file_name: &str, contents: &str) {
107107
Ok(true) => format!("modify {}\n\nCommit body goes here\n", file_name),
108108
_ => format!("add {}\n\nCommit body goes here\n", file_name),
109109
};
110+
commit_with_message(dir, &message, file_name, contents);
111+
}
112+
113+
pub fn commit_with_message(dir: &Path, message: &str, file_name: &str, contents: &str) {
114+
let path = dir.to_path_buf().join(file_name);
110115
fs::write(path, contents).expect("error writing to file");
111116
run(dir, &["git", "add", file_name]);
112117
run(dir, &["git", "commit", "-m", &message]);

src/tests/mod.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ mod stage;
3232
mod stash;
3333
mod unstage;
3434

35-
use helpers::{clone_and_commit, commit, keys, run, TestContext};
35+
use helpers::{clone_and_commit, commit, commit_with_message, keys, run, TestContext};
3636

3737
#[test]
3838
fn no_repo() {
@@ -233,6 +233,41 @@ fn fetch_all() {
233233
snapshot!(ctx, "fa");
234234
}
235235

236+
mod wrap {
237+
use super::*;
238+
239+
const LONG_LINE: &str = "This is a long line that is longer than 80 columns, to ensure that it definitely wraps on to the line below it\n";
240+
241+
#[test]
242+
fn unstaged_changes() {
243+
let ctx = TestContext::setup_init();
244+
commit(ctx.dir.path(), "testfile", "testing\ntesttest\n");
245+
fs::write(ctx.dir.child("testfile"), LONG_LINE).expect("error writing to file");
246+
snapshot!(ctx, "jj<tab>j<ctrl+j><ctrl+j><ctrl+j>");
247+
}
248+
249+
#[test]
250+
fn staged_changes() {
251+
let ctx = TestContext::setup_init();
252+
fs::write(ctx.dir.child("testfile"), LONG_LINE).expect("error writing to file");
253+
snapshot!(ctx, "jsjj");
254+
}
255+
256+
#[test]
257+
fn recent_commits() {
258+
let ctx = TestContext::setup_init();
259+
commit_with_message(ctx.dir.path(), LONG_LINE, "testfile", "testing\ntesttest\n");
260+
snapshot!(ctx, "jj");
261+
}
262+
263+
#[test]
264+
fn show() {
265+
let ctx = TestContext::setup_clone();
266+
commit(ctx.dir.path(), "firstfile", LONG_LINE);
267+
snapshot!(ctx, "ll<enter><ctrl+j>");
268+
}
269+
}
270+
236271
mod show_refs {
237272
use super::*;
238273

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
source: src/tests/mod.rs
3+
expression: ctx.redact_buffer()
4+
---
5+
On branch main |
6+
|
7+
Recent commits |
8+
ebac8f4 main This is a long line that is longer than 80 columns, to ensure |
9+
▌↪that it definitely wraps on to the line below it |
10+
|
11+
|
12+
|
13+
|
14+
|
15+
|
16+
|
17+
|
18+
|
19+
|
20+
|
21+
|
22+
|
23+
|
24+
|
25+
styles_hash: 556aa06940e8aa8
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
source: src/tests/mod.rs
3+
expression: ctx.redact_buffer()
4+
---
5+
commit cb440e69a12513df5754d1a8683ccfefcb89c1dd |
6+
Author: Author Name <author@email.com> |
7+
Date: Fri, 16 Feb 2024 11:11:00 +0100 |
8+
|
9+
add firstfile |
10+
|
11+
Commit body goes here |
12+
|
13+
added firstfile |
14+
@@ -0,0 +1 @@ |
15+
+This is a long line that is longer than 80 columns, to ensure that it |
16+
▌↪definitely wraps on to the line below it |
17+
|
18+
|
19+
|
20+
|
21+
|
22+
|
23+
|
24+
|
25+
styles_hash: 9ec7ed8a8eda4208
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
source: src/tests/mod.rs
3+
expression: ctx.redact_buffer()
4+
---
5+
No branch |
6+
|
7+
Staged changes (1) |
8+
added testfile |
9+
▌ @@ -0,0 +1 @@ |
10+
+This is a long line that is longer than 80 columns, to ensure that it |
11+
▌↪definitely wraps on to the line below it |
12+
|
13+
Recent commits |
14+
|
15+
|
16+
|
17+
|
18+
|
19+
|
20+
|
21+
|
22+
|
23+
|
24+
|
25+
styles_hash: 2fce71be5221797c
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
source: src/tests/mod.rs
3+
expression: ctx.redact_buffer()
4+
---
5+
On branch main |
6+
|
7+
Unstaged changes (1) |
8+
modified testfile |
9+
@@ -1,2 +1 @@ |
10+
-testing |
11+
-testtest |
12+
+This is a long line that is longer than 80 columns, to ensure that it |
13+
▌↪definitely wraps on to the line below it |
14+
|
15+
Recent commits |
16+
f431046 main add testfile |
17+
|
18+
|
19+
|
20+
|
21+
|
22+
|
23+
|
24+
|
25+
styles_hash: 5ec29d64b309dd10

0 commit comments

Comments
 (0)