1246 lines · 65.2 KB · Rust
RawDownload
use std::{    any::{Any, TypeId},    cmp::Ordering,    collections::HashSet,    ops::Range,    time::Duration,}; use anyhow::Context as _;use collections::{BTreeMap, HashMap};use feature_flags::FeatureFlagAppExt;use git::{    diff::{BufferDiff, DiffHunk},    repository::GitFileStatus,};use gpui::{    actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,    InteractiveElement, Model, Render, Subscription, Task, View, WeakView,};use language::{Buffer, BufferRow};use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer};use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};use text::{OffsetRangeExt, ToPoint};use theme::ActiveTheme;use ui::{    div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,    ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext,};use util::{paths::compare_paths, ResultExt};use workspace::{    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},    ItemNavHistory, ToolbarItemLocation, Workspace,}; use crate::{Editor, EditorEvent, DEFAULT_MULTIBUFFER_CONTEXT}; actions!(project_diff, [Deploy]); pub fn init(cx: &mut AppContext) {    cx.observe_new_views(ProjectDiffEditor::register).detach();} const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); struct ProjectDiffEditor {    buffer_changes: BTreeMap<WorktreeId, HashMap<ProjectEntryId, Changes>>,    entry_order: HashMap<WorktreeId, Vec<(ProjectPath, ProjectEntryId)>>,    excerpts: Model<MultiBuffer>,    editor: View<Editor>,     project: Model<Project>,    workspace: WeakView<Workspace>,    focus_handle: FocusHandle,    worktree_rescans: HashMap<WorktreeId, Task<()>>,    _subscriptions: Vec<Subscription>,} #[derive(Debug)]struct Changes {    _status: GitFileStatus,    buffer: Model<Buffer>,    hunks: Vec<DiffHunk>,} impl ProjectDiffEditor {    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {        workspace.register_action(Self::deploy);    }     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {        if !cx.is_staff() {            return;        }         if let Some(existing) = workspace.item_of_type::<Self>(cx) {            workspace.activate_item(&existing, true, true, cx);        } else {            let workspace_handle = cx.view().downgrade();            let project_diff =                cx.new_view(|cx| Self::new(workspace.project().clone(), workspace_handle, cx));            workspace.add_item_to_active_pane(Box::new(project_diff), None, true, cx);        }    }     fn new(        project: Model<Project>,        workspace: WeakView<Workspace>,        cx: &mut ViewContext<Self>,    ) -> Self {        // TODO diff change subscriptions. For that, needed:        // * `-20/+50` stats retrieval: some background process that reacts on file changes        let focus_handle = cx.focus_handle();        let changed_entries_subscription =            cx.subscribe(&project, |project_diff_editor, _, e, cx| {                let mut worktree_to_rescan = None;                match e {                    project::Event::WorktreeAdded(id) => {                        worktree_to_rescan = Some(*id);                        // project_diff_editor                        //     .buffer_changes                        //     .insert(*id, HashMap::default());                    }                    project::Event::WorktreeRemoved(id) => {                        project_diff_editor.buffer_changes.remove(id);                    }                    project::Event::WorktreeUpdatedEntries(id, _updated_entries) => {                        // TODO cannot invalidate buffer entries without invalidating the corresponding excerpts and order entries.                        worktree_to_rescan = Some(*id);                        // let entry_changes =                        //     project_diff_editor.buffer_changes.entry(*id).or_default();                        // for (_, entry_id, change) in updated_entries.iter() {                        //     let changes = entry_changes.entry(*entry_id);                        //     match change {                        //         project::PathChange::Removed => {                        //             if let hash_map::Entry::Occupied(entry) = changes {                        //                 entry.remove();                        //             }                        //         }                        //         // TODO understand the invalidation case better: now, we do that but still rescan the entire worktree                        //         // What if we already have the buffer loaded inside the diff multi buffer and it was edited there? We should not do anything.                        //         _ => match changes {                        //             hash_map::Entry::Occupied(mut o) => o.get_mut().invalidate(),                        //             hash_map::Entry::Vacant(v) => {                        //                 v.insert(None);                        //             }                        //         },                        //     }                        // }                    }                    project::Event::WorktreeUpdatedGitRepositories(id) => {                        worktree_to_rescan = Some(*id);                        // project_diff_editor.buffer_changes.clear();                    }                    project::Event::DeletedEntry(id, _entry_id) => {                        worktree_to_rescan = Some(*id);                        // if let Some(entries) = project_diff_editor.buffer_changes.get_mut(id) {                        //     entries.remove(entry_id);                        // }                    }                    project::Event::Closed => {                        project_diff_editor.buffer_changes.clear();                    }                    _ => {}                }                 if let Some(worktree_to_rescan) = worktree_to_rescan {                    project_diff_editor.schedule_worktree_rescan(worktree_to_rescan, cx);                }            });         let excerpts = cx.new_model(|cx| MultiBuffer::new(project.read(cx).capability()));         let editor = cx.new_view(|cx| {            let mut diff_display_editor =                Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx);            diff_display_editor.set_expand_all_diff_hunks();            diff_display_editor        });         let mut new_self = Self {            project,            workspace,            buffer_changes: BTreeMap::default(),            entry_order: HashMap::default(),            worktree_rescans: HashMap::default(),            focus_handle,            editor,            excerpts,            _subscriptions: vec![changed_entries_subscription],        };        new_self.schedule_rescan_all(cx);        new_self    }     fn schedule_rescan_all(&mut self, cx: &mut ViewContext<Self>) {        let mut current_worktrees = HashSet::<WorktreeId>::default();        for worktree in self.project.read(cx).worktrees(cx).collect::<Vec<_>>() {            let worktree_id = worktree.read(cx).id();            current_worktrees.insert(worktree_id);            self.schedule_worktree_rescan(worktree_id, cx);        }         self.worktree_rescans            .retain(|worktree_id, _| current_worktrees.contains(worktree_id));        self.buffer_changes            .retain(|worktree_id, _| current_worktrees.contains(worktree_id));        self.entry_order            .retain(|worktree_id, _| current_worktrees.contains(worktree_id));    }     fn schedule_worktree_rescan(&mut self, id: WorktreeId, cx: &mut ViewContext<Self>) {        let project = self.project.clone();        self.worktree_rescans.insert(            id,            cx.spawn(|project_diff_editor, mut cx| async move {                cx.background_executor().timer(UPDATE_DEBOUNCE).await;                let open_tasks = project                    .update(&mut cx, |project, cx| {                        let worktree = project.worktree_for_id(id, cx)?;                        let applicable_entries = worktree                            .read(cx)                            .entries(false, 0)                            .filter(|entry| !entry.is_external)                            .filter(|entry| entry.is_file())                            .filter_map(|entry| Some((entry.git_status?, entry)))                            .filter_map(|(git_status, entry)| {                                Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?))                            })                            .collect::<Vec<_>>();                        Some(                            applicable_entries                                .into_iter()                                .map(|(status, entry_id, entry_path)| {                                    let open_task = project.open_path(entry_path.clone(), cx);                                    (status, entry_id, entry_path, open_task)                                })                                .collect::<Vec<_>>(),                        )                    })                    .ok()                    .flatten()                    .unwrap_or_default();                 let Some((buffers, mut new_entries, change_sets)) = cx                    .spawn(|mut cx| async move {                        let mut new_entries = Vec::new();                        let mut buffers = HashMap::<                            ProjectEntryId,                            (                                GitFileStatus,                                text::BufferSnapshot,                                Model<Buffer>,                                BufferDiff,                            ),                        >::default();                        let mut change_sets = Vec::new();                        for (status, entry_id, entry_path, open_task) in open_tasks {                            let (_, opened_model) = open_task.await.with_context(|| {                                format!("loading buffer {:?} for git diff", entry_path.path)                            })?;                            let buffer = match opened_model.downcast::<Buffer>() {                                Ok(buffer) => buffer,                                Err(_model) => anyhow::bail!(                                    "Could not load {:?} as a buffer for git diff",                                    entry_path.path                                ),                            };                             let change_set = project                                .update(&mut cx, |project, cx| {                                    project.open_unstaged_changes(buffer.clone(), cx)                                })?                                .await?;                             cx.update(|cx| {                                buffers.insert(                                    entry_id,                                    (                                        status,                                        buffer.read(cx).text_snapshot(),                                        buffer,                                        change_set.read(cx).diff_to_buffer.clone(),                                    ),                                );                            })?;                            change_sets.push(change_set);                            new_entries.push((entry_path, entry_id));                        }                         Ok((buffers, new_entries, change_sets))                    })                    .await                    .log_err()                else {                    return;                };                 let (new_changes, new_entry_order) = cx                    .background_executor()                    .spawn(async move {                        let mut new_changes = HashMap::<ProjectEntryId, Changes>::default();                        for (entry_id, (status, buffer_snapshot, buffer, buffer_diff)) in buffers {                            new_changes.insert(                                entry_id,                                Changes {                                    _status: status,                                    buffer,                                    hunks: buffer_diff                                        .hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)                                        .collect::<Vec<_>>(),                                },                            );                        }                         new_entries.sort_by(|(project_path_a, _), (project_path_b, _)| {                            compare_paths(                                (project_path_a.path.as_ref(), true),                                (project_path_b.path.as_ref(), true),                            )                        });                        (new_changes, new_entries)                    })                    .await;                 project_diff_editor                    .update(&mut cx, |project_diff_editor, cx| {                        project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);                        for change_set in change_sets {                            project_diff_editor.editor.update(cx, |editor, cx| {                                editor.diff_map.add_change_set(change_set, cx)                            });                        }                    })                    .ok();            }),        );    }     fn update_excerpts(        &mut self,        worktree_id: WorktreeId,        new_changes: HashMap<ProjectEntryId, Changes>,        new_entry_order: Vec<(ProjectPath, ProjectEntryId)>,        cx: &mut ViewContext<ProjectDiffEditor>,    ) {        if let Some(current_order) = self.entry_order.get(&worktree_id) {            let current_entries = self.buffer_changes.entry(worktree_id).or_default();            let mut new_order_entries = new_entry_order.iter().fuse().peekable();            let mut excerpts_to_remove = Vec::new();            let mut new_excerpt_hunks = BTreeMap::<                ExcerptId,                Vec<(ProjectPath, Model<Buffer>, Vec<Range<text::Anchor>>)>,            >::new();            let mut excerpt_to_expand =                HashMap::<(u32, ExpandExcerptDirection), Vec<ExcerptId>>::default();            let mut latest_excerpt_id = ExcerptId::min();             for (current_path, current_entry_id) in current_order {                let current_changes = match current_entries.get(current_entry_id) {                    Some(current_changes) => {                        if current_changes.hunks.is_empty() {                            continue;                        }                        current_changes                    }                    None => continue,                };                let buffer_excerpts = self                    .excerpts                    .read(cx)                    .excerpts_for_buffer(&current_changes.buffer, cx);                let last_current_excerpt_id =                    buffer_excerpts.last().map(|(excerpt_id, _)| *excerpt_id);                let mut current_excerpts = buffer_excerpts.into_iter().fuse().peekable();                loop {                    match new_order_entries.peek() {                        Some((new_path, new_entry)) => {                            match compare_paths(                                (current_path.path.as_ref(), true),                                (new_path.path.as_ref(), true),                            ) {                                Ordering::Less => {                                    excerpts_to_remove                                        .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id));                                    break;                                }                                Ordering::Greater => {                                    if let Some(new_changes) = new_changes.get(new_entry) {                                        if !new_changes.hunks.is_empty() {                                            let hunks = new_excerpt_hunks                                                .entry(latest_excerpt_id)                                                .or_default();                                            match hunks.binary_search_by(|(probe, ..)| {                                                compare_paths(                                                    (new_path.path.as_ref(), true),                                                    (probe.path.as_ref(), true),                                                )                                            }) {                                                Ok(i) => hunks[i].2.extend(                                                    new_changes                                                        .hunks                                                        .iter()                                                        .map(|hunk| hunk.buffer_range.clone()),                                                ),                                                Err(i) => hunks.insert(                                                    i,                                                    (                                                        new_path.clone(),                                                        new_changes.buffer.clone(),                                                        new_changes                                                            .hunks                                                            .iter()                                                            .map(|hunk| hunk.buffer_range.clone())                                                            .collect(),                                                    ),                                                ),                                            }                                        }                                    };                                    let _ = new_order_entries.next();                                }                                Ordering::Equal => {                                    match new_changes.get(new_entry) {                                        Some(new_changes) => {                                            let buffer_snapshot =                                                new_changes.buffer.read(cx).snapshot();                                            let mut current_hunks =                                                current_changes.hunks.iter().fuse().peekable();                                            let mut new_hunks_unchanged =                                                Vec::with_capacity(new_changes.hunks.len());                                            let mut new_hunks_with_updates =                                                Vec::with_capacity(new_changes.hunks.len());                                            'new_changes: for new_hunk in &new_changes.hunks {                                                loop {                                                    match current_hunks.peek() {                                                        Some(current_hunk) => {                                                            match (                                                                current_hunk                                                                    .buffer_range                                                                    .start                                                                    .cmp(                                                                        &new_hunk                                                                            .buffer_range                                                                            .start,                                                                        &buffer_snapshot,                                                                    ),                                                                current_hunk.buffer_range.end.cmp(                                                                    &new_hunk.buffer_range.end,                                                                    &buffer_snapshot,                                                                ),                                                            ) {                                                                (                                                                    Ordering::Equal,                                                                    Ordering::Equal,                                                                ) => {                                                                    new_hunks_unchanged                                                                        .push(new_hunk);                                                                    let _ = current_hunks.next();                                                                    continue 'new_changes;                                                                }                                                                (Ordering::Equal, _)                                                                | (_, Ordering::Equal) => {                                                                    new_hunks_with_updates                                                                        .push(new_hunk);                                                                    continue 'new_changes;                                                                }                                                                (                                                                    Ordering::Less,                                                                    Ordering::Greater,                                                                )                                                                | (                                                                    Ordering::Greater,                                                                    Ordering::Less,                                                                ) => {                                                                    new_hunks_with_updates                                                                        .push(new_hunk);                                                                    continue 'new_changes;                                                                }                                                                (                                                                    Ordering::Less,                                                                    Ordering::Less,                                                                ) => {                                                                    if current_hunk                                                                        .buffer_range                                                                        .start                                                                        .cmp(                                                                            &new_hunk                                                                                .buffer_range                                                                                .end,                                                                            &buffer_snapshot,                                                                        )                                                                        .is_le()                                                                    {                                                                        new_hunks_with_updates                                                                            .push(new_hunk);                                                                        continue 'new_changes;                                                                    } else {                                                                        let _ =                                                                            current_hunks.next();                                                                    }                                                                }                                                                (                                                                    Ordering::Greater,                                                                    Ordering::Greater,                                                                ) => {                                                                    if current_hunk                                                                        .buffer_range                                                                        .end                                                                        .cmp(                                                                            &new_hunk                                                                                .buffer_range                                                                                .start,                                                                            &buffer_snapshot,                                                                        )                                                                        .is_ge()                                                                    {                                                                        new_hunks_with_updates                                                                            .push(new_hunk);                                                                        continue 'new_changes;                                                                    } else {

Lines 1–500 of 1246.