project_diff.rs

   1use std::{
   2    any::{Any, TypeId},
   3    cmp::Ordering,
   4    collections::HashSet,
   5    ops::Range,
   6    time::Duration,
   7};
   8
   9use anyhow::{anyhow, Context as _};
  10use collections::{BTreeMap, HashMap};
  11use feature_flags::FeatureFlagAppExt;
  12use git::diff::{BufferDiff, DiffHunk};
  13use gpui::{
  14    actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
  15    InteractiveElement, Model, Render, Subscription, Task, View, WeakView,
  16};
  17use language::{Buffer, BufferRow};
  18use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer};
  19use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
  20use text::{OffsetRangeExt, ToPoint};
  21use theme::ActiveTheme;
  22use ui::prelude::*;
  23use util::{paths::compare_paths, ResultExt};
  24use workspace::{
  25    item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
  26    ItemNavHistory, ToolbarItemLocation, Workspace,
  27};
  28
  29use crate::{Editor, EditorEvent, DEFAULT_MULTIBUFFER_CONTEXT};
  30
  31actions!(project_diff, [Deploy]);
  32
  33pub fn init(cx: &mut AppContext) {
  34    cx.observe_new_views(ProjectDiffEditor::register).detach();
  35}
  36
  37const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
  38
  39struct ProjectDiffEditor {
  40    buffer_changes: BTreeMap<WorktreeId, HashMap<ProjectEntryId, Changes>>,
  41    entry_order: HashMap<WorktreeId, Vec<(ProjectPath, ProjectEntryId)>>,
  42    excerpts: Model<MultiBuffer>,
  43    editor: View<Editor>,
  44
  45    project: Model<Project>,
  46    workspace: WeakView<Workspace>,
  47    focus_handle: FocusHandle,
  48    worktree_rescans: HashMap<WorktreeId, Task<()>>,
  49    _subscriptions: Vec<Subscription>,
  50}
  51
  52#[derive(Debug)]
  53struct Changes {
  54    buffer: Model<Buffer>,
  55    hunks: Vec<DiffHunk>,
  56}
  57
  58impl ProjectDiffEditor {
  59    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  60        workspace.register_action(Self::deploy);
  61    }
  62
  63    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
  64        if !cx.is_staff() {
  65            return;
  66        }
  67
  68        if let Some(existing) = workspace.item_of_type::<Self>(cx) {
  69            workspace.activate_item(&existing, true, true, cx);
  70        } else {
  71            let workspace_handle = cx.view().downgrade();
  72            let project_diff =
  73                cx.new_view(|cx| Self::new(workspace.project().clone(), workspace_handle, cx));
  74            workspace.add_item_to_active_pane(Box::new(project_diff), None, true, cx);
  75        }
  76    }
  77
  78    fn new(
  79        project: Model<Project>,
  80        workspace: WeakView<Workspace>,
  81        cx: &mut ViewContext<Self>,
  82    ) -> Self {
  83        // TODO diff change subscriptions. For that, needed:
  84        // * `-20/+50` stats retrieval: some background process that reacts on file changes
  85        let focus_handle = cx.focus_handle();
  86        let changed_entries_subscription =
  87            cx.subscribe(&project, |project_diff_editor, _, e, cx| {
  88                let mut worktree_to_rescan = None;
  89                match e {
  90                    project::Event::WorktreeAdded(id) => {
  91                        worktree_to_rescan = Some(*id);
  92                        // project_diff_editor
  93                        //     .buffer_changes
  94                        //     .insert(*id, HashMap::default());
  95                    }
  96                    project::Event::WorktreeRemoved(id) => {
  97                        project_diff_editor.buffer_changes.remove(id);
  98                    }
  99                    project::Event::WorktreeUpdatedEntries(id, _updated_entries) => {
 100                        // TODO cannot invalidate buffer entries without invalidating the corresponding excerpts and order entries.
 101                        worktree_to_rescan = Some(*id);
 102                        // let entry_changes =
 103                        //     project_diff_editor.buffer_changes.entry(*id).or_default();
 104                        // for (_, entry_id, change) in updated_entries.iter() {
 105                        //     let changes = entry_changes.entry(*entry_id);
 106                        //     match change {
 107                        //         project::PathChange::Removed => {
 108                        //             if let hash_map::Entry::Occupied(entry) = changes {
 109                        //                 entry.remove();
 110                        //             }
 111                        //         }
 112                        //         // TODO understand the invalidation case better: now, we do that but still rescan the entire worktree
 113                        //         // What if we already have the buffer loaded inside the diff multi buffer and it was edited there? We should not do anything.
 114                        //         _ => match changes {
 115                        //             hash_map::Entry::Occupied(mut o) => o.get_mut().invalidate(),
 116                        //             hash_map::Entry::Vacant(v) => {
 117                        //                 v.insert(None);
 118                        //             }
 119                        //         },
 120                        //     }
 121                        // }
 122                    }
 123                    project::Event::WorktreeUpdatedGitRepositories(id) => {
 124                        worktree_to_rescan = Some(*id);
 125                        // project_diff_editor.buffer_changes.clear();
 126                    }
 127                    project::Event::DeletedEntry(id, _entry_id) => {
 128                        worktree_to_rescan = Some(*id);
 129                        // if let Some(entries) = project_diff_editor.buffer_changes.get_mut(id) {
 130                        //     entries.remove(entry_id);
 131                        // }
 132                    }
 133                    project::Event::Closed => {
 134                        project_diff_editor.buffer_changes.clear();
 135                    }
 136                    _ => {}
 137                }
 138
 139                if let Some(worktree_to_rescan) = worktree_to_rescan {
 140                    project_diff_editor.schedule_worktree_rescan(worktree_to_rescan, cx);
 141                }
 142            });
 143
 144        let excerpts = cx.new_model(|cx| MultiBuffer::new(project.read(cx).capability()));
 145
 146        let editor = cx.new_view(|cx| {
 147            let mut diff_display_editor =
 148                Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx);
 149            diff_display_editor.set_expand_all_diff_hunks(cx);
 150            diff_display_editor
 151        });
 152
 153        let mut new_self = Self {
 154            project,
 155            workspace,
 156            buffer_changes: BTreeMap::default(),
 157            entry_order: HashMap::default(),
 158            worktree_rescans: HashMap::default(),
 159            focus_handle,
 160            editor,
 161            excerpts,
 162            _subscriptions: vec![changed_entries_subscription],
 163        };
 164        new_self.schedule_rescan_all(cx);
 165        new_self
 166    }
 167
 168    fn schedule_rescan_all(&mut self, cx: &mut ViewContext<Self>) {
 169        let mut current_worktrees = HashSet::<WorktreeId>::default();
 170        for worktree in self.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
 171            let worktree_id = worktree.read(cx).id();
 172            current_worktrees.insert(worktree_id);
 173            self.schedule_worktree_rescan(worktree_id, cx);
 174        }
 175
 176        self.worktree_rescans
 177            .retain(|worktree_id, _| current_worktrees.contains(worktree_id));
 178        self.buffer_changes
 179            .retain(|worktree_id, _| current_worktrees.contains(worktree_id));
 180        self.entry_order
 181            .retain(|worktree_id, _| current_worktrees.contains(worktree_id));
 182    }
 183
 184    fn schedule_worktree_rescan(&mut self, id: WorktreeId, cx: &mut ViewContext<Self>) {
 185        let project = self.project.clone();
 186        self.worktree_rescans.insert(
 187            id,
 188            cx.spawn(|project_diff_editor, mut cx| async move {
 189                cx.background_executor().timer(UPDATE_DEBOUNCE).await;
 190                let open_tasks = project
 191                    .update(&mut cx, |project, cx| {
 192                        let worktree = project.worktree_for_id(id, cx)?;
 193                        let snapshot = worktree.read(cx).snapshot();
 194                        let applicable_entries = snapshot
 195                            .repositories()
 196                            .iter()
 197                            .flat_map(|entry| {
 198                                entry
 199                                    .status()
 200                                    .map(|git_entry| entry.join(git_entry.repo_path))
 201                            })
 202                            .filter_map(|path| {
 203                                let id = snapshot.entry_for_path(&path)?.id;
 204                                Some((
 205                                    id,
 206                                    ProjectPath {
 207                                        worktree_id: snapshot.id(),
 208                                        path: path.into(),
 209                                    },
 210                                ))
 211                            })
 212                            .collect::<Vec<_>>();
 213                        Some(
 214                            applicable_entries
 215                                .into_iter()
 216                                .map(|(entry_id, entry_path)| {
 217                                    let open_task = project.open_path(entry_path.clone(), cx);
 218                                    (entry_id, entry_path, open_task)
 219                                })
 220                                .collect::<Vec<_>>(),
 221                        )
 222                    })
 223                    .ok()
 224                    .flatten()
 225                    .unwrap_or_default();
 226
 227                let Some((buffers, mut new_entries, change_sets)) = cx
 228                    .spawn(|mut cx| async move {
 229                        let mut new_entries = Vec::new();
 230                        let mut buffers = HashMap::<
 231                            ProjectEntryId,
 232                            (text::BufferSnapshot, Model<Buffer>, BufferDiff),
 233                        >::default();
 234                        let mut change_sets = Vec::new();
 235                        for (entry_id, entry_path, open_task) in open_tasks {
 236                            let Some(buffer) = open_task
 237                                .await
 238                                .and_then(|(_, opened_model)| {
 239                                    opened_model
 240                                        .downcast::<Buffer>()
 241                                        .map_err(|_| anyhow!("Unexpected non-buffer"))
 242                                })
 243                                .with_context(|| {
 244                                    format!("loading {:?} for git diff", entry_path.path)
 245                                })
 246                                .log_err()
 247                            else {
 248                                continue;
 249                            };
 250
 251                            let Some(change_set) = project
 252                                .update(&mut cx, |project, cx| {
 253                                    project.open_unstaged_changes(buffer.clone(), cx)
 254                                })?
 255                                .await
 256                                .log_err()
 257                            else {
 258                                continue;
 259                            };
 260
 261                            cx.update(|cx| {
 262                                buffers.insert(
 263                                    entry_id,
 264                                    (
 265                                        buffer.read(cx).text_snapshot(),
 266                                        buffer,
 267                                        change_set.read(cx).diff_to_buffer.clone(),
 268                                    ),
 269                                );
 270                            })?;
 271                            change_sets.push(change_set);
 272                            new_entries.push((entry_path, entry_id));
 273                        }
 274
 275                        anyhow::Ok((buffers, new_entries, change_sets))
 276                    })
 277                    .await
 278                    .log_err()
 279                else {
 280                    return;
 281                };
 282
 283                let (new_changes, new_entry_order) = cx
 284                    .background_executor()
 285                    .spawn(async move {
 286                        let mut new_changes = HashMap::<ProjectEntryId, Changes>::default();
 287                        for (entry_id, (buffer_snapshot, buffer, buffer_diff)) in buffers {
 288                            new_changes.insert(
 289                                entry_id,
 290                                Changes {
 291                                    buffer,
 292                                    hunks: buffer_diff
 293                                        .hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot)
 294                                        .collect::<Vec<_>>(),
 295                                },
 296                            );
 297                        }
 298
 299                        new_entries.sort_by(|(project_path_a, _), (project_path_b, _)| {
 300                            compare_paths(
 301                                (project_path_a.path.as_ref(), true),
 302                                (project_path_b.path.as_ref(), true),
 303                            )
 304                        });
 305                        (new_changes, new_entries)
 306                    })
 307                    .await;
 308
 309                project_diff_editor
 310                    .update(&mut cx, |project_diff_editor, cx| {
 311                        project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
 312                        project_diff_editor.editor.update(cx, |editor, cx| {
 313                            editor.buffer.update(cx, |buffer, cx| {
 314                                for change_set in change_sets {
 315                                    buffer.add_change_set(change_set, cx)
 316                                }
 317                            });
 318                        });
 319                    })
 320                    .ok();
 321            }),
 322        );
 323    }
 324
 325    fn update_excerpts(
 326        &mut self,
 327        worktree_id: WorktreeId,
 328        new_changes: HashMap<ProjectEntryId, Changes>,
 329        new_entry_order: Vec<(ProjectPath, ProjectEntryId)>,
 330        cx: &mut ViewContext<ProjectDiffEditor>,
 331    ) {
 332        if let Some(current_order) = self.entry_order.get(&worktree_id) {
 333            let current_entries = self.buffer_changes.entry(worktree_id).or_default();
 334            let mut new_order_entries = new_entry_order.iter().fuse().peekable();
 335            let mut excerpts_to_remove = Vec::new();
 336            let mut new_excerpt_hunks = BTreeMap::<
 337                ExcerptId,
 338                Vec<(ProjectPath, Model<Buffer>, Vec<Range<text::Anchor>>)>,
 339            >::new();
 340            let mut excerpt_to_expand =
 341                HashMap::<(u32, ExpandExcerptDirection), Vec<ExcerptId>>::default();
 342            let mut latest_excerpt_id = ExcerptId::min();
 343
 344            for (current_path, current_entry_id) in current_order {
 345                let current_changes = match current_entries.get(current_entry_id) {
 346                    Some(current_changes) => {
 347                        if current_changes.hunks.is_empty() {
 348                            continue;
 349                        }
 350                        current_changes
 351                    }
 352                    None => continue,
 353                };
 354                let buffer_excerpts = self
 355                    .excerpts
 356                    .read(cx)
 357                    .excerpts_for_buffer(&current_changes.buffer, cx);
 358                let last_current_excerpt_id =
 359                    buffer_excerpts.last().map(|(excerpt_id, _)| *excerpt_id);
 360                let mut current_excerpts = buffer_excerpts.into_iter().fuse().peekable();
 361                loop {
 362                    match new_order_entries.peek() {
 363                        Some((new_path, new_entry)) => {
 364                            match compare_paths(
 365                                (current_path.path.as_ref(), true),
 366                                (new_path.path.as_ref(), true),
 367                            ) {
 368                                Ordering::Less => {
 369                                    excerpts_to_remove
 370                                        .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id));
 371                                    break;
 372                                }
 373                                Ordering::Greater => {
 374                                    if let Some(new_changes) = new_changes.get(new_entry) {
 375                                        if !new_changes.hunks.is_empty() {
 376                                            let hunks = new_excerpt_hunks
 377                                                .entry(latest_excerpt_id)
 378                                                .or_default();
 379                                            match hunks.binary_search_by(|(probe, ..)| {
 380                                                compare_paths(
 381                                                    (new_path.path.as_ref(), true),
 382                                                    (probe.path.as_ref(), true),
 383                                                )
 384                                            }) {
 385                                                Ok(i) => hunks[i].2.extend(
 386                                                    new_changes
 387                                                        .hunks
 388                                                        .iter()
 389                                                        .map(|hunk| hunk.buffer_range.clone()),
 390                                                ),
 391                                                Err(i) => hunks.insert(
 392                                                    i,
 393                                                    (
 394                                                        new_path.clone(),
 395                                                        new_changes.buffer.clone(),
 396                                                        new_changes
 397                                                            .hunks
 398                                                            .iter()
 399                                                            .map(|hunk| hunk.buffer_range.clone())
 400                                                            .collect(),
 401                                                    ),
 402                                                ),
 403                                            }
 404                                        }
 405                                    };
 406                                    let _ = new_order_entries.next();
 407                                }
 408                                Ordering::Equal => {
 409                                    match new_changes.get(new_entry) {
 410                                        Some(new_changes) => {
 411                                            let buffer_snapshot =
 412                                                new_changes.buffer.read(cx).snapshot();
 413                                            let mut current_hunks =
 414                                                current_changes.hunks.iter().fuse().peekable();
 415                                            let mut new_hunks_unchanged =
 416                                                Vec::with_capacity(new_changes.hunks.len());
 417                                            let mut new_hunks_with_updates =
 418                                                Vec::with_capacity(new_changes.hunks.len());
 419                                            'new_changes: for new_hunk in &new_changes.hunks {
 420                                                loop {
 421                                                    match current_hunks.peek() {
 422                                                        Some(current_hunk) => {
 423                                                            match (
 424                                                                current_hunk
 425                                                                    .buffer_range
 426                                                                    .start
 427                                                                    .cmp(
 428                                                                        &new_hunk
 429                                                                            .buffer_range
 430                                                                            .start,
 431                                                                        &buffer_snapshot,
 432                                                                    ),
 433                                                                current_hunk.buffer_range.end.cmp(
 434                                                                    &new_hunk.buffer_range.end,
 435                                                                    &buffer_snapshot,
 436                                                                ),
 437                                                            ) {
 438                                                                (
 439                                                                    Ordering::Equal,
 440                                                                    Ordering::Equal,
 441                                                                ) => {
 442                                                                    new_hunks_unchanged
 443                                                                        .push(new_hunk);
 444                                                                    let _ = current_hunks.next();
 445                                                                    continue 'new_changes;
 446                                                                }
 447                                                                (Ordering::Equal, _)
 448                                                                | (_, Ordering::Equal) => {
 449                                                                    new_hunks_with_updates
 450                                                                        .push(new_hunk);
 451                                                                    continue 'new_changes;
 452                                                                }
 453                                                                (
 454                                                                    Ordering::Less,
 455                                                                    Ordering::Greater,
 456                                                                )
 457                                                                | (
 458                                                                    Ordering::Greater,
 459                                                                    Ordering::Less,
 460                                                                ) => {
 461                                                                    new_hunks_with_updates
 462                                                                        .push(new_hunk);
 463                                                                    continue 'new_changes;
 464                                                                }
 465                                                                (
 466                                                                    Ordering::Less,
 467                                                                    Ordering::Less,
 468                                                                ) => {
 469                                                                    if current_hunk
 470                                                                        .buffer_range
 471                                                                        .start
 472                                                                        .cmp(
 473                                                                            &new_hunk
 474                                                                                .buffer_range
 475                                                                                .end,
 476                                                                            &buffer_snapshot,
 477                                                                        )
 478                                                                        .is_le()
 479                                                                    {
 480                                                                        new_hunks_with_updates
 481                                                                            .push(new_hunk);
 482                                                                        continue 'new_changes;
 483                                                                    } else {
 484                                                                        let _ =
 485                                                                            current_hunks.next();
 486                                                                    }
 487                                                                }
 488                                                                (
 489                                                                    Ordering::Greater,
 490                                                                    Ordering::Greater,
 491                                                                ) => {
 492                                                                    if current_hunk
 493                                                                        .buffer_range
 494                                                                        .end
 495                                                                        .cmp(
 496                                                                            &new_hunk
 497                                                                                .buffer_range
 498                                                                                .start,
 499                                                                            &buffer_snapshot,
 500                                                                        )
 501                                                                        .is_ge()
 502                                                                    {
 503                                                                        new_hunks_with_updates
 504                                                                            .push(new_hunk);
 505                                                                        continue 'new_changes;
 506                                                                    } else {
 507                                                                        let _ =
 508                                                                            current_hunks.next();
 509                                                                    }
 510                                                                }
 511                                                            }
 512                                                        }
 513                                                        None => {
 514                                                            new_hunks_with_updates.push(new_hunk);
 515                                                            continue 'new_changes;
 516                                                        }
 517                                                    }
 518                                                }
 519                                            }
 520
 521                                            let mut excerpts_with_new_changes =
 522                                                HashSet::<ExcerptId>::default();
 523                                            'new_hunks: for new_hunk in new_hunks_with_updates {
 524                                                loop {
 525                                                    match current_excerpts.peek() {
 526                                                        Some((
 527                                                            current_excerpt_id,
 528                                                            current_excerpt_range,
 529                                                        )) => {
 530                                                            match (
 531                                                                current_excerpt_range
 532                                                                    .context
 533                                                                    .start
 534                                                                    .cmp(
 535                                                                        &new_hunk
 536                                                                            .buffer_range
 537                                                                            .start,
 538                                                                        &buffer_snapshot,
 539                                                                    ),
 540                                                                current_excerpt_range
 541                                                                    .context
 542                                                                    .end
 543                                                                    .cmp(
 544                                                                        &new_hunk.buffer_range.end,
 545                                                                        &buffer_snapshot,
 546                                                                    ),
 547                                                            ) {
 548                                                                (
 549                                                                    Ordering::Less
 550                                                                    | Ordering::Equal,
 551                                                                    Ordering::Greater
 552                                                                    | Ordering::Equal,
 553                                                                ) => {
 554                                                                    excerpts_with_new_changes
 555                                                                        .insert(
 556                                                                            *current_excerpt_id,
 557                                                                        );
 558                                                                    continue 'new_hunks;
 559                                                                }
 560                                                                (
 561                                                                    Ordering::Greater
 562                                                                    | Ordering::Equal,
 563                                                                    Ordering::Less
 564                                                                    | Ordering::Equal,
 565                                                                ) => {
 566                                                                    let expand_up = current_excerpt_range
 567                                                                .context
 568                                                                .start
 569                                                                .to_point(&buffer_snapshot)
 570                                                                .row
 571                                                                .saturating_sub(
 572                                                                    new_hunk
 573                                                                        .buffer_range
 574                                                                        .start
 575                                                                        .to_point(&buffer_snapshot)
 576                                                                        .row,
 577                                                                );
 578                                                                    let expand_down = new_hunk
 579                                                                    .buffer_range
 580                                                                    .end
 581                                                                    .to_point(&buffer_snapshot)
 582                                                                    .row
 583                                                                    .saturating_sub(
 584                                                                        current_excerpt_range
 585                                                                            .context
 586                                                                            .end
 587                                                                            .to_point(
 588                                                                                &buffer_snapshot,
 589                                                                            )
 590                                                                            .row,
 591                                                                    );
 592                                                                    excerpt_to_expand.entry((expand_up.max(expand_down).max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::UpAndDown)).or_default().push(*current_excerpt_id);
 593                                                                    excerpts_with_new_changes
 594                                                                        .insert(
 595                                                                            *current_excerpt_id,
 596                                                                        );
 597                                                                    continue 'new_hunks;
 598                                                                }
 599                                                                (
 600                                                                    Ordering::Less,
 601                                                                    Ordering::Less,
 602                                                                ) => {
 603                                                                    if current_excerpt_range
 604                                                                        .context
 605                                                                        .start
 606                                                                        .cmp(
 607                                                                            &new_hunk
 608                                                                                .buffer_range
 609                                                                                .end,
 610                                                                            &buffer_snapshot,
 611                                                                        )
 612                                                                        .is_le()
 613                                                                    {
 614                                                                        let expand_up = current_excerpt_range
 615                                                                        .context
 616                                                                        .start
 617                                                                        .to_point(&buffer_snapshot)
 618                                                                        .row
 619                                                                        .saturating_sub(
 620                                                                            new_hunk.buffer_range
 621                                                                                .start
 622                                                                                .to_point(
 623                                                                                    &buffer_snapshot,
 624                                                                                )
 625                                                                                .row,
 626                                                                        );
 627                                                                        excerpt_to_expand.entry((expand_up.max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::Up)).or_default().push(*current_excerpt_id);
 628                                                                        excerpts_with_new_changes
 629                                                                            .insert(
 630                                                                                *current_excerpt_id,
 631                                                                            );
 632                                                                        continue 'new_hunks;
 633                                                                    } else {
 634                                                                        if !new_changes
 635                                                                            .hunks
 636                                                                            .is_empty()
 637                                                                        {
 638                                                                            let hunks = new_excerpt_hunks
 639                                                                                .entry(latest_excerpt_id)
 640                                                                                .or_default();
 641                                                                            match hunks.binary_search_by(|(probe, ..)| {
 642                                                                                compare_paths(
 643                                                                                    (new_path.path.as_ref(), true),
 644                                                                                    (probe.path.as_ref(), true),
 645                                                                                )
 646                                                                            }) {
 647                                                                                Ok(i) => hunks[i].2.extend(
 648                                                                                    new_changes
 649                                                                                        .hunks
 650                                                                                        .iter()
 651                                                                                        .map(|hunk| hunk.buffer_range.clone()),
 652                                                                                ),
 653                                                                                Err(i) => hunks.insert(
 654                                                                                    i,
 655                                                                                    (
 656                                                                                        new_path.clone(),
 657                                                                                        new_changes.buffer.clone(),
 658                                                                                        new_changes
 659                                                                                            .hunks
 660                                                                                            .iter()
 661                                                                                            .map(|hunk| hunk.buffer_range.clone())
 662                                                                                            .collect(),
 663                                                                                    ),
 664                                                                                ),
 665                                                                            }
 666                                                                        }
 667                                                                        continue 'new_hunks;
 668                                                                    }
 669                                                                }
 670                                                                /* TODO remove or leave?
 671                                                                    [    ><<<<<<<<new_e
 672                                                                ----[---->--]----<--
 673                                                                   cur_s > cur_e <
 674                                                                         >       <
 675                                                                    new_s>>>>>>>><
 676                                                                */
 677                                                                (
 678                                                                    Ordering::Greater,
 679                                                                    Ordering::Greater,
 680                                                                ) => {
 681                                                                    if current_excerpt_range
 682                                                                        .context
 683                                                                        .end
 684                                                                        .cmp(
 685                                                                            &new_hunk
 686                                                                                .buffer_range
 687                                                                                .start,
 688                                                                            &buffer_snapshot,
 689                                                                        )
 690                                                                        .is_ge()
 691                                                                    {
 692                                                                        let expand_down = new_hunk
 693                                                                    .buffer_range
 694                                                                    .end
 695                                                                    .to_point(&buffer_snapshot)
 696                                                                    .row
 697                                                                    .saturating_sub(
 698                                                                        current_excerpt_range
 699                                                                            .context
 700                                                                            .end
 701                                                                            .to_point(
 702                                                                                &buffer_snapshot,
 703                                                                            )
 704                                                                            .row,
 705                                                                    );
 706                                                                        excerpt_to_expand.entry((expand_down.max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::Down)).or_default().push(*current_excerpt_id);
 707                                                                        excerpts_with_new_changes
 708                                                                            .insert(
 709                                                                                *current_excerpt_id,
 710                                                                            );
 711                                                                        continue 'new_hunks;
 712                                                                    } else {
 713                                                                        latest_excerpt_id =
 714                                                                            *current_excerpt_id;
 715                                                                        let _ =
 716                                                                            current_excerpts.next();
 717                                                                    }
 718                                                                }
 719                                                            }
 720                                                        }
 721                                                        None => {
 722                                                            let hunks = new_excerpt_hunks
 723                                                                .entry(latest_excerpt_id)
 724                                                                .or_default();
 725                                                            match hunks.binary_search_by(
 726                                                                |(probe, ..)| {
 727                                                                    compare_paths(
 728                                                                        (
 729                                                                            new_path.path.as_ref(),
 730                                                                            true,
 731                                                                        ),
 732                                                                        (probe.path.as_ref(), true),
 733                                                                    )
 734                                                                },
 735                                                            ) {
 736                                                                Ok(i) => hunks[i].2.extend(
 737                                                                    new_changes.hunks.iter().map(
 738                                                                        |hunk| {
 739                                                                            hunk.buffer_range
 740                                                                                .clone()
 741                                                                        },
 742                                                                    ),
 743                                                                ),
 744                                                                Err(i) => hunks.insert(
 745                                                                    i,
 746                                                                    (
 747                                                                        new_path.clone(),
 748                                                                        new_changes.buffer.clone(),
 749                                                                        new_changes
 750                                                                            .hunks
 751                                                                            .iter()
 752                                                                            .map(|hunk| {
 753                                                                                hunk.buffer_range
 754                                                                                    .clone()
 755                                                                            })
 756                                                                            .collect(),
 757                                                                    ),
 758                                                                ),
 759                                                            }
 760                                                            continue 'new_hunks;
 761                                                        }
 762                                                    }
 763                                                }
 764                                            }
 765
 766                                            for (excerpt_id, excerpt_range) in current_excerpts {
 767                                                if !excerpts_with_new_changes.contains(&excerpt_id)
 768                                                    && !new_hunks_unchanged.iter().any(|hunk| {
 769                                                        excerpt_range
 770                                                            .context
 771                                                            .start
 772                                                            .cmp(
 773                                                                &hunk.buffer_range.end,
 774                                                                &buffer_snapshot,
 775                                                            )
 776                                                            .is_le()
 777                                                            && excerpt_range
 778                                                                .context
 779                                                                .end
 780                                                                .cmp(
 781                                                                    &hunk.buffer_range.start,
 782                                                                    &buffer_snapshot,
 783                                                                )
 784                                                                .is_ge()
 785                                                    })
 786                                                {
 787                                                    excerpts_to_remove.push(excerpt_id);
 788                                                }
 789                                                latest_excerpt_id = excerpt_id;
 790                                            }
 791                                        }
 792                                        None => excerpts_to_remove.extend(
 793                                            current_excerpts.map(|(excerpt_id, _)| excerpt_id),
 794                                        ),
 795                                    }
 796                                    let _ = new_order_entries.next();
 797                                    break;
 798                                }
 799                            }
 800                        }
 801                        None => {
 802                            excerpts_to_remove
 803                                .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id));
 804                            break;
 805                        }
 806                    }
 807                }
 808                latest_excerpt_id = last_current_excerpt_id.unwrap_or(latest_excerpt_id);
 809            }
 810
 811            for (path, project_entry_id) in new_order_entries {
 812                if let Some(changes) = new_changes.get(project_entry_id) {
 813                    if !changes.hunks.is_empty() {
 814                        let hunks = new_excerpt_hunks.entry(latest_excerpt_id).or_default();
 815                        match hunks.binary_search_by(|(probe, ..)| {
 816                            compare_paths((path.path.as_ref(), true), (probe.path.as_ref(), true))
 817                        }) {
 818                            Ok(i) => hunks[i]
 819                                .2
 820                                .extend(changes.hunks.iter().map(|hunk| hunk.buffer_range.clone())),
 821                            Err(i) => hunks.insert(
 822                                i,
 823                                (
 824                                    path.clone(),
 825                                    changes.buffer.clone(),
 826                                    changes
 827                                        .hunks
 828                                        .iter()
 829                                        .map(|hunk| hunk.buffer_range.clone())
 830                                        .collect(),
 831                                ),
 832                            ),
 833                        }
 834                    }
 835                }
 836            }
 837
 838            self.excerpts.update(cx, |multi_buffer, cx| {
 839                for (mut after_excerpt_id, excerpts_to_add) in new_excerpt_hunks {
 840                    for (_, buffer, hunk_ranges) in excerpts_to_add {
 841                        let buffer_snapshot = buffer.read(cx).snapshot();
 842                        let max_point = buffer_snapshot.max_point();
 843                        let new_excerpts = multi_buffer.insert_excerpts_after(
 844                            after_excerpt_id,
 845                            buffer,
 846                            hunk_ranges.into_iter().map(|range| {
 847                                let mut extended_point_range = range.to_point(&buffer_snapshot);
 848                                extended_point_range.start.row = extended_point_range
 849                                    .start
 850                                    .row
 851                                    .saturating_sub(DEFAULT_MULTIBUFFER_CONTEXT);
 852                                extended_point_range.end.row = (extended_point_range.end.row
 853                                    + DEFAULT_MULTIBUFFER_CONTEXT)
 854                                    .min(max_point.row);
 855                                ExcerptRange {
 856                                    context: extended_point_range,
 857                                    primary: None,
 858                                }
 859                            }),
 860                            cx,
 861                        );
 862                        after_excerpt_id = new_excerpts.last().copied().unwrap_or(after_excerpt_id);
 863                    }
 864                }
 865                multi_buffer.remove_excerpts(excerpts_to_remove, cx);
 866                for ((line_count, direction), excerpts) in excerpt_to_expand {
 867                    multi_buffer.expand_excerpts(excerpts, line_count, direction, cx);
 868                }
 869            });
 870        } else {
 871            self.excerpts.update(cx, |multi_buffer, cx| {
 872                for new_changes in new_entry_order
 873                    .iter()
 874                    .filter_map(|(_, entry_id)| new_changes.get(entry_id))
 875                {
 876                    multi_buffer.push_excerpts_with_context_lines(
 877                        new_changes.buffer.clone(),
 878                        new_changes
 879                            .hunks
 880                            .iter()
 881                            .map(|hunk| hunk.buffer_range.clone())
 882                            .collect(),
 883                        DEFAULT_MULTIBUFFER_CONTEXT,
 884                        cx,
 885                    );
 886                }
 887            });
 888        };
 889
 890        let mut new_changes = new_changes;
 891        let mut new_entry_order = new_entry_order;
 892        std::mem::swap(
 893            self.buffer_changes.entry(worktree_id).or_default(),
 894            &mut new_changes,
 895        );
 896        std::mem::swap(
 897            self.entry_order.entry(worktree_id).or_default(),
 898            &mut new_entry_order,
 899        );
 900    }
 901}
 902
 903impl EventEmitter<EditorEvent> for ProjectDiffEditor {}
 904
 905impl FocusableView for ProjectDiffEditor {
 906    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
 907        self.focus_handle.clone()
 908    }
 909}
 910
 911impl Item for ProjectDiffEditor {
 912    type Event = EditorEvent;
 913
 914    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 915        Editor::to_item_events(event, f)
 916    }
 917
 918    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 919        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 920    }
 921
 922    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 923        self.editor
 924            .update(cx, |editor, cx| editor.navigate(data, cx))
 925    }
 926
 927    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
 928        Some("Project Diff".into())
 929    }
 930
 931    fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
 932        if self.buffer_changes.is_empty() {
 933            Label::new("No changes")
 934                .color(if params.selected {
 935                    Color::Default
 936                } else {
 937                    Color::Muted
 938                })
 939                .into_any_element()
 940        } else {
 941            h_flex()
 942                .gap_1()
 943                .when(true, |then| {
 944                    then.child(
 945                        h_flex()
 946                            .gap_1()
 947                            .child(Icon::new(IconName::XCircle).color(Color::Error))
 948                            .child(Label::new(self.buffer_changes.len().to_string()).color(
 949                                if params.selected {
 950                                    Color::Default
 951                                } else {
 952                                    Color::Muted
 953                                },
 954                            )),
 955                    )
 956                })
 957                .when(true, |then| {
 958                    then.child(
 959                        h_flex()
 960                            .gap_1()
 961                            .child(Icon::new(IconName::Indicator).color(Color::Warning))
 962                            .child(Label::new(self.buffer_changes.len().to_string()).color(
 963                                if params.selected {
 964                                    Color::Default
 965                                } else {
 966                                    Color::Muted
 967                                },
 968                            )),
 969                    )
 970                })
 971                .into_any_element()
 972        }
 973    }
 974
 975    fn telemetry_event_text(&self) -> Option<&'static str> {
 976        Some("project diagnostics")
 977    }
 978
 979    fn for_each_project_item(
 980        &self,
 981        cx: &AppContext,
 982        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 983    ) {
 984        self.editor.for_each_project_item(cx, f)
 985    }
 986
 987    fn is_singleton(&self, _: &AppContext) -> bool {
 988        false
 989    }
 990
 991    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 992        self.editor.update(cx, |editor, _| {
 993            editor.set_nav_history(Some(nav_history));
 994        });
 995    }
 996
 997    fn clone_on_split(
 998        &self,
 999        _workspace_id: Option<workspace::WorkspaceId>,
1000        cx: &mut ViewContext<Self>,
1001    ) -> Option<View<Self>>
1002    where
1003        Self: Sized,
1004    {
1005        Some(cx.new_view(|cx| {
1006            ProjectDiffEditor::new(self.project.clone(), self.workspace.clone(), cx)
1007        }))
1008    }
1009
1010    fn is_dirty(&self, cx: &AppContext) -> bool {
1011        self.excerpts.read(cx).is_dirty(cx)
1012    }
1013
1014    fn has_conflict(&self, cx: &AppContext) -> bool {
1015        self.excerpts.read(cx).has_conflict(cx)
1016    }
1017
1018    fn can_save(&self, _: &AppContext) -> bool {
1019        true
1020    }
1021
1022    fn save(
1023        &mut self,
1024        format: bool,
1025        project: Model<Project>,
1026        cx: &mut ViewContext<Self>,
1027    ) -> Task<anyhow::Result<()>> {
1028        self.editor.save(format, project, cx)
1029    }
1030
1031    fn save_as(
1032        &mut self,
1033        _: Model<Project>,
1034        _: ProjectPath,
1035        _: &mut ViewContext<Self>,
1036    ) -> Task<anyhow::Result<()>> {
1037        unreachable!()
1038    }
1039
1040    fn reload(
1041        &mut self,
1042        project: Model<Project>,
1043        cx: &mut ViewContext<Self>,
1044    ) -> Task<anyhow::Result<()>> {
1045        self.editor.reload(project, cx)
1046    }
1047
1048    fn act_as_type<'a>(
1049        &'a self,
1050        type_id: TypeId,
1051        self_handle: &'a View<Self>,
1052        _: &'a AppContext,
1053    ) -> Option<AnyView> {
1054        if type_id == TypeId::of::<Self>() {
1055            Some(self_handle.to_any())
1056        } else if type_id == TypeId::of::<Editor>() {
1057            Some(self.editor.to_any())
1058        } else {
1059            None
1060        }
1061    }
1062
1063    fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation {
1064        ToolbarItemLocation::PrimaryLeft
1065    }
1066
1067    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
1068        self.editor.breadcrumbs(theme, cx)
1069    }
1070
1071    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
1072        self.editor
1073            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
1074    }
1075}
1076
1077impl Render for ProjectDiffEditor {
1078    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1079        let child = if self.buffer_changes.is_empty() {
1080            div()
1081                .bg(cx.theme().colors().editor_background)
1082                .flex()
1083                .items_center()
1084                .justify_center()
1085                .size_full()
1086                .child(Label::new("No changes in the workspace"))
1087        } else {
1088            div().size_full().child(self.editor.clone())
1089        };
1090
1091        div()
1092            .track_focus(&self.focus_handle)
1093            .size_full()
1094            .child(child)
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use git::status::{StatusCode, TrackedStatus};
1101    use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
1102    use project::buffer_store::BufferChangeSet;
1103    use serde_json::json;
1104    use settings::SettingsStore;
1105    use std::{
1106        ops::Deref as _,
1107        path::{Path, PathBuf},
1108    };
1109
1110    use crate::test::editor_test_context::assert_state_with_diff;
1111
1112    use super::*;
1113
1114    // TODO finish
1115    // #[gpui::test]
1116    // async fn randomized_tests(cx: &mut TestAppContext) {
1117    //     // Create a new project (how?? temp fs?),
1118    //     let fs = FakeFs::new(cx.executor());
1119    //     let project = Project::test(fs, [], cx).await;
1120
1121    //     // create random files with random content
1122
1123    //     // Commit it into git somehow (technically can do with "real" fs in a temp dir)
1124    //     //
1125    //     // Apply randomized changes to the project: select a random file, random change and apply to buffers
1126    // }
1127
1128    #[gpui::test(iterations = 30)]
1129    async fn simple_edit_test(cx: &mut TestAppContext) {
1130        cx.executor().allow_parking();
1131        init_test(cx);
1132
1133        let fs = fs::FakeFs::new(cx.executor().clone());
1134        fs.insert_tree(
1135            "/root",
1136            json!({
1137                ".git": {},
1138                "file_a": "This is file_a",
1139                "file_b": "This is file_b",
1140            }),
1141        )
1142        .await;
1143
1144        let project = Project::test(fs.clone(), [Path::new("/root")], cx).await;
1145        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1146        let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
1147
1148        let file_a_editor = workspace
1149            .update(cx, |workspace, cx| {
1150                let file_a_editor =
1151                    workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx);
1152                ProjectDiffEditor::deploy(workspace, &Deploy, cx);
1153                file_a_editor
1154            })
1155            .unwrap()
1156            .await
1157            .expect("did not open an item at all")
1158            .downcast::<Editor>()
1159            .expect("did not open an editor for file_a");
1160        let project_diff_editor = workspace
1161            .update(cx, |workspace, cx| {
1162                workspace
1163                    .active_pane()
1164                    .read(cx)
1165                    .items()
1166                    .find_map(|item| item.downcast::<ProjectDiffEditor>())
1167            })
1168            .unwrap()
1169            .expect("did not find a ProjectDiffEditor");
1170        project_diff_editor.update(cx, |project_diff_editor, cx| {
1171            assert!(
1172                project_diff_editor.editor.read(cx).text(cx).is_empty(),
1173                "Should have no changes after opening the diff on no git changes"
1174            );
1175        });
1176
1177        let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx));
1178        let change = "an edit after git add";
1179        file_a_editor
1180            .update(cx, |file_a_editor, cx| {
1181                file_a_editor.insert(change, cx);
1182                file_a_editor.save(false, project.clone(), cx)
1183            })
1184            .await
1185            .expect("failed to save a file");
1186        file_a_editor.update(cx, |file_a_editor, cx| {
1187            let change_set = cx.new_model(|cx| {
1188                BufferChangeSet::new_with_base_text(
1189                    old_text.clone(),
1190                    &file_a_editor.buffer().read(cx).as_singleton().unwrap(),
1191                    cx,
1192                )
1193            });
1194            file_a_editor.buffer.update(cx, |buffer, cx| {
1195                buffer.add_change_set(change_set.clone(), cx)
1196            });
1197            project.update(cx, |project, cx| {
1198                project.buffer_store().update(cx, |buffer_store, cx| {
1199                    buffer_store.set_change_set(
1200                        file_a_editor
1201                            .buffer()
1202                            .read(cx)
1203                            .as_singleton()
1204                            .unwrap()
1205                            .read(cx)
1206                            .remote_id(),
1207                        change_set,
1208                    );
1209                });
1210            });
1211        });
1212        fs.set_status_for_repo_via_git_operation(
1213            Path::new("/root/.git"),
1214            &[(
1215                Path::new("file_a"),
1216                TrackedStatus {
1217                    worktree_status: StatusCode::Modified,
1218                    index_status: StatusCode::Unmodified,
1219                }
1220                .into(),
1221            )],
1222        );
1223        cx.executor()
1224            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
1225        cx.run_until_parked();
1226        let editor = project_diff_editor.update(cx, |view, _| view.editor.clone());
1227
1228        assert_state_with_diff(
1229            &editor,
1230            cx,
1231            indoc::indoc! {
1232            "
1233                - This is file_a
1234                + an edit after git addThis is file_aˇ",
1235            },
1236        );
1237    }
1238
1239    fn init_test(cx: &mut gpui::TestAppContext) {
1240        if std::env::var("RUST_LOG").is_ok() {
1241            env_logger::try_init().ok();
1242        }
1243
1244        cx.update(|cx| {
1245            assets::Assets.load_test_fonts(cx);
1246            let settings_store = SettingsStore::test(cx);
1247            cx.set_global(settings_store);
1248            theme::init(theme::LoadThemes::JustBase, cx);
1249            release_channel::init(SemanticVersion::default(), cx);
1250            client::init_settings(cx);
1251            language::init(cx);
1252            Project::init_settings(cx);
1253            workspace::init_settings(cx);
1254            crate::init(cx);
1255            cx.set_staff(true);
1256        });
1257    }
1258}