Skip to content

Add Go to line feature for the blame view #2262

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

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 4 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* new command-line option to override the default log file path (`--logfile`) [[@acuteenvy](https://github.com/acuteenvy)] ([#2539](https://github.com/gitui-org/gitui/pull/2539))
* dx: `make check` checks Cargo.toml dependency ordering using `cargo sort` [[@naseschwarz](https://github.com/naseschwarz)]
* add `use_selection_fg` to theme file to allow customizing selection foreground color [[@Upsylonbare](https://github.com/Upsylonbare)] ([#2515](https://github.com/gitui-org/gitui/pull/2515))
* Add "go to line" command for the blame view [[@andrea-berling](https://github.com/andrea-berling)] ([#2262](https://github.com/extrawurst/gitui/pull/2262))

### Changed
* improve syntax highlighting file detection [[@acuteenvy](https://github.com/acuteenvy)] ([#2524](https://github.com/extrawurst/gitui/pull/2524))
@@ -34,13 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* use default shell instead of bash on Unix-like OS [[@yerke](https://github.com/yerke)] ([#2343](https://github.com/gitui-org/gitui/pull/2343))

### Added
* add popups for viewing, adding, updating and removing remotes [[@robin-thoene](https://github.com/robin-thoene)] ([#2172](https://github.com/gitui-org/gitui/issues/2172))
* support for `Copy Path` action in WSL [[@johnDeSilencio](https://github.com/johnDeSilencio)] ([#2413](https://github.com/gitui-org/gitui/pull/2413))
* help popup scrollbar [[@wugeer](https://github.com/wugeer)] ([#2388](https://github.com/gitui-org/gitui/pull/2388))

### Fixes
* respect env vars like `GIT_CONFIG_GLOBAL` ([#2298](https://github.com/gitui-org/gitui/issues/2298))
* Set `CREATE_NO_WINDOW` flag when executing Git hooks on Windows ([#2371](https://github.com/gitui-org/gitui/pull/2371))
* support for "Copy Path" action in WSL [[@johnDeSilencio](https://github.com/johnDeSilencio)] ([#2413](https://github.com/extrawurst/gitui/pull/2413))
* help popup scrollbar [[@wugeer](https://github.com/wugeer)](https://github.com/extrawurst/gitui/pull/2388))
* add popups for viewing, adding, updating and removing remotes [[@robin-thoene](https://github.com/robin-thoene)] ([#2172](https://github.com/extrawurst/gitui/issues/2172))

## [0.26.3] - 2024-06-02

6 changes: 4 additions & 2 deletions asyncgit/src/blame.rs
Original file line number Diff line number Diff line change
@@ -14,23 +14,25 @@ use std::{
};

///
#[derive(Hash, Clone, PartialEq, Eq)]
#[derive(Hash, Clone, PartialEq, Eq, Debug)]
pub struct BlameParams {
/// path to the file to blame
pub file_path: String,
/// blame at a specific revision
pub commit_id: Option<CommitId>,
}

#[derive(Debug)]
struct Request<R, A>(R, Option<A>);

#[derive(Default, Clone)]
#[derive(Debug, Default, Clone)]
struct LastResult<P, R> {
params: P,
result: R,
}

///
#[derive(Debug, Clone)]
pub struct AsyncBlame {
current: Arc<Mutex<Request<u64, FileBlame>>>,
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
49 changes: 38 additions & 11 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -10,16 +10,16 @@ use crate::{
options::{Options, SharedOptions},
popup_stack::PopupStack,
popups::{
AppOption, BlameFilePopup, BranchListPopup, CommitPopup,
CompareCommitsPopup, ConfirmPopup, CreateBranchPopup,
CreateRemotePopup, ExternalEditorPopup, FetchPopup,
FileRevlogPopup, FuzzyFindPopup, HelpPopup,
InspectCommitPopup, LogSearchPopupPopup, MsgPopup,
OptionsPopup, PullPopup, PushPopup, PushTagsPopup,
RemoteListPopup, RenameBranchPopup, RenameRemotePopup,
ResetPopup, RevisionFilesPopup, StashMsgPopup,
SubmodulesListPopup, TagCommitPopup, TagListPopup,
UpdateRemoteUrlPopup,
AppOption, BlameFileOpen, BlameFilePopup, BlameRequest,
BranchListPopup, CommitPopup, CompareCommitsPopup,
ConfirmPopup, CreateBranchPopup, CreateRemotePopup,
ExternalEditorPopup, FetchPopup, FileRevlogPopup,
FuzzyFindPopup, GotoLinePopup, HelpPopup, InspectCommitPopup,
LogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup,
PushPopup, PushTagsPopup, RemoteListPopup, RenameBranchPopup,
RenameRemotePopup, ResetPopup, RevisionFilesPopup,
StashMsgPopup, SubmodulesListPopup, TagCommitPopup,
TagListPopup, UpdateRemoteUrlPopup,
},
queue::{
Action, AppTabs, InternalEvent, NeedsUpdate, Queue,
@@ -112,6 +112,7 @@ pub struct App {
popup_stack: PopupStack,
options: SharedOptions,
repo_path_text: String,
goto_line_popup: GotoLinePopup,

// "Flags"
requires_redraw: Cell<bool>,
@@ -218,6 +219,7 @@ impl App {
stashing_tab: Stashing::new(&env),
stashlist_tab: StashList::new(&env),
files_tab: FilesTab::new(&env),
goto_line_popup: GotoLinePopup::new(&env),
tab: 0,
queue: env.queue,
theme: env.theme,
@@ -481,6 +483,7 @@ impl App {
msg_popup,
confirm_popup,
commit_popup,
goto_line_popup,
blame_file_popup,
file_revlog_popup,
stashmsg_popup,
@@ -544,7 +547,8 @@ impl App {
fetch_popup,
options_popup,
confirm_popup,
msg_popup
msg_popup,
goto_line_popup
]
);

@@ -691,6 +695,9 @@ impl App {
StackablePopupOpen::CompareCommits(param) => {
self.compare_commits_popup.open(param)?;
}
StackablePopupOpen::GotoLine(param) => {
self.goto_line_popup.open(param);
}
}

Ok(())
@@ -905,6 +912,26 @@ impl App {
InternalEvent::CommitSearch(options) => {
self.revlog.search(options);
}
InternalEvent::GotoLine(line) => {
if let Some(popup) = self.popup_stack.pop() {
if let StackablePopupOpen::BlameFile(params) =
popup
{
self.popup_stack.push(
StackablePopupOpen::BlameFile(
BlameFileOpen {
selection: Some(line),
blame: BlameRequest::KeepExisting,
..params
},
),
);
}
flags.insert(
NeedsUpdate::ALL | NeedsUpdate::COMMANDS,
);
}
}
};

Ok(flags)
3 changes: 2 additions & 1 deletion src/components/revision_files.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ use super::{
use crate::{
app::Environment,
keys::{key_match, SharedKeyConfig},
popups::{BlameFileOpen, FileRevOpen},
popups::{BlameFileOpen, BlameRequest, FileRevOpen},
queue::{InternalEvent, Queue, StackablePopupOpen},
strings::{self, order, symbol},
try_or_popup,
@@ -193,6 +193,7 @@ impl RevisionFilesComponent {
file_path: path,
commit_id: self.revision.as_ref().map(|c| c.id),
selection: None,
blame: BlameRequest::StartNew,
}),
));

3 changes: 2 additions & 1 deletion src/components/status_tree.rs
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ use crate::{
app::Environment,
components::{CommandInfo, Component, EventState},
keys::{key_match, SharedKeyConfig},
popups::{BlameFileOpen, FileRevOpen},
popups::{BlameFileOpen, BlameRequest, FileRevOpen},
queue::{InternalEvent, NeedsUpdate, Queue, StackablePopupOpen},
strings::{self, order},
ui::{self, style::SharedTheme},
@@ -456,6 +456,7 @@ impl Component for StatusTreeComponent {
file_path: status_item.path,
commit_id: self.revision,
selection: None,
blame: BlameRequest::StartNew,
},
),
));
2 changes: 2 additions & 0 deletions src/keys/key_list.rs
Original file line number Diff line number Diff line change
@@ -128,6 +128,7 @@ pub struct KeysList {
pub commit_history_next: GituiKeyEvent,
pub commit: GituiKeyEvent,
pub newline: GituiKeyEvent,
pub goto_line: GituiKeyEvent,
}

#[rustfmt::skip]
@@ -225,6 +226,7 @@ impl Default for KeysList {
commit_history_next: GituiKeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL),
commit: GituiKeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
newline: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
goto_line: GituiKeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT),
}
}
}
69 changes: 60 additions & 9 deletions src/popups/blame_file.rs
Original file line number Diff line number Diff line change
@@ -30,12 +30,15 @@ use ratatui::{
};
use std::path::Path;

use super::{goto_line::GotoLineContext, GotoLineOpen};

static NO_COMMIT_ID: &str = "0000000";
static NO_AUTHOR: &str = "<no author>";
static MIN_AUTHOR_WIDTH: usize = 3;
static MAX_AUTHOR_WIDTH: usize = 20;

struct SyntaxFileBlame {
#[derive(Debug, Clone)]
pub struct SyntaxFileBlame {
pub file_blame: FileBlame,
pub styled_text: Option<SyntaxText>,
}
@@ -54,7 +57,8 @@ impl SyntaxFileBlame {
}
}

enum BlameProcess {
#[derive(Clone, Debug)]
pub enum BlameProcess {
GettingBlame(AsyncBlame),
SyntaxHighlighting {
unstyled_file_blame: SyntaxFileBlame,
@@ -76,11 +80,18 @@ impl BlameProcess {
}
}

#[derive(Clone, Debug)]
pub enum BlameRequest {
StartNew,
KeepExisting,
}

#[derive(Clone, Debug)]
pub struct BlameFileOpen {
pub file_path: String,
pub commit_id: Option<CommitId>,
pub selection: Option<usize>,
pub blame: BlameRequest,
}

pub struct BlameFilePopup {
@@ -234,6 +245,16 @@ impl Component for BlameFilePopup {
)
.order(1),
);
out.push(
CommandInfo::new(
strings::commands::open_line_number_popup(
&self.key_config,
),
true,
has_result,
)
.order(1),
);
}

visibility_blocking(self)
@@ -307,6 +328,31 @@ impl Component for BlameFilePopup {
),
));
}
} else if key_match(
key,
self.key_config.keys.goto_line,
) {
let maybe_blame_result = &self
.blame
.as_ref()
.and_then(|blame| blame.result());
if maybe_blame_result.is_some() {
let max_line = maybe_blame_result
.expect("This can not be None")
.lines()
.len() - 1;
self.hide_stacked(true);
self.visible = true;
self.queue.push(InternalEvent::OpenPopup(
StackablePopupOpen::GotoLine(
GotoLineOpen {
context: GotoLineContext {
max_line,
},
},
),
));
}
}

return Ok(EventState::Consumed);
@@ -356,6 +402,7 @@ impl BlameFilePopup {
file_path: request.file_path,
commit_id: request.commit_id,
selection: self.get_selection(),
blame: BlameRequest::KeepExisting,
}),
));
}
@@ -371,11 +418,13 @@ impl BlameFilePopup {
file_path: open.file_path,
commit_id: open.commit_id,
});
self.blame =
Some(BlameProcess::GettingBlame(AsyncBlame::new(
self.repo.borrow().clone(),
&self.git_sender,
)));
if matches!(open.blame, BlameRequest::StartNew) {
self.blame =
Some(BlameProcess::GettingBlame(AsyncBlame::new(
self.repo.borrow().clone(),
&self.git_sender,
)));
}
self.table_state.get_mut().select(Some(0));
self.visible = true;
self.update()?;
@@ -438,7 +487,6 @@ impl BlameFilePopup {
),
},
);
self.set_open_selection();
self.highlight_blame_lines();

return Ok(());
@@ -449,6 +497,7 @@ impl BlameFilePopup {
}
}
}
self.set_open_selection();

Ok(())
}
@@ -722,7 +771,9 @@ impl BlameFilePopup {
self.open_request.as_ref().and_then(|req| req.selection)
{
let mut table_state = self.table_state.take();
table_state.select(Some(selection));
let max_line_number = self.get_max_line_number();
table_state
.select(Some(selection.clamp(0, max_line_number)));
self.table_state.set(table_state);
}
}
3 changes: 2 additions & 1 deletion src/popups/file_revlog.rs
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ use ratatui::{
Frame,
};

use super::{BlameFileOpen, InspectCommitOpen};
use super::{BlameFileOpen, BlameRequest, InspectCommitOpen};

const SLICE_SIZE: usize = 1200;

@@ -533,6 +533,7 @@ impl Component for FileRevlogPopup {
file_path: open_request.file_path,
commit_id: self.selected_commit(),
selection: None,
blame: BlameRequest::StartNew,
},
),
));
180 changes: 180 additions & 0 deletions src/popups/goto_line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use crate::{
app::Environment,
components::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState,
},
keys::{key_match, SharedKeyConfig},
queue::{InternalEvent, Queue},
strings,
ui::{self, style::SharedTheme},
};

use ratatui::{
layout::Rect,
style::{Color, Style},
widgets::{Block, Clear, Paragraph},
Frame,
};

use anyhow::Result;

use crossterm::event::{Event, KeyCode};

#[derive(Debug)]
pub struct GotoLineContext {
pub max_line: usize,
}

#[derive(Debug)]
pub struct GotoLineOpen {
pub context: GotoLineContext,
}

pub struct GotoLinePopup {
visible: bool,
input: String,
line_number: usize,
key_config: SharedKeyConfig,
queue: Queue,
theme: SharedTheme,
invalid_input: bool,
context: GotoLineContext,
}

impl GotoLinePopup {
pub fn new(env: &Environment) -> Self {
Self {
visible: false,
input: String::new(),
key_config: env.key_config.clone(),
queue: env.queue.clone(),
theme: env.theme.clone(),
invalid_input: false,
context: GotoLineContext { max_line: 0 },
line_number: 0,
}
}

pub fn open(&mut self, open: GotoLineOpen) {
self.visible = true;
self.context = open.context;
}
}

impl Component for GotoLinePopup {
///
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if self.is_visible() || force_all {
out.push(
CommandInfo::new(
strings::commands::close_popup(&self.key_config),
true,
true,
)
.order(1),
);
out.push(
CommandInfo::new(
strings::commands::goto_line(&self.key_config),
true,
true,
)
.order(1),
);
}

visibility_blocking(self)
}

fn is_visible(&self) -> bool {
self.visible
}

///
fn event(&mut self, event: &Event) -> Result<EventState> {
if self.is_visible() {
if let Event::Key(key) = event {
if key_match(key, self.key_config.keys.exit_popup) {
self.visible = false;
self.input.clear();
self.queue.push(InternalEvent::PopupStackPop);
} else if let KeyCode::Char(c) = key.code {
if c.is_ascii_digit() || c == '-' {
self.input.push(c);
}
} else if key.code == KeyCode::Backspace {
self.input.pop();
} else if key_match(key, self.key_config.keys.enter) {
self.visible = false;
if self.invalid_input {
self.queue.push(InternalEvent::ShowErrorMsg(
format!("Invalid input: only numbers between -{} and {} (included) are allowed (-1 denotes the last line, -2 denotes the second to last line, and so on)",self.context.max_line + 1, self.context.max_line))
,
);
} else if !self.input.is_empty() {
self.queue.push(InternalEvent::GotoLine(
self.line_number,
));
}
self.queue.push(InternalEvent::PopupStackPop);
self.input.clear();
self.invalid_input = false;
}
}
match self.input.parse::<isize>() {
Ok(input) => {
let mut max_value_allowed_abs =
self.context.max_line;
// negative indices are 1 based
if input < 0 {
max_value_allowed_abs += 1;
}
let input_abs = input.unsigned_abs();
if input_abs > max_value_allowed_abs {
self.invalid_input = true;
} else {
self.invalid_input = false;
self.line_number = if input >= 0 {
input_abs
} else {
max_value_allowed_abs - input_abs
}
}
}
Err(_) => {
if !self.input.is_empty() {
self.invalid_input = true;
}
}
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
}

impl DrawableComponent for GotoLinePopup {
fn draw(&self, f: &mut Frame, area: Rect) -> Result<()> {
if self.is_visible() {
let style = if self.invalid_input {
Style::default().fg(Color::Red)
} else {
self.theme.text(true, false)
};
let input = Paragraph::new(self.input.as_str())
.style(style)
.block(Block::bordered().title("Go to Line"));

let input_area = ui::centered_rect_absolute(15, 3, area);
f.render_widget(Clear, input_area);
f.render_widget(input, input_area);
}

Ok(())
}
}
4 changes: 3 additions & 1 deletion src/popups/mod.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ mod externaleditor;
mod fetch;
mod file_revlog;
mod fuzzy_find;
mod goto_line;
mod help;
mod inspect_commit;
mod log_search;
@@ -28,7 +29,7 @@ mod tag_commit;
mod taglist;
mod update_remote_url;

pub use blame_file::{BlameFileOpen, BlameFilePopup};
pub use blame_file::{BlameFileOpen, BlameFilePopup, BlameRequest};
pub use branchlist::BranchListPopup;
pub use commit::CommitPopup;
pub use compare_commits::CompareCommitsPopup;
@@ -39,6 +40,7 @@ pub use externaleditor::ExternalEditorPopup;
pub use fetch::FetchPopup;
pub use file_revlog::{FileRevOpen, FileRevlogPopup};
pub use fuzzy_find::FuzzyFindPopup;
pub use goto_line::{GotoLineOpen, GotoLinePopup};
pub use help::HelpPopup;
pub use inspect_commit::{InspectCommitOpen, InspectCommitPopup};
pub use log_search::LogSearchPopupPopup;
6 changes: 5 additions & 1 deletion src/queue.rs
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ use crate::{
components::FuzzyFinderTarget,
popups::{
AppOption, BlameFileOpen, FileRevOpen, FileTreeOpen,
InspectCommitOpen,
GotoLineOpen, InspectCommitOpen,
},
tabs::StashingOptions,
};
@@ -71,6 +71,8 @@ pub enum StackablePopupOpen {
InspectCommit(InspectCommitOpen),
///
CompareCommits(InspectCommitOpen),
///
GotoLine(GotoLineOpen),
}

pub enum AppTabs {
@@ -157,6 +159,8 @@ pub enum InternalEvent {
RewordCommit(CommitId),
///
CommitSearch(LogFilterSearchOptions),
///
GotoLine(usize),
}

/// single threaded simple queue for components to communicate with each other
23 changes: 23 additions & 0 deletions src/strings.rs
Original file line number Diff line number Diff line change
@@ -1449,6 +1449,18 @@ pub mod commands {
CMD_GROUP_LOG,
)
}
pub fn open_line_number_popup(
key_config: &SharedKeyConfig,
) -> CommandText {
CommandText::new(
format!(
"Go to Line [{}]",
key_config.get_hint(key_config.keys.goto_line),
),
"go to a given line number in the blame view",
CMD_GROUP_GENERAL,
)
}
pub fn log_tag_commit(
key_config: &SharedKeyConfig,
) -> CommandText {
@@ -1870,4 +1882,15 @@ pub mod commands {
CMD_GROUP_LOG,
)
}

pub fn goto_line(key_config: &SharedKeyConfig) -> CommandText {
CommandText::new(
format!(
"Go To Line [{}]",
key_config.get_hint(key_config.keys.enter),
),
"Go to the given line",
CMD_GROUP_GENERAL,
)
}
}
5 changes: 4 additions & 1 deletion src/ui/syntax_text.rs
Original file line number Diff line number Diff line change
@@ -23,10 +23,12 @@ use crate::{AsyncAppNotification, SyntaxHighlightProgress};

pub const DEFAULT_SYNTAX_THEME: &str = "base16-eighties.dark";

#[derive(Debug, Clone)]
struct SyntaxLine {
items: Vec<(Style, usize, Range<usize>)>,
}

#[derive(Debug, Clone)]
pub struct SyntaxText {
text: String,
lines: Vec<SyntaxLine>,
@@ -222,12 +224,13 @@ fn syntact_style_to_tui(style: &Style) -> ratatui::style::Style {
res
}

#[derive(Debug)]
enum JobState {
Request((String, String)),
Response(SyntaxText),
}

#[derive(Clone, Default)]
#[derive(Clone, Default, Debug)]
pub struct AsyncSyntaxJob {
state: Arc<Mutex<Option<JobState>>>,
syntax: String,