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