7555 lines · 278.2 KB · Rust
RawDownload
pub use crate::commit_context_menu::{CopyCommitSha, CopyCommitTag, OpenCommitView};use crate::{    commit_context_menu::{CommitContextMenuData, CommitContextMenuSource, commit_context_menu},    commit_tooltip::CommitAvatar,    commit_view::CommitView,    git_status_icon,};use collections::{BTreeMap, HashMap, IndexSet};use editor::Editor;use file_icons::FileIcons;use git::{    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, ParsedGitRemote,    parse_git_remote_url,    repository::{        CommitDiff, CommitFile, InitialGraphCommitData, LogOrder, LogSource, RepoPath,        SearchCommitArgs,    },    status::{FileStatus, StatusCode, TrackedStatus},};use gpui::{    Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, DismissEvent,    DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla,    MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy,    ScrollWheelEvent, SharedString, Subscription, Task, TextStyleRefinement,    UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*,    px, uniform_list,};use language::line_diff;use markdown::{Markdown, MarkdownElement};use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};use picker::{Picker, PickerDelegate};use project::{    ProjectPath,    git_store::{        CommitDataState, GitGraphEvent, GitStore, GitStoreEvent, GraphDataResponse, Repository,        RepositoryEvent, RepositoryId,    },};use search::{    SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch,    ToggleCaseSensitive, buffer_search,};use smallvec::{SmallVec, smallvec};use std::{    cell::Cell,    ops::Range,    rc::Rc,    sync::{Arc, OnceLock},    time::{Duration, Instant},}; use theme::AccentColors;use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};use ui::{    Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, DiffStat, Divider,    HeaderResizeInfo, HighlightedLabel, IndentGuideColors, ListItem, ListItemSpacing,    RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState,    TableRenderContext, TableResizeBehavior, Tooltip, WithScrollbar, bind_redistributable_columns,    prelude::*, redistribute_hidden_fractions, redistribute_hidden_widths,    render_redistributable_columns_resize_handles, render_table_header, table_row::TableRow,};use util::{ResultExt, debug_panic};use workspace::{    ModalView, Workspace,    item::{Item, ItemEvent, 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);const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.);const TREE_INDENT: f32 = 20.0;const TABLE_COLUMN_COUNT: usize = 4;const ROW_VERTICAL_PADDING: Pixels = px(4.0); 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; struct CommitTagPicker {    picker: Entity<Picker<CommitTagPickerDelegate>>,} impl CommitTagPicker {    fn new(tag_names: Vec<SharedString>, window: &mut Window, cx: &mut Context<Self>) -> Self {        let delegate = CommitTagPickerDelegate {            picker: cx.entity().downgrade(),            tag_names,            selected_index: 0,        };        let picker = cx.new(|cx| {            Picker::nonsearchable_uniform_list(delegate, window, cx)                .initial_width(COMMIT_TAG_LIST_WIDTH_IN_REMS)        });        Self { picker }    }} impl EventEmitter<DismissEvent> for CommitTagPicker {}impl ModalView for CommitTagPicker {} impl Focusable for CommitTagPicker {    fn focus_handle(&self, cx: &App) -> FocusHandle {        self.picker.focus_handle(cx)    }} impl Render for CommitTagPicker {    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {        v_flex().child(self.picker.clone())    }} struct CommitTagPickerDelegate {    picker: WeakEntity<CommitTagPicker>,    tag_names: Vec<SharedString>,    selected_index: usize,} impl PickerDelegate for CommitTagPickerDelegate {    type ListItem = ListItem;     fn name() -> &'static str {        "commit-tag"    }     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Copy Tag".into()    }     fn match_count(&self) -> usize {        self.tag_names.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) {        self.selected_index = ix;    }     fn update_matches(        &mut self,        _query: String,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        Task::ready(())    }     fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {        if let Some(tag_name) = self.tag_names.get(self.selected_index) {            cx.write_to_clipboard(ClipboardItem::new_string(tag_name.to_string()));        }        self.dismissed(window, cx);    }     fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {        self.picker            .update(cx, |_this, cx| cx.emit(DismissEvent))            .ok();    }     fn render_match(        &self,        ix: usize,        selected: bool,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) -> Option<Self::ListItem> {        Some(            ListItem::new(ix)                .inset(true)                .spacing(ListItemSpacing::Sparse)                .toggle_state(selected)                .child(Label::new(self.tag_names.get(ix)?.clone())),        )    }} #[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,        depth: usize,        directory_label: Option<SharedString>,        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();         ListItem::new(("changed-file", ix))            .spacing(ListItemSpacing::Sparse)            .indent_level(depth)            .indent_step_size(px(TREE_INDENT))            .start_slot(git_status_icon(self.status))            .child(                Label::new(file_name.clone())                    .size(LabelSize::Small)                    .truncate(),            )            .when_some(directory_label, |this, directory_label| {                this.child(                    Label::new(directory_label)                        .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()    }} enum ChangedFileTreeEntry {    Directory(ChangedFileDirectoryEntry),    File(ChangedFileTreeStatusEntry),} struct ChangedFileTreeStatusEntry {    entry: ChangedFileEntry,    depth: usize,} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]enum ChangedFilesViewMode {    Flat,    #[default]    Tree,} impl ChangedFilesViewMode {    fn toggled(self) -> Self {        match self {            Self::Flat => Self::Tree,            Self::Tree => Self::Flat,        }    }     fn is_tree(self) -> bool {        matches!(self, Self::Tree)    }} struct ChangedFileDirectoryEntry {    path: RepoPath,    name: SharedString,    depth: usize,    expanded: bool,} impl ChangedFileDirectoryEntry {    fn render(&self, ix: usize, git_graph: WeakEntity<GitGraph>, cx: &App) -> AnyElement {        let path = self.path.clone();        let expanded = self.expanded;        let folder_icon = FileIcons::get_folder_icon(expanded, path.as_std_path(), cx)            .map(|icon| {                Icon::from_path(icon)                    .size(IconSize::Small)                    .color(Color::Muted)            })            .unwrap_or_else(|| {                let icon = if expanded {                    IconName::FolderOpen                } else {                    IconName::Folder                };                Icon::new(icon).size(IconSize::Small).color(Color::Muted)            });         ListItem::new(("changed-file-dir", ix))            .spacing(ListItemSpacing::Sparse)            .indent_level(self.depth)            .indent_step_size(px(TREE_INDENT))            .start_slot(folder_icon)            .child(                Label::new(self.name.clone())                    .size(LabelSize::Small)                    .color(Color::Muted)                    .truncate(),            )            .tooltip({                let name = self.name.clone();                move |_, cx| Tooltip::with_meta("Toggle Folder", None, name.clone(), cx)            })            .on_click(move |_, _, cx| {                git_graph                    .update(cx, |git_graph, cx| {                        git_graph                            .changed_files_expanded_dirs                            .insert(path.clone(), !expanded);                        cx.notify();                    })                    .ok();            })            .into_any_element()    }} #[derive(Default)]struct ChangedFileTreeNode {    name: SharedString,    path: Option<RepoPath>,    children: BTreeMap<SharedString, ChangedFileTreeNode>,    files: Vec<ChangedFileEntry>,} fn build_changed_file_tree_entries(    mut files: Vec<ChangedFileEntry>,    expanded_dirs: &HashMap<RepoPath, bool>,) -> Vec<ChangedFileTreeEntry> {    files.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));     let mut root = ChangedFileTreeNode::default();    for file in files {        let components: Vec<&str> = file.repo_path.components().collect();        if components.is_empty() {            root.files.push(file);            continue;        }         let mut current = &mut root;        let mut current_path = String::new();         for (ix, component) in components.iter().enumerate() {            if ix == components.len() - 1 {                current.files.push(file.clone());            } else {                if !current_path.is_empty() {                    current_path.push('/');                }                current_path.push_str(component);                 let Ok(dir_path) = RepoPath::new(&current_path) else {                    continue;                };                let component = SharedString::from(component.to_string());                 current = current                    .children                    .entry(component.clone())                    .or_insert_with(|| ChangedFileTreeNode {                        name: component,                        path: Some(dir_path),                        ..Default::default()                    });            }        }    }     flatten_changed_file_tree(&root, 0, expanded_dirs)} fn flatten_changed_file_tree(    node: &ChangedFileTreeNode,    depth: usize,    expanded_dirs: &HashMap<RepoPath, bool>,) -> Vec<ChangedFileTreeEntry> {    let mut entries = Vec::new();     for child in node.children.values() {        let (terminal, name) = compact_changed_file_directory_chain(child);        let Some(path) = terminal.path.clone().or_else(|| child.path.clone()) else {            continue;        };        let expanded = *expanded_dirs.get(&path).unwrap_or(&true);        let child_entries = flatten_changed_file_tree(terminal, depth + 1, expanded_dirs);         entries.push(ChangedFileTreeEntry::Directory(ChangedFileDirectoryEntry {            path,            name,            depth,            expanded,        }));         if expanded {            entries.extend(child_entries);        }    }     entries.extend(        node.files            .iter()            .cloned()            .map(|entry| ChangedFileTreeEntry::File(ChangedFileTreeStatusEntry { entry, depth })),    );    entries} fn compact_changed_file_directory_chain(    mut node: &ChangedFileTreeNode,) -> (&ChangedFileTreeNode, SharedString) {    let mut parts = vec![node.name.clone()];    while node.files.is_empty() && node.children.len() == 1 {        let Some(child) = node.children.values().next() else {            continue;

Lines 1–500 of 7555.