use collections::{BTreeMap, HashMap};use feature_flags::{FeatureFlagAppExt as _, GitGraphFeatureFlag};use git::{ BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote, parse_git_remote_url, repository::{CommitDiff, CommitFile, InitialGraphCommitData, LogOrder, LogSource, RepoPath}, status::{FileStatus, StatusCode, TrackedStatus},};use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView, git_status_icon};use gpui::{ AnyElement, App, Bounds, ClickEvent, ClipboardItem, Corner, DefiniteLength, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, PathBuilder, Pixels, Point, ScrollStrategy, ScrollWheelEvent, SharedString, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*, px, uniform_list,};use language::line_diff;use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};use project::{ Project, git_store::{ CommitDataState, GitGraphEvent, GitStoreEvent, GraphDataResponse, Repository, RepositoryEvent, RepositoryId, },};use settings::Settings;use smallvec::{SmallVec, smallvec};use std::{ cell::Cell, ops::Range, rc::Rc, sync::Arc, sync::OnceLock, time::{Duration, Instant},};use theme::{AccentColors, ThemeSettings};use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};use ui::{ ButtonLike, Chip, CommonAnimationExt as _, ContextMenu, DiffStat, Divider, ScrollableHandle, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, WithScrollbar, prelude::*,};use workspace::{ Workspace, item::{Item, ItemEvent, SerializableItem, TabTooltipContent},}; const COMMIT_CIRCLE_RADIUS: Pixels = px(3.5);const COMMIT_CIRCLE_STROKE_WIDTH: Pixels = px(1.5);const LANE_WIDTH: Pixels = px(16.0);const LEFT_PADDING: Pixels = px(12.0);const LINE_WIDTH: Pixels = px(1.5);const RESIZE_HANDLE_WIDTH: f32 = 8.0;const COPIED_STATE_DURATION: Duration = Duration::from_secs(2); struct CopiedState { copied_at: Option<Instant>,} impl CopiedState { fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self { Self { copied_at: None } } fn is_copied(&self) -> bool { self.copied_at .map(|t| t.elapsed() < COPIED_STATE_DURATION) .unwrap_or(false) } fn mark_copied(&mut self) { self.copied_at = Some(Instant::now()); }} struct DraggedSplitHandle; #[derive(Clone)]struct ChangedFileEntry { status: FileStatus, file_name: SharedString, dir_path: SharedString, repo_path: RepoPath,} impl ChangedFileEntry { fn from_commit_file(file: &CommitFile, _cx: &App) -> Self { let file_name: SharedString = file .path .file_name() .map(|n| n.to_string()) .unwrap_or_default() .into(); let dir_path: SharedString = file .path .parent() .map(|p| p.as_unix_str().to_string()) .unwrap_or_default() .into(); let status_code = match (&file.old_text, &file.new_text) { (None, Some(_)) => StatusCode::Added, (Some(_), None) => StatusCode::Deleted, _ => StatusCode::Modified, }; let status = FileStatus::Tracked(TrackedStatus { index_status: status_code, worktree_status: StatusCode::Unmodified, }); Self { status, file_name, dir_path, repo_path: file.path.clone(), } } fn open_in_commit_view( &self, commit_sha: &SharedString, repository: &WeakEntity<Repository>, workspace: &WeakEntity<Workspace>, window: &mut Window, cx: &mut App, ) { CommitView::open( commit_sha.to_string(), repository.clone(), workspace.clone(), None, Some(self.repo_path.clone()), window, cx, ); } fn render( &self, ix: usize, commit_sha: SharedString, repository: WeakEntity<Repository>, workspace: WeakEntity<Workspace>, _cx: &App, ) -> AnyElement { let file_name = self.file_name.clone(); let dir_path = self.dir_path.clone(); div() .w_full() .child( ButtonLike::new(("changed-file", ix)) .child( h_flex() .min_w_0() .w_full() .gap_1() .overflow_hidden() .child(git_status_icon(self.status)) .child( Label::new(file_name.clone()) .size(LabelSize::Small) .truncate(), ) .when(!dir_path.is_empty(), |this| { this.child( Label::new(dir_path.clone()) .size(LabelSize::Small) .color(Color::Muted) .truncate_start(), ) }), ) .tooltip({ let meta = if dir_path.is_empty() { file_name } else { format!("{}/{}", dir_path, file_name).into() }; move |_, cx| Tooltip::with_meta("View Changes", None, meta.clone(), cx) }) .on_click({ let entry = self.clone(); move |_, window, cx| { entry.open_in_commit_view( &commit_sha, &repository, &workspace, window, cx, ); } }), ) .into_any_element() }} pub struct SplitState { left_ratio: f32, visible_left_ratio: f32,} impl SplitState { pub fn new() -> Self { Self { left_ratio: 1.0, visible_left_ratio: 1.0, } } pub fn right_ratio(&self) -> f32 { 1.0 - self.visible_left_ratio } fn on_drag_move( &mut self, drag_event: &DragMoveEvent<DraggedSplitHandle>, _window: &mut Window, _cx: &mut Context<Self>, ) { let drag_position = drag_event.event.position; let bounds = drag_event.bounds; let bounds_width = bounds.right() - bounds.left(); let min_ratio = 0.1; let max_ratio = 0.9; let new_ratio = (drag_position.x - bounds.left()) / bounds_width; self.visible_left_ratio = new_ratio.clamp(min_ratio, max_ratio); } fn commit_ratio(&mut self) { self.left_ratio = self.visible_left_ratio; } fn on_double_click(&mut self) { self.left_ratio = 1.0; self.visible_left_ratio = 1.0; }} actions!( git_graph, [ /// Opens the commit view for the selected commit. OpenCommitView, ]); fn timestamp_format() -> &'static [BorrowedFormatItem<'static>] { static FORMAT: OnceLock<Vec<BorrowedFormatItem<'static>>> = OnceLock::new(); FORMAT.get_or_init(|| { time::format_description::parse("[day] [month repr:short] [year] [hour]:[minute]") .unwrap_or_default() })} fn format_timestamp(timestamp: i64) -> String { let Ok(datetime) = OffsetDateTime::from_unix_timestamp(timestamp) else { return "Unknown".to_string(); }; let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); let local_datetime = datetime.to_offset(local_offset); local_datetime .format(timestamp_format()) .unwrap_or_default()} fn accent_colors_count(accents: &AccentColors) -> usize { accents.0.len()} #[derive(Copy, Clone, Debug)]struct BranchColor(u8); #[derive(Debug)]enum LaneState { Empty, Active { child: Oid, parent: Oid, color: Option<BranchColor>, starting_row: usize, starting_col: usize, destination_column: Option<usize>, segments: SmallVec<[CommitLineSegment; 1]>, },} impl LaneState { fn to_commit_lines( &mut self, ending_row: usize, lane_column: usize, parent_column: usize, parent_color: BranchColor, ) -> Option<CommitLine> { let state = std::mem::replace(self, LaneState::Empty); match state { LaneState::Active { #[cfg_attr(not(test), allow(unused_variables))] parent, #[cfg_attr(not(test), allow(unused_variables))] child, color, starting_row, starting_col, destination_column, mut segments, } => { let final_destination = destination_column.unwrap_or(parent_column); let final_color = color.unwrap_or(parent_color); Some(CommitLine { #[cfg(test)] child, #[cfg(test)] parent, child_column: starting_col, full_interval: starting_row..ending_row, color_idx: final_color.0 as usize, segments: { match segments.last_mut() { Some(CommitLineSegment::Straight { to_row }) if *to_row == usize::MAX => { if final_destination != lane_column { *to_row = ending_row - 1; let curved_line = CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }; if *to_row == starting_row { let last_index = segments.len() - 1; segments[last_index] = curved_line; } else { segments.push(curved_line); } } else { *to_row = ending_row; } } Some(CommitLineSegment::Curve { on_row, to_column, curve_kind, }) if *on_row == usize::MAX => { if *to_column == usize::MAX { *to_column = final_destination; } if matches!(curve_kind, CurveKind::Merge) { *on_row = starting_row + 1; if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } else { *on_row = ending_row; if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } } Some(CommitLineSegment::Curve { on_row, to_column, .. }) => { if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } _ => {} } segments }, }) } LaneState::Empty => None, } } fn is_empty(&self) -> bool { match self { LaneState::Empty => true, LaneState::Active { .. } => false, } }} struct CommitEntry { data: Arc<InitialGraphCommitData>, lane: usize, color_idx: usize,} type ActiveLaneIdx = usize; enum AllCommitCount { NotLoaded, Loaded(usize),} #[derive(Debug)]enum CurveKind { Merge, Checkout,} #[derive(Debug)]enum CommitLineSegment { Straight { to_row: usize, }, Curve { to_column: usize, on_row: usize, curve_kind: CurveKind, },} #[derive(Debug)]struct CommitLine { #[cfg(test)] child: Oid, #[cfg(test)] parent: Oid, child_column: usize, full_interval: Range<usize>, color_idx: usize, segments: SmallVec<[CommitLineSegment; 1]>,} impl CommitLine { fn get_first_visible_segment_idx(&self, first_visible_row: usize) -> Option<(usize, usize)> { if first_visible_row > self.full_interval.end { return None; } else if first_visible_row <= self.full_interval.start { return Some((0, self.child_column)); } let mut current_column = self.child_column; for (idx, segment) in self.segments.iter().enumerate() { match segment { CommitLineSegment::Straight { to_row } => { if *to_row >= first_visible_row { return Some((idx, current_column)); }Lines 1–500 of 3302. Load more
use collections::{BTreeMap, HashMap}; use feature_flags::{FeatureFlagAppExt as _, GitGraphFeatureFlag}; use git::{ BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote, parse_git_remote_url, repository::{CommitDiff, CommitFile, InitialGraphCommitData, LogOrder, LogSource, RepoPath}, status::{FileStatus, StatusCode, TrackedStatus}, }; use git_ui::{commit_tooltip::CommitAvatar, commit_view::CommitView, git_status_icon}; use gpui::{ AnyElement, App, Bounds, ClickEvent, ClipboardItem, Corner, DefiniteLength, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, PathBuilder, Pixels, Point, ScrollStrategy, ScrollWheelEvent, SharedString, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*, px, uniform_list, }; use language::line_diff; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::{ Project, git_store::{ CommitDataState, GitGraphEvent, GitStoreEvent, GraphDataResponse, Repository, RepositoryEvent, RepositoryId, }, }; use settings::Settings; use smallvec::{SmallVec, smallvec}; use std::{ cell::Cell, ops::Range, rc::Rc, sync::Arc, sync::OnceLock, time::{Duration, Instant}, }; use theme::{AccentColors, ThemeSettings}; use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem}; use ui::{ ButtonLike, Chip, CommonAnimationExt as _, ContextMenu, DiffStat, Divider, ScrollableHandle, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, WithScrollbar, prelude::*, }; use workspace::{ Workspace, item::{Item, ItemEvent, SerializableItem, TabTooltipContent}, }; const COMMIT_CIRCLE_RADIUS: Pixels = px(3.5); const COMMIT_CIRCLE_STROKE_WIDTH: Pixels = px(1.5); const LANE_WIDTH: Pixels = px(16.0); const LEFT_PADDING: Pixels = px(12.0); const LINE_WIDTH: Pixels = px(1.5); const RESIZE_HANDLE_WIDTH: f32 = 8.0; const COPIED_STATE_DURATION: Duration = Duration::from_secs(2); struct CopiedState { copied_at: Option<Instant>, } impl CopiedState { fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self { Self { copied_at: None } } fn is_copied(&self) -> bool { self.copied_at .map(|t| t.elapsed() < COPIED_STATE_DURATION) .unwrap_or(false) } fn mark_copied(&mut self) { self.copied_at = Some(Instant::now()); } } struct DraggedSplitHandle; #[derive(Clone)] struct ChangedFileEntry { status: FileStatus, file_name: SharedString, dir_path: SharedString, repo_path: RepoPath, } impl ChangedFileEntry { fn from_commit_file(file: &CommitFile, _cx: &App) -> Self { let file_name: SharedString = file .path .file_name() .map(|n| n.to_string()) .unwrap_or_default() .into(); let dir_path: SharedString = file .path .parent() .map(|p| p.as_unix_str().to_string()) .unwrap_or_default() .into(); let status_code = match (&file.old_text, &file.new_text) { (None, Some(_)) => StatusCode::Added, (Some(_), None) => StatusCode::Deleted, _ => StatusCode::Modified, }; let status = FileStatus::Tracked(TrackedStatus { index_status: status_code, worktree_status: StatusCode::Unmodified, }); Self { status, file_name, dir_path, repo_path: file.path.clone(), } } fn open_in_commit_view( &self, commit_sha: &SharedString, repository: &WeakEntity<Repository>, workspace: &WeakEntity<Workspace>, window: &mut Window, cx: &mut App, ) { CommitView::open( commit_sha.to_string(), repository.clone(), workspace.clone(), None, Some(self.repo_path.clone()), window, cx, ); } fn render( &self, ix: usize, commit_sha: SharedString, repository: WeakEntity<Repository>, workspace: WeakEntity<Workspace>, _cx: &App, ) -> AnyElement { let file_name = self.file_name.clone(); let dir_path = self.dir_path.clone(); div() .w_full() .child( ButtonLike::new(("changed-file", ix)) .child( h_flex() .min_w_0() .w_full() .gap_1() .overflow_hidden() .child(git_status_icon(self.status)) .child( Label::new(file_name.clone()) .size(LabelSize::Small) .truncate(), ) .when(!dir_path.is_empty(), |this| { this.child( Label::new(dir_path.clone()) .size(LabelSize::Small) .color(Color::Muted) .truncate_start(), ) }), ) .tooltip({ let meta = if dir_path.is_empty() { file_name } else { format!("{}/{}", dir_path, file_name).into() }; move |_, cx| Tooltip::with_meta("View Changes", None, meta.clone(), cx) }) .on_click({ let entry = self.clone(); move |_, window, cx| { entry.open_in_commit_view( &commit_sha, &repository, &workspace, window, cx, ); } }), ) .into_any_element() } } pub struct SplitState { left_ratio: f32, visible_left_ratio: f32, } impl SplitState { pub fn new() -> Self { Self { left_ratio: 1.0, visible_left_ratio: 1.0, } } pub fn right_ratio(&self) -> f32 { 1.0 - self.visible_left_ratio } fn on_drag_move( &mut self, drag_event: &DragMoveEvent<DraggedSplitHandle>, _window: &mut Window, _cx: &mut Context<Self>, ) { let drag_position = drag_event.event.position; let bounds = drag_event.bounds; let bounds_width = bounds.right() - bounds.left(); let min_ratio = 0.1; let max_ratio = 0.9; let new_ratio = (drag_position.x - bounds.left()) / bounds_width; self.visible_left_ratio = new_ratio.clamp(min_ratio, max_ratio); } fn commit_ratio(&mut self) { self.left_ratio = self.visible_left_ratio; } fn on_double_click(&mut self) { self.left_ratio = 1.0; self.visible_left_ratio = 1.0; } } actions!( git_graph, [ /// Opens the commit view for the selected commit. OpenCommitView, ] ); fn timestamp_format() -> &'static [BorrowedFormatItem<'static>] { static FORMAT: OnceLock<Vec<BorrowedFormatItem<'static>>> = OnceLock::new(); FORMAT.get_or_init(|| { time::format_description::parse("[day] [month repr:short] [year] [hour]:[minute]") .unwrap_or_default() }) } fn format_timestamp(timestamp: i64) -> String { let Ok(datetime) = OffsetDateTime::from_unix_timestamp(timestamp) else { return "Unknown".to_string(); }; let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); let local_datetime = datetime.to_offset(local_offset); local_datetime .format(timestamp_format()) .unwrap_or_default() } fn accent_colors_count(accents: &AccentColors) -> usize { accents.0.len() } #[derive(Copy, Clone, Debug)] struct BranchColor(u8); #[derive(Debug)] enum LaneState { Empty, Active { child: Oid, parent: Oid, color: Option<BranchColor>, starting_row: usize, starting_col: usize, destination_column: Option<usize>, segments: SmallVec<[CommitLineSegment; 1]>, }, } impl LaneState { fn to_commit_lines( &mut self, ending_row: usize, lane_column: usize, parent_column: usize, parent_color: BranchColor, ) -> Option<CommitLine> { let state = std::mem::replace(self, LaneState::Empty); match state { LaneState::Active { #[cfg_attr(not(test), allow(unused_variables))] parent, #[cfg_attr(not(test), allow(unused_variables))] child, color, starting_row, starting_col, destination_column, mut segments, } => { let final_destination = destination_column.unwrap_or(parent_column); let final_color = color.unwrap_or(parent_color); Some(CommitLine { #[cfg(test)] child, #[cfg(test)] parent, child_column: starting_col, full_interval: starting_row..ending_row, color_idx: final_color.0 as usize, segments: { match segments.last_mut() { Some(CommitLineSegment::Straight { to_row }) if *to_row == usize::MAX => { if final_destination != lane_column { *to_row = ending_row - 1; let curved_line = CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }; if *to_row == starting_row { let last_index = segments.len() - 1; segments[last_index] = curved_line; } else { segments.push(curved_line); } } else { *to_row = ending_row; } } Some(CommitLineSegment::Curve { on_row, to_column, curve_kind, }) if *on_row == usize::MAX => { if *to_column == usize::MAX { *to_column = final_destination; } if matches!(curve_kind, CurveKind::Merge) { *on_row = starting_row + 1; if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } else { *on_row = ending_row; if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } } Some(CommitLineSegment::Curve { on_row, to_column, .. }) => { if *on_row < ending_row { if *to_column != final_destination { segments.push(CommitLineSegment::Straight { to_row: ending_row - 1, }); segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } else { segments.push(CommitLineSegment::Straight { to_row: ending_row, }); } } else if *to_column != final_destination { segments.push(CommitLineSegment::Curve { to_column: final_destination, on_row: ending_row, curve_kind: CurveKind::Checkout, }); } } _ => {} } segments }, }) } LaneState::Empty => None, } } fn is_empty(&self) -> bool { match self { LaneState::Empty => true, LaneState::Active { .. } => false, } } } struct CommitEntry { data: Arc<InitialGraphCommitData>, lane: usize, color_idx: usize, } type ActiveLaneIdx = usize; enum AllCommitCount { NotLoaded, Loaded(usize), } #[derive(Debug)] enum CurveKind { Merge, Checkout, } #[derive(Debug)] enum CommitLineSegment { Straight { to_row: usize, }, Curve { to_column: usize, on_row: usize, curve_kind: CurveKind, }, } #[derive(Debug)] struct CommitLine { #[cfg(test)] child: Oid, #[cfg(test)] parent: Oid, child_column: usize, full_interval: Range<usize>, color_idx: usize, segments: SmallVec<[CommitLineSegment; 1]>, } impl CommitLine { fn get_first_visible_segment_idx(&self, first_visible_row: usize) -> Option<(usize, usize)> { if first_visible_row > self.full_interval.end { return None; } else if first_visible_row <= self.full_interval.start { return Some((0, self.child_column)); } let mut current_column = self.child_column; for (idx, segment) in self.segments.iter().enumerate() { match segment { CommitLineSegment::Straight { to_row } => { if *to_row >= first_visible_row { return Some((idx, current_column)); }