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