mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Add Go to line feature for the blame view (#2262)
This commit is contained in:
parent
cb17cfe105
commit
2ab4143d6b
8 changed files with 247 additions and 2 deletions
|
|
@ -18,6 +18,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 error messages [[@acuteenvy](https://github.com/acuteenvy)] ([#2617](https://github.com/gitui-org/gitui/pull/2617))
|
||||
|
|
|
|||
16
src/app.rs
16
src/app.rs
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
AppOption, BlameFilePopup, BranchListPopup, CommitPopup,
|
||||
CompareCommitsPopup, ConfirmPopup, CreateBranchPopup,
|
||||
CreateRemotePopup, ExternalEditorPopup, FetchPopup,
|
||||
FileRevlogPopup, FuzzyFindPopup, HelpPopup,
|
||||
FileRevlogPopup, FuzzyFindPopup, GotoLinePopup, HelpPopup,
|
||||
InspectCommitPopup, LogSearchPopupPopup, MsgPopup,
|
||||
OptionsPopup, PullPopup, PushPopup, PushTagsPopup,
|
||||
RemoteListPopup, RenameBranchPopup, RenameRemotePopup,
|
||||
|
|
@ -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
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -905,6 +909,14 @@ impl App {
|
|||
InternalEvent::CommitSearch(options) => {
|
||||
self.revlog.search(options);
|
||||
}
|
||||
InternalEvent::OpenGotoLinePopup(max_line) => {
|
||||
self.goto_line_popup.open(max_line);
|
||||
}
|
||||
InternalEvent::GotoLine(line) => {
|
||||
if self.blame_file_popup.is_visible() {
|
||||
self.blame_file_popup.goto_line(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(flags)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,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 +317,22 @@ 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 let Some(blame_result) = maybe_blame_result {
|
||||
let max_line = blame_result.lines().len() - 1;
|
||||
self.queue.push(
|
||||
InternalEvent::OpenGotoLinePopup(
|
||||
max_line,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
|
|
@ -742,6 +768,14 @@ impl BlameFilePopup {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn goto_line(&mut self, line: usize) {
|
||||
self.visible = true;
|
||||
let mut table_state = self.table_state.take();
|
||||
table_state
|
||||
.select(Some(line.clamp(0, self.get_max_line_number())));
|
||||
self.table_state.set(table_state);
|
||||
}
|
||||
|
||||
fn selected_commit(&self) -> Option<CommitId> {
|
||||
self.blame
|
||||
.as_ref()
|
||||
|
|
|
|||
167
src/popups/goto_line.rs
Normal file
167
src/popups/goto_line.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
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};
|
||||
|
||||
pub struct GotoLinePopup {
|
||||
visible: bool,
|
||||
input: String,
|
||||
line_number: usize,
|
||||
key_config: SharedKeyConfig,
|
||||
queue: Queue,
|
||||
theme: SharedTheme,
|
||||
invalid_input: bool,
|
||||
max_line: usize,
|
||||
}
|
||||
|
||||
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,
|
||||
max_line: 0,
|
||||
line_number: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&mut self, max_line: usize) {
|
||||
self.visible = true;
|
||||
self.max_line = max_line;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} 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.max_line + 1, self.max_line))
|
||||
,
|
||||
);
|
||||
} else if !self.input.is_empty() {
|
||||
self.queue.push(InternalEvent::GotoLine(
|
||||
self.line_number,
|
||||
));
|
||||
}
|
||||
self.input.clear();
|
||||
self.invalid_input = false;
|
||||
}
|
||||
}
|
||||
match self.input.parse::<isize>() {
|
||||
Ok(input) => {
|
||||
let mut max_value_allowed_abs = self.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"));
|
||||
|
||||
let input_area = ui::centered_rect_absolute(15, 3, area);
|
||||
f.render_widget(Clear, input_area);
|
||||
f.render_widget(input, input_area);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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::GotoLinePopup;
|
||||
pub use help::HelpPopup;
|
||||
pub use inspect_commit::{InspectCommitOpen, InspectCommitPopup};
|
||||
pub use log_search::LogSearchPopupPopup;
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@ pub enum InternalEvent {
|
|||
RewordCommit(CommitId),
|
||||
///
|
||||
CommitSearch(LogFilterSearchOptions),
|
||||
///
|
||||
OpenGotoLinePopup(usize),
|
||||
///
|
||||
GotoLine(usize),
|
||||
}
|
||||
|
||||
/// single threaded simple queue for components to communicate with each other
|
||||
|
|
|
|||
|
|
@ -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 [{}]",
|
||||
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 [{}]",
|
||||
key_config.get_hint(key_config.keys.enter),
|
||||
),
|
||||
"Go to the given line",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue