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