1use std::{
   2    io::{BufRead, BufReader},
   3    path::{Path, PathBuf},
   4    pin::pin,
   5    sync::{Arc, atomic::AtomicUsize},
   6};
   7
   8use anyhow::{Context as _, Result, anyhow, bail};
   9use collections::{HashMap, HashSet};
  10use fs::{Fs, copy_recursive};
  11use futures::{
  12    FutureExt, SinkExt,
  13    future::{BoxFuture, Shared},
  14};
  15use gpui::{
  16    App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity,
  17};
  18use postage::oneshot;
  19use rpc::{
  20    AnyProtoClient, ErrorExt, TypedEnvelope,
  21    proto::{self, REMOTE_SERVER_PROJECT_ID},
  22};
  23use smol::{
  24    channel::{Receiver, Sender},
  25    stream::StreamExt,
  26};
  27use text::ReplicaId;
  28use util::{
  29    ResultExt,
  30    paths::{PathStyle, RemotePathBuf, SanitizedPath},
  31    rel_path::RelPath,
  32};
  33use worktree::{
  34    CreatedEntry, Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree,
  35    WorktreeId, WorktreeSettings,
  36};
  37
  38use crate::{ProjectPath, search::SearchQuery};
  39
  40struct MatchingEntry {
  41    worktree_root: Arc<Path>,
  42    path: ProjectPath,
  43    respond: oneshot::Sender<ProjectPath>,
  44}
  45
  46enum WorktreeStoreState {
  47    Local {
  48        fs: Arc<dyn Fs>,
  49    },
  50    Remote {
  51        upstream_client: AnyProtoClient,
  52        upstream_project_id: u64,
  53        path_style: PathStyle,
  54    },
  55}
  56
  57pub struct WorktreeStore {
  58    next_entry_id: Arc<AtomicUsize>,
  59    downstream_client: Option<(AnyProtoClient, u64)>,
  60    retain_worktrees: bool,
  61    worktrees: Vec<WorktreeHandle>,
  62    worktrees_reordered: bool,
  63    #[allow(clippy::type_complexity)]
  64    loading_worktrees:
  65        HashMap<Arc<SanitizedPath>, Shared<Task<Result<Entity<Worktree>, Arc<anyhow::Error>>>>>,
  66    state: WorktreeStoreState,
  67}
  68
  69#[derive(Debug)]
  70pub enum WorktreeStoreEvent {
  71    WorktreeAdded(Entity<Worktree>),
  72    WorktreeRemoved(EntityId, WorktreeId),
  73    WorktreeReleased(EntityId, WorktreeId),
  74    WorktreeOrderChanged,
  75    WorktreeUpdateSent(Entity<Worktree>),
  76    WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
  77    WorktreeUpdatedGitRepositories(WorktreeId, UpdatedGitRepositoriesSet),
  78    WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
  79}
  80
  81impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
  82
  83impl WorktreeStore {
  84    pub fn init(client: &AnyProtoClient) {
  85        client.add_entity_request_handler(Self::handle_create_project_entry);
  86        client.add_entity_request_handler(Self::handle_copy_project_entry);
  87        client.add_entity_request_handler(Self::handle_delete_project_entry);
  88        client.add_entity_request_handler(Self::handle_expand_project_entry);
  89        client.add_entity_request_handler(Self::handle_expand_all_for_project_entry);
  90    }
  91
  92    pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
  93        Self {
  94            next_entry_id: Default::default(),
  95            loading_worktrees: Default::default(),
  96            downstream_client: None,
  97            worktrees: Vec::new(),
  98            worktrees_reordered: false,
  99            retain_worktrees,
 100            state: WorktreeStoreState::Local { fs },
 101        }
 102    }
 103
 104    pub fn remote(
 105        retain_worktrees: bool,
 106        upstream_client: AnyProtoClient,
 107        upstream_project_id: u64,
 108        path_style: PathStyle,
 109    ) -> Self {
 110        Self {
 111            next_entry_id: Default::default(),
 112            loading_worktrees: Default::default(),
 113            downstream_client: None,
 114            worktrees: Vec::new(),
 115            worktrees_reordered: false,
 116            retain_worktrees,
 117            state: WorktreeStoreState::Remote {
 118                upstream_client,
 119                upstream_project_id,
 120                path_style,
 121            },
 122        }
 123    }
 124
 125    /// Iterates through all worktrees, including ones that don't appear in the project panel
 126    pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Entity<Worktree>> {
 127        self.worktrees
 128            .iter()
 129            .filter_map(move |worktree| worktree.upgrade())
 130    }
 131
 132    /// Iterates through all user-visible worktrees, the ones that appear in the project panel.
 133    pub fn visible_worktrees<'a>(
 134        &'a self,
 135        cx: &'a App,
 136    ) -> impl 'a + DoubleEndedIterator<Item = Entity<Worktree>> {
 137        self.worktrees()
 138            .filter(|worktree| worktree.read(cx).is_visible())
 139    }
 140
 141    /// Iterates through all user-visible worktrees (directories and files that appear in the project panel) and other, invisible single files that could appear e.g. due to drag and drop.
 142    pub fn visible_worktrees_and_single_files<'a>(
 143        &'a self,
 144        cx: &'a App,
 145    ) -> impl 'a + DoubleEndedIterator<Item = Entity<Worktree>> {
 146        self.worktrees()
 147            .filter(|worktree| worktree.read(cx).is_visible() || worktree.read(cx).is_single_file())
 148    }
 149
 150    pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {
 151        self.worktrees()
 152            .find(|worktree| worktree.read(cx).id() == id)
 153    }
 154
 155    pub fn worktree_for_entry(
 156        &self,
 157        entry_id: ProjectEntryId,
 158        cx: &App,
 159    ) -> Option<Entity<Worktree>> {
 160        self.worktrees()
 161            .find(|worktree| worktree.read(cx).contains_entry(entry_id))
 162    }
 163
 164    pub fn find_worktree(
 165        &self,
 166        abs_path: impl AsRef<Path>,
 167        cx: &App,
 168    ) -> Option<(Entity<Worktree>, Arc<RelPath>)> {
 169        let abs_path = SanitizedPath::new(abs_path.as_ref());
 170        for tree in self.worktrees() {
 171            let path_style = tree.read(cx).path_style();
 172            if let Ok(relative_path) = abs_path.as_ref().strip_prefix(tree.read(cx).abs_path())
 173                && let Ok(relative_path) = RelPath::new(relative_path, path_style)
 174            {
 175                return Some((tree.clone(), relative_path.into_arc()));
 176            }
 177        }
 178        None
 179    }
 180
 181    pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
 182        let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
 183        Some(worktree.read(cx).absolutize(&project_path.path))
 184    }
 185
 186    pub fn path_style(&self) -> PathStyle {
 187        match &self.state {
 188            WorktreeStoreState::Local { .. } => PathStyle::local(),
 189            WorktreeStoreState::Remote { path_style, .. } => *path_style,
 190        }
 191    }
 192
 193    pub fn find_or_create_worktree(
 194        &mut self,
 195        abs_path: impl AsRef<Path>,
 196        visible: bool,
 197        cx: &mut Context<Self>,
 198    ) -> Task<Result<(Entity<Worktree>, Arc<RelPath>)>> {
 199        let abs_path = abs_path.as_ref();
 200        if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
 201            Task::ready(Ok((tree, relative_path)))
 202        } else {
 203            let worktree = self.create_worktree(abs_path, visible, cx);
 204            cx.background_spawn(async move { Ok((worktree.await?, RelPath::empty().into())) })
 205        }
 206    }
 207
 208    pub fn entry_for_id<'a>(&'a self, entry_id: ProjectEntryId, cx: &'a App) -> Option<&'a Entry> {
 209        self.worktrees()
 210            .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id))
 211    }
 212
 213    pub fn worktree_and_entry_for_id<'a>(
 214        &'a self,
 215        entry_id: ProjectEntryId,
 216        cx: &'a App,
 217    ) -> Option<(Entity<Worktree>, &'a Entry)> {
 218        self.worktrees().find_map(|worktree| {
 219            worktree
 220                .read(cx)
 221                .entry_for_id(entry_id)
 222                .map(|e| (worktree.clone(), e))
 223        })
 224    }
 225
 226    pub fn entry_for_path<'a>(&'a self, path: &ProjectPath, cx: &'a App) -> Option<&'a Entry> {
 227        self.worktree_for_id(path.worktree_id, cx)?
 228            .read(cx)
 229            .entry_for_path(&path.path)
 230    }
 231
 232    pub fn copy_entry(
 233        &mut self,
 234        entry_id: ProjectEntryId,
 235        new_project_path: ProjectPath,
 236        cx: &mut Context<Self>,
 237    ) -> Task<Result<Option<Entry>>> {
 238        let Some(old_worktree) = self.worktree_for_entry(entry_id, cx) else {
 239            return Task::ready(Err(anyhow!("no such worktree")));
 240        };
 241        let Some(old_entry) = old_worktree.read(cx).entry_for_id(entry_id) else {
 242            return Task::ready(Err(anyhow!("no such entry")));
 243        };
 244        let Some(new_worktree) = self.worktree_for_id(new_project_path.worktree_id, cx) else {
 245            return Task::ready(Err(anyhow!("no such worktree")));
 246        };
 247
 248        match &self.state {
 249            WorktreeStoreState::Local { fs } => {
 250                let old_abs_path = old_worktree.read(cx).absolutize(&old_entry.path);
 251                let new_abs_path = new_worktree.read(cx).absolutize(&new_project_path.path);
 252                let fs = fs.clone();
 253                let copy = cx.background_spawn(async move {
 254                    copy_recursive(
 255                        fs.as_ref(),
 256                        &old_abs_path,
 257                        &new_abs_path,
 258                        Default::default(),
 259                    )
 260                    .await
 261                });
 262
 263                cx.spawn(async move |_, cx| {
 264                    copy.await?;
 265                    new_worktree
 266                        .update(cx, |this, cx| {
 267                            this.as_local_mut().unwrap().refresh_entry(
 268                                new_project_path.path,
 269                                None,
 270                                cx,
 271                            )
 272                        })?
 273                        .await
 274                })
 275            }
 276            WorktreeStoreState::Remote {
 277                upstream_client,
 278                upstream_project_id,
 279                ..
 280            } => {
 281                let response = upstream_client.request(proto::CopyProjectEntry {
 282                    project_id: *upstream_project_id,
 283                    entry_id: entry_id.to_proto(),
 284                    new_path: new_project_path.path.to_proto(),
 285                    new_worktree_id: new_project_path.worktree_id.to_proto(),
 286                });
 287                cx.spawn(async move |_, cx| {
 288                    let response = response.await?;
 289                    match response.entry {
 290                        Some(entry) => new_worktree
 291                            .update(cx, |worktree, cx| {
 292                                worktree.as_remote_mut().unwrap().insert_entry(
 293                                    entry,
 294                                    response.worktree_scan_id as usize,
 295                                    cx,
 296                                )
 297                            })?
 298                            .await
 299                            .map(Some),
 300                        None => Ok(None),
 301                    }
 302                })
 303            }
 304        }
 305    }
 306
 307    pub fn rename_entry(
 308        &mut self,
 309        entry_id: ProjectEntryId,
 310        new_project_path: ProjectPath,
 311        cx: &mut Context<Self>,
 312    ) -> Task<Result<CreatedEntry>> {
 313        let Some(old_worktree) = self.worktree_for_entry(entry_id, cx) else {
 314            return Task::ready(Err(anyhow!("no such worktree")));
 315        };
 316        let Some(old_entry) = old_worktree.read(cx).entry_for_id(entry_id).cloned() else {
 317            return Task::ready(Err(anyhow!("no such entry")));
 318        };
 319        let Some(new_worktree) = self.worktree_for_id(new_project_path.worktree_id, cx) else {
 320            return Task::ready(Err(anyhow!("no such worktree")));
 321        };
 322
 323        match &self.state {
 324            WorktreeStoreState::Local { fs } => {
 325                let abs_old_path = old_worktree.read(cx).absolutize(&old_entry.path);
 326                let new_worktree_ref = new_worktree.read(cx);
 327                let is_root_entry = new_worktree_ref
 328                    .root_entry()
 329                    .is_some_and(|e| e.id == entry_id);
 330                let abs_new_path = if is_root_entry {
 331                    let abs_path = new_worktree_ref.abs_path();
 332                    let Some(root_parent_path) = abs_path.parent() else {
 333                        return Task::ready(Err(anyhow!("no parent for path {:?}", abs_path)));
 334                    };
 335                    root_parent_path.join(new_project_path.path.as_std_path())
 336                } else {
 337                    new_worktree_ref.absolutize(&new_project_path.path)
 338                };
 339
 340                let fs = fs.clone();
 341                let case_sensitive = new_worktree
 342                    .read(cx)
 343                    .as_local()
 344                    .unwrap()
 345                    .fs_is_case_sensitive();
 346
 347                let do_rename =
 348                    async move |fs: &dyn Fs, old_path: &Path, new_path: &Path, overwrite| {
 349                        fs.rename(
 350                            &old_path,
 351                            &new_path,
 352                            fs::RenameOptions {
 353                                overwrite,
 354                                ..fs::RenameOptions::default()
 355                            },
 356                        )
 357                        .await
 358                        .with_context(|| format!("renaming {old_path:?} into {new_path:?}"))
 359                    };
 360
 361                let rename = cx.background_spawn({
 362                    let abs_new_path = abs_new_path.clone();
 363                    async move {
 364                        // If we're on a case-insensitive FS and we're doing a case-only rename (i.e. `foobar` to `FOOBAR`)
 365                        // we want to overwrite, because otherwise we run into a file-already-exists error.
 366                        let overwrite = !case_sensitive
 367                            && abs_old_path != abs_new_path
 368                            && abs_old_path.to_str().map(|p| p.to_lowercase())
 369                                == abs_new_path.to_str().map(|p| p.to_lowercase());
 370
 371                        // The directory we're renaming into might not exist yet
 372                        if let Err(e) =
 373                            do_rename(fs.as_ref(), &abs_old_path, &abs_new_path, overwrite).await
 374                        {
 375                            if let Some(err) = e.downcast_ref::<std::io::Error>()
 376                                && err.kind() == std::io::ErrorKind::NotFound
 377                            {
 378                                if let Some(parent) = abs_new_path.parent() {
 379                                    fs.create_dir(parent).await.with_context(|| {
 380                                        format!("creating parent directory {parent:?}")
 381                                    })?;
 382                                    return do_rename(
 383                                        fs.as_ref(),
 384                                        &abs_old_path,
 385                                        &abs_new_path,
 386                                        overwrite,
 387                                    )
 388                                    .await;
 389                                }
 390                            }
 391                            return Err(e);
 392                        }
 393                        Ok(())
 394                    }
 395                });
 396
 397                cx.spawn(async move |_, cx| {
 398                    rename.await?;
 399                    Ok(new_worktree
 400                        .update(cx, |this, cx| {
 401                            let local = this.as_local_mut().unwrap();
 402                            if is_root_entry {
 403                                // We eagerly update `abs_path` and refresh this worktree.
 404                                // Otherwise, the FS watcher would do it on the `RootUpdated` event,
 405                                // but with a noticeable delay, so we handle it proactively.
 406                                local.update_abs_path_and_refresh(
 407                                    SanitizedPath::new_arc(&abs_new_path),
 408                                    cx,
 409                                );
 410                                Task::ready(Ok(this.root_entry().cloned()))
 411                            } else {
 412                                // First refresh the parent directory (in case it was newly created)
 413                                if let Some(parent) = new_project_path.path.parent() {
 414                                    let _ = local.refresh_entries_for_paths(vec![parent.into()]);
 415                                }
 416                                // Then refresh the new path
 417                                local.refresh_entry(
 418                                    new_project_path.path.clone(),
 419                                    Some(old_entry.path),
 420                                    cx,
 421                                )
 422                            }
 423                        })?
 424                        .await?
 425                        .map(CreatedEntry::Included)
 426                        .unwrap_or_else(|| CreatedEntry::Excluded {
 427                            abs_path: abs_new_path,
 428                        }))
 429                })
 430            }
 431            WorktreeStoreState::Remote {
 432                upstream_client,
 433                upstream_project_id,
 434                ..
 435            } => {
 436                let response = upstream_client.request(proto::RenameProjectEntry {
 437                    project_id: *upstream_project_id,
 438                    entry_id: entry_id.to_proto(),
 439                    new_path: new_project_path.path.to_proto(),
 440                    new_worktree_id: new_project_path.worktree_id.to_proto(),
 441                });
 442                cx.spawn(async move |_, cx| {
 443                    let response = response.await?;
 444                    match response.entry {
 445                        Some(entry) => new_worktree
 446                            .update(cx, |worktree, cx| {
 447                                worktree.as_remote_mut().unwrap().insert_entry(
 448                                    entry,
 449                                    response.worktree_scan_id as usize,
 450                                    cx,
 451                                )
 452                            })?
 453                            .await
 454                            .map(CreatedEntry::Included),
 455                        None => {
 456                            let abs_path = new_worktree.read_with(cx, |worktree, _| {
 457                                worktree.absolutize(&new_project_path.path)
 458                            })?;
 459                            Ok(CreatedEntry::Excluded { abs_path })
 460                        }
 461                    }
 462                })
 463            }
 464        }
 465    }
 466    pub fn create_worktree(
 467        &mut self,
 468        abs_path: impl AsRef<Path>,
 469        visible: bool,
 470        cx: &mut Context<Self>,
 471    ) -> Task<Result<Entity<Worktree>>> {
 472        let abs_path: Arc<SanitizedPath> = SanitizedPath::new_arc(&abs_path);
 473        if !self.loading_worktrees.contains_key(&abs_path) {
 474            let task = match &self.state {
 475                WorktreeStoreState::Remote {
 476                    upstream_client,
 477                    path_style,
 478                    ..
 479                } => {
 480                    if upstream_client.is_via_collab() {
 481                        Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
 482                    } else {
 483                        let abs_path = RemotePathBuf::new(abs_path.to_string(), *path_style);
 484                        self.create_remote_worktree(upstream_client.clone(), abs_path, visible, cx)
 485                    }
 486                }
 487                WorktreeStoreState::Local { fs } => {
 488                    self.create_local_worktree(fs.clone(), abs_path.clone(), visible, cx)
 489                }
 490            };
 491
 492            self.loading_worktrees
 493                .insert(abs_path.clone(), task.shared());
 494        }
 495        let task = self.loading_worktrees.get(&abs_path).unwrap().clone();
 496        cx.spawn(async move |this, cx| {
 497            let result = task.await;
 498            this.update(cx, |this, _| this.loading_worktrees.remove(&abs_path))
 499                .ok();
 500            match result {
 501                Ok(worktree) => Ok(worktree),
 502                Err(err) => Err((*err).cloned()),
 503            }
 504        })
 505    }
 506
 507    fn create_remote_worktree(
 508        &mut self,
 509        client: AnyProtoClient,
 510        abs_path: RemotePathBuf,
 511        visible: bool,
 512        cx: &mut Context<Self>,
 513    ) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
 514        let path_style = abs_path.path_style();
 515        let mut abs_path = abs_path.to_string();
 516        // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
 517        // in which case want to strip the leading the `/`.
 518        // On the host-side, the `~` will get expanded.
 519        // That's what git does too: https://github.com/libgit2/libgit2/issues/3345#issuecomment-127050850
 520        if abs_path.starts_with("/~") {
 521            abs_path = abs_path[1..].to_string();
 522        }
 523        if abs_path.is_empty() {
 524            abs_path = "~/".to_string();
 525        }
 526
 527        cx.spawn(async move |this, cx| {
 528            let this = this.upgrade().context("Dropped worktree store")?;
 529
 530            let path = RemotePathBuf::new(abs_path, path_style);
 531            let response = client
 532                .request(proto::AddWorktree {
 533                    project_id: REMOTE_SERVER_PROJECT_ID,
 534                    path: path.to_proto(),
 535                    visible,
 536                })
 537                .await?;
 538
 539            if let Some(existing_worktree) = this.read_with(cx, |this, cx| {
 540                this.worktree_for_id(WorktreeId::from_proto(response.worktree_id), cx)
 541            })? {
 542                return Ok(existing_worktree);
 543            }
 544
 545            let root_path_buf = PathBuf::from(response.canonicalized_path.clone());
 546            let root_name = root_path_buf
 547                .file_name()
 548                .map(|n| n.to_string_lossy().into_owned())
 549                .unwrap_or(root_path_buf.to_string_lossy().into_owned());
 550
 551            let worktree = cx.update(|cx| {
 552                Worktree::remote(
 553                    REMOTE_SERVER_PROJECT_ID,
 554                    ReplicaId::REMOTE_SERVER,
 555                    proto::WorktreeMetadata {
 556                        id: response.worktree_id,
 557                        root_name,
 558                        visible,
 559                        abs_path: response.canonicalized_path,
 560                    },
 561                    client,
 562                    path_style,
 563                    cx,
 564                )
 565            })?;
 566
 567            this.update(cx, |this, cx| {
 568                this.add(&worktree, cx);
 569            })?;
 570            Ok(worktree)
 571        })
 572    }
 573
 574    fn create_local_worktree(
 575        &mut self,
 576        fs: Arc<dyn Fs>,
 577        abs_path: Arc<SanitizedPath>,
 578        visible: bool,
 579        cx: &mut Context<Self>,
 580    ) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
 581        let next_entry_id = self.next_entry_id.clone();
 582
 583        cx.spawn(async move |this, cx| {
 584            let worktree = Worktree::local(
 585                SanitizedPath::cast_arc(abs_path.clone()),
 586                visible,
 587                fs,
 588                next_entry_id,
 589                cx,
 590            )
 591            .await;
 592
 593            let worktree = worktree?;
 594
 595            this.update(cx, |this, cx| this.add(&worktree, cx))?;
 596
 597            if visible {
 598                cx.update(|cx| {
 599                    cx.add_recent_document(abs_path.as_path());
 600                })
 601                .log_err();
 602            }
 603
 604            Ok(worktree)
 605        })
 606    }
 607
 608    pub fn add(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {
 609        let worktree_id = worktree.read(cx).id();
 610        debug_assert!(self.worktrees().all(|w| w.read(cx).id() != worktree_id));
 611
 612        let push_strong_handle = self.retain_worktrees || worktree.read(cx).is_visible();
 613        let handle = if push_strong_handle {
 614            WorktreeHandle::Strong(worktree.clone())
 615        } else {
 616            WorktreeHandle::Weak(worktree.downgrade())
 617        };
 618        if self.worktrees_reordered {
 619            self.worktrees.push(handle);
 620        } else {
 621            let i = match self
 622                .worktrees
 623                .binary_search_by_key(&Some(worktree.read(cx).abs_path()), |other| {
 624                    other.upgrade().map(|worktree| worktree.read(cx).abs_path())
 625                }) {
 626                Ok(i) | Err(i) => i,
 627            };
 628            self.worktrees.insert(i, handle);
 629        }
 630
 631        cx.emit(WorktreeStoreEvent::WorktreeAdded(worktree.clone()));
 632        self.send_project_updates(cx);
 633
 634        let handle_id = worktree.entity_id();
 635        cx.subscribe(worktree, |_, worktree, event, cx| {
 636            let worktree_id = worktree.read(cx).id();
 637            match event {
 638                worktree::Event::UpdatedEntries(changes) => {
 639                    cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries(
 640                        worktree_id,
 641                        changes.clone(),
 642                    ));
 643                }
 644                worktree::Event::UpdatedGitRepositories(set) => {
 645                    cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(
 646                        worktree_id,
 647                        set.clone(),
 648                    ));
 649                }
 650                worktree::Event::DeletedEntry(id) => {
 651                    cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id))
 652                }
 653            }
 654        })
 655        .detach();
 656        cx.observe_release(worktree, move |this, worktree, cx| {
 657            cx.emit(WorktreeStoreEvent::WorktreeReleased(
 658                handle_id,
 659                worktree.id(),
 660            ));
 661            cx.emit(WorktreeStoreEvent::WorktreeRemoved(
 662                handle_id,
 663                worktree.id(),
 664            ));
 665            this.send_project_updates(cx);
 666        })
 667        .detach();
 668    }
 669
 670    pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {
 671        self.worktrees.retain(|worktree| {
 672            if let Some(worktree) = worktree.upgrade() {
 673                if worktree.read(cx).id() == id_to_remove {
 674                    cx.emit(WorktreeStoreEvent::WorktreeRemoved(
 675                        worktree.entity_id(),
 676                        id_to_remove,
 677                    ));
 678                    false
 679                } else {
 680                    true
 681                }
 682            } else {
 683                false
 684            }
 685        });
 686        self.send_project_updates(cx);
 687    }
 688
 689    pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {
 690        self.worktrees_reordered = worktrees_reordered;
 691    }
 692
 693    fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
 694        match &self.state {
 695            WorktreeStoreState::Remote {
 696                upstream_client,
 697                upstream_project_id,
 698                ..
 699            } => Some((upstream_client.clone(), *upstream_project_id)),
 700            WorktreeStoreState::Local { .. } => None,
 701        }
 702    }
 703
 704    pub fn set_worktrees_from_proto(
 705        &mut self,
 706        worktrees: Vec<proto::WorktreeMetadata>,
 707        replica_id: ReplicaId,
 708        cx: &mut Context<Self>,
 709    ) -> Result<()> {
 710        let mut old_worktrees_by_id = self
 711            .worktrees
 712            .drain(..)
 713            .filter_map(|worktree| {
 714                let worktree = worktree.upgrade()?;
 715                Some((worktree.read(cx).id(), worktree))
 716            })
 717            .collect::<HashMap<_, _>>();
 718
 719        let (client, project_id) = self.upstream_client().context("invalid project")?;
 720
 721        for worktree in worktrees {
 722            if let Some(old_worktree) =
 723                old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
 724            {
 725                let push_strong_handle =
 726                    self.retain_worktrees || old_worktree.read(cx).is_visible();
 727                let handle = if push_strong_handle {
 728                    WorktreeHandle::Strong(old_worktree.clone())
 729                } else {
 730                    WorktreeHandle::Weak(old_worktree.downgrade())
 731                };
 732                self.worktrees.push(handle);
 733            } else {
 734                self.add(
 735                    &Worktree::remote(
 736                        project_id,
 737                        replica_id,
 738                        worktree,
 739                        client.clone(),
 740                        self.path_style(),
 741                        cx,
 742                    ),
 743                    cx,
 744                );
 745            }
 746        }
 747        self.send_project_updates(cx);
 748
 749        Ok(())
 750    }
 751
 752    pub fn move_worktree(
 753        &mut self,
 754        source: WorktreeId,
 755        destination: WorktreeId,
 756        cx: &mut Context<Self>,
 757    ) -> Result<()> {
 758        if source == destination {
 759            return Ok(());
 760        }
 761
 762        let mut source_index = None;
 763        let mut destination_index = None;
 764        for (i, worktree) in self.worktrees.iter().enumerate() {
 765            if let Some(worktree) = worktree.upgrade() {
 766                let worktree_id = worktree.read(cx).id();
 767                if worktree_id == source {
 768                    source_index = Some(i);
 769                    if destination_index.is_some() {
 770                        break;
 771                    }
 772                } else if worktree_id == destination {
 773                    destination_index = Some(i);
 774                    if source_index.is_some() {
 775                        break;
 776                    }
 777                }
 778            }
 779        }
 780
 781        let source_index =
 782            source_index.with_context(|| format!("Missing worktree for id {source}"))?;
 783        let destination_index =
 784            destination_index.with_context(|| format!("Missing worktree for id {destination}"))?;
 785
 786        if source_index == destination_index {
 787            return Ok(());
 788        }
 789
 790        let worktree_to_move = self.worktrees.remove(source_index);
 791        self.worktrees.insert(destination_index, worktree_to_move);
 792        self.worktrees_reordered = true;
 793        cx.emit(WorktreeStoreEvent::WorktreeOrderChanged);
 794        cx.notify();
 795        Ok(())
 796    }
 797
 798    pub fn disconnected_from_host(&mut self, cx: &mut App) {
 799        for worktree in &self.worktrees {
 800            if let Some(worktree) = worktree.upgrade() {
 801                worktree.update(cx, |worktree, _| {
 802                    if let Some(worktree) = worktree.as_remote_mut() {
 803                        worktree.disconnected_from_host();
 804                    }
 805                });
 806            }
 807        }
 808    }
 809
 810    pub fn send_project_updates(&mut self, cx: &mut Context<Self>) {
 811        let Some((downstream_client, project_id)) = self.downstream_client.clone() else {
 812            return;
 813        };
 814
 815        let update = proto::UpdateProject {
 816            project_id,
 817            worktrees: self.worktree_metadata_protos(cx),
 818        };
 819
 820        // collab has bad concurrency guarantees, so we send requests in serial.
 821        let update_project = if downstream_client.is_via_collab() {
 822            Some(downstream_client.request(update))
 823        } else {
 824            downstream_client.send(update).log_err();
 825            None
 826        };
 827        cx.spawn(async move |this, cx| {
 828            if let Some(update_project) = update_project {
 829                update_project.await?;
 830            }
 831
 832            this.update(cx, |this, cx| {
 833                let worktrees = this.worktrees().collect::<Vec<_>>();
 834
 835                for worktree in worktrees {
 836                    worktree.update(cx, |worktree, cx| {
 837                        let client = downstream_client.clone();
 838                        worktree.observe_updates(project_id, cx, {
 839                            move |update| {
 840                                let client = client.clone();
 841                                async move {
 842                                    if client.is_via_collab() {
 843                                        client
 844                                            .request(update)
 845                                            .map(|result| result.log_err().is_some())
 846                                            .await
 847                                    } else {
 848                                        client.send(update).log_err().is_some()
 849                                    }
 850                                }
 851                            }
 852                        });
 853                    });
 854
 855                    cx.emit(WorktreeStoreEvent::WorktreeUpdateSent(worktree.clone()))
 856                }
 857
 858                anyhow::Ok(())
 859            })
 860        })
 861        .detach_and_log_err(cx);
 862    }
 863
 864    pub fn worktree_metadata_protos(&self, cx: &App) -> Vec<proto::WorktreeMetadata> {
 865        self.worktrees()
 866            .map(|worktree| {
 867                let worktree = worktree.read(cx);
 868                proto::WorktreeMetadata {
 869                    id: worktree.id().to_proto(),
 870                    root_name: worktree.root_name_str().to_owned(),
 871                    visible: worktree.is_visible(),
 872                    abs_path: worktree.abs_path().to_string_lossy().into_owned(),
 873                }
 874            })
 875            .collect()
 876    }
 877
 878    pub fn shared(
 879        &mut self,
 880        remote_id: u64,
 881        downstream_client: AnyProtoClient,
 882        cx: &mut Context<Self>,
 883    ) {
 884        self.retain_worktrees = true;
 885        self.downstream_client = Some((downstream_client, remote_id));
 886
 887        // When shared, retain all worktrees
 888        for worktree_handle in self.worktrees.iter_mut() {
 889            match worktree_handle {
 890                WorktreeHandle::Strong(_) => {}
 891                WorktreeHandle::Weak(worktree) => {
 892                    if let Some(worktree) = worktree.upgrade() {
 893                        *worktree_handle = WorktreeHandle::Strong(worktree);
 894                    }
 895                }
 896            }
 897        }
 898        self.send_project_updates(cx);
 899    }
 900
 901    pub fn unshared(&mut self, cx: &mut Context<Self>) {
 902        self.retain_worktrees = false;
 903        self.downstream_client.take();
 904
 905        // When not shared, only retain the visible worktrees
 906        for worktree_handle in self.worktrees.iter_mut() {
 907            if let WorktreeHandle::Strong(worktree) = worktree_handle {
 908                let is_visible = worktree.update(cx, |worktree, _| {
 909                    worktree.stop_observing_updates();
 910                    worktree.is_visible()
 911                });
 912                if !is_visible {
 913                    *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
 914                }
 915            }
 916        }
 917    }
 918
 919    /// search over all worktrees and return buffers that *might* match the search.
 920    pub fn find_search_candidates(
 921        &self,
 922        query: SearchQuery,
 923        limit: usize,
 924        open_entries: HashSet<ProjectEntryId>,
 925        fs: Arc<dyn Fs>,
 926        cx: &Context<Self>,
 927    ) -> Receiver<ProjectPath> {
 928        let snapshots = self
 929            .visible_worktrees(cx)
 930            .filter_map(|tree| {
 931                let tree = tree.read(cx);
 932                Some((tree.snapshot(), tree.as_local()?.settings()))
 933            })
 934            .collect::<Vec<_>>();
 935
 936        let executor = cx.background_executor().clone();
 937
 938        // We want to return entries in the order they are in the worktrees, so we have one
 939        // thread that iterates over the worktrees (and ignored directories) as necessary,
 940        // and pushes a oneshot::Receiver to the output channel and a oneshot::Sender to the filter
 941        // channel.
 942        // We spawn a number of workers that take items from the filter channel and check the query
 943        // against the version of the file on disk.
 944        let (filter_tx, filter_rx) = smol::channel::bounded(64);
 945        let (output_tx, output_rx) = smol::channel::bounded(64);
 946        let (matching_paths_tx, matching_paths_rx) = smol::channel::unbounded();
 947
 948        let input = cx.background_spawn({
 949            let fs = fs.clone();
 950            let query = query.clone();
 951            async move {
 952                Self::find_candidate_paths(
 953                    fs,
 954                    snapshots,
 955                    open_entries,
 956                    query,
 957                    filter_tx,
 958                    output_tx,
 959                )
 960                .await
 961                .log_err();
 962            }
 963        });
 964        const MAX_CONCURRENT_FILE_SCANS: usize = 64;
 965        let filters = cx.background_spawn(async move {
 966            let fs = &fs;
 967            let query = &query;
 968            executor
 969                .scoped(move |scope| {
 970                    for _ in 0..MAX_CONCURRENT_FILE_SCANS {
 971                        let filter_rx = filter_rx.clone();
 972                        scope.spawn(async move {
 973                            Self::filter_paths(fs, filter_rx, query)
 974                                .await
 975                                .log_with_level(log::Level::Debug);
 976                        })
 977                    }
 978                })
 979                .await;
 980        });
 981        cx.background_spawn(async move {
 982            let mut matched = 0;
 983            while let Ok(mut receiver) = output_rx.recv().await {
 984                let Some(path) = receiver.next().await else {
 985                    continue;
 986                };
 987                let Ok(_) = matching_paths_tx.send(path).await else {
 988                    break;
 989                };
 990                matched += 1;
 991                if matched == limit {
 992                    break;
 993                }
 994            }
 995            drop(input);
 996            drop(filters);
 997        })
 998        .detach();
 999        matching_paths_rx
1000    }
1001
1002    fn scan_ignored_dir<'a>(
1003        fs: &'a Arc<dyn Fs>,
1004        snapshot: &'a worktree::Snapshot,
1005        path: &'a RelPath,
1006        query: &'a SearchQuery,
1007        filter_tx: &'a Sender<MatchingEntry>,
1008        output_tx: &'a Sender<oneshot::Receiver<ProjectPath>>,
1009    ) -> BoxFuture<'a, Result<()>> {
1010        async move {
1011            let abs_path = snapshot.absolutize(path);
1012            let Some(mut files) = fs
1013                .read_dir(&abs_path)
1014                .await
1015                .with_context(|| format!("listing ignored path {abs_path:?}"))
1016                .log_err()
1017            else {
1018                return Ok(());
1019            };
1020
1021            let mut results = Vec::new();
1022
1023            while let Some(Ok(file)) = files.next().await {
1024                let Some(metadata) = fs
1025                    .metadata(&file)
1026                    .await
1027                    .with_context(|| format!("fetching fs metadata for {abs_path:?}"))
1028                    .log_err()
1029                    .flatten()
1030                else {
1031                    continue;
1032                };
1033                if metadata.is_symlink || metadata.is_fifo {
1034                    continue;
1035                }
1036                let relative_path = file.strip_prefix(snapshot.abs_path())?;
1037                let relative_path = RelPath::new(&relative_path, snapshot.path_style())
1038                    .context("getting relative path")?;
1039                results.push((relative_path.into_arc(), !metadata.is_dir))
1040            }
1041            results.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path));
1042            for (path, is_file) in results {
1043                if is_file {
1044                    if query.filters_path() {
1045                        let matched_path = if query.match_full_paths() {
1046                            let mut full_path = snapshot.root_name().as_std_path().to_owned();
1047                            full_path.push(path.as_std_path());
1048                            query.match_path(&full_path)
1049                        } else {
1050                            query.match_path(&path.as_std_path())
1051                        };
1052                        if !matched_path {
1053                            continue;
1054                        }
1055                    }
1056                    let (tx, rx) = oneshot::channel();
1057                    output_tx.send(rx).await?;
1058                    filter_tx
1059                        .send(MatchingEntry {
1060                            respond: tx,
1061                            worktree_root: snapshot.abs_path().clone(),
1062                            path: ProjectPath {
1063                                worktree_id: snapshot.id(),
1064                                path: path.into_arc(),
1065                            },
1066                        })
1067                        .await?;
1068                } else {
1069                    Self::scan_ignored_dir(fs, snapshot, &path, query, filter_tx, output_tx)
1070                        .await?;
1071                }
1072            }
1073            Ok(())
1074        }
1075        .boxed()
1076    }
1077
1078    async fn find_candidate_paths(
1079        fs: Arc<dyn Fs>,
1080        snapshots: Vec<(worktree::Snapshot, WorktreeSettings)>,
1081        open_entries: HashSet<ProjectEntryId>,
1082        query: SearchQuery,
1083        filter_tx: Sender<MatchingEntry>,
1084        output_tx: Sender<oneshot::Receiver<ProjectPath>>,
1085    ) -> Result<()> {
1086        for (snapshot, settings) in snapshots {
1087            for entry in snapshot.entries(query.include_ignored(), 0) {
1088                if entry.is_dir() && entry.is_ignored {
1089                    if !settings.is_path_excluded(&entry.path) {
1090                        Self::scan_ignored_dir(
1091                            &fs,
1092                            &snapshot,
1093                            &entry.path,
1094                            &query,
1095                            &filter_tx,
1096                            &output_tx,
1097                        )
1098                        .await?;
1099                    }
1100                    continue;
1101                }
1102
1103                if entry.is_fifo || !entry.is_file() {
1104                    continue;
1105                }
1106
1107                if query.filters_path() {
1108                    let matched_path = if query.match_full_paths() {
1109                        let mut full_path = snapshot.root_name().as_std_path().to_owned();
1110                        full_path.push(entry.path.as_std_path());
1111                        query.match_path(&full_path)
1112                    } else {
1113                        query.match_path(entry.path.as_std_path())
1114                    };
1115                    if !matched_path {
1116                        continue;
1117                    }
1118                }
1119
1120                let (mut tx, rx) = oneshot::channel();
1121
1122                if open_entries.contains(&entry.id) {
1123                    tx.send(ProjectPath {
1124                        worktree_id: snapshot.id(),
1125                        path: entry.path.clone(),
1126                    })
1127                    .await?;
1128                } else {
1129                    filter_tx
1130                        .send(MatchingEntry {
1131                            respond: tx,
1132                            worktree_root: snapshot.abs_path().clone(),
1133                            path: ProjectPath {
1134                                worktree_id: snapshot.id(),
1135                                path: entry.path.clone(),
1136                            },
1137                        })
1138                        .await?;
1139                }
1140
1141                output_tx.send(rx).await?;
1142            }
1143        }
1144        Ok(())
1145    }
1146
1147    async fn filter_paths(
1148        fs: &Arc<dyn Fs>,
1149        input: Receiver<MatchingEntry>,
1150        query: &SearchQuery,
1151    ) -> Result<()> {
1152        let mut input = pin!(input);
1153        while let Some(mut entry) = input.next().await {
1154            let abs_path = entry.worktree_root.join(entry.path.path.as_std_path());
1155            let Some(file) = fs.open_sync(&abs_path).await.log_err() else {
1156                continue;
1157            };
1158
1159            let mut file = BufReader::new(file);
1160            let file_start = file.fill_buf()?;
1161
1162            if let Err(Some(starting_position)) =
1163                std::str::from_utf8(file_start).map_err(|e| e.error_len())
1164            {
1165                // Before attempting to match the file content, throw away files that have invalid UTF-8 sequences early on;
1166                // That way we can still match files in a streaming fashion without having look at "obviously binary" files.
1167                log::debug!(
1168                    "Invalid UTF-8 sequence in file {abs_path:?} at byte position {starting_position}"
1169                );
1170                continue;
1171            }
1172
1173            if query.detect(file).unwrap_or(false) {
1174                entry.respond.send(entry.path).await?
1175            }
1176        }
1177
1178        Ok(())
1179    }
1180
1181    pub async fn handle_create_project_entry(
1182        this: Entity<Self>,
1183        envelope: TypedEnvelope<proto::CreateProjectEntry>,
1184        mut cx: AsyncApp,
1185    ) -> Result<proto::ProjectEntryResponse> {
1186        let worktree = this.update(&mut cx, |this, cx| {
1187            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1188            this.worktree_for_id(worktree_id, cx)
1189                .context("worktree not found")
1190        })??;
1191        Worktree::handle_create_entry(worktree, envelope.payload, cx).await
1192    }
1193
1194    pub async fn handle_copy_project_entry(
1195        this: Entity<Self>,
1196        envelope: TypedEnvelope<proto::CopyProjectEntry>,
1197        mut cx: AsyncApp,
1198    ) -> Result<proto::ProjectEntryResponse> {
1199        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1200        let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
1201        let new_project_path = (
1202            new_worktree_id,
1203            RelPath::from_proto(&envelope.payload.new_path)?,
1204        );
1205        let (scan_id, entry) = this.update(&mut cx, |this, cx| {
1206            let Some((_, project_id)) = this.downstream_client else {
1207                bail!("no downstream client")
1208            };
1209            let Some(entry) = this.entry_for_id(entry_id, cx) else {
1210                bail!("no such entry");
1211            };
1212            if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
1213                bail!("entry is private")
1214            }
1215
1216            let new_worktree = this
1217                .worktree_for_id(new_worktree_id, cx)
1218                .context("no such worktree")?;
1219            let scan_id = new_worktree.read(cx).scan_id();
1220            anyhow::Ok((
1221                scan_id,
1222                this.copy_entry(entry_id, new_project_path.into(), cx),
1223            ))
1224        })??;
1225        let entry = entry.await?;
1226        Ok(proto::ProjectEntryResponse {
1227            entry: entry.as_ref().map(|entry| entry.into()),
1228            worktree_scan_id: scan_id as u64,
1229        })
1230    }
1231
1232    pub async fn handle_delete_project_entry(
1233        this: Entity<Self>,
1234        envelope: TypedEnvelope<proto::DeleteProjectEntry>,
1235        mut cx: AsyncApp,
1236    ) -> Result<proto::ProjectEntryResponse> {
1237        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1238        let worktree = this.update(&mut cx, |this, cx| {
1239            let Some((_, project_id)) = this.downstream_client else {
1240                bail!("no downstream client")
1241            };
1242            let Some(entry) = this.entry_for_id(entry_id, cx) else {
1243                bail!("no entry")
1244            };
1245            if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
1246                bail!("entry is private")
1247            }
1248            this.worktree_for_entry(entry_id, cx)
1249                .context("worktree not found")
1250        })??;
1251        Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
1252    }
1253
1254    pub async fn handle_rename_project_entry(
1255        this: Entity<Self>,
1256        request: proto::RenameProjectEntry,
1257        mut cx: AsyncApp,
1258    ) -> Result<proto::ProjectEntryResponse> {
1259        let entry_id = ProjectEntryId::from_proto(request.entry_id);
1260        let new_worktree_id = WorktreeId::from_proto(request.new_worktree_id);
1261        let rel_path = RelPath::from_proto(&request.new_path)
1262            .with_context(|| format!("received invalid relative path {:?}", &request.new_path))?;
1263
1264        let (scan_id, task) = this.update(&mut cx, |this, cx| {
1265            let worktree = this
1266                .worktree_for_entry(entry_id, cx)
1267                .context("no such worktree")?;
1268
1269            let Some((_, project_id)) = this.downstream_client else {
1270                bail!("no downstream client")
1271            };
1272            let entry = worktree
1273                .read(cx)
1274                .entry_for_id(entry_id)
1275                .ok_or_else(|| anyhow!("missing entry"))?;
1276            if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
1277                bail!("entry is private")
1278            }
1279
1280            let scan_id = worktree.read(cx).scan_id();
1281            anyhow::Ok((
1282                scan_id,
1283                this.rename_entry(entry_id, (new_worktree_id, rel_path).into(), cx),
1284            ))
1285        })??;
1286        Ok(proto::ProjectEntryResponse {
1287            entry: match &task.await? {
1288                CreatedEntry::Included(entry) => Some(entry.into()),
1289                CreatedEntry::Excluded { .. } => None,
1290            },
1291            worktree_scan_id: scan_id as u64,
1292        })
1293    }
1294
1295    pub async fn handle_expand_project_entry(
1296        this: Entity<Self>,
1297        envelope: TypedEnvelope<proto::ExpandProjectEntry>,
1298        mut cx: AsyncApp,
1299    ) -> Result<proto::ExpandProjectEntryResponse> {
1300        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1301        let worktree = this
1302            .update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
1303            .context("invalid request")?;
1304        Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
1305    }
1306
1307    pub async fn handle_expand_all_for_project_entry(
1308        this: Entity<Self>,
1309        envelope: TypedEnvelope<proto::ExpandAllForProjectEntry>,
1310        mut cx: AsyncApp,
1311    ) -> Result<proto::ExpandAllForProjectEntryResponse> {
1312        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
1313        let worktree = this
1314            .update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
1315            .context("invalid request")?;
1316        Worktree::handle_expand_all_for_entry(worktree, envelope.payload, cx).await
1317    }
1318
1319    pub fn fs(&self) -> Option<Arc<dyn Fs>> {
1320        match &self.state {
1321            WorktreeStoreState::Local { fs } => Some(fs.clone()),
1322            WorktreeStoreState::Remote { .. } => None,
1323        }
1324    }
1325}
1326
1327#[derive(Clone, Debug)]
1328enum WorktreeHandle {
1329    Strong(Entity<Worktree>),
1330    Weak(WeakEntity<Worktree>),
1331}
1332
1333impl WorktreeHandle {
1334    fn upgrade(&self) -> Option<Entity<Worktree>> {
1335        match self {
1336            WorktreeHandle::Strong(handle) => Some(handle.clone()),
1337            WorktreeHandle::Weak(handle) => handle.upgrade(),
1338        }
1339    }
1340}