mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
Merge 5cf6719003 into 8619c07f3f
This commit is contained in:
commit
26c58b5c55
11 changed files with 793 additions and 37 deletions
|
|
@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Changed
|
### Changed
|
||||||
* use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting
|
* use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting
|
||||||
* open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805))
|
* open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805))
|
||||||
|
* change diff mode toggle shortcut from `Alt+p` to `m`
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
* crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895))
|
* crash when opening submodule ([#2895](https://github.com/gitui-org/gitui/issues/2895))
|
||||||
* when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748))
|
* when staging the last file in a directory, the first item after the directory is no longer skipped [[@Tillerino](https://github.com/Tillerino)] ([#2748](https://github.com/gitui-org/gitui/issues/2748))
|
||||||
|
* fixed duplicated "Toggle Diff Mode" in help message
|
||||||
|
|
||||||
## [0.28.1] - 2026-03-21
|
## [0.28.1] - 2026-03-21
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,27 @@ use asyncgit::{
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::{
|
||||||
|
Constraint, Direction as RatatuiDirection, Layout, Rect,
|
||||||
|
},
|
||||||
symbols,
|
symbols,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{borrow::Cow, cell::Cell, cmp, path::Path};
|
use std::{borrow::Cow, cell::Cell, cmp, path::Path};
|
||||||
|
|
||||||
|
/// Diff display mode
|
||||||
|
#[derive(
|
||||||
|
Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub enum DiffMode {
|
||||||
|
#[default]
|
||||||
|
Unified,
|
||||||
|
SideBySide,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Current {
|
struct Current {
|
||||||
path: String,
|
path: String,
|
||||||
|
|
@ -101,6 +114,24 @@ impl Selection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single line in side-by-side diff view
|
||||||
|
struct SideBySideLine {
|
||||||
|
left_content: String,
|
||||||
|
left_line_num: Option<u32>,
|
||||||
|
right_content: String,
|
||||||
|
right_line_num: Option<u32>,
|
||||||
|
left_type: DiffLineType,
|
||||||
|
right_type: DiffLineType,
|
||||||
|
/// Global line index for selection tracking
|
||||||
|
global_line_idx: usize,
|
||||||
|
/// Index of the hunk this line belongs to
|
||||||
|
hunk_idx: usize,
|
||||||
|
/// Whether this is the first line of a hunk
|
||||||
|
is_hunk_start: bool,
|
||||||
|
/// Whether this is the last line of a hunk
|
||||||
|
is_hunk_end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
pub struct DiffComponent {
|
pub struct DiffComponent {
|
||||||
repo: RepoPathRef,
|
repo: RepoPathRef,
|
||||||
|
|
@ -119,6 +150,7 @@ pub struct DiffComponent {
|
||||||
key_config: SharedKeyConfig,
|
key_config: SharedKeyConfig,
|
||||||
is_immutable: bool,
|
is_immutable: bool,
|
||||||
options: SharedOptions,
|
options: SharedOptions,
|
||||||
|
diff_mode: DiffMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffComponent {
|
impl DiffComponent {
|
||||||
|
|
@ -141,6 +173,7 @@ impl DiffComponent {
|
||||||
is_immutable,
|
is_immutable,
|
||||||
repo: env.repo.clone(),
|
repo: env.repo.clone(),
|
||||||
options: env.options.clone(),
|
options: env.options.clone(),
|
||||||
|
diff_mode: env.options.borrow().diff_mode(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
///
|
///
|
||||||
|
|
@ -223,11 +256,19 @@ impl DiffComponent {
|
||||||
|
|
||||||
fn move_selection(&mut self, move_type: ScrollType) {
|
fn move_selection(&mut self, move_type: ScrollType) {
|
||||||
if let Some(diff) = &self.diff {
|
if let Some(diff) = &self.diff {
|
||||||
let max = diff.lines.saturating_sub(1);
|
// In side-by-side mode, display lines differ from diff.lines
|
||||||
|
// because Delete+Add pairs are shown as one line
|
||||||
|
let max = if self.diff_mode == DiffMode::SideBySide {
|
||||||
|
self.side_by_side_lines_count().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
diff.lines.saturating_sub(1)
|
||||||
|
};
|
||||||
|
|
||||||
let new_start = match move_type {
|
let new_start = match move_type {
|
||||||
ScrollType::Down => {
|
ScrollType::Down => {
|
||||||
self.selection.get_bottom().saturating_add(1)
|
let next =
|
||||||
|
self.selection.get_bottom().saturating_add(1);
|
||||||
|
cmp::min(next, max)
|
||||||
}
|
}
|
||||||
ScrollType::Up => {
|
ScrollType::Up => {
|
||||||
self.selection.get_top().saturating_sub(1)
|
self.selection.get_top().saturating_sub(1)
|
||||||
|
|
@ -235,10 +276,14 @@ impl DiffComponent {
|
||||||
ScrollType::Home => 0,
|
ScrollType::Home => 0,
|
||||||
ScrollType::End => max,
|
ScrollType::End => max,
|
||||||
ScrollType::PageDown => {
|
ScrollType::PageDown => {
|
||||||
self.selection.get_bottom().saturating_add(
|
let next =
|
||||||
self.current_size.get().1.saturating_sub(1)
|
self.selection.get_bottom().saturating_add(
|
||||||
as usize,
|
self.current_size
|
||||||
)
|
.get()
|
||||||
|
.1
|
||||||
|
.saturating_sub(1) as usize,
|
||||||
|
);
|
||||||
|
cmp::min(next, max)
|
||||||
}
|
}
|
||||||
ScrollType::PageUp => {
|
ScrollType::PageUp => {
|
||||||
self.selection.get_top().saturating_sub(
|
self.selection.get_top().saturating_sub(
|
||||||
|
|
@ -254,11 +299,20 @@ impl DiffComponent {
|
||||||
|
|
||||||
fn update_selection(&mut self, new_start: usize) {
|
fn update_selection(&mut self, new_start: usize) {
|
||||||
if let Some(diff) = &self.diff {
|
if let Some(diff) = &self.diff {
|
||||||
let max = diff.lines.saturating_sub(1);
|
// In side-by-side mode, display lines differ from diff.lines
|
||||||
|
let max = if self.diff_mode == DiffMode::SideBySide {
|
||||||
|
self.side_by_side_lines_count().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
diff.lines.saturating_sub(1)
|
||||||
|
};
|
||||||
let new_start = cmp::min(max, new_start);
|
let new_start = cmp::min(max, new_start);
|
||||||
self.selection = Selection::Single(new_start);
|
self.selection = Selection::Single(new_start);
|
||||||
self.selected_hunk =
|
self.selected_hunk =
|
||||||
Self::find_selected_hunk(diff, new_start);
|
Self::find_selected_hunk_for_display_line(
|
||||||
|
diff,
|
||||||
|
new_start,
|
||||||
|
self.diff_mode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,9 +320,52 @@ impl DiffComponent {
|
||||||
self.diff.as_ref().map_or(0, |diff| diff.lines)
|
self.diff.as_ref().map_or(0, |diff| diff.lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the actual display line count for side-by-side mode
|
||||||
|
/// In side-by-side mode, Delete+Add pairs are shown as one line
|
||||||
|
fn side_by_side_lines_count(&self) -> usize {
|
||||||
|
let Some(diff) = &self.diff else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if diff.hunks.is_empty() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut count = 0_usize;
|
||||||
|
for hunk in &diff.hunks {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < hunk.lines.len() {
|
||||||
|
let line = &hunk.lines[i];
|
||||||
|
if line.line_type == DiffLineType::Delete {
|
||||||
|
// Check if next line is Add (they will be paired)
|
||||||
|
if let Some(next) = hunk.lines.get(i + 1) {
|
||||||
|
if next.line_type == DiffLineType::Add {
|
||||||
|
i += 1; // Skip the Add line in counting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
fn max_scroll_right(&self) -> usize {
|
fn max_scroll_right(&self) -> usize {
|
||||||
self.longest_line
|
let available_width: usize = if self.diff_mode
|
||||||
.saturating_sub(self.current_size.get().0.into())
|
== DiffMode::SideBySide
|
||||||
|
{
|
||||||
|
// In side-by-side mode, each panel's content width:
|
||||||
|
// chunks[0].width ≈ r.width / 2
|
||||||
|
// content width = chunks[0].width - 7 (border + marker + line_num + space)
|
||||||
|
// current_width = r.width - 2
|
||||||
|
// So: r.width / 2 - 7 ≈ current_width / 2 - 6
|
||||||
|
(self.current_size.get().0 / 2).saturating_sub(6).into()
|
||||||
|
} else {
|
||||||
|
self.current_size.get().0.into()
|
||||||
|
};
|
||||||
|
self.longest_line.saturating_sub(available_width)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modify_selection(&mut self, direction: Direction) {
|
fn modify_selection(&mut self, direction: Direction) {
|
||||||
|
|
@ -328,6 +425,50 @@ impl DiffComponent {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the hunk index for a display line (accounting for side-by-side pairing)
|
||||||
|
fn find_selected_hunk_for_display_line(
|
||||||
|
diff: &FileDiff,
|
||||||
|
display_line_selected: usize,
|
||||||
|
diff_mode: DiffMode,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if diff_mode == DiffMode::Unified {
|
||||||
|
return Self::find_selected_hunk(
|
||||||
|
diff,
|
||||||
|
display_line_selected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For side-by-side mode, count display lines (where Delete+Add pairs count as 1)
|
||||||
|
let mut display_cursor = 0_usize;
|
||||||
|
for (i, hunk) in diff.hunks.iter().enumerate() {
|
||||||
|
let mut j = 0;
|
||||||
|
let hunk_start = display_cursor;
|
||||||
|
while j < hunk.lines.len() {
|
||||||
|
let line = &hunk.lines[j];
|
||||||
|
if display_cursor == display_line_selected {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
if line.line_type == DiffLineType::Delete {
|
||||||
|
if let Some(next) = hunk.lines.get(j + 1) {
|
||||||
|
if next.line_type == DiffLineType::Add {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display_cursor += 1;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
// Check if this is the last line of the hunk
|
||||||
|
if display_line_selected >= hunk_start
|
||||||
|
&& display_line_selected < display_cursor
|
||||||
|
{
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn get_text(&self, width: u16, height: u16) -> Vec<Line<'_>> {
|
fn get_text(&self, width: u16, height: u16) -> Vec<Line<'_>> {
|
||||||
if let Some(diff) = &self.diff {
|
if let Some(diff) = &self.diff {
|
||||||
return if diff.hunks.is_empty() {
|
return if diff.hunks.is_empty() {
|
||||||
|
|
@ -677,6 +818,485 @@ impl DiffComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle between unified and side-by-side diff mode
|
||||||
|
pub fn toggle_diff_mode(&mut self) {
|
||||||
|
self.diff_mode = match self.diff_mode {
|
||||||
|
DiffMode::Unified => DiffMode::SideBySide,
|
||||||
|
DiffMode::SideBySide => DiffMode::Unified,
|
||||||
|
};
|
||||||
|
self.options.borrow_mut().set_diff_mode(self.diff_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_side_by_side_lines(
|
||||||
|
&self,
|
||||||
|
height: u16,
|
||||||
|
) -> Vec<SideBySideLine> {
|
||||||
|
let Some(diff) = &self.diff else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
if diff.hunks.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let min = self.vertical_scroll.get_top();
|
||||||
|
let max = min + height as usize;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
// Use display_cursor to track display line index (where Delete+Add pairs count as 1)
|
||||||
|
let mut display_cursor = 0_usize;
|
||||||
|
|
||||||
|
for (hunk_idx, hunk) in diff.hunks.iter().enumerate() {
|
||||||
|
// Calculate display line range for this hunk
|
||||||
|
let hunk_display_start = display_cursor;
|
||||||
|
let mut hunk_display_len = 0_usize;
|
||||||
|
{
|
||||||
|
let mut j = 0;
|
||||||
|
while j < hunk.lines.len() {
|
||||||
|
let line = &hunk.lines[j];
|
||||||
|
if line.line_type == DiffLineType::Delete {
|
||||||
|
if let Some(next) = hunk.lines.get(j + 1) {
|
||||||
|
if next.line_type == DiffLineType::Add {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hunk_display_len += 1;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let hunk_display_end =
|
||||||
|
hunk_display_start + hunk_display_len;
|
||||||
|
|
||||||
|
if Self::hunk_visible(
|
||||||
|
hunk_display_start,
|
||||||
|
hunk_display_end,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
) {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < hunk.lines.len() {
|
||||||
|
let line = &hunk.lines[i];
|
||||||
|
let global_display_idx = display_cursor;
|
||||||
|
let is_hunk_start = i == 0;
|
||||||
|
// Calculate if this is the last display line of the hunk
|
||||||
|
let is_hunk_end = {
|
||||||
|
let mut remaining = hunk.lines.len() - i;
|
||||||
|
let next = hunk.lines.get(i + 1);
|
||||||
|
if line.line_type == DiffLineType::Delete
|
||||||
|
&& next.is_some_and(|n| {
|
||||||
|
n.line_type == DiffLineType::Add
|
||||||
|
}) {
|
||||||
|
remaining -= 1;
|
||||||
|
}
|
||||||
|
remaining == 1
|
||||||
|
};
|
||||||
|
|
||||||
|
if global_display_idx >= min
|
||||||
|
&& global_display_idx <= max
|
||||||
|
{
|
||||||
|
match line.line_type {
|
||||||
|
DiffLineType::Delete => {
|
||||||
|
// Look ahead for a matching add line
|
||||||
|
let next_line = hunk.lines.get(i + 1);
|
||||||
|
let (
|
||||||
|
right_content,
|
||||||
|
right_num,
|
||||||
|
right_type,
|
||||||
|
) = if let Some(next) = next_line {
|
||||||
|
if next.line_type
|
||||||
|
== DiffLineType::Add
|
||||||
|
{
|
||||||
|
i += 1;
|
||||||
|
(
|
||||||
|
tabs_to_spaces(
|
||||||
|
next.content
|
||||||
|
.as_ref()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
next.position.new_lineno,
|
||||||
|
DiffLineType::Add,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
String::new(),
|
||||||
|
None,
|
||||||
|
DiffLineType::None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
String::new(),
|
||||||
|
None,
|
||||||
|
DiffLineType::None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(SideBySideLine {
|
||||||
|
left_content: tabs_to_spaces(
|
||||||
|
line.content
|
||||||
|
.as_ref()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
left_line_num: line
|
||||||
|
.position
|
||||||
|
.old_lineno,
|
||||||
|
right_content,
|
||||||
|
right_line_num: right_num,
|
||||||
|
left_type: DiffLineType::Delete,
|
||||||
|
right_type,
|
||||||
|
global_line_idx:
|
||||||
|
global_display_idx,
|
||||||
|
hunk_idx,
|
||||||
|
is_hunk_start,
|
||||||
|
is_hunk_end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DiffLineType::Add => {
|
||||||
|
// Add line not paired with a delete
|
||||||
|
result.push(SideBySideLine {
|
||||||
|
left_content: String::new(),
|
||||||
|
left_line_num: None,
|
||||||
|
right_content: tabs_to_spaces(
|
||||||
|
line.content
|
||||||
|
.as_ref()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
right_line_num: line
|
||||||
|
.position
|
||||||
|
.new_lineno,
|
||||||
|
left_type: DiffLineType::None,
|
||||||
|
right_type: DiffLineType::Add,
|
||||||
|
global_line_idx:
|
||||||
|
global_display_idx,
|
||||||
|
hunk_idx,
|
||||||
|
is_hunk_start,
|
||||||
|
is_hunk_end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DiffLineType::Header => {
|
||||||
|
let header_content = tabs_to_spaces(
|
||||||
|
line.content.as_ref().to_string(),
|
||||||
|
);
|
||||||
|
result.push(SideBySideLine {
|
||||||
|
left_content: header_content,
|
||||||
|
left_line_num: None,
|
||||||
|
right_content: String::new(),
|
||||||
|
right_line_num: None,
|
||||||
|
left_type: DiffLineType::Header,
|
||||||
|
right_type: DiffLineType::Header,
|
||||||
|
global_line_idx:
|
||||||
|
global_display_idx,
|
||||||
|
hunk_idx,
|
||||||
|
is_hunk_start,
|
||||||
|
is_hunk_end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DiffLineType::None => {
|
||||||
|
// Context line - appears in both columns
|
||||||
|
result.push(SideBySideLine {
|
||||||
|
left_content: tabs_to_spaces(
|
||||||
|
line.content
|
||||||
|
.as_ref()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
left_line_num: line
|
||||||
|
.position
|
||||||
|
.old_lineno,
|
||||||
|
right_content: tabs_to_spaces(
|
||||||
|
line.content
|
||||||
|
.as_ref()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
right_line_num: line
|
||||||
|
.position
|
||||||
|
.new_lineno,
|
||||||
|
left_type: DiffLineType::None,
|
||||||
|
right_type: DiffLineType::None,
|
||||||
|
global_line_idx:
|
||||||
|
global_display_idx,
|
||||||
|
hunk_idx,
|
||||||
|
is_hunk_start,
|
||||||
|
is_hunk_end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment display cursor for each display line
|
||||||
|
display_cursor += 1;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Skip this hunk's display lines
|
||||||
|
display_cursor += hunk_display_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_side_by_side(
|
||||||
|
&self,
|
||||||
|
f: &mut Frame,
|
||||||
|
r: Rect,
|
||||||
|
title: &str,
|
||||||
|
height: u16,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Split area into left and right columns
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(RatatuiDirection::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(r);
|
||||||
|
|
||||||
|
// Calculate available width for content (subtract borders, marker, line number, space)
|
||||||
|
// Each panel has: 1 border + 1 marker + 4 line num + 1 space = 7 chars overhead
|
||||||
|
let panel_width = chunks[0].width.saturating_sub(7) as usize;
|
||||||
|
|
||||||
|
let lines = self.get_side_by_side_lines(height);
|
||||||
|
let scrolled_right = self.horizontal_scroll.get_right();
|
||||||
|
let selected_hunk = self.selected_hunk;
|
||||||
|
|
||||||
|
// Get current selection index
|
||||||
|
let current_selection = self.selection.get_end();
|
||||||
|
|
||||||
|
// Build left column text with selection highlighting
|
||||||
|
let left_txt: Vec<Line> = lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| {
|
||||||
|
let selected = self.focused()
|
||||||
|
&& line.global_line_idx == current_selection;
|
||||||
|
let hunk_selected = self.focused()
|
||||||
|
&& selected_hunk
|
||||||
|
.is_some_and(|h| h == line.hunk_idx);
|
||||||
|
let left_content =
|
||||||
|
trim_offset(&line.left_content, scrolled_right);
|
||||||
|
let line_num_str = line
|
||||||
|
.left_line_num
|
||||||
|
.map_or(String::from(" "), |n| {
|
||||||
|
format!("{n:4}")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get hunk marker style
|
||||||
|
let marker_style =
|
||||||
|
self.theme.diff_hunk_marker(hunk_selected);
|
||||||
|
let marker = if line.is_hunk_end {
|
||||||
|
symbols::line::BOTTOM_LEFT
|
||||||
|
} else if line.is_hunk_start {
|
||||||
|
symbols::line::TOP_LEFT
|
||||||
|
} else {
|
||||||
|
symbols::line::VERTICAL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pad content to fill width when selected
|
||||||
|
let content = if selected {
|
||||||
|
format!("{:w$}\n", left_content, w = panel_width)
|
||||||
|
} else {
|
||||||
|
format!("{left_content}\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
// For lines where left side is empty (e.g., Add lines without Delete pair),
|
||||||
|
// still apply selection highlight to maintain visual consistency.
|
||||||
|
// Show line_break symbol (¶) for empty Add/Delete lines, same as unified mode.
|
||||||
|
if line.left_content.is_empty() {
|
||||||
|
// Show line_break symbol for empty Add/Delete lines
|
||||||
|
let display_content =
|
||||||
|
if line.left_type != DiffLineType::None {
|
||||||
|
self.theme.line_break()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let content = if selected {
|
||||||
|
format!(
|
||||||
|
"{:w$}\n",
|
||||||
|
display_content,
|
||||||
|
w = panel_width
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{display_content}\n")
|
||||||
|
};
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(Cow::from(marker), marker_style),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(line_num_str),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
// Gap - never highlighted
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(" "),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(content),
|
||||||
|
self.theme
|
||||||
|
.diff_line(line.left_type, selected),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(Cow::from(marker), marker_style),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(line_num_str),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
// Gap between line number and content - never highlighted
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(" "),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(content),
|
||||||
|
self.theme
|
||||||
|
.diff_line(line.left_type, selected),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Build right column text with selection highlighting
|
||||||
|
let right_txt: Vec<Line> = lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| {
|
||||||
|
let selected = self.focused()
|
||||||
|
&& line.global_line_idx == current_selection;
|
||||||
|
let hunk_selected = self.focused()
|
||||||
|
&& selected_hunk
|
||||||
|
.is_some_and(|h| h == line.hunk_idx);
|
||||||
|
let right_content =
|
||||||
|
trim_offset(&line.right_content, scrolled_right);
|
||||||
|
let line_num_str = line
|
||||||
|
.right_line_num
|
||||||
|
.map_or(String::from(" "), |n| {
|
||||||
|
format!("{n:4}")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get hunk marker style
|
||||||
|
let marker_style =
|
||||||
|
self.theme.diff_hunk_marker(hunk_selected);
|
||||||
|
let marker = if line.is_hunk_end {
|
||||||
|
symbols::line::BOTTOM_LEFT
|
||||||
|
} else if line.is_hunk_start {
|
||||||
|
symbols::line::TOP_LEFT
|
||||||
|
} else {
|
||||||
|
symbols::line::VERTICAL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pad content to fill width when selected
|
||||||
|
let content = if selected {
|
||||||
|
format!("{:w$}\n", right_content, w = panel_width)
|
||||||
|
} else {
|
||||||
|
format!("{right_content}\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
// For lines where right side is empty (Header or paired Delete),
|
||||||
|
// still apply selection highlight to maintain visual consistency.
|
||||||
|
// Show line_break symbol (¶) for empty Add/Delete lines, same as unified mode.
|
||||||
|
if line.right_type == DiffLineType::Header
|
||||||
|
|| line.right_content.is_empty()
|
||||||
|
{
|
||||||
|
// Show line_break symbol for empty Add/Delete lines (but not Header)
|
||||||
|
let display_content = if line.right_type
|
||||||
|
!= DiffLineType::None
|
||||||
|
&& line.right_type != DiffLineType::Header
|
||||||
|
{
|
||||||
|
self.theme.line_break()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let filler = if selected {
|
||||||
|
format!(
|
||||||
|
"{:w$}\n",
|
||||||
|
display_content,
|
||||||
|
w = panel_width
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{display_content}\n")
|
||||||
|
};
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(Cow::from(marker), marker_style),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(line_num_str),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
// Gap - never highlighted
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(" "),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(filler),
|
||||||
|
self.theme
|
||||||
|
.diff_line(line.right_type, selected),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(Cow::from(marker), marker_style),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(line_num_str),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
// Gap between line number and content - never highlighted
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(" "),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
Cow::from(content),
|
||||||
|
self.theme
|
||||||
|
.diff_line(line.right_type, selected),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Draw left column
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(left_txt).block(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
format!("{title} [Old]"),
|
||||||
|
self.theme.title(self.focused()),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(self.theme.block(self.focused())),
|
||||||
|
),
|
||||||
|
chunks[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw right column
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(right_txt).block(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
"[New]",
|
||||||
|
self.theme.title(self.focused()),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(self.theme.block(self.focused())),
|
||||||
|
),
|
||||||
|
chunks[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.focused() {
|
||||||
|
self.vertical_scroll.draw(f, r, &self.theme);
|
||||||
|
|
||||||
|
if self.max_scroll_right() > 0 {
|
||||||
|
self.horizontal_scroll.draw(f, r, &self.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
const fn is_stage(&self) -> bool {
|
const fn is_stage(&self) -> bool {
|
||||||
self.current.is_stage
|
self.current.is_stage
|
||||||
}
|
}
|
||||||
|
|
@ -692,15 +1312,32 @@ impl DrawableComponent for DiffComponent {
|
||||||
let current_width = self.current_size.get().0;
|
let current_width = self.current_size.get().0;
|
||||||
let current_height = self.current_size.get().1;
|
let current_height = self.current_size.get().1;
|
||||||
|
|
||||||
|
// Use display line count for side-by-side mode
|
||||||
|
let lines_count = if self.diff_mode == DiffMode::SideBySide {
|
||||||
|
self.side_by_side_lines_count()
|
||||||
|
} else {
|
||||||
|
self.lines_count()
|
||||||
|
};
|
||||||
|
|
||||||
self.vertical_scroll.update(
|
self.vertical_scroll.update(
|
||||||
self.selection.get_end(),
|
self.selection.get_end(),
|
||||||
self.lines_count(),
|
lines_count,
|
||||||
usize::from(current_height),
|
usize::from(current_height),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// In side-by-side mode, each panel content width is smaller
|
||||||
|
// chunks[0].width ≈ r.width / 2, content = chunks[0].width - 7
|
||||||
|
// ≈ current_width / 2 - 6
|
||||||
|
let panel_content_width: usize =
|
||||||
|
if self.diff_mode == DiffMode::SideBySide {
|
||||||
|
(current_width / 2).saturating_sub(6).into()
|
||||||
|
} else {
|
||||||
|
current_width.into()
|
||||||
|
};
|
||||||
|
|
||||||
self.horizontal_scroll.update_no_selection(
|
self.horizontal_scroll.update_no_selection(
|
||||||
self.longest_line,
|
self.longest_line,
|
||||||
current_width.into(),
|
panel_content_width,
|
||||||
);
|
);
|
||||||
|
|
||||||
let title = format!(
|
let title = format!(
|
||||||
|
|
@ -709,33 +1346,41 @@ impl DrawableComponent for DiffComponent {
|
||||||
self.current.path
|
self.current.path
|
||||||
);
|
);
|
||||||
|
|
||||||
let txt = if self.pending {
|
if self.diff_mode == DiffMode::SideBySide && !self.pending {
|
||||||
vec![Line::from(vec![Span::styled(
|
self.draw_side_by_side(f, r, &title, current_height)?;
|
||||||
Cow::from(strings::loading_text(&self.key_config)),
|
|
||||||
self.theme.text(false, false),
|
|
||||||
)])]
|
|
||||||
} else {
|
} else {
|
||||||
self.get_text(r.width, current_height)
|
let txt = if self.pending {
|
||||||
};
|
vec![Line::from(vec![Span::styled(
|
||||||
|
Cow::from(strings::loading_text(
|
||||||
|
&self.key_config,
|
||||||
|
)),
|
||||||
|
self.theme.text(false, false),
|
||||||
|
)])]
|
||||||
|
} else {
|
||||||
|
self.get_text(r.width, current_height)
|
||||||
|
};
|
||||||
|
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(txt).block(
|
Paragraph::new(txt).block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(Span::styled(
|
.title(Span::styled(
|
||||||
title.as_str(),
|
title.as_str(),
|
||||||
self.theme.title(self.focused()),
|
self.theme.title(self.focused()),
|
||||||
))
|
))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(self.theme.block(self.focused())),
|
.border_style(
|
||||||
),
|
self.theme.block(self.focused()),
|
||||||
r,
|
),
|
||||||
);
|
),
|
||||||
|
r,
|
||||||
|
);
|
||||||
|
|
||||||
if self.focused() {
|
if self.focused() {
|
||||||
self.vertical_scroll.draw(f, r, &self.theme);
|
self.vertical_scroll.draw(f, r, &self.theme);
|
||||||
|
|
||||||
if self.max_scroll_right() > 0 {
|
if self.max_scroll_right() > 0 {
|
||||||
self.horizontal_scroll.draw(f, r, &self.theme);
|
self.horizontal_scroll.draw(f, r, &self.theme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -943,6 +1588,12 @@ impl Component for DiffComponent {
|
||||||
} else if key_match(e, self.key_config.keys.copy) {
|
} else if key_match(e, self.key_config.keys.copy) {
|
||||||
self.copy_selection();
|
self.copy_selection();
|
||||||
Ok(EventState::Consumed)
|
Ok(EventState::Consumed)
|
||||||
|
} else if key_match(
|
||||||
|
e,
|
||||||
|
self.key_config.keys.diff_mode_toggle,
|
||||||
|
) {
|
||||||
|
self.toggle_diff_mode();
|
||||||
|
Ok(EventState::Consumed)
|
||||||
} else {
|
} else {
|
||||||
Ok(EventState::NotConsumed)
|
Ok(EventState::NotConsumed)
|
||||||
};
|
};
|
||||||
|
|
@ -1059,4 +1710,35 @@ mod tests {
|
||||||
if path == "src/main.rs"
|
if path == "src/main.rs"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_commands_no_longer_contains_toggle_diff() {
|
||||||
|
let env = Environment::test_env();
|
||||||
|
let diff = DiffComponent::new(&env, false);
|
||||||
|
let mut cmds = Vec::new();
|
||||||
|
diff.commands(&mut cmds, true);
|
||||||
|
|
||||||
|
let contains_toggle = cmds.iter().any(|c| {
|
||||||
|
c.text.name
|
||||||
|
== strings::commands::diff_toggle_mode(&env.key_config)
|
||||||
|
.name
|
||||||
|
});
|
||||||
|
assert!(!contains_toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_diff_mode_toggle_event() {
|
||||||
|
let env = Environment::test_env();
|
||||||
|
let mut diff = DiffComponent::new(&env, false);
|
||||||
|
diff.focus(true);
|
||||||
|
|
||||||
|
let event = Event::Key(KeyEvent::from(
|
||||||
|
&env.key_config.keys.diff_mode_toggle,
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
diff.event(&event).unwrap(),
|
||||||
|
EventState::Consumed
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ pub use command::{CommandInfo, CommandText};
|
||||||
pub use commit_details::CommitDetailsComponent;
|
pub use commit_details::CommitDetailsComponent;
|
||||||
pub use commitlist::CommitList;
|
pub use commitlist::CommitList;
|
||||||
pub use cred::CredComponent;
|
pub use cred::CredComponent;
|
||||||
pub use diff::DiffComponent;
|
pub use diff::{DiffComponent, DiffMode};
|
||||||
pub use revision_files::RevisionFilesComponent;
|
pub use revision_files::RevisionFilesComponent;
|
||||||
pub use syntax_text::SyntaxTextComponent;
|
pub use syntax_text::SyntaxTextComponent;
|
||||||
pub use textinput::{InputType, TextInputComponent};
|
pub use textinput::{InputType, TextInputComponent};
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ pub struct KeysList {
|
||||||
pub commit: GituiKeyEvent,
|
pub commit: GituiKeyEvent,
|
||||||
pub newline: GituiKeyEvent,
|
pub newline: GituiKeyEvent,
|
||||||
pub goto_line: GituiKeyEvent,
|
pub goto_line: GituiKeyEvent,
|
||||||
|
pub diff_mode_toggle: GituiKeyEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|
@ -227,6 +228,7 @@ impl Default for KeysList {
|
||||||
commit: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
|
commit: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
|
||||||
newline: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
newline: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
||||||
goto_line: GituiKeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT),
|
goto_line: GituiKeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT),
|
||||||
|
diff_mode_toggle: GituiKeyEvent::new(KeyCode::Char('m'), KeyModifiers::empty()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::components::DiffMode;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use asyncgit::sync::{
|
use asyncgit::sync::{
|
||||||
diff::DiffOptions, repo_dir, RepoPathRef,
|
diff::DiffOptions, repo_dir, RepoPathRef,
|
||||||
|
|
@ -22,6 +23,7 @@ struct OptionsData {
|
||||||
pub diff: DiffOptions,
|
pub diff: DiffOptions,
|
||||||
pub status_show_untracked: Option<ShowUntrackedFilesConfig>,
|
pub status_show_untracked: Option<ShowUntrackedFilesConfig>,
|
||||||
pub commit_msgs: Vec<String>,
|
pub commit_msgs: Vec<String>,
|
||||||
|
pub diff_mode: DiffMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMIT_MSG_HISTORY_LENGTH: usize = 20;
|
const COMMIT_MSG_HISTORY_LENGTH: usize = 20;
|
||||||
|
|
@ -107,6 +109,15 @@ impl Options {
|
||||||
self.save();
|
self.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn diff_mode(&self) -> DiffMode {
|
||||||
|
self.data.diff_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_diff_mode(&mut self, mode: DiffMode) {
|
||||||
|
self.data.diff_mode = mode;
|
||||||
|
self.save();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_commit_msg(&mut self, msg: &str) {
|
pub fn add_commit_msg(&mut self, msg: &str) {
|
||||||
self.data.commit_msgs.push(msg.to_owned());
|
self.data.commit_msgs.push(msg.to_owned());
|
||||||
while self.data.commit_msgs.len() > COMMIT_MSG_HISTORY_LENGTH
|
while self.data.commit_msgs.len() > COMMIT_MSG_HISTORY_LENGTH
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,12 @@ impl Component for CompareCommitsPopup {
|
||||||
!self.diff.focused() || force_all,
|
!self.diff.focused() || force_all,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::diff_toggle_mode(&self.key_config),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
out.push(CommandInfo::new(
|
out.push(CommandInfo::new(
|
||||||
strings::commands::diff_focus_left(&self.key_config),
|
strings::commands::diff_focus_left(&self.key_config),
|
||||||
true,
|
true,
|
||||||
|
|
@ -134,6 +140,11 @@ impl Component for CompareCommitsPopup {
|
||||||
} else if key_match(e, self.key_config.keys.move_left)
|
} else if key_match(e, self.key_config.keys.move_left)
|
||||||
{
|
{
|
||||||
self.hide_stacked(false);
|
self.hide_stacked(false);
|
||||||
|
} else if key_match(
|
||||||
|
e,
|
||||||
|
self.key_config.keys.diff_mode_toggle,
|
||||||
|
) {
|
||||||
|
self.diff.toggle_diff_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(EventState::Consumed);
|
return Ok(EventState::Consumed);
|
||||||
|
|
|
||||||
|
|
@ -569,6 +569,11 @@ impl Component for FileRevlogPopup {
|
||||||
self.key_config.keys.page_down,
|
self.key_config.keys.page_down,
|
||||||
) {
|
) {
|
||||||
self.move_selection(ScrollType::PageDown)?;
|
self.move_selection(ScrollType::PageDown)?;
|
||||||
|
} else if key_match(
|
||||||
|
key,
|
||||||
|
self.key_config.keys.diff_mode_toggle,
|
||||||
|
) {
|
||||||
|
self.diff.toggle_diff_mode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -610,6 +615,11 @@ impl Component for FileRevlogPopup {
|
||||||
)
|
)
|
||||||
.order(1),
|
.order(1),
|
||||||
);
|
);
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::diff_toggle_mode(&self.key_config),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
out.push(CommandInfo::new(
|
out.push(CommandInfo::new(
|
||||||
strings::commands::diff_focus_right(&self.key_config),
|
strings::commands::diff_focus_right(&self.key_config),
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,12 @@ impl Component for InspectCommitPopup {
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::diff_toggle_mode(&self.key_config),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
visibility_blocking(self)
|
visibility_blocking(self)
|
||||||
|
|
@ -171,6 +177,11 @@ impl Component for InspectCommitPopup {
|
||||||
} else if key_match(e, self.key_config.keys.move_left)
|
} else if key_match(e, self.key_config.keys.move_left)
|
||||||
{
|
{
|
||||||
self.hide_stacked(false);
|
self.hide_stacked(false);
|
||||||
|
} else if key_match(
|
||||||
|
e,
|
||||||
|
self.key_config.keys.diff_mode_toggle,
|
||||||
|
) {
|
||||||
|
self.diff.toggle_diff_mode();
|
||||||
} else if key_match(
|
} else if key_match(
|
||||||
e,
|
e,
|
||||||
self.key_config.keys.open_file_tree,
|
self.key_config.keys.open_file_tree,
|
||||||
|
|
|
||||||
|
|
@ -834,6 +834,18 @@ pub mod commands {
|
||||||
CMD_GROUP_DIFF,
|
CMD_GROUP_DIFF,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
pub fn diff_toggle_mode(
|
||||||
|
key_config: &SharedKeyConfig,
|
||||||
|
) -> CommandText {
|
||||||
|
CommandText::new(
|
||||||
|
format!(
|
||||||
|
"Toggle Diff Mode [{}]",
|
||||||
|
key_config.get_hint(key_config.keys.diff_mode_toggle),
|
||||||
|
),
|
||||||
|
"toggle between unified and side-by-side diff",
|
||||||
|
CMD_GROUP_DIFF,
|
||||||
|
)
|
||||||
|
}
|
||||||
pub fn close_fuzzy_finder(
|
pub fn close_fuzzy_finder(
|
||||||
key_config: &SharedKeyConfig,
|
key_config: &SharedKeyConfig,
|
||||||
) -> CommandText {
|
) -> CommandText {
|
||||||
|
|
|
||||||
|
|
@ -804,6 +804,12 @@ impl Component for Status {
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::diff_toggle_mode(&self.key_config),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.commands_nav(out, force_all);
|
self.commands_nav(out, force_all);
|
||||||
|
|
@ -947,6 +953,12 @@ impl Component for Status {
|
||||||
) {
|
) {
|
||||||
self.queue.push(InternalEvent::ViewSubmodules);
|
self.queue.push(InternalEvent::ViewSubmodules);
|
||||||
Ok(EventState::Consumed)
|
Ok(EventState::Consumed)
|
||||||
|
} else if key_match(
|
||||||
|
k,
|
||||||
|
self.key_config.keys.diff_mode_toggle,
|
||||||
|
) {
|
||||||
|
self.diff.toggle_diff_mode();
|
||||||
|
Ok(EventState::Consumed)
|
||||||
} else {
|
} else {
|
||||||
Ok(EventState::NotConsumed)
|
Ok(EventState::NotConsumed)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@
|
||||||
diff_reset_lines: Some(( code: Char('u'), modifiers: "")),
|
diff_reset_lines: Some(( code: Char('u'), modifiers: "")),
|
||||||
diff_stage_lines: Some(( code: Char('s'), modifiers: "")),
|
diff_stage_lines: Some(( code: Char('s'), modifiers: "")),
|
||||||
|
|
||||||
|
// toggle between unified and side-by-side diff mode
|
||||||
|
diff_mode_toggle: Some(( code: Char('m'), modifiers: "")),
|
||||||
|
|
||||||
stashing_save: Some(( code: Char('w'), modifiers: "")),
|
stashing_save: Some(( code: Char('w'), modifiers: "")),
|
||||||
stashing_toggle_index: Some(( code: Char('m'), modifiers: "")),
|
stashing_toggle_index: Some(( code: Char('m'), modifiers: "")),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue