1146 lines
45 KiB
Rust
1146 lines
45 KiB
Rust
use std::collections::HashMap;
|
|
#[cfg(feature = "local_fs")]
|
|
use std::collections::HashSet;
|
|
#[cfg(feature = "local_fs")]
|
|
use std::path::Path;
|
|
#[cfg(feature = "local_fs")]
|
|
use std::path::PathBuf;
|
|
|
|
use galaxy_core::SessionId;
|
|
#[cfg(feature = "local_fs")]
|
|
use indexmap::IndexSet;
|
|
#[cfg(feature = "local_fs")]
|
|
use remote_server::manager::RemoteServerManager;
|
|
#[cfg(feature = "local_fs")]
|
|
use repo_metadata::repositories::DetectedRepositories;
|
|
#[cfg(feature = "local_fs")]
|
|
use warp_util::remote_path::RemotePath;
|
|
#[cfg(feature = "local_fs")]
|
|
use warpui::{AppContext, SingletonEntity as _};
|
|
use warpui::{Entity, EntityId, ModelContext, ModelHandle, ViewHandle};
|
|
|
|
use crate::code::buffer_location::LocalOrRemotePath;
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::code::file_tree::FileTreeView;
|
|
use crate::code_review::code_review_view::CodeReviewView;
|
|
use crate::code_review::comments::{
|
|
AttachedReviewComment, PendingImportedReviewComment, ReviewCommentBatch,
|
|
};
|
|
use crate::code_review::diff_state::{DiffMode, DiffStateModel};
|
|
use crate::workspace::view::global_search::view::GlobalSearchView;
|
|
|
|
/// Type-safe wrapper around the map of `LocalOrRemotePath` → `DiffStateModel`.
|
|
///
|
|
/// Enforces that local keys are always paired with local-backend models and
|
|
/// remote keys with remote-backend models via dedicated insertion methods.
|
|
#[cfg(feature = "local_fs")]
|
|
#[derive(Default)]
|
|
struct DiffStateModelMap {
|
|
models: HashMap<LocalOrRemotePath, ModelHandle<DiffStateModel>>,
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
impl DiffStateModelMap {
|
|
fn get(&self, key: &LocalOrRemotePath) -> Option<&ModelHandle<DiffStateModel>> {
|
|
self.models.get(key)
|
|
}
|
|
|
|
/// Insert a model that was created from a `LocalOrRemotePath::Local` key.
|
|
fn insert_local(
|
|
&mut self,
|
|
path: PathBuf,
|
|
model: ModelHandle<DiffStateModel>,
|
|
ctx: &AppContext,
|
|
) {
|
|
debug_assert!(
|
|
matches!(model.as_ref(ctx), DiffStateModel::Local(_)),
|
|
"insert_local called with a remote-backend DiffStateModel",
|
|
);
|
|
self.models.insert(LocalOrRemotePath::Local(path), model);
|
|
}
|
|
|
|
/// Insert a model that was created from a `LocalOrRemotePath::Remote` key.
|
|
fn insert_remote(
|
|
&mut self,
|
|
remote_id: RemotePath,
|
|
model: ModelHandle<DiffStateModel>,
|
|
ctx: &AppContext,
|
|
) {
|
|
debug_assert!(
|
|
matches!(model.as_ref(ctx), DiffStateModel::Remote(_)),
|
|
"insert_remote called with a local-backend DiffStateModel",
|
|
);
|
|
self.models
|
|
.insert(LocalOrRemotePath::Remote(remote_id), model);
|
|
}
|
|
|
|
fn remove(&mut self, key: &LocalOrRemotePath) -> Option<ModelHandle<DiffStateModel>> {
|
|
self.models.remove(key)
|
|
}
|
|
}
|
|
|
|
/// Bidirectional map of pane groups to the repository roots they reference.
|
|
///
|
|
/// Maintains both a forward map (`pane_group_id -> ordered set of repo paths`)
|
|
/// and a reverse map (`repo path -> set of pane group ids that reference it`)
|
|
/// in lockstep, so callers can answer "is this repo still referenced by any
|
|
/// pane group?" in O(1) without scanning every pane group's set.
|
|
///
|
|
/// All mutations go through methods on this wrapper to guarantee the two
|
|
/// maps stay in sync.
|
|
#[cfg(feature = "local_fs")]
|
|
#[derive(Default)]
|
|
struct PaneGroupRepositoryRoots {
|
|
/// Forward: per-pane-group ordered set of repository roots.
|
|
/// IndexSet maintains insertion order so most recently added repos appear later.
|
|
pane_group_to_paths: HashMap<EntityId, IndexSet<LocalOrRemotePath>>,
|
|
/// Reverse: which pane groups reference each repo path.
|
|
/// Maintained in lockstep with `pane_group_to_paths`.
|
|
path_to_pane_groups: HashMap<LocalOrRemotePath, HashSet<EntityId>>,
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
impl PaneGroupRepositoryRoots {
|
|
/// Read-only view of a pane group's repository roots, preserving the
|
|
/// existing `HashMap::get(&pane_group_id)` semantics.
|
|
fn get(&self, pane_group_id: EntityId) -> Option<&IndexSet<LocalOrRemotePath>> {
|
|
self.pane_group_to_paths.get(&pane_group_id)
|
|
}
|
|
|
|
/// Insert a single repo for a pane group. Returns `true` if it was newly
|
|
/// added to the pane group (matching `IndexSet::insert` semantics).
|
|
///
|
|
/// Always keeps the reverse map in sync: if the path was newly added to
|
|
/// the pane group, the pane group is added to the path's reverse entry.
|
|
fn insert(&mut self, pane_group_id: EntityId, path: LocalOrRemotePath) -> bool {
|
|
let added = self
|
|
.pane_group_to_paths
|
|
.entry(pane_group_id)
|
|
.or_default()
|
|
.insert(path.clone());
|
|
|
|
if added {
|
|
self.path_to_pane_groups
|
|
.entry(path)
|
|
.or_default()
|
|
.insert(pane_group_id);
|
|
}
|
|
|
|
added
|
|
}
|
|
|
|
/// Set the full list of repository roots for a pane group,
|
|
/// preserving the insertion-order of the previous set.
|
|
///
|
|
/// Returns the paths that left this pane group's set AND no longer have
|
|
/// any other pane group referencing them — i.e. the paths whose shared
|
|
/// `DiffStateModel` is now safe to drop. Any paths that were already referenced
|
|
/// by other pane groups are kept in their original order.
|
|
fn set_paths(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
new_paths: impl IntoIterator<Item = LocalOrRemotePath>,
|
|
) -> Vec<LocalOrRemotePath> {
|
|
let new_paths: Vec<LocalOrRemotePath> = new_paths.into_iter().collect();
|
|
let new_set: HashSet<&LocalOrRemotePath> = new_paths.iter().collect();
|
|
|
|
// Update the forward map and capture which paths left this pane group
|
|
// (`removed`) and which were newly inserted into it (`newly_added`).
|
|
// Tracking `newly_added` separately lets us skip redundant reverse-map
|
|
// updates for paths the pane group already referenced.
|
|
let (removed, newly_added): (Vec<LocalOrRemotePath>, Vec<LocalOrRemotePath>) = {
|
|
let forward = self.pane_group_to_paths.entry(pane_group_id).or_default();
|
|
let removed: Vec<LocalOrRemotePath> = forward
|
|
.iter()
|
|
.filter(|item| !new_set.contains(*item))
|
|
.cloned()
|
|
.collect();
|
|
forward.retain(|item| new_set.contains(item));
|
|
let mut newly_added: Vec<LocalOrRemotePath> = Vec::new();
|
|
for item in &new_paths {
|
|
if forward.insert(item.clone()) {
|
|
newly_added.push(item.clone());
|
|
}
|
|
}
|
|
(removed, newly_added)
|
|
};
|
|
|
|
// Add pane_group_id only for paths that are newly referenced by this
|
|
// pane group; paths it already referenced are already in the reverse
|
|
// entry by the invariant maintained on every mutation.
|
|
for path in newly_added {
|
|
self.path_to_pane_groups
|
|
.entry(path)
|
|
.or_default()
|
|
.insert(pane_group_id);
|
|
}
|
|
|
|
// Drop pane_group_id from the reverse map for paths it no longer
|
|
// references; collect the paths whose reverse entry became empty.
|
|
removed
|
|
.into_iter()
|
|
.filter(|path| self.remove_path(pane_group_id, path))
|
|
.collect()
|
|
}
|
|
|
|
/// Drop all entries for a pane group (used when a tab is closed or the
|
|
/// pane group becomes empty). Returns `Some(orphans)` when the pane group
|
|
/// had a `repository_roots` entry, where `orphans` are the paths that no
|
|
/// longer have any pane group referencing them. Returns `None` when the
|
|
/// pane group was not tracked, so callers can distinguish "present with
|
|
/// no orphans" from "not present" in a single call.
|
|
fn remove_pane_group(&mut self, pane_group_id: EntityId) -> Option<Vec<LocalOrRemotePath>> {
|
|
let paths = self.pane_group_to_paths.remove(&pane_group_id)?;
|
|
Some(
|
|
paths
|
|
.into_iter()
|
|
.filter(|path| self.remove_path(pane_group_id, path))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
/// Remove `pane_group_id` from the reverse-map entry for `path`.
|
|
/// Returns `true` if removing this reference left `path` with no pane groups
|
|
/// referencing it — i.e. the path is now globally orphaned.
|
|
///
|
|
/// This only mutates the reverse map; callers are responsible for
|
|
/// removing `path` from `pane_group_id`'s forward entry before (or
|
|
/// after) calling this.
|
|
fn remove_path(&mut self, pane_group_id: EntityId, path: &LocalOrRemotePath) -> bool {
|
|
let became_empty = self.path_to_pane_groups.get_mut(path).is_some_and(|set| {
|
|
set.remove(&pane_group_id);
|
|
set.is_empty()
|
|
});
|
|
if became_empty {
|
|
self.path_to_pane_groups.remove(path);
|
|
}
|
|
became_empty
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct WorkingDirectory {
|
|
pub path: LocalOrRemotePath,
|
|
pub terminal_id: Option<EntityId>,
|
|
}
|
|
|
|
/// Events emitted when the set of working directories changes
|
|
#[derive(Clone, Debug)]
|
|
pub enum WorkingDirectoriesEvent {
|
|
/// The set of working directories has changed for a specific pane group.
|
|
DirectoriesChanged {
|
|
/// The PaneGroup whose directories changed
|
|
pane_group_id: EntityId,
|
|
/// All active directories (deduplicated) in most to least recently added order.
|
|
directories: Vec<WorkingDirectory>,
|
|
},
|
|
/// The set of repositories has changed for a specific pane group.
|
|
RepositoriesChanged {
|
|
/// The PaneGroup whose repositories changed
|
|
pane_group_id: EntityId,
|
|
/// All active repository roots (deduplicated) in most to least recently added order.
|
|
repositories: Vec<LocalOrRemotePath>,
|
|
},
|
|
/// The focused repository changed for a specific pane group.
|
|
/// This fires when the user focuses a different pane or CDs within the focused pane.
|
|
FocusedRepoChanged {
|
|
/// The PaneGroup whose focused repo changed
|
|
pane_group_id: EntityId,
|
|
/// All active repository-terminal ID pairs (deduplicated)
|
|
repository_terminal_map: HashMap<LocalOrRemotePath, EntityId>,
|
|
/// The repository path of the focused terminal, if any
|
|
focused_repo: Option<LocalOrRemotePath>,
|
|
},
|
|
}
|
|
|
|
#[derive(Default)]
|
|
#[cfg(feature = "local_fs")]
|
|
/// Workspace model that tracks working directories across all pane groups.
|
|
/// Emits events when the set of directories changes for any pane group.
|
|
pub struct WorkingDirectoriesModel {
|
|
/// Per-pane-group tracking of active directories (both local and remote) as a
|
|
/// deduplicated, ordered set.
|
|
///
|
|
/// This stores the *display roots* for the left panel (file tree / global search),
|
|
/// not the raw working directories reported by each pane.
|
|
///
|
|
/// Concretely, for each pane group's active paths we store:
|
|
/// - the detected repository root when the path belongs to a repo
|
|
/// - otherwise, the normalized path itself (local) or the remote CWD/editor path
|
|
///
|
|
/// IndexSet maintains insertion order - most recently added directories appear later.
|
|
pane_groups: HashMap<EntityId, IndexSet<LocalOrRemotePath>>,
|
|
/// Per-pane-group tracking of active repository roots as a deduplicated, ordered set.
|
|
/// Covers both local and remote repositories in a single map.
|
|
repository_roots: PaneGroupRepositoryRoots,
|
|
/// Per-pane-group mapping from root paths to a matching terminal view ID.
|
|
/// This allows looking up which terminal is associated with each root path.
|
|
/// Note, a single root path can be associated with multiple terminals.
|
|
/// we're just storing an arbitrary terminal ID for each root path.
|
|
directory_to_terminal: HashMap<EntityId, HashMap<LocalOrRemotePath, EntityId>>,
|
|
/// Global mapping from repository keys to their DiffStateModel.
|
|
/// Since git state is inherently tied to a repository (not a pane group),
|
|
/// this is stored globally and shared across all pane groups viewing the same repo.
|
|
diff_state_models: DiffStateModelMap,
|
|
/// Global mapping from repository locations to their CommentBatch.
|
|
/// Like the DiffStateModel mapping, comments are inherently tied to git diffs
|
|
/// and are shared across all pane groups viewing the same repo.
|
|
comment_models: HashMap<LocalOrRemotePath, ModelHandle<ReviewCommentBatch>>,
|
|
/// Per-pane-group mapping from repository root locations to their CodeReviewView.
|
|
/// This allows reusing code review views across multiple requests for the same repo.
|
|
code_review_views: HashMap<EntityId, HashMap<LocalOrRemotePath, ViewHandle<CodeReviewView>>>,
|
|
/// Per-pane-group tracking of the focused repository root path.
|
|
focused_repo: HashMap<EntityId, Option<LocalOrRemotePath>>,
|
|
/// Per-pane-group tracking of the repository the user has manually selected for the
|
|
/// code review (right) panel. This is the repo that should be restored when the user
|
|
/// leaves the pane group's session and returns to it later, even if the auto-selection
|
|
/// logic would otherwise pick a different default.
|
|
selected_review_repo: HashMap<EntityId, LocalOrRemotePath>,
|
|
global_search_views: HashMap<EntityId, ViewHandle<GlobalSearchView>>,
|
|
file_tree_views: HashMap<EntityId, ViewHandle<FileTreeView>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
#[cfg(not(feature = "local_fs"))]
|
|
/// Does nothing without a local file system
|
|
pub struct WorkingDirectoriesModel {}
|
|
|
|
/// Index Sets are ordered by insertion order. This function updates an index set to match a new set of items.
|
|
#[cfg(feature = "local_fs")]
|
|
pub fn update_index_set(
|
|
index_set: &mut IndexSet<LocalOrRemotePath>,
|
|
new_items: impl IntoIterator<Item = LocalOrRemotePath>,
|
|
) {
|
|
let new_items: Vec<LocalOrRemotePath> = new_items.into_iter().collect();
|
|
index_set.retain(|item| new_items.iter().any(|new_item| new_item == item));
|
|
for item in new_items {
|
|
index_set.insert(item);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
impl WorkingDirectoriesModel {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Get the unique directories for a specific pane group in insertion order (oldest first).
|
|
fn least_recent_directories_for_pane_group(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
) -> Option<&IndexSet<LocalOrRemotePath>> {
|
|
self.pane_groups.get(&pane_group_id)
|
|
}
|
|
|
|
/// Get the unique directories for a specific pane group in most to least recently added order.
|
|
pub fn most_recent_directories_for_pane_group(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
) -> Option<impl Iterator<Item = WorkingDirectory> + '_> {
|
|
self.least_recent_directories_for_pane_group(pane_group_id)
|
|
.map(move |dirs| {
|
|
dirs.iter().rev().map(move |lor| WorkingDirectory {
|
|
path: lor.clone(),
|
|
terminal_id: self.get_terminal_id_for_root_path(pane_group_id, lor),
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Get the unique repository roots for a specific pane group in insertion order (oldest first).
|
|
fn least_recent_repositories_for_pane_group(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
) -> Option<&IndexSet<LocalOrRemotePath>> {
|
|
self.repository_roots.get(pane_group_id)
|
|
}
|
|
|
|
/// Get the unique repository roots for a specific pane group in most to least recently added order.
|
|
pub fn most_recent_repositories_for_pane_group(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
) -> Option<impl Iterator<Item = LocalOrRemotePath> + '_> {
|
|
self.least_recent_repositories_for_pane_group(pane_group_id)
|
|
.map(|repos| repos.iter().rev().cloned())
|
|
}
|
|
|
|
/// Get the terminal view ID associated with a specific root path in a pane group.
|
|
pub fn get_terminal_id_for_root_path(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
root_path: &LocalOrRemotePath,
|
|
) -> Option<EntityId> {
|
|
self.directory_to_terminal
|
|
.get(&pane_group_id)
|
|
.and_then(|roots| roots.get(root_path).copied())
|
|
}
|
|
|
|
/// Get or create a DiffStateModel for a specific repository.
|
|
///
|
|
/// If the model doesn't exist, it will be created. For remote
|
|
/// repositories we require a connected session for the host; returns
|
|
/// `None` when none exists so callers treat the panel as unavailable
|
|
/// for that repo rather than producing a model that cannot subscribe.
|
|
pub fn get_or_create_diff_state_model(
|
|
&mut self,
|
|
key: LocalOrRemotePath,
|
|
preferred_session: Option<SessionId>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Option<ModelHandle<DiffStateModel>> {
|
|
if let Some(model) = self.diff_state_models.get(&key) {
|
|
return Some(model.clone());
|
|
}
|
|
|
|
let diff_state_model = match &key {
|
|
LocalOrRemotePath::Local(path) => {
|
|
let path = path.clone();
|
|
ctx.add_model(|ctx| DiffStateModel::new_local(path, ctx))
|
|
}
|
|
LocalOrRemotePath::Remote(remote_path) => {
|
|
let mgr_handle = RemoteServerManager::handle(ctx);
|
|
mgr_handle
|
|
.as_ref(ctx)
|
|
.client_for_host(&remote_path.host_id)?;
|
|
let remote_path = remote_path.clone();
|
|
ctx.add_model(|ctx| DiffStateModel::new_remote(remote_path, preferred_session, ctx))
|
|
}
|
|
};
|
|
|
|
match key {
|
|
LocalOrRemotePath::Local(path) => {
|
|
self.diff_state_models
|
|
.insert_local(path, diff_state_model.clone(), ctx);
|
|
}
|
|
LocalOrRemotePath::Remote(remote_id) => {
|
|
self.diff_state_models
|
|
.insert_remote(remote_id, diff_state_model.clone(), ctx);
|
|
}
|
|
}
|
|
|
|
Some(diff_state_model)
|
|
}
|
|
|
|
/// Drops diff state models for repos that are no longer referenced by any
|
|
/// pane group. The input must already be pre-filtered to orphans, so this
|
|
/// method stops the watcher and removes stale model and view cache entries.
|
|
fn drop_unused_diff_state_models(
|
|
&mut self,
|
|
orphaned_repos: impl IntoIterator<Item = LocalOrRemotePath>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
for repo_key in orphaned_repos {
|
|
if let Some(model) = self.diff_state_models.remove(&repo_key) {
|
|
model.update(ctx, |model, ctx| {
|
|
model.stop_active_watcher(ctx);
|
|
});
|
|
}
|
|
for views in self.code_review_views.values_mut() {
|
|
views.remove(&repo_key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get or create a ReviewCommentBatch for a specific repository.
|
|
/// If the model doesn't exist, it will be created.
|
|
pub fn get_or_create_code_review_comments(
|
|
&mut self,
|
|
repo_path: &LocalOrRemotePath,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Option<ModelHandle<ReviewCommentBatch>> {
|
|
if let Some(existing) = self.comment_models.get(repo_path) {
|
|
return Some(existing.clone());
|
|
}
|
|
let model = ctx.add_model(|_ctx| ReviewCommentBatch::default());
|
|
self.comment_models.insert(repo_path.clone(), model.clone());
|
|
Some(model)
|
|
}
|
|
|
|
/// Store a CodeReviewView for a specific repository in a pane group.
|
|
pub fn store_code_review_view(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
repo_path: LocalOrRemotePath,
|
|
view: ViewHandle<CodeReviewView>,
|
|
) {
|
|
let pane_group_views = self.code_review_views.entry(pane_group_id).or_default();
|
|
pane_group_views.insert(repo_path, view);
|
|
|
|
// Remove any inactive code reviews here. This allows these to be garbage collected.
|
|
self.remove_inactive_code_reviews(pane_group_id);
|
|
}
|
|
|
|
/// Remove any code review view state that is not active in any of the terminal views that belong to this pane group.
|
|
fn remove_inactive_code_reviews(&mut self, pane_group_id: EntityId) {
|
|
let Some(code_review_views) = self.code_review_views.get_mut(&pane_group_id) else {
|
|
return;
|
|
};
|
|
|
|
let Some(terminal_mapping) = self.directory_to_terminal.get(&pane_group_id) else {
|
|
return;
|
|
};
|
|
|
|
code_review_views.retain(|path, _| terminal_mapping.contains_key(path));
|
|
}
|
|
|
|
/// Get an existing CodeReviewView for a specific repository in a pane group.
|
|
/// Returns None if no view exists for this combination.
|
|
pub fn get_code_review_view(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
repo_path: &LocalOrRemotePath,
|
|
) -> Option<ViewHandle<CodeReviewView>> {
|
|
self.code_review_views
|
|
.get(&pane_group_id)
|
|
.and_then(|pane_group_views| pane_group_views.get(repo_path))
|
|
.cloned()
|
|
}
|
|
|
|
/// Get the repository path the user has manually selected for the code review
|
|
/// panel in a given pane group, if any. Used to restore the selection when the
|
|
/// user navigates back to the pane group's session.
|
|
pub fn get_selected_review_repo(&self, pane_group_id: EntityId) -> Option<&LocalOrRemotePath> {
|
|
self.selected_review_repo.get(&pane_group_id)
|
|
}
|
|
|
|
/// Persist the repository the user manually selected for the code review panel
|
|
/// in a given pane group. This is only called for explicit user-driven
|
|
/// selections (e.g. via the dropdown), not for auto-selected defaults.
|
|
pub fn set_selected_review_repo(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
repo_path: LocalOrRemotePath,
|
|
) {
|
|
self.selected_review_repo.insert(pane_group_id, repo_path);
|
|
}
|
|
|
|
/// Clear the saved code review panel selection for a pane group.
|
|
pub fn clear_selected_review_repo(&mut self, pane_group_id: EntityId) {
|
|
self.selected_review_repo.remove(&pane_group_id);
|
|
}
|
|
|
|
pub fn store_global_search_view(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
view: ViewHandle<GlobalSearchView>,
|
|
) {
|
|
self.global_search_views.insert(pane_group_id, view);
|
|
}
|
|
|
|
pub fn get_global_search_view(
|
|
&self,
|
|
pane_group_id: EntityId,
|
|
) -> Option<ViewHandle<GlobalSearchView>> {
|
|
self.global_search_views.get(&pane_group_id).cloned()
|
|
}
|
|
|
|
pub fn store_file_tree_view(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
view: ViewHandle<FileTreeView>,
|
|
) {
|
|
self.file_tree_views.insert(pane_group_id, view);
|
|
}
|
|
|
|
pub fn get_file_tree_view(&self, pane_group_id: EntityId) -> Option<ViewHandle<FileTreeView>> {
|
|
self.file_tree_views.get(&pane_group_id).cloned()
|
|
}
|
|
|
|
/// Permanently removes all state associated with a pane group.
|
|
/// This should be called when a tab is closed (pane group is destroyed),
|
|
/// as opposed to handle_empty_pane_group which is called when working directories
|
|
/// become empty but the pane group still exists (e.g., settings page).
|
|
pub fn remove_pane_group(&mut self, pane_group_id: EntityId, ctx: &mut ModelContext<Self>) {
|
|
// Clean up directories, terminals, and repos (emits events for subscribers)
|
|
self.handle_empty_pane_group(pane_group_id, ctx);
|
|
|
|
// Clean up views that should persist in handle_empty_pane_group e.g. there's only a settings pane in the pane group
|
|
// but need to be removed when the pane group is destroyed
|
|
self.global_search_views.remove(&pane_group_id);
|
|
self.file_tree_views.remove(&pane_group_id);
|
|
self.code_review_views.remove(&pane_group_id);
|
|
self.focused_repo.remove(&pane_group_id);
|
|
self.selected_review_repo.remove(&pane_group_id);
|
|
}
|
|
|
|
fn handle_empty_pane_group(&mut self, pane_group_id: EntityId, ctx: &mut ModelContext<Self>) {
|
|
let did_remove_dirs = self.pane_groups.remove(&pane_group_id).is_some();
|
|
let did_remove_terminals = self.directory_to_terminal.remove(&pane_group_id).is_some();
|
|
let orphaned_repos = self.repository_roots.remove_pane_group(pane_group_id);
|
|
let did_remove_repos = orphaned_repos.is_some();
|
|
|
|
if let Some(orphaned_repos) = orphaned_repos {
|
|
self.drop_unused_diff_state_models(orphaned_repos, ctx);
|
|
}
|
|
|
|
if did_remove_dirs {
|
|
ctx.emit(WorkingDirectoriesEvent::DirectoriesChanged {
|
|
pane_group_id,
|
|
directories: vec![],
|
|
});
|
|
}
|
|
if did_remove_repos {
|
|
ctx.emit(WorkingDirectoriesEvent::RepositoriesChanged {
|
|
pane_group_id,
|
|
repositories: vec![],
|
|
});
|
|
}
|
|
if did_remove_terminals {
|
|
ctx.emit(WorkingDirectoriesEvent::FocusedRepoChanged {
|
|
pane_group_id,
|
|
repository_terminal_map: HashMap::new(),
|
|
focused_repo: None,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Refreshes the working directories for a pane group from terminal CWDs
|
|
/// (both local and remote) and code editor paths.
|
|
///
|
|
/// If `focused_terminal_id` is provided, the repo_to_terminal map will prioritize
|
|
pub fn refresh_working_directories_for_pane_group(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
terminal_cwds: Vec<(EntityId, LocalOrRemotePath)>,
|
|
editor_paths: Vec<(EntityId, LocalOrRemotePath)>,
|
|
focused_terminal_id: Option<EntityId>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
if terminal_cwds.is_empty() && editor_paths.is_empty() {
|
|
self.handle_empty_pane_group(pane_group_id, ctx);
|
|
return;
|
|
}
|
|
|
|
let old_directories: Vec<WorkingDirectory> = self
|
|
.least_recent_directories_for_pane_group(pane_group_id)
|
|
.map(|dirs| {
|
|
dirs.iter()
|
|
.map(|lor| WorkingDirectory {
|
|
path: lor.clone(),
|
|
terminal_id: self.get_terminal_id_for_root_path(pane_group_id, lor),
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
let old_repos: Vec<LocalOrRemotePath> = self
|
|
.least_recent_repositories_for_pane_group(pane_group_id)
|
|
.map(|repos| repos.iter().cloned().collect())
|
|
.unwrap_or_default();
|
|
let old_focused_repo: Option<LocalOrRemotePath> =
|
|
self.focused_repo.get(&pane_group_id).cloned().flatten();
|
|
|
|
// Resolve a local path to its detected repository root, or keep the path as-is if no repo is found.
|
|
let root_for_path = |path: PathBuf| {
|
|
DetectedRepositories::as_ref(ctx)
|
|
.get_root_for_path(&LocalOrRemotePath::Local(path.clone()))
|
|
.and_then(|r| PathBuf::try_from(r).ok())
|
|
.unwrap_or(path)
|
|
};
|
|
|
|
let root_for_raw_path = |raw_path: &str| normalize_cwd(raw_path).map(root_for_path);
|
|
|
|
// Split terminal CWDs into local and remote buckets.
|
|
let mut local_terminal_cwds: Vec<(EntityId, String)> = Vec::new();
|
|
let mut remote_terminal_cwds: Vec<(EntityId, RemotePath)> = Vec::new();
|
|
for (terminal_id, cwd) in &terminal_cwds {
|
|
match cwd {
|
|
LocalOrRemotePath::Local(path) => {
|
|
local_terminal_cwds.push((*terminal_id, path.to_string_lossy().into_owned()));
|
|
}
|
|
LocalOrRemotePath::Remote(remote_path) => {
|
|
remote_terminal_cwds.push((*terminal_id, remote_path.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collapse working directories to their nearest repository root (when detected).
|
|
let mut file_path_ancestors: HashSet<PathBuf> = local_terminal_cwds
|
|
.iter()
|
|
.filter_map(|(_, cwd)| root_for_raw_path(cwd))
|
|
.collect();
|
|
|
|
// Split editor paths into local and remote buckets.
|
|
let mut local_editor_paths: Vec<(EntityId, String)> = Vec::new();
|
|
let mut remote_editor_paths: Vec<(EntityId, RemotePath)> = Vec::new();
|
|
for (view_id, path) in &editor_paths {
|
|
match path {
|
|
LocalOrRemotePath::Local(p) => {
|
|
local_editor_paths.push((*view_id, p.to_string_lossy().into_owned()));
|
|
}
|
|
LocalOrRemotePath::Remote(remote_path) => {
|
|
remote_editor_paths.push((*view_id, remote_path.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
let local_cwds: Vec<(EntityId, String)> = local_editor_paths
|
|
.into_iter()
|
|
.filter_map(|(view_id, path)| {
|
|
let path_buf = PathBuf::from(&path);
|
|
let resolved_path = self
|
|
.get_repo_root_for_path(&path_buf, ctx)
|
|
.or_else(|| path_buf.parent().map(|p| p.to_path_buf()))?;
|
|
|
|
if file_path_ancestors.insert(resolved_path.clone()) {
|
|
Some((view_id, resolved_path.display().to_string()))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
// Build the local root paths for pane_groups.
|
|
let new_local_root_paths: Vec<PathBuf> = local_terminal_cwds
|
|
.iter()
|
|
.chain(local_cwds.iter())
|
|
.filter_map(|(_, cwd)| root_for_raw_path(cwd))
|
|
.collect();
|
|
|
|
// Build remote root paths for pane_groups from remote terminal CWDs
|
|
// and remote editor paths (resolved to repo root when possible).
|
|
let mut new_remote_display_roots: Vec<LocalOrRemotePath> = Vec::new();
|
|
for (_terminal_id, remote_path) in &remote_terminal_cwds {
|
|
let remote_key = LocalOrRemotePath::Remote(remote_path.clone());
|
|
let root = DetectedRepositories::as_ref(ctx)
|
|
.get_root_for_path(&remote_key)
|
|
.unwrap_or(remote_key);
|
|
new_remote_display_roots.push(root);
|
|
}
|
|
for (_view_id, remote_path) in &remote_editor_paths {
|
|
let remote_key = LocalOrRemotePath::Remote(remote_path.clone());
|
|
if let Some(repo_root) =
|
|
DetectedRepositories::as_ref(ctx).get_root_for_path(&remote_key)
|
|
{
|
|
new_remote_display_roots.push(repo_root);
|
|
} else if let Some(parent) = remote_path.path.parent() {
|
|
// Fall back to the parent directory, matching the local editor path behavior.
|
|
new_remote_display_roots.push(LocalOrRemotePath::Remote(RemotePath::new(
|
|
remote_path.host_id.clone(),
|
|
parent,
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Combine local + remote into the unified display roots set.
|
|
let new_display_roots: Vec<LocalOrRemotePath> = new_local_root_paths
|
|
.iter()
|
|
.cloned()
|
|
.map(LocalOrRemotePath::Local)
|
|
.chain(new_remote_display_roots.iter().cloned())
|
|
.collect();
|
|
|
|
// Get or create the IndexSet for this pane group
|
|
// (IndexSet maintains insertion order and auto-deduplicates)
|
|
let pane_group_roots = self.pane_groups.entry(pane_group_id).or_default();
|
|
update_index_set(pane_group_roots, new_display_roots);
|
|
|
|
// Build repo roots and their terminal associations
|
|
// First pass: collect all local repo roots and build initial mapping
|
|
let new_local_repo_roots: Vec<PathBuf> = self
|
|
.pane_groups
|
|
.get(&pane_group_id)
|
|
.into_iter()
|
|
.flat_map(|dirs| dirs.iter())
|
|
.filter_map(|lor| lor.to_local_path())
|
|
.filter_map(|dir| self.get_repo_root_for_path(dir, ctx))
|
|
.collect();
|
|
let mut new_roots: HashSet<PathBuf> =
|
|
HashSet::from_iter(new_local_repo_roots.iter().cloned());
|
|
new_roots.extend(new_local_root_paths.iter().cloned());
|
|
|
|
// Build mapping from directories to their terminal IDs (keyed by LocalOrRemotePath).
|
|
// Local paths come from `root_for_raw_path` → `normalize_cwd`.
|
|
let mut new_root_to_terminal: HashMap<LocalOrRemotePath, EntityId> = local_terminal_cwds
|
|
.iter()
|
|
.filter_map(|(terminal_id, cwd)| {
|
|
root_for_raw_path(cwd).map(|p| (LocalOrRemotePath::Local(p), *terminal_id))
|
|
})
|
|
.collect();
|
|
new_root_to_terminal
|
|
.retain(|cwd, _terminal_id| cwd.to_local_path().is_some_and(|p| new_roots.contains(p)));
|
|
|
|
// Resolve remote terminal CWDs to their repo roots and add to mappings.
|
|
let mut new_remote_repo_roots: Vec<LocalOrRemotePath> = Vec::new();
|
|
for (terminal_id, remote_path) in &remote_terminal_cwds {
|
|
let remote_key = LocalOrRemotePath::Remote(remote_path.clone());
|
|
if let Some(repo_root) =
|
|
DetectedRepositories::as_ref(ctx).get_root_for_path(&remote_key)
|
|
{
|
|
new_root_to_terminal.insert(repo_root.clone(), *terminal_id);
|
|
new_remote_repo_roots.push(repo_root);
|
|
} else {
|
|
// No repo detected — still track the CWD → terminal mapping
|
|
// so `find_review_terminal` can resolve it.
|
|
new_root_to_terminal.insert(remote_key, *terminal_id);
|
|
}
|
|
}
|
|
|
|
// Resolve remote editor paths to their repo roots.
|
|
for (_view_id, remote_path) in &remote_editor_paths {
|
|
let remote_key = LocalOrRemotePath::Remote(remote_path.clone());
|
|
if let Some(repo_root) =
|
|
DetectedRepositories::as_ref(ctx).get_root_for_path(&remote_key)
|
|
{
|
|
new_remote_repo_roots.push(repo_root);
|
|
}
|
|
}
|
|
|
|
// Second pass: if we have a focused terminal, ensure its repo maps to it
|
|
// This ensures the dropdown selects the correct repo when a pane is focused or CD'd
|
|
let mut focused_repo: Option<LocalOrRemotePath> = None;
|
|
if let Some(focused_id) = focused_terminal_id {
|
|
let mut repos_to_insert = Vec::new();
|
|
for (dir, terminal_id) in &new_root_to_terminal {
|
|
if *terminal_id == focused_id {
|
|
if let Some(repo_root) =
|
|
DetectedRepositories::as_ref(ctx).get_root_for_path(dir)
|
|
{
|
|
repos_to_insert.push((repo_root.clone(), focused_id));
|
|
focused_repo = Some(repo_root);
|
|
}
|
|
}
|
|
}
|
|
for (repo_key, focused_id) in repos_to_insert {
|
|
new_root_to_terminal.insert(repo_key, focused_id);
|
|
}
|
|
}
|
|
|
|
// Build the unified set of repo roots (local + remote).
|
|
let mut new_repo_roots_wrapped: Vec<LocalOrRemotePath> = new_local_repo_roots
|
|
.into_iter()
|
|
.map(LocalOrRemotePath::Local)
|
|
.chain(new_remote_repo_roots)
|
|
.chain(new_remote_display_roots)
|
|
.collect();
|
|
// Deduplicate (IndexSet handles this, but avoid duplicates in the input).
|
|
let seen: HashSet<_> = new_repo_roots_wrapped.iter().cloned().collect();
|
|
new_repo_roots_wrapped.retain({
|
|
let mut first_seen = HashSet::new();
|
|
move |item| first_seen.insert(item.clone())
|
|
});
|
|
let _ = seen; // consumed by retain closure above
|
|
|
|
let orphaned_repos = self
|
|
.repository_roots
|
|
.set_paths(pane_group_id, new_repo_roots_wrapped);
|
|
|
|
self.directory_to_terminal
|
|
.insert(pane_group_id, new_root_to_terminal);
|
|
|
|
let new_directories: Vec<WorkingDirectory> = self
|
|
.pane_groups
|
|
.get(&pane_group_id)
|
|
.map(|dirs| {
|
|
dirs.iter()
|
|
.map(|lor| WorkingDirectory {
|
|
path: lor.clone(),
|
|
terminal_id: self.get_terminal_id_for_root_path(pane_group_id, lor),
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
let new_deduplicated_repos: Vec<LocalOrRemotePath> = self
|
|
.repository_roots
|
|
.get(pane_group_id)
|
|
.map(|repos| repos.iter().cloned().collect())
|
|
.unwrap_or_default();
|
|
if old_directories != new_directories {
|
|
self.emit_directories_changed(pane_group_id, ctx);
|
|
}
|
|
|
|
if old_repos != new_deduplicated_repos {
|
|
self.drop_unused_diff_state_models(orphaned_repos, ctx);
|
|
self.emit_repositories_changed(pane_group_id, ctx);
|
|
}
|
|
|
|
if old_focused_repo != focused_repo {
|
|
self.focused_repo
|
|
.insert(pane_group_id, focused_repo.clone());
|
|
self.emit_focused_repo_changed(pane_group_id, focused_repo, ctx);
|
|
}
|
|
}
|
|
|
|
/// Maps a repository to a specific terminal view ID so that
|
|
/// `get_terminal_id_for_root_path` can resolve the preferred terminal
|
|
/// for that repo (used by `find_review_terminal`).
|
|
pub fn register_terminal_for_repo(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
repo_key: LocalOrRemotePath,
|
|
terminal_id: EntityId,
|
|
) {
|
|
self.directory_to_terminal
|
|
.entry(pane_group_id)
|
|
.or_default()
|
|
.insert(repo_key, terminal_id);
|
|
}
|
|
|
|
/// Registers a remote repository root for a pane group. Inserts it into
|
|
/// the unified `repository_roots` map and emits `RepositoriesChanged` if
|
|
/// the repo was newly added.
|
|
pub fn register_remote_repo(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
repo_key: LocalOrRemotePath,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
if self.repository_roots.insert(pane_group_id, repo_key) {
|
|
self.emit_repositories_changed(pane_group_id, ctx);
|
|
}
|
|
}
|
|
|
|
/// Get the repository root for a given path.
|
|
fn get_repo_root_for_path(&self, path: &Path, ctx: &AppContext) -> Option<PathBuf> {
|
|
DetectedRepositories::as_ref(ctx)
|
|
.get_root_for_path(&LocalOrRemotePath::Local(path.to_path_buf()))
|
|
.and_then(|r| PathBuf::try_from(r).ok())
|
|
}
|
|
|
|
/// Emit a DirectoriesChanged event with the current state for a specific pane group.
|
|
/// Directories are returned in most recent first order for use in the UI.
|
|
fn emit_directories_changed(&mut self, pane_group_id: EntityId, ctx: &mut ModelContext<Self>) {
|
|
ctx.emit(WorkingDirectoriesEvent::DirectoriesChanged {
|
|
pane_group_id,
|
|
directories: self
|
|
.most_recent_directories_for_pane_group(pane_group_id)
|
|
.map(|iter| iter.collect())
|
|
.unwrap_or_default(),
|
|
});
|
|
}
|
|
|
|
/// Emit a RepositoriesChanged event with the current state for a specific pane group.
|
|
/// Repositories are returned in most recent first order for use in the UI.
|
|
fn emit_repositories_changed(&mut self, pane_group_id: EntityId, ctx: &mut ModelContext<Self>) {
|
|
ctx.emit(WorkingDirectoriesEvent::RepositoriesChanged {
|
|
pane_group_id,
|
|
repositories: self
|
|
.most_recent_repositories_for_pane_group(pane_group_id)
|
|
.map(|iter| iter.collect())
|
|
.unwrap_or_default(),
|
|
});
|
|
}
|
|
|
|
fn emit_focused_repo_changed(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
focused_repo: Option<LocalOrRemotePath>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
ctx.emit(WorkingDirectoriesEvent::FocusedRepoChanged {
|
|
pane_group_id,
|
|
repository_terminal_map: self
|
|
.directory_to_terminal
|
|
.get(&pane_group_id)
|
|
.cloned()
|
|
.unwrap_or_default(),
|
|
focused_repo,
|
|
});
|
|
}
|
|
|
|
pub(crate) fn insert_code_review_comments(
|
|
&mut self,
|
|
pane_group_id: EntityId,
|
|
repo_path: &LocalOrRemotePath,
|
|
comments: &Vec<PendingImportedReviewComment>,
|
|
diff_mode: &DiffMode,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
if let Some(code_review_view) = self.get_code_review_view(pane_group_id, repo_path) {
|
|
code_review_view.update(ctx, |code_review_view, ctx| {
|
|
code_review_view.set_diff_base(diff_mode.to_owned(), ctx);
|
|
code_review_view.expand_comment_list(ctx);
|
|
})
|
|
} else {
|
|
log::error!(
|
|
"WorkingDirectoriesModel did not find CodeReviewView for repo path {:?}",
|
|
repo_path
|
|
);
|
|
}
|
|
|
|
if let Some(comment_batch) = self.get_or_create_code_review_comments(repo_path, ctx) {
|
|
let comments = comments.to_owned();
|
|
comment_batch.update(ctx, |comment_batch, ctx| {
|
|
comment_batch.add_pending_imported_comments(comments, diff_mode.to_owned(), ctx);
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Inserts pre-flattened (already attached) review comments into the comment batch for the
|
|
/// given repository, creating the batch if needed. Unlike `insert_code_review_comments`, these
|
|
/// comments have already been thread-flattened and converted to `AttachedReviewComment`, so
|
|
/// they are ready to be repositioned onto diff editors immediately.
|
|
pub(crate) fn upsert_flattened_code_review_comments(
|
|
&mut self,
|
|
repo_path: &LocalOrRemotePath,
|
|
comments: Vec<AttachedReviewComment>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
if let Some(comment_batch) = self.get_or_create_code_review_comments(repo_path, ctx) {
|
|
comment_batch.update(ctx, |comment_batch, ctx| {
|
|
comment_batch.upsert_imported_comments(comments, ctx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "local_fs"))]
|
|
impl WorkingDirectoriesModel {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Get the unique directories for a specific pane group in most to least recently added order.
|
|
pub fn most_recent_directories_for_pane_group(
|
|
&self,
|
|
_pane_group_id: EntityId,
|
|
) -> Option<impl Iterator<Item = WorkingDirectory> + '_> {
|
|
Option::<std::iter::Empty<WorkingDirectory>>::None
|
|
}
|
|
|
|
/// Get the unique repository roots for a specific pane group in most to least recently added order.
|
|
pub fn most_recent_repositories_for_pane_group(
|
|
&self,
|
|
_pane_group_id: EntityId,
|
|
) -> Option<impl Iterator<Item = LocalOrRemotePath> + '_> {
|
|
Option::<std::iter::Empty<LocalOrRemotePath>>::None
|
|
}
|
|
|
|
/// Get the terminal view ID associated with a specific repository in a pane group.
|
|
pub fn get_terminal_id_for_root_path(
|
|
&self,
|
|
_pane_group_id: EntityId,
|
|
_root_path: &LocalOrRemotePath,
|
|
) -> Option<EntityId> {
|
|
None
|
|
}
|
|
|
|
pub fn refresh_working_directories_for_pane_group(
|
|
&mut self,
|
|
_pane_group_id: EntityId,
|
|
_terminal_cwds: Vec<(EntityId, LocalOrRemotePath)>,
|
|
_editor_paths: Vec<(EntityId, LocalOrRemotePath)>,
|
|
_focused_terminal_id: Option<EntityId>,
|
|
_ctx: &mut ModelContext<Self>,
|
|
) {
|
|
}
|
|
|
|
pub fn get_or_create_diff_state_model(
|
|
&mut self,
|
|
_key: LocalOrRemotePath,
|
|
_preferred_session: Option<SessionId>,
|
|
_ctx: &mut ModelContext<Self>,
|
|
) -> Option<ModelHandle<DiffStateModel>> {
|
|
None
|
|
}
|
|
|
|
pub fn get_or_create_code_review_comments(
|
|
&mut self,
|
|
_repo_path: &LocalOrRemotePath,
|
|
_ctx: &mut ModelContext<Self>,
|
|
) -> Option<ModelHandle<ReviewCommentBatch>> {
|
|
None
|
|
}
|
|
|
|
pub fn store_code_review_view(
|
|
&mut self,
|
|
_pane_group_id: EntityId,
|
|
_repo_path: LocalOrRemotePath,
|
|
_view: ViewHandle<CodeReviewView>,
|
|
) {
|
|
}
|
|
|
|
pub fn get_code_review_view(
|
|
&self,
|
|
_pane_group_id: EntityId,
|
|
_repo_path: &LocalOrRemotePath,
|
|
) -> Option<ViewHandle<CodeReviewView>> {
|
|
None
|
|
}
|
|
|
|
pub fn get_selected_review_repo(&self, _pane_group_id: EntityId) -> Option<&LocalOrRemotePath> {
|
|
None
|
|
}
|
|
|
|
pub fn set_selected_review_repo(
|
|
&mut self,
|
|
_pane_group_id: EntityId,
|
|
_repo_path: LocalOrRemotePath,
|
|
) {
|
|
}
|
|
|
|
pub fn clear_selected_review_repo(&mut self, _pane_group_id: EntityId) {}
|
|
|
|
pub fn store_global_search_view(
|
|
&mut self,
|
|
_pane_group_id: EntityId,
|
|
_view: ViewHandle<GlobalSearchView>,
|
|
) {
|
|
}
|
|
|
|
pub fn get_global_search_view(
|
|
&self,
|
|
_pane_group_id: EntityId,
|
|
) -> Option<ViewHandle<GlobalSearchView>> {
|
|
None
|
|
}
|
|
|
|
pub fn store_file_tree_view(
|
|
&mut self,
|
|
_pane_group_id: EntityId,
|
|
_view: ViewHandle<crate::code::file_tree::FileTreeView>,
|
|
) {
|
|
}
|
|
|
|
pub fn get_file_tree_view(
|
|
&self,
|
|
_pane_group_id: EntityId,
|
|
) -> Option<ViewHandle<crate::code::file_tree::FileTreeView>> {
|
|
None
|
|
}
|
|
|
|
pub fn remove_pane_group(&mut self, _pane_group_id: EntityId, _ctx: &mut ModelContext<Self>) {}
|
|
|
|
pub(crate) fn insert_code_review_comments(
|
|
&mut self,
|
|
_pane_group_id: EntityId,
|
|
_repo_path: &LocalOrRemotePath,
|
|
_comments: &Vec<PendingImportedReviewComment>,
|
|
_diff_mode: &DiffMode,
|
|
_ctx: &mut ModelContext<Self>,
|
|
) {
|
|
}
|
|
|
|
pub(crate) fn upsert_flattened_code_review_comments(
|
|
&mut self,
|
|
_repo_path: &LocalOrRemotePath,
|
|
_comments: Vec<AttachedReviewComment>,
|
|
_ctx: &mut ModelContext<Self>,
|
|
) {
|
|
}
|
|
}
|
|
|
|
impl Entity for WorkingDirectoriesModel {
|
|
type Event = WorkingDirectoriesEvent;
|
|
}
|
|
|
|
/// Normalize a CWD path string to a canonical PathBuf
|
|
///
|
|
/// This function attempts to canonicalize (resolve symlinks, make absolute)
|
|
///
|
|
/// Returns None if the path is empty, invalid, or cannot be canonicalized.
|
|
/// Canonicalization failure may indicate remote paths or non-existent directories,
|
|
/// which could be supported in the future.
|
|
#[cfg(feature = "local_fs")]
|
|
fn normalize_cwd(raw_cwd: &str) -> Option<PathBuf> {
|
|
if raw_cwd.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let path = PathBuf::from(raw_cwd.to_string());
|
|
// Use dunce::canonicalize to avoid Windows extended-length path prefix (\\?\)
|
|
// which would cause path comparison mismatches with CanonicalizedPath.
|
|
dunce::canonicalize(&path).ok()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "working_directories_tests.rs"]
|
|
mod tests;
|