mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 19:33:37 +08:00
feat: add word diff highlighting to tui diffs
This commit is contained in:
@@ -38,8 +38,8 @@ const MAX_FILE_ACTIVITY_PATCH_LINES: usize = 3;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct WorktreeDiffColumns {
|
struct WorktreeDiffColumns {
|
||||||
removals: String,
|
removals: Text<'static>,
|
||||||
additions: String,
|
additions: Text<'static>,
|
||||||
hunk_offsets: Vec<usize>,
|
hunk_offsets: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,20 +591,24 @@ impl Dashboard {
|
|||||||
(self.output_title(), content)
|
(self.output_title(), content)
|
||||||
}
|
}
|
||||||
OutputMode::WorktreeDiff => {
|
OutputMode::WorktreeDiff => {
|
||||||
let content = self
|
let content = if let Some(patch) = self.selected_diff_patch.as_ref() {
|
||||||
.selected_diff_patch
|
build_unified_diff_text(patch, self.theme_palette())
|
||||||
.clone()
|
} else {
|
||||||
.or_else(|| {
|
Text::from(
|
||||||
self.selected_diff_summary.as_ref().map(|summary| {
|
self.selected_diff_summary
|
||||||
format!(
|
.as_ref()
|
||||||
"{summary}\n\nNo patch content to preview yet. The worktree may be clean or only have summary-level changes."
|
.map(|summary| {
|
||||||
)
|
format!(
|
||||||
})
|
"{summary}\n\nNo patch content to preview yet. The worktree may be clean or only have summary-level changes."
|
||||||
})
|
)
|
||||||
.unwrap_or_else(|| {
|
})
|
||||||
"No worktree diff available for the selected session.".to_string()
|
.unwrap_or_else(|| {
|
||||||
});
|
"No worktree diff available for the selected session."
|
||||||
(self.output_title(), Text::from(content))
|
.to_string()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(self.output_title(), content)
|
||||||
}
|
}
|
||||||
OutputMode::ConflictProtocol => {
|
OutputMode::ConflictProtocol => {
|
||||||
let content = self.selected_conflict_protocol.clone().unwrap_or_else(|| {
|
let content = self.selected_conflict_protocol.clone().unwrap_or_else(|| {
|
||||||
@@ -646,7 +650,7 @@ impl Dashboard {
|
|||||||
let Some(patch) = self.selected_diff_patch.as_ref() else {
|
let Some(patch) = self.selected_diff_patch.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let columns = build_worktree_diff_columns(patch);
|
let columns = build_worktree_diff_columns(patch, self.theme_palette());
|
||||||
let column_chunks = Layout::default()
|
let column_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
@@ -3243,7 +3247,7 @@ impl Dashboard {
|
|||||||
self.selected_diff_hunk_offsets_split = self
|
self.selected_diff_hunk_offsets_split = self
|
||||||
.selected_diff_patch
|
.selected_diff_patch
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|patch| build_worktree_diff_columns(patch).hunk_offsets)
|
.map(|patch| build_worktree_diff_columns(patch, self.theme_palette()).hunk_offsets)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if self.selected_diff_hunk >= self.current_diff_hunk_offsets().len() {
|
if self.selected_diff_hunk >= self.current_diff_hunk_offsets().len() {
|
||||||
self.selected_diff_hunk = 0;
|
self.selected_diff_hunk = 0;
|
||||||
@@ -5544,73 +5548,423 @@ fn highlight_output_line(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_worktree_diff_columns(patch: &str) -> WorktreeDiffColumns {
|
fn build_worktree_diff_columns(patch: &str, palette: ThemePalette) -> WorktreeDiffColumns {
|
||||||
let mut removals = Vec::new();
|
let mut removals = Vec::new();
|
||||||
let mut additions = Vec::new();
|
let mut additions = Vec::new();
|
||||||
let mut hunk_offsets = Vec::new();
|
let mut hunk_offsets = Vec::new();
|
||||||
|
let mut pending_removals = Vec::new();
|
||||||
|
let mut pending_additions = Vec::new();
|
||||||
|
|
||||||
for line in patch.lines() {
|
for line in patch.lines() {
|
||||||
|
if is_diff_removal_line(line) {
|
||||||
|
pending_removals.push(line[1..].to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_diff_addition_line(line) {
|
||||||
|
pending_additions.push(line[1..].to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_split_diff_change_block(
|
||||||
|
&mut removals,
|
||||||
|
&mut additions,
|
||||||
|
&mut pending_removals,
|
||||||
|
&mut pending_additions,
|
||||||
|
palette,
|
||||||
|
);
|
||||||
|
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if line.starts_with("--- ") && !line.starts_with("--- a/") {
|
if line.starts_with("@@") {
|
||||||
removals.push(line.to_string());
|
hunk_offsets.push(removals.len().max(additions.len()));
|
||||||
additions.push(line.to_string());
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(path) = line.strip_prefix("--- a/") {
|
let styled_line = if line.starts_with(' ') {
|
||||||
removals.push(format!("File {path}"));
|
styled_diff_context_line(line, palette)
|
||||||
continue;
|
} else {
|
||||||
}
|
styled_diff_meta_line(split_diff_display_line(line), palette)
|
||||||
|
};
|
||||||
if let Some(path) = line.strip_prefix("+++ b/") {
|
removals.push(styled_line.clone());
|
||||||
additions.push(format!("File {path}"));
|
additions.push(styled_line);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.starts_with("diff --git ") || line.starts_with("@@") {
|
|
||||||
if line.starts_with("@@") {
|
|
||||||
hunk_offsets.push(removals.len().max(additions.len()));
|
|
||||||
}
|
|
||||||
removals.push(line.to_string());
|
|
||||||
additions.push(line.to_string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.starts_with('-') {
|
|
||||||
removals.push(line.to_string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.starts_with('+') {
|
|
||||||
additions.push(line.to_string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush_split_diff_change_block(
|
||||||
|
&mut removals,
|
||||||
|
&mut additions,
|
||||||
|
&mut pending_removals,
|
||||||
|
&mut pending_additions,
|
||||||
|
palette,
|
||||||
|
);
|
||||||
|
|
||||||
WorktreeDiffColumns {
|
WorktreeDiffColumns {
|
||||||
removals: if removals.is_empty() {
|
removals: if removals.is_empty() {
|
||||||
"No removals in this bounded preview.".to_string()
|
Text::from("No removals in this bounded preview.")
|
||||||
} else {
|
} else {
|
||||||
removals.join("\n")
|
Text::from(removals)
|
||||||
},
|
},
|
||||||
additions: if additions.is_empty() {
|
additions: if additions.is_empty() {
|
||||||
"No additions in this bounded preview.".to_string()
|
Text::from("No additions in this bounded preview.")
|
||||||
} else {
|
} else {
|
||||||
additions.join("\n")
|
Text::from(additions)
|
||||||
},
|
},
|
||||||
hunk_offsets,
|
hunk_offsets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_unified_diff_text(patch: &str, palette: ThemePalette) -> Text<'static> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut pending_removals = Vec::new();
|
||||||
|
let mut pending_additions = Vec::new();
|
||||||
|
|
||||||
|
for line in patch.lines() {
|
||||||
|
if is_diff_removal_line(line) {
|
||||||
|
pending_removals.push(line[1..].to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_diff_addition_line(line) {
|
||||||
|
pending_additions.push(line[1..].to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_unified_diff_change_block(
|
||||||
|
&mut lines,
|
||||||
|
&mut pending_removals,
|
||||||
|
&mut pending_additions,
|
||||||
|
palette,
|
||||||
|
);
|
||||||
|
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(if line.starts_with(' ') {
|
||||||
|
styled_diff_context_line(line, palette)
|
||||||
|
} else {
|
||||||
|
styled_diff_meta_line(line, palette)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_unified_diff_change_block(
|
||||||
|
&mut lines,
|
||||||
|
&mut pending_removals,
|
||||||
|
&mut pending_additions,
|
||||||
|
palette,
|
||||||
|
);
|
||||||
|
|
||||||
|
Text::from(lines)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_unified_diff_hunk_offsets(patch: &str) -> Vec<usize> {
|
fn build_unified_diff_hunk_offsets(patch: &str) -> Vec<usize> {
|
||||||
patch
|
let mut offsets = Vec::new();
|
||||||
.lines()
|
let mut rendered_index = 0usize;
|
||||||
.enumerate()
|
let mut pending_removals = 0usize;
|
||||||
.filter_map(|(index, line)| line.starts_with("@@").then_some(index))
|
let mut pending_additions = 0usize;
|
||||||
.collect()
|
|
||||||
|
for line in patch.lines() {
|
||||||
|
if is_diff_removal_line(line) {
|
||||||
|
pending_removals += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_diff_addition_line(line) {
|
||||||
|
pending_additions += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_removals > 0 || pending_additions > 0 {
|
||||||
|
rendered_index += pending_removals + pending_additions;
|
||||||
|
pending_removals = 0;
|
||||||
|
pending_additions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.starts_with("@@") {
|
||||||
|
offsets.push(rendered_index);
|
||||||
|
}
|
||||||
|
rendered_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
offsets
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_split_diff_change_block(
|
||||||
|
removals: &mut Vec<Line<'static>>,
|
||||||
|
additions: &mut Vec<Line<'static>>,
|
||||||
|
pending_removals: &mut Vec<String>,
|
||||||
|
pending_additions: &mut Vec<String>,
|
||||||
|
palette: ThemePalette,
|
||||||
|
) {
|
||||||
|
let pair_count = pending_removals.len().max(pending_additions.len());
|
||||||
|
for index in 0..pair_count {
|
||||||
|
match (pending_removals.get(index), pending_additions.get(index)) {
|
||||||
|
(Some(removal), Some(addition)) => {
|
||||||
|
let (removal_mask, addition_mask) =
|
||||||
|
diff_word_change_masks(removal.as_str(), addition.as_str());
|
||||||
|
removals.push(styled_diff_change_line(
|
||||||
|
'-',
|
||||||
|
removal,
|
||||||
|
&removal_mask,
|
||||||
|
diff_removal_style(palette),
|
||||||
|
diff_removal_word_style(),
|
||||||
|
));
|
||||||
|
additions.push(styled_diff_change_line(
|
||||||
|
'+',
|
||||||
|
addition,
|
||||||
|
&addition_mask,
|
||||||
|
diff_addition_style(palette),
|
||||||
|
diff_addition_word_style(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(Some(removal), None) => {
|
||||||
|
removals.push(styled_diff_change_line(
|
||||||
|
'-',
|
||||||
|
removal,
|
||||||
|
&vec![false; tokenize_diff_words(removal).len()],
|
||||||
|
diff_removal_style(palette),
|
||||||
|
diff_removal_word_style(),
|
||||||
|
));
|
||||||
|
additions.push(Line::from(""));
|
||||||
|
}
|
||||||
|
(None, Some(addition)) => {
|
||||||
|
removals.push(Line::from(""));
|
||||||
|
additions.push(styled_diff_change_line(
|
||||||
|
'+',
|
||||||
|
addition,
|
||||||
|
&vec![false; tokenize_diff_words(addition).len()],
|
||||||
|
diff_addition_style(palette),
|
||||||
|
diff_addition_word_style(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_removals.clear();
|
||||||
|
pending_additions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush_unified_diff_change_block(
|
||||||
|
lines: &mut Vec<Line<'static>>,
|
||||||
|
pending_removals: &mut Vec<String>,
|
||||||
|
pending_additions: &mut Vec<String>,
|
||||||
|
palette: ThemePalette,
|
||||||
|
) {
|
||||||
|
let pair_count = pending_removals.len().max(pending_additions.len());
|
||||||
|
for index in 0..pair_count {
|
||||||
|
match (pending_removals.get(index), pending_additions.get(index)) {
|
||||||
|
(Some(removal), Some(addition)) => {
|
||||||
|
let (removal_mask, addition_mask) =
|
||||||
|
diff_word_change_masks(removal.as_str(), addition.as_str());
|
||||||
|
lines.push(styled_diff_change_line(
|
||||||
|
'-',
|
||||||
|
removal,
|
||||||
|
&removal_mask,
|
||||||
|
diff_removal_style(palette),
|
||||||
|
diff_removal_word_style(),
|
||||||
|
));
|
||||||
|
lines.push(styled_diff_change_line(
|
||||||
|
'+',
|
||||||
|
addition,
|
||||||
|
&addition_mask,
|
||||||
|
diff_addition_style(palette),
|
||||||
|
diff_addition_word_style(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(Some(removal), None) => lines.push(styled_diff_change_line(
|
||||||
|
'-',
|
||||||
|
removal,
|
||||||
|
&vec![false; tokenize_diff_words(removal).len()],
|
||||||
|
diff_removal_style(palette),
|
||||||
|
diff_removal_word_style(),
|
||||||
|
)),
|
||||||
|
(None, Some(addition)) => lines.push(styled_diff_change_line(
|
||||||
|
'+',
|
||||||
|
addition,
|
||||||
|
&vec![false; tokenize_diff_words(addition).len()],
|
||||||
|
diff_addition_style(palette),
|
||||||
|
diff_addition_word_style(),
|
||||||
|
)),
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_removals.clear();
|
||||||
|
pending_additions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_diff_display_line(line: &str) -> String {
|
||||||
|
if line.starts_with("--- ") && !line.starts_with("--- a/") {
|
||||||
|
return line.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(path) = line.strip_prefix("--- a/") {
|
||||||
|
return format!("File {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(path) = line.strip_prefix("+++ b/") {
|
||||||
|
return format!("File {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
line.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_diff_removal_line(line: &str) -> bool {
|
||||||
|
line.starts_with('-') && !line.starts_with("--- ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_diff_addition_line(line: &str) -> bool {
|
||||||
|
line.starts_with('+') && !line.starts_with("+++ ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styled_diff_meta_line(text: impl Into<String>, palette: ThemePalette) -> Line<'static> {
|
||||||
|
Line::from(vec![Span::styled(text.into(), diff_meta_style(palette))])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styled_diff_context_line(text: &str, palette: ThemePalette) -> Line<'static> {
|
||||||
|
Line::from(vec![Span::styled(
|
||||||
|
text.to_string(),
|
||||||
|
diff_context_style(palette),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn styled_diff_change_line(
|
||||||
|
prefix: char,
|
||||||
|
body: &str,
|
||||||
|
change_mask: &[bool],
|
||||||
|
base_style: Style,
|
||||||
|
changed_style: Style,
|
||||||
|
) -> Line<'static> {
|
||||||
|
let tokens = tokenize_diff_words(body);
|
||||||
|
let mut spans = vec![Span::styled(
|
||||||
|
prefix.to_string(),
|
||||||
|
base_style.add_modifier(Modifier::BOLD),
|
||||||
|
)];
|
||||||
|
|
||||||
|
for (index, token) in tokens.into_iter().enumerate() {
|
||||||
|
let style = if change_mask.get(index).copied().unwrap_or(false) {
|
||||||
|
changed_style
|
||||||
|
} else {
|
||||||
|
base_style
|
||||||
|
};
|
||||||
|
spans.push(Span::styled(token, style));
|
||||||
|
}
|
||||||
|
|
||||||
|
Line::from(spans)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize_diff_words(text: &str) -> Vec<String> {
|
||||||
|
if text.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut current = String::new();
|
||||||
|
let mut current_is_whitespace: Option<bool> = None;
|
||||||
|
|
||||||
|
for ch in text.chars() {
|
||||||
|
let is_whitespace = ch.is_whitespace();
|
||||||
|
match current_is_whitespace {
|
||||||
|
Some(state) if state == is_whitespace => current.push(ch),
|
||||||
|
Some(_) => {
|
||||||
|
tokens.push(std::mem::take(&mut current));
|
||||||
|
current.push(ch);
|
||||||
|
current_is_whitespace = Some(is_whitespace);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
current.push(ch);
|
||||||
|
current_is_whitespace = Some(is_whitespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current.is_empty() {
|
||||||
|
tokens.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_word_change_masks(left: &str, right: &str) -> (Vec<bool>, Vec<bool>) {
|
||||||
|
let left_tokens = tokenize_diff_words(left);
|
||||||
|
let right_tokens = tokenize_diff_words(right);
|
||||||
|
let left_len = left_tokens.len();
|
||||||
|
let right_len = right_tokens.len();
|
||||||
|
let mut lcs = vec![vec![0usize; right_len + 1]; left_len + 1];
|
||||||
|
|
||||||
|
for left_index in (0..left_len).rev() {
|
||||||
|
for right_index in (0..right_len).rev() {
|
||||||
|
lcs[left_index][right_index] = if left_tokens[left_index] == right_tokens[right_index] {
|
||||||
|
lcs[left_index + 1][right_index + 1] + 1
|
||||||
|
} else {
|
||||||
|
lcs[left_index + 1][right_index].max(lcs[left_index][right_index + 1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut left_changed = vec![true; left_len];
|
||||||
|
let mut right_changed = vec![true; right_len];
|
||||||
|
let (mut left_index, mut right_index) = (0usize, 0usize);
|
||||||
|
while left_index < left_len && right_index < right_len {
|
||||||
|
if left_tokens[left_index] == right_tokens[right_index] {
|
||||||
|
left_changed[left_index] = false;
|
||||||
|
right_changed[right_index] = false;
|
||||||
|
left_index += 1;
|
||||||
|
right_index += 1;
|
||||||
|
} else if lcs[left_index + 1][right_index] >= lcs[left_index][right_index + 1] {
|
||||||
|
left_index += 1;
|
||||||
|
} else {
|
||||||
|
right_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(left_changed, right_changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_meta_style(palette: ThemePalette) -> Style {
|
||||||
|
Style::default()
|
||||||
|
.fg(palette.accent)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_context_style(palette: ThemePalette) -> Style {
|
||||||
|
Style::default().fg(palette.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_removal_style(palette: ThemePalette) -> Style {
|
||||||
|
let color = match palette.accent {
|
||||||
|
Color::Blue => Color::Red,
|
||||||
|
_ => Color::LightRed,
|
||||||
|
};
|
||||||
|
Style::default().fg(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_addition_style(palette: ThemePalette) -> Style {
|
||||||
|
let color = match palette.accent {
|
||||||
|
Color::Blue => Color::Green,
|
||||||
|
_ => Color::LightGreen,
|
||||||
|
};
|
||||||
|
Style::default().fg(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_removal_word_style() -> Style {
|
||||||
|
Style::default()
|
||||||
|
.bg(Color::Red)
|
||||||
|
.fg(Color::Black)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_addition_word_style() -> Style {
|
||||||
|
Style::default()
|
||||||
|
.bg(Color::Green)
|
||||||
|
.fg(Color::Black)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_state_label(state: &SessionState) -> &'static str {
|
fn session_state_label(state: &SessionState) -> &'static str {
|
||||||
@@ -6262,7 +6616,7 @@ diff --git a/src/lib.rs b/src/lib.rs\n\
|
|||||||
dashboard.selected_diff_summary = Some("1 file changed".to_string());
|
dashboard.selected_diff_summary = Some("1 file changed".to_string());
|
||||||
dashboard.selected_diff_patch = Some(patch.clone());
|
dashboard.selected_diff_patch = Some(patch.clone());
|
||||||
dashboard.selected_diff_hunk_offsets_split =
|
dashboard.selected_diff_hunk_offsets_split =
|
||||||
build_worktree_diff_columns(&patch).hunk_offsets;
|
build_worktree_diff_columns(&patch, dashboard.theme_palette()).hunk_offsets;
|
||||||
dashboard.selected_diff_hunk_offsets_unified = build_unified_diff_hunk_offsets(&patch);
|
dashboard.selected_diff_hunk_offsets_unified = build_unified_diff_hunk_offsets(&patch);
|
||||||
dashboard.toggle_output_mode();
|
dashboard.toggle_output_mode();
|
||||||
|
|
||||||
@@ -6306,7 +6660,8 @@ diff --git a/src/lib.rs b/src/lib.rs\n\
|
|||||||
+second new"
|
+second new"
|
||||||
.to_string();
|
.to_string();
|
||||||
dashboard.selected_diff_patch = Some(patch.clone());
|
dashboard.selected_diff_patch = Some(patch.clone());
|
||||||
let split_offsets = build_worktree_diff_columns(&patch).hunk_offsets;
|
let split_offsets =
|
||||||
|
build_worktree_diff_columns(&patch, dashboard.theme_palette()).hunk_offsets;
|
||||||
dashboard.selected_diff_hunk_offsets_split = split_offsets.clone();
|
dashboard.selected_diff_hunk_offsets_split = split_offsets.clone();
|
||||||
dashboard.selected_diff_hunk_offsets_unified = build_unified_diff_hunk_offsets(&patch);
|
dashboard.selected_diff_hunk_offsets_unified = build_unified_diff_hunk_offsets(&patch);
|
||||||
dashboard.output_mode = OutputMode::WorktreeDiff;
|
dashboard.output_mode = OutputMode::WorktreeDiff;
|
||||||
@@ -6688,13 +7043,74 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
-bye
|
-bye
|
||||||
+hello";
|
+hello";
|
||||||
|
|
||||||
let columns = build_worktree_diff_columns(patch);
|
let palette = test_dashboard(Vec::new(), 0).theme_palette();
|
||||||
assert!(columns.removals.contains("Branch diff vs main"));
|
let columns = build_worktree_diff_columns(patch, palette);
|
||||||
assert!(columns.removals.contains("-old line"));
|
let removals = text_plain_text(&columns.removals);
|
||||||
assert!(columns.removals.contains("-bye"));
|
let additions = text_plain_text(&columns.additions);
|
||||||
assert!(columns.additions.contains("Working tree diff"));
|
assert!(removals.contains("Branch diff vs main"));
|
||||||
assert!(columns.additions.contains("+new line"));
|
assert!(removals.contains("-old line"));
|
||||||
assert!(columns.additions.contains("+hello"));
|
assert!(removals.contains("-bye"));
|
||||||
|
assert!(additions.contains("Working tree diff"));
|
||||||
|
assert!(additions.contains("+new line"));
|
||||||
|
assert!(additions.contains("+hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_diff_highlights_changed_words() {
|
||||||
|
let palette = test_dashboard(Vec::new(), 0).theme_palette();
|
||||||
|
let patch = "\
|
||||||
|
diff --git a/src/lib.rs b/src/lib.rs
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-old line
|
||||||
|
+new line";
|
||||||
|
|
||||||
|
let columns = build_worktree_diff_columns(patch, palette);
|
||||||
|
let removal = columns
|
||||||
|
.removals
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line_plain_text(line) == "-old line")
|
||||||
|
.expect("removal line");
|
||||||
|
let addition = columns
|
||||||
|
.additions
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line_plain_text(line) == "+new line")
|
||||||
|
.expect("addition line");
|
||||||
|
|
||||||
|
assert_eq!(removal.spans[1].content.as_ref(), "old");
|
||||||
|
assert_eq!(removal.spans[1].style, diff_removal_word_style());
|
||||||
|
assert_eq!(removal.spans[2].content.as_ref(), " ");
|
||||||
|
assert_eq!(removal.spans[2].style, diff_removal_style(palette));
|
||||||
|
assert_eq!(addition.spans[1].content.as_ref(), "new");
|
||||||
|
assert_eq!(addition.spans[1].style, diff_addition_word_style());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unified_diff_highlights_changed_words() {
|
||||||
|
let palette = test_dashboard(Vec::new(), 0).theme_palette();
|
||||||
|
let patch = "\
|
||||||
|
diff --git a/src/lib.rs b/src/lib.rs
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-old line
|
||||||
|
+new line";
|
||||||
|
|
||||||
|
let text = build_unified_diff_text(patch, palette);
|
||||||
|
let removal = text
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line_plain_text(line) == "-old line")
|
||||||
|
.expect("removal line");
|
||||||
|
let addition = text
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line_plain_text(line) == "+new line")
|
||||||
|
.expect("addition line");
|
||||||
|
|
||||||
|
assert_eq!(removal.spans[1].content.as_ref(), "old");
|
||||||
|
assert_eq!(removal.spans[1].style, diff_removal_word_style());
|
||||||
|
assert_eq!(addition.spans[1].content.as_ref(), "new");
|
||||||
|
assert_eq!(addition.spans[1].style, diff_addition_word_style());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -9859,6 +10275,21 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn line_plain_text(line: &Line<'_>) -> String {
|
||||||
|
line.spans
|
||||||
|
.iter()
|
||||||
|
.map(|span| span.content.as_ref())
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_plain_text(text: &Text<'_>) -> String {
|
||||||
|
text.lines
|
||||||
|
.iter()
|
||||||
|
.map(line_plain_text)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn test_dashboard(sessions: Vec<Session>, selected_session: usize) -> Dashboard {
|
fn test_dashboard(sessions: Vec<Session>, selected_session: usize) -> Dashboard {
|
||||||
let selected_session = selected_session.min(sessions.len().saturating_sub(1));
|
let selected_session = selected_session.min(sessions.len().saturating_sub(1));
|
||||||
let cfg = Config::default();
|
let cfg = Config::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user