Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: force tabs to fixed size #37

Merged
merged 4 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use cassowary::{Solver, Variable};
use num_traits::cast;
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::text::{Line, Span};
use ratatui::widgets::{StatefulWidget, Widget};
use ratatui::Frame;
use unicode_width::UnicodeWidthStr;
Expand Down Expand Up @@ -523,15 +523,40 @@ impl<'a, ComponentId: Clone + Debug + Eq + Hash> Viewport<'a, ComponentId> {
span_rect
}

/// Draw a [`Line`] directly to the screen at `(x, y)` location.
pub fn draw_line(&mut self, x: isize, y: isize, line: &Line) -> Rect {
let line_rect = Rect {
x,
y,
width: line.width(),
height: 1,
};
self.current_trace_mut().merge_rect(line_rect);

let draw_rect = self.rect.intersect(line_rect);

let draw_rect = match self.mask {
Some(mask) => mask.apply(draw_rect),
None => draw_rect,
};
if !draw_rect.is_empty() {
let buf_rect = self.translate_rect(draw_rect);
line.render(buf_rect, self.buf);
}

line_rect
}

/// Draw the given text. If the text would overflow the current mask, then
/// it is truncated with an ellipsis.
pub fn draw_text(&mut self, x: isize, y: isize, span: &Span) -> Rect {
let span_rect = self.draw_span(x, y, span);
pub fn draw_text<'line>(&mut self, x: isize, y: isize, line: impl Into<Line<'line>>) -> Rect {
let line_rect = self.draw_line(x, y, &line.into());

let mask_rect = self.mask_rect();
if span_rect.end_x() > mask_rect.end_x() {
self.draw_span(mask_rect.end_x() - 1, span_rect.y, &Span::raw("…"));
if line_rect.end_x() > mask_rect.end_x() {
self.draw_span(mask_rect.end_x() - 1, line_rect.y, &Span::raw("…"));
}
span_rect
line_rect
}

pub fn draw_widget(&mut self, rect: ratatui::layout::Rect, widget: impl Widget) {
Expand Down
108 changes: 59 additions & 49 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crossterm::terminal::{
use ratatui::backend::{Backend, TestBackend};
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::{backend::CrosstermBackend, Terminal};
use tracing::warn;
Expand Down Expand Up @@ -2306,7 +2306,7 @@ impl<'a> Component for CommitMessageView<'a> {
viewport.draw_text(
divider_rect.end_x() + 1,
y,
&Span::styled(
Span::styled(
Cow::Borrowed({
let first_line = match message.split_once('\n') {
Some((before, _after)) => before,
Expand Down Expand Up @@ -2377,7 +2377,7 @@ impl Component for CommitView<'_> {
50,
50,
);
viewport.draw_text(message_rect.x, message_rect.y, &Span::raw(message));
viewport.draw_text(message_rect.x, message_rect.y, Span::raw(message));
return;
}

Expand Down Expand Up @@ -2674,7 +2674,7 @@ impl Component for FileViewHeader<'_> {
viewport.draw_text(
x + toggle_box_rect.width.unwrap_isize() + 1,
y,
&Span::styled(
Span::styled(
format!(
"{}{}",
match old_path {
Expand Down Expand Up @@ -2900,7 +2900,7 @@ impl Component for SectionView<'_> {
viewport.draw_text(
x + toggle_box_rect.width.unwrap_isize() + 1,
y,
&Span::styled(
Span::styled(
format!(
"Section {editable_section_num}/{total_num_editable_sections}"
),
Expand Down Expand Up @@ -3005,7 +3005,7 @@ impl Component for SectionView<'_> {
let toggle_box_rect = viewport.draw_component(x, y, &toggle_box);
let x = x + toggle_box_rect.width.unwrap_isize() + 1;
let text = format!("File mode changed from {before} to {after}");
viewport.draw_text(x, y, &Span::styled(text, Style::default().fg(Color::Blue)));
viewport.draw_text(x, y, Span::styled(text, Style::default().fg(Color::Blue)));
if is_focused {
highlight_rect(
viewport,
Expand Down Expand Up @@ -3061,7 +3061,7 @@ impl Component for SectionView<'_> {
result.push(description.join(" -> "));
format!("({})", result.join(" "))
};
viewport.draw_text(x, y, &Span::styled(text, Style::default().fg(Color::Blue)));
viewport.draw_text(x, y, Span::styled(text, Style::default().fg(Color::Blue)));

if is_focused {
highlight_rect(
Expand Down Expand Up @@ -3106,40 +3106,61 @@ impl Component for SectionLineView<'_> {
}

fn draw(&self, viewport: &mut Viewport<Self::Id>, x: isize, y: isize) {
const NEWLINE_ICON: &str = "⏎";
let Self { line_key: _, inner } = self;
fn replace_control_character(character: char) -> Option<&'static str> {
match character {
// Characters end up writing over each-other and end up
// displaying incorrectly if ignored. Replacing tabs
// with a known length string fixes the issue for now.
'\t' => Some("→ "),
'\n' => Some("⏎"),
_ => None,
}
}

/// Split the line into a sequence of [`Span`]s where control characters are
/// replaced with styled [`Span`]'s and push them to the [`spans`] argument.
fn push_spans_from_line<'line>(line: &'line str, spans: &mut Vec<Span<'line>>) {
const CONTROL_CHARACTER_STYLE: Style = Style::new().fg(Color::DarkGray);

let mut last_index = 0;
// Find index of the start of each character to replace
for (idx, char) in line.match_indices(|char| replace_control_character(char).is_some())
{
// Push the string leading up to the character and the styled replacement string
if let Some(replacement_string) =
char.chars().next().and_then(replace_control_character)
{
spans.push(Span::raw(&line[last_index..idx]));
spans.push(Span::styled(replacement_string, CONTROL_CHARACTER_STYLE));
// Move the "cursor" to just after the character we're replacing
last_index = idx + char.len();
}
}
// Append anything remaining after the last replacement
let remaining_line = &line[last_index..];
if !remaining_line.is_empty() {
spans.push(Span::raw(remaining_line));
}
}

viewport.draw_blank(Rect {
x: viewport.mask_rect().x,
y,
width: viewport.mask_rect().width,
height: 1,
});
match inner {

match &self.inner {
SectionLineViewInner::Unchanged { line, line_num } => {
let style = Style::default().add_modifier(Modifier::DIM);
// Pad the number in 5 columns because that will align the
// beginning of the actual text with the `+`/`-` of the changed
// lines.
let line_num_rect =
viewport.draw_span(x, y, &Span::styled(format!("{line_num:5} "), style));
let (line, line_end) = match line.strip_suffix('\n') {
Some(line) => (
Span::styled(line, style),
Some(Span::styled(
NEWLINE_ICON,
Style::default().fg(Color::DarkGray),
)),
),
None => (Span::styled(*line, style), None),
};
let line_rect = viewport.draw_text(
line_num_rect.x + line_num_rect.width.unwrap_isize(),
line_num_rect.y,
&line,
);
if let Some(line_end) = line_end {
viewport.draw_span(line_rect.x + line_rect.width.unwrap_isize(), y, &line_end);
}
let line_number = Span::raw(format!("{line_num:5} "));
let mut spans = vec![line_number];
push_spans_from_line(line, &mut spans);

const UI_UNCHANGED_STYLE: Style = Style::new().add_modifier(Modifier::DIM);
viewport.draw_text(x, y, Line::from(spans).style(UI_UNCHANGED_STYLE));
}

SectionLineViewInner::Changed {
Expand All @@ -3148,28 +3169,17 @@ impl Component for SectionLineView<'_> {
line,
} => {
let toggle_box_rect = viewport.draw_component(x, y, toggle_box);
let x = x + toggle_box_rect.width.unwrap_isize() + 1;
let x = toggle_box_rect.end_x() + 1;

let (change_type_text, style) = match change_type {
let (change_type_text, changed_line_style) = match change_type {
ChangeType::Added => ("+ ", Style::default().fg(Color::Green)),
ChangeType::Removed => ("- ", Style::default().fg(Color::Red)),
};
viewport.draw_span(x, y, &Span::styled(change_type_text, style));
let x = x + change_type_text.width().unwrap_isize();
let (line, line_end) = match line.strip_suffix('\n') {
Some(line) => (
Span::styled(line, style),
Some(Span::styled(
NEWLINE_ICON,
Style::default().fg(Color::DarkGray),
)),
),
None => (Span::styled(*line, style), None),
};
let line_rect = viewport.draw_text(x, y, &line);
if let Some(line_end) = line_end {
viewport.draw_span(line_rect.x + line_rect.width.unwrap_isize(), y, &line_end);
}

let mut spans = vec![Span::raw(change_type_text)];
push_spans_from_line(line, &mut spans);

viewport.draw_text(x, y, Line::from(spans).style(changed_line_style));
}
}
}
Expand Down
108 changes: 108 additions & 0 deletions tests/test_scm_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3211,3 +3211,111 @@ fn test_no_files() -> eyre::Result<()> {

Ok(())
}

#[test]
fn test_tabs_in_files() -> eyre::Result<()> {
let state = RecordState {
is_read_only: false,
commits: Default::default(),
files: vec![File {
old_path: None,
path: Cow::Borrowed(Path::new("foo/bar")),
file_mode: None,
sections: vec![
Section::Unchanged {
lines: iter::repeat(Cow::Borrowed("\tthis is some indented text\n"))
.take(10)
.collect(),
},
Section::Changed {
lines: vec![
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Removed,
line: Cow::Borrowed("before text\t1\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Added,
line: Cow::Borrowed("after text 1\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Removed,
line: Cow::Borrowed("before text 2\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Added,
line: Cow::Borrowed("after text\t2\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Removed,
line: Cow::Borrowed("\tbefore text 3\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Added,
line: Cow::Borrowed("\tafter text\t3\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Removed,
line: Cow::Borrowed("\tbefore text\t4\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Added,
line: Cow::Borrowed("\tafter text 4\n"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Removed,
line: Cow::Borrowed("\tbefore text\t5"),
},
SectionChangedLine {
is_checked: true,
change_type: ChangeType::Added,
line: Cow::Borrowed("\tafter text\t5"),
},
],
},
Section::Unchanged {
lines: vec![Cow::Borrowed("this is some trailing\ttext\n")],
},
],
}],
};
let initial = TestingScreenshot::default();
let mut input = TestingInput::new(
80,
18,
[Event::ExpandAll, initial.event(), Event::QuitAccept],
);
let recorder = Recorder::new(state, &mut input);
recorder.run()?;

insta::assert_snapshot!(initial, @r###"
"[File] [Edit] [Select] [View] "
"(●) foo/bar (-)"
" ⋮ "
" 8 → this is some indented text⏎ "
" 9 → this is some indented text⏎ "
" 10 → this is some indented text⏎ "
" [●] Section 1/1 [-]"
" [●] - before text→ 1⏎ "
" [●] + after text 1⏎ "
" [●] - before text 2⏎ "
" [●] + after text→ 2⏎ "
" [●] - → before text 3⏎ "
" [●] + → after text→ 3⏎ "
" [●] - → before text→ 4⏎ "
" [●] + → after text 4⏎ "
" [●] - → before text→ 5 "
" [●] + → after text→ 5 "
" 16 this is some trailing→ text⏎ "
"###);

Ok(())
}
Loading