Detailed changes
@@ -5245,6 +5245,7 @@ dependencies = [
"git",
"gpui",
"menu",
+ "picker",
"project",
"schemars",
"serde",
@@ -5256,7 +5257,6 @@ dependencies = [
"util",
"windows 0.58.0",
"workspace",
- "worktree",
]
[[package]]
@@ -7045,7 +7045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
]
[[package]]
@@ -9511,9 +9511,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.15"
+version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@@ -13372,6 +13372,7 @@ dependencies = [
"client",
"collections",
"feature_flags",
+ "git_ui",
"gpui",
"http_client",
"notifications",
@@ -15264,7 +15265,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -489,6 +489,7 @@ strum = { version = "0.26.0", features = ["derive"] }
subtle = "2.5.0"
sys-locale = "0.3.1"
sysinfo = "0.31.0"
+take-until = "0.2.0"
tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.6.0"
@@ -31,7 +31,7 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
-worktree.workspace = true
+picker.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
@@ -7,13 +7,13 @@ use editor::scroll::ScrollbarAutoHide;
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
use futures::channel::mpsc;
use futures::StreamExt as _;
-use git::repository::{GitRepository, RepoPath};
+use git::repository::RepoPath;
use git::status::FileStatus;
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
use gpui::*;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
-use project::git::GitState;
-use project::{Fs, Project, ProjectPath, WorktreeId};
+use project::git::RepositoryHandle;
+use project::{Fs, Project, ProjectPath};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -22,14 +22,13 @@ use theme::ThemeSettings;
use ui::{
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
};
-use util::{maybe, ResultExt, TryFutureExt};
+use util::{ResultExt, TryFutureExt};
use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::Toast;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
-use worktree::RepositoryEntry;
actions!(
git_panel,
@@ -80,7 +79,6 @@ pub struct GitListEntry {
}
pub struct GitPanel {
- weak_workspace: WeakView<Workspace>,
current_modifiers: Modifiers,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
@@ -88,6 +86,7 @@ pub struct GitPanel {
pending_serialization: Task<Option<()>>,
workspace: WeakView<Workspace>,
project: Model<Project>,
+ active_repository: Option<RepositoryHandle>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
selected_entry: Option<usize>,
@@ -97,46 +96,46 @@ pub struct GitPanel {
visible_entries: Vec<GitListEntry>,
all_staged: Option<bool>,
width: Option<Pixels>,
- reveal_in_editor: Task<()>,
err_sender: mpsc::Sender<anyhow::Error>,
}
-fn first_worktree_repository(
- project: &Model<Project>,
- worktree_id: WorktreeId,
- cx: &mut AppContext,
-) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
- project
- .read(cx)
- .worktree_for_id(worktree_id, cx)
- .and_then(|worktree| {
- let snapshot = worktree.read(cx).snapshot();
- let repo = snapshot.repositories().iter().next()?.clone();
- let git_repo = worktree
- .read(cx)
- .as_local()?
- .get_local_repo(&repo)?
- .repo()
- .clone();
- Some((repo, git_repo))
- })
-}
-
-fn first_repository_in_project(
- project: &Model<Project>,
- cx: &mut AppContext,
-) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
- project.read(cx).worktrees(cx).next().and_then(|worktree| {
- let snapshot = worktree.read(cx).snapshot();
- let repo = snapshot.repositories().iter().next()?.clone();
- let git_repo = worktree
- .read(cx)
- .as_local()?
- .get_local_repo(&repo)?
- .repo()
- .clone();
- Some((snapshot.id(), repo, git_repo))
- })
+fn commit_message_editor(
+ active_repository: Option<&RepositoryHandle>,
+ cx: &mut ViewContext<'_, Editor>,
+) -> Editor {
+ let theme = ThemeSettings::get_global(cx);
+
+ let mut text_style = cx.text_style();
+ let refinement = TextStyleRefinement {
+ font_family: Some(theme.buffer_font.family.clone()),
+ font_features: Some(FontFeatures::disable_ligatures()),
+ font_size: Some(px(12.).into()),
+ color: Some(cx.theme().colors().editor_foreground),
+ background_color: Some(gpui::transparent_black()),
+ ..Default::default()
+ };
+ text_style.refine(&refinement);
+
+ let mut commit_editor = if let Some(active_repository) = active_repository.as_ref() {
+ let buffer =
+ cx.new_model(|cx| MultiBuffer::singleton(active_repository.commit_message(), cx));
+ Editor::new(
+ EditorMode::AutoHeight { max_lines: 10 },
+ buffer,
+ None,
+ false,
+ cx,
+ )
+ } else {
+ Editor::auto_height(10, cx)
+ };
+ commit_editor.set_use_autoclose(false);
+ commit_editor.set_show_gutter(false, cx);
+ commit_editor.set_show_wrap_guides(false, cx);
+ commit_editor.set_show_indent_guides(false, cx);
+ commit_editor.set_text_style_refinement(refinement);
+ commit_editor.set_placeholder_text("Enter commit message", cx);
+ commit_editor
}
impl GitPanel {
@@ -150,8 +149,8 @@ impl GitPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let fs = workspace.app_state().fs.clone();
let project = workspace.project().clone();
- let weak_workspace = cx.view().downgrade();
let git_state = project.read(cx).git_state().cloned();
+ let active_repository = project.read(cx).active_repository(cx);
let (err_sender, mut err_receiver) = mpsc::channel(1);
let workspace = cx.view().downgrade();
@@ -162,143 +161,12 @@ impl GitPanel {
this.hide_scrollbar(cx);
})
.detach();
- cx.subscribe(&project, {
- let git_state = git_state.clone();
- move |this, project, event, cx| {
- use project::Event;
-
- let first_worktree_id = project.read(cx).worktrees(cx).next().map(|worktree| {
- let snapshot = worktree.read(cx).snapshot();
- snapshot.id()
- });
- let first_repo_in_project = first_repository_in_project(&project, cx);
-
- let Some(git_state) = git_state.clone() else {
- return;
- };
- git_state.update(cx, |git_state, _| {
- match event {
- project::Event::WorktreeRemoved(id) => {
- let Some((worktree_id, _, _)) =
- git_state.active_repository.as_ref()
- else {
- return;
- };
- if worktree_id == id {
- git_state.active_repository = first_repo_in_project;
- this.schedule_update();
- }
- }
- project::Event::WorktreeOrderChanged => {
- // activate the new first worktree if the first was moved
- let Some(first_id) = first_worktree_id else {
- return;
- };
- if !git_state
- .active_repository
- .as_ref()
- .is_some_and(|(id, _, _)| id == &first_id)
- {
- git_state.active_repository = first_repo_in_project;
- this.schedule_update();
- }
- }
- Event::WorktreeAdded(_) => {
- let Some(first_id) = first_worktree_id else {
- return;
- };
- if !git_state
- .active_repository
- .as_ref()
- .is_some_and(|(id, _, _)| id == &first_id)
- {
- git_state.active_repository = first_repo_in_project;
- this.schedule_update();
- }
- }
- project::Event::WorktreeUpdatedEntries(id, _) => {
- if git_state
- .active_repository
- .as_ref()
- .is_some_and(|(active_id, _, _)| active_id == id)
- {
- git_state.active_repository = first_repo_in_project;
- this.schedule_update();
- }
- }
- project::Event::WorktreeUpdatedGitRepositories(_) => {
- let Some(first) = first_repo_in_project else {
- return;
- };
- git_state.active_repository = Some(first);
- this.schedule_update();
- }
- project::Event::Closed => {
- this.reveal_in_editor = Task::ready(());
- this.visible_entries.clear();
- }
- _ => {}
- };
- });
- }
- })
- .detach();
- let commit_editor = cx.new_view(|cx| {
- let theme = ThemeSettings::get_global(cx);
-
- let mut text_style = cx.text_style();
- let refinement = TextStyleRefinement {
- font_family: Some(theme.buffer_font.family.clone()),
- font_features: Some(FontFeatures::disable_ligatures()),
- font_size: Some(px(12.).into()),
- color: Some(cx.theme().colors().editor_foreground),
- background_color: Some(gpui::transparent_black()),
- ..Default::default()
- };
- text_style.refine(&refinement);
-
- let mut commit_editor = if let Some(git_state) = git_state.as_ref() {
- let buffer = cx.new_model(|cx| {
- MultiBuffer::singleton(git_state.read(cx).commit_message.clone(), cx)
- });
- // TODO should we attach the project?
- Editor::new(
- EditorMode::AutoHeight { max_lines: 10 },
- buffer,
- None,
- false,
- cx,
- )
- } else {
- Editor::auto_height(10, cx)
- };
- commit_editor.set_use_autoclose(false);
- commit_editor.set_show_gutter(false, cx);
- commit_editor.set_show_wrap_guides(false, cx);
- commit_editor.set_show_indent_guides(false, cx);
- commit_editor.set_text_style_refinement(refinement);
- commit_editor.set_placeholder_text("Enter commit message", cx);
- commit_editor
- });
+ let commit_editor =
+ cx.new_view(|cx| commit_message_editor(active_repository.as_ref(), cx));
let scroll_handle = UniformListScrollHandle::new();
- let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
- let first_worktree = visible_worktrees.next();
- drop(visible_worktrees);
- if let Some(first_worktree) = first_worktree {
- let snapshot = first_worktree.read(cx).snapshot();
-
- if let Some(((repo, git_repo), git_state)) =
- first_worktree_repository(&project, snapshot.id(), cx).zip(git_state)
- {
- git_state.update(cx, |git_state, _| {
- git_state.activate_repository(snapshot.id(), repo, git_repo);
- });
- }
- };
-
let rebuild_requested = Arc::new(AtomicBool::new(false));
let flag = rebuild_requested.clone();
let handle = cx.view().downgrade();
@@ -309,6 +177,9 @@ impl GitPanel {
if let Some(this) = handle.upgrade() {
this.update(&mut cx, |this, cx| {
this.update_visible_entries(cx);
+ let active_repository = this.active_repository.as_ref();
+ this.commit_editor =
+ cx.new_view(|cx| commit_message_editor(active_repository, cx));
})
.ok();
}
@@ -318,24 +189,33 @@ impl GitPanel {
})
.detach();
+ if let Some(git_state) = git_state {
+ cx.subscribe(&git_state, move |this, git_state, event, cx| match event {
+ project::git::Event::RepositoriesUpdated => {
+ this.active_repository = git_state.read(cx).active_repository();
+ this.schedule_update();
+ }
+ })
+ .detach();
+ }
+
let mut git_panel = Self {
- weak_workspace,
focus_handle: cx.focus_handle(),
- fs,
pending_serialization: Task::ready(None),
visible_entries: Vec::new(),
all_staged: None,
current_modifiers: cx.modifiers(),
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
- scroll_handle,
selected_entry: None,
show_scrollbar: false,
hide_scrollbar_task: None,
+ active_repository,
+ scroll_handle,
+ fs,
rebuild_requested,
commit_editor,
project,
- reveal_in_editor: Task::ready(()),
err_sender,
workspace,
};
@@ -380,19 +260,6 @@ impl GitPanel {
git_panel
}
- fn git_state(&self, cx: &AppContext) -> Option<Model<GitState>> {
- self.project.read(cx).git_state().cloned()
- }
-
- fn active_repository<'a>(
- &self,
- cx: &'a AppContext,
- ) -> Option<&'a (WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
- let git_state = self.git_state(cx)?;
- let active_repository = git_state.read(cx).active_repository.as_ref()?;
- Some(active_repository)
- }
-
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
// TODO: we can store stage status here
let width = self.width;
@@ -595,7 +462,13 @@ impl GitPanel {
}
fn select_first_entry_if_none(&mut self, cx: &mut ViewContext<Self>) {
- if !self.no_entries(cx) && self.selected_entry.is_none() {
+ let have_entries = self
+ .active_repository
+ .as_ref()
+ .map_or(false, |active_repository| {
+ active_repository.entry_count() > 0
+ });
+ if have_entries && self.selected_entry.is_none() {
self.selected_entry = Some(0);
self.scroll_to_selected_entry(cx);
cx.notify();
@@ -624,16 +497,15 @@ impl GitPanel {
}
fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
- let Some(git_state) = self.git_state(cx) else {
+ let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
- let result = git_state.update(cx, |git_state, _| {
- if entry.status.is_staged().unwrap_or(false) {
- git_state.unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
- } else {
- git_state.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
- }
- });
+ let result = if entry.status.is_staged().unwrap_or(false) {
+ active_repository
+ .unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
+ } else {
+ active_repository.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
+ };
if let Err(e) = result {
self.show_err_toast("toggle staged error", e, cx);
}
@@ -647,26 +519,24 @@ impl GitPanel {
}
fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
- let Some((worktree_id, path)) = maybe!({
- let git_state = self.git_state(cx)?;
- let (id, repo, _) = git_state.read(cx).active_repository.as_ref()?;
- let path = repo.work_directory.unrelativize(&entry.repo_path)?;
- Some((*id, path))
- }) else {
+ let Some(active_repository) = self.active_repository.as_ref() else {
+ return;
+ };
+ let Some(path) = active_repository.unrelativize(&entry.repo_path) else {
return;
};
- let path = (worktree_id, path).into();
let path_exists = self.project.update(cx, |project, cx| {
project.entry_for_path(&path, cx).is_some()
});
if !path_exists {
return;
}
+ // TODO maybe move all of this into project?
cx.emit(Event::OpenedEntry { path });
}
fn stage_all(&mut self, _: &git::StageAll, cx: &mut ViewContext<Self>) {
- let Some(git_state) = self.git_state(cx) else {
+ let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
for entry in &mut self.visible_entries {
@@ -674,20 +544,20 @@ impl GitPanel {
}
self.all_staged = Some(true);
- if let Err(e) = git_state.read(cx).stage_all(self.err_sender.clone()) {
+ if let Err(e) = active_repository.stage_all(self.err_sender.clone()) {
self.show_err_toast("stage all error", e, cx);
};
}
fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
- let Some(git_state) = self.git_state(cx) else {
+ let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
for entry in &mut self.visible_entries {
entry.is_staged = Some(false);
}
self.all_staged = Some(false);
- if let Err(e) = git_state.read(cx).unstage_all(self.err_sender.clone()) {
+ if let Err(e) = active_repository.unstage_all(self.err_sender.clone()) {
self.show_err_toast("unstage all error", e, cx);
};
}
@@ -699,12 +569,10 @@ impl GitPanel {
/// Commit all staged changes
fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
- let Some(git_state) = self.git_state(cx) else {
+ let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
- if let Err(e) = git_state.update(cx, |git_state, cx| {
- git_state.commit(self.err_sender.clone(), cx)
- }) {
+ if let Err(e) = active_repository.commit(self.err_sender.clone(), cx) {
self.show_err_toast("commit error", e, cx);
};
self.commit_editor
@@ -713,12 +581,10 @@ impl GitPanel {
/// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
- let Some(git_state) = self.git_state(cx) else {
+ let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
- if let Err(e) = git_state.update(cx, |git_state, cx| {
- git_state.commit_all(self.err_sender.clone(), cx)
- }) {
+ if let Err(e) = active_repository.commit_all(self.err_sender.clone(), cx) {
self.show_err_toast("commit all error", e, cx);
};
self.commit_editor
@@ -790,11 +656,6 @@ impl GitPanel {
});
}
- fn no_entries(&self, cx: &mut ViewContext<Self>) -> bool {
- self.git_state(cx)
- .map_or(true, |git_state| git_state.read(cx).entry_count() == 0)
- }
-
fn for_each_visible_entry(
&self,
range: Range<usize>,
@@ -832,11 +693,10 @@ impl GitPanel {
self.rebuild_requested.store(true, Ordering::Relaxed);
}
- #[track_caller]
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
self.visible_entries.clear();
- let Some((_, repo, _)) = self.active_repository(cx) else {
+ let Some(repo) = self.active_repository.as_ref() else {
// Just clear entries if no repository is active.
cx.notify();
return;
@@ -882,7 +742,7 @@ impl GitPanel {
let entry = GitListEntry {
depth,
display_name,
- repo_path: entry.repo_path,
+ repo_path: entry.repo_path.clone(),
status: entry.status,
is_staged,
};
@@ -901,7 +761,7 @@ impl GitPanel {
}
fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext<Self>) {
- let Some(workspace) = self.weak_workspace.upgrade() else {
+ let Some(workspace) = self.workspace.upgrade() else {
return;
};
let notif_id = NotificationId::Named(id.into());
@@ -942,8 +802,9 @@ impl GitPanel {
pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let entry_count = self
- .git_state(cx)
- .map_or(0, |git_state| git_state.read(cx).entry_count());
+ .active_repository
+ .as_ref()
+ .map_or(0, RepositoryHandle::entry_count);
let changes_string = match entry_count {
0 => "No changes".to_string(),
@@ -965,7 +826,7 @@ impl GitPanel {
.child(
Checkbox::new(
"all-changes",
- if self.no_entries(cx) {
+ if entry_count == 0 {
ToggleState::Selected
} else {
self.all_staged
@@ -1056,13 +917,15 @@ impl GitPanel {
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let editor = self.commit_editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
- let (can_commit, can_commit_all) = self.git_state(cx).map_or((false, false), |git_state| {
- let git_state = git_state.read(cx);
- (
- git_state.can_commit(false, cx),
- git_state.can_commit(true, cx),
- )
- });
+ let (can_commit, can_commit_all) = self.active_repository.as_ref().map_or_else(
+ || (false, false),
+ |active_repository| {
+ (
+ active_repository.can_commit(false, cx),
+ active_repository.can_commit(true, cx),
+ )
+ },
+ );
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();
@@ -1316,15 +1179,17 @@ impl GitPanel {
ToggleState::Indeterminate => None,
};
let repo_path = repo_path.clone();
- let Some(git_state) = this.git_state(cx) else {
+ let Some(active_repository) = this.active_repository.as_ref() else {
return;
};
- let result = git_state.update(cx, |git_state, _| match toggle {
- ToggleState::Selected | ToggleState::Indeterminate => git_state
- .stage_entries(vec![repo_path], this.err_sender.clone()),
- ToggleState::Unselected => git_state
+ let result = match toggle {
+ ToggleState::Selected | ToggleState::Indeterminate => {
+ active_repository
+ .stage_entries(vec![repo_path], this.err_sender.clone())
+ }
+ ToggleState::Unselected => active_repository
.unstage_entries(vec![repo_path], this.err_sender.clone()),
- });
+ };
if let Err(e) = result {
this.show_err_toast("toggle staged error", e, cx);
}
@@ -1373,6 +1238,12 @@ impl GitPanel {
impl Render for GitPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let project = self.project.read(cx);
+ let has_entries = self
+ .active_repository
+ .as_ref()
+ .map_or(false, |active_repository| {
+ active_repository.entry_count() > 0
+ });
let has_co_authors = self
.workspace
.upgrade()
@@ -1437,7 +1308,7 @@ impl Render for GitPanel {
.bg(ElevationIndex::Surface.bg(cx))
.child(self.render_panel_header(cx))
.child(self.render_divider(cx))
- .child(if !self.no_entries(cx) {
+ .child(if has_entries {
self.render_entries(cx).into_any_element()
} else {
self.render_empty_state(cx).into_any_element()
@@ -6,6 +6,7 @@ use ui::{Color, Icon, IconName, IntoElement};
pub mod git_panel;
mod git_panel_settings;
+pub mod repository_selector;
pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
@@ -0,0 +1,232 @@
+use gpui::{
+ AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+ Subscription, Task, View, WeakModel, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use project::{
+ git::{GitState, RepositoryHandle},
+ Project,
+};
+use std::sync::Arc;
+use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
+
+pub struct RepositorySelector {
+ picker: View<Picker<RepositorySelectorDelegate>>,
+ /// The task used to update the picker's matches when there is a change to
+ /// the repository list.
+ update_matches_task: Option<Task<()>>,
+ _subscriptions: Vec<Subscription>,
+}
+
+impl RepositorySelector {
+ pub fn new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
+ let git_state = project.read(cx).git_state().cloned();
+ let all_repositories = git_state
+ .as_ref()
+ .map_or(vec![], |git_state| git_state.read(cx).all_repositories());
+ let filtered_repositories = all_repositories.clone();
+ let delegate = RepositorySelectorDelegate {
+ project: project.downgrade(),
+ repository_selector: cx.view().downgrade(),
+ repository_entries: all_repositories,
+ filtered_repositories,
+ selected_index: 0,
+ };
+
+ let picker =
+ cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
+
+ let _subscriptions = if let Some(git_state) = git_state {
+ vec![cx.subscribe(&git_state, Self::handle_project_git_event)]
+ } else {
+ Vec::new()
+ };
+
+ RepositorySelector {
+ picker,
+ update_matches_task: None,
+ _subscriptions,
+ }
+ }
+
+ fn handle_project_git_event(
+ &mut self,
+ git_state: Model<GitState>,
+ _event: &project::git::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ // TODO handle events individually
+ let task = self.picker.update(cx, |this, cx| {
+ let query = this.query(cx);
+ this.delegate.repository_entries = git_state.read(cx).all_repositories();
+ this.delegate.update_matches(query, cx)
+ });
+ self.update_matches_task = Some(task);
+ }
+}
+
+impl EventEmitter<DismissEvent> for RepositorySelector {}
+
+impl FocusableView for RepositorySelector {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl Render for RepositorySelector {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ self.picker.clone()
+ }
+}
+
+#[derive(IntoElement)]
+pub struct RepositorySelectorPopoverMenu<T>
+where
+ T: PopoverTrigger,
+{
+ repository_selector: View<RepositorySelector>,
+ trigger: T,
+ handle: Option<PopoverMenuHandle<RepositorySelector>>,
+}
+
+impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
+ pub fn new(repository_selector: View<RepositorySelector>, trigger: T) -> Self {
+ Self {
+ repository_selector,
+ trigger,
+ handle: None,
+ }
+ }
+
+ pub fn with_handle(mut self, handle: PopoverMenuHandle<RepositorySelector>) -> Self {
+ self.handle = Some(handle);
+ self
+ }
+}
+
+impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
+ fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+ let repository_selector = self.repository_selector.clone();
+
+ PopoverMenu::new("repository-switcher")
+ .menu(move |_cx| Some(repository_selector.clone()))
+ .trigger(self.trigger)
+ .attach(gpui::Corner::BottomLeft)
+ .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
+ }
+}
+
+pub struct RepositorySelectorDelegate {
+ project: WeakModel<Project>,
+ repository_selector: WeakView<RepositorySelector>,
+ repository_entries: Vec<RepositoryHandle>,
+ filtered_repositories: Vec<RepositoryHandle>,
+ selected_index: usize,
+}
+
+impl RepositorySelectorDelegate {
+ pub fn update_repository_entries(&mut self, all_repositories: Vec<RepositoryHandle>) {
+ self.repository_entries = all_repositories.clone();
+ self.filtered_repositories = all_repositories;
+ self.selected_index = 0;
+ }
+}
+
+impl PickerDelegate for RepositorySelectorDelegate {
+ type ListItem = ListItem;
+
+ fn match_count(&self) -> usize {
+ self.filtered_repositories.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix.min(self.filtered_repositories.len().saturating_sub(1));
+ cx.notify();
+ }
+
+ fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+ "Select a repository...".into()
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+ let all_repositories = self.repository_entries.clone();
+
+ cx.spawn(|this, mut cx| async move {
+ let filtered_repositories = cx
+ .background_executor()
+ .spawn(async move {
+ if query.is_empty() {
+ all_repositories
+ } else {
+ all_repositories
+ .into_iter()
+ .filter(|_repo_info| {
+ // TODO: Implement repository filtering logic
+ true
+ })
+ .collect()
+ }
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.delegate.filtered_repositories = filtered_repositories;
+ this.delegate.set_selected_index(0, cx);
+ cx.notify();
+ })
+ .ok();
+ })
+ }
+
+ fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
+ let Some(selected_repo) = self.filtered_repositories.get(self.selected_index) else {
+ return;
+ };
+ selected_repo.activate(cx);
+ self.dismissed(cx);
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ self.repository_selector
+ .update(cx, |_this, cx| cx.emit(DismissEvent))
+ .ok();
+ }
+
+ fn render_header(&self, _cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
+ // TODO: Implement header rendering if needed
+ None
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let project = self.project.upgrade()?;
+ let repo_info = self.filtered_repositories.get(ix)?;
+ let display_name = repo_info.display_name(project.read(cx), cx);
+ // TODO: Implement repository item rendering
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(Label::new(display_name)),
+ )
+ }
+
+ fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
+ // TODO: Implement footer rendering if needed
+ Some(
+ div()
+ .text_ui_sm(cx)
+ .child("Temporary location for repo selector")
+ .into_any_element(),
+ )
+ }
+}
@@ -1,27 +1,57 @@
-use anyhow::{anyhow, Context as _};
+use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
+use crate::{Project, ProjectPath};
+use anyhow::anyhow;
use futures::channel::mpsc;
use futures::{SinkExt as _, StreamExt as _};
use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
};
-use gpui::{AppContext, Context as _, Model};
+use gpui::{
+ AppContext, Context as _, EventEmitter, Model, ModelContext, SharedString, Subscription,
+ WeakModel,
+};
use language::{Buffer, LanguageRegistry};
use settings::WorktreeId;
use std::sync::Arc;
use text::Rope;
-use worktree::RepositoryEntry;
+use util::maybe;
+use worktree::{RepositoryEntry, StatusEntry};
pub struct GitState {
- pub commit_message: Model<Buffer>,
-
- /// When a git repository is selected, this is used to track which repository's changes
- /// are currently being viewed or modified in the UI.
- pub active_repository: Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)>,
+ repositories: Vec<RepositoryHandle>,
+ active_index: Option<usize>,
+ update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
+ languages: Arc<LanguageRegistry>,
+ _subscription: Subscription,
+}
+#[derive(Clone)]
+pub struct RepositoryHandle {
+ git_state: WeakModel<GitState>,
+ worktree_id: WorktreeId,
+ repository_entry: RepositoryEntry,
+ git_repo: Arc<dyn GitRepository>,
+ commit_message: Model<Buffer>,
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
}
+impl PartialEq<Self> for RepositoryHandle {
+ fn eq(&self, other: &Self) -> bool {
+ self.worktree_id == other.worktree_id
+ && self.repository_entry.work_directory_id()
+ == other.repository_entry.work_directory_id()
+ }
+}
+
+impl Eq for RepositoryHandle {}
+
+impl PartialEq<RepositoryEntry> for RepositoryHandle {
+ fn eq(&self, other: &RepositoryEntry) -> bool {
+ self.repository_entry.work_directory_id() == other.work_directory_id()
+ }
+}
+
enum Message {
StageAndCommit(Arc<dyn GitRepository>, Rope, Vec<RepoPath>),
Commit(Arc<dyn GitRepository>, Rope),
@@ -29,11 +59,21 @@ enum Message {
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
}
+pub enum Event {
+ RepositoriesUpdated,
+}
+
+impl EventEmitter<Event> for GitState {}
+
impl GitState {
- pub fn new(languages: Arc<LanguageRegistry>, cx: &mut AppContext) -> Self {
+ pub fn new(
+ worktree_store: &Model<WorktreeStore>,
+ languages: Arc<LanguageRegistry>,
+ cx: &mut ModelContext<'_, Self>,
+ ) -> Self {
let (update_sender, mut update_receiver) =
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
- cx.spawn(|cx| async move {
+ cx.spawn(|_, cx| async move {
while let Some((msg, mut err_sender)) = update_receiver.next().await {
let result = cx
.background_executor()
@@ -57,39 +97,147 @@ impl GitState {
})
.detach();
- let commit_message = cx.new_model(|cx| Buffer::local("", cx));
- let markdown = languages.language_for_name("Markdown");
- cx.spawn({
- let commit_message = commit_message.clone();
- |mut cx| async move {
- let markdown = markdown.await.context("failed to load Markdown language")?;
- commit_message.update(&mut cx, |commit_message, cx| {
- commit_message.set_language(Some(markdown), cx)
- })
- }
- })
- .detach_and_log_err(cx);
+ let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
GitState {
- commit_message,
- active_repository: None,
+ languages,
+ repositories: vec![],
+ active_index: None,
update_sender,
+ _subscription,
}
}
- pub fn activate_repository(
+ pub fn active_repository(&self) -> Option<RepositoryHandle> {
+ self.active_index
+ .map(|index| self.repositories[index].clone())
+ }
+
+ fn on_worktree_store_event(
&mut self,
- worktree_id: WorktreeId,
- active_repository: RepositoryEntry,
- git_repo: Arc<dyn GitRepository>,
+ worktree_store: Model<WorktreeStore>,
+ _event: &WorktreeStoreEvent,
+ cx: &mut ModelContext<'_, Self>,
) {
- self.active_repository = Some((worktree_id, active_repository, git_repo));
+ // TODO inspect the event
+
+ let mut new_repositories = Vec::new();
+ let mut new_active_index = None;
+ let this = cx.weak_model();
+
+ worktree_store.update(cx, |worktree_store, cx| {
+ for worktree in worktree_store.worktrees() {
+ worktree.update(cx, |worktree, cx| {
+ let snapshot = worktree.snapshot();
+ let Some(local) = worktree.as_local() else {
+ return;
+ };
+ for repo in snapshot.repositories().iter() {
+ let Some(local_repo) = local.get_local_repo(repo) else {
+ continue;
+ };
+ let existing = self
+ .repositories
+ .iter()
+ .enumerate()
+ .find(|(_, existing_handle)| existing_handle == &repo);
+ let handle = if let Some((index, handle)) = existing {
+ if self.active_index == Some(index) {
+ new_active_index = Some(new_repositories.len());
+ }
+ // Update the statuses but keep everything else.
+ let mut existing_handle = handle.clone();
+ existing_handle.repository_entry = repo.clone();
+ existing_handle
+ } else {
+ let commit_message = cx.new_model(|cx| Buffer::local("", cx));
+ cx.spawn({
+ let commit_message = commit_message.downgrade();
+ let languages = self.languages.clone();
+ |_, mut cx| async move {
+ let markdown = languages.language_for_name("Markdown").await?;
+ commit_message.update(&mut cx, |commit_message, cx| {
+ commit_message.set_language(Some(markdown), cx);
+ })?;
+ anyhow::Ok(())
+ }
+ })
+ .detach_and_log_err(cx);
+ RepositoryHandle {
+ git_state: this.clone(),
+ worktree_id: worktree.id(),
+ repository_entry: repo.clone(),
+ git_repo: local_repo.repo().clone(),
+ commit_message,
+ update_sender: self.update_sender.clone(),
+ }
+ };
+ new_repositories.push(handle);
+ }
+ })
+ }
+ });
+
+ if new_active_index == None && new_repositories.len() > 0 {
+ new_active_index = Some(0);
+ }
+
+ self.repositories = new_repositories;
+ self.active_index = new_active_index;
+
+ cx.emit(Event::RepositoriesUpdated);
}
- pub fn active_repository(
- &self,
- ) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
- self.active_repository.as_ref()
+ pub fn all_repositories(&self) -> Vec<RepositoryHandle> {
+ self.repositories.clone()
+ }
+}
+
+impl RepositoryHandle {
+ pub fn display_name(&self, project: &Project, cx: &AppContext) -> SharedString {
+ maybe!({
+ let path = self.unrelativize(&"".into())?;
+ Some(
+ project
+ .absolute_path(&path, cx)?
+ .file_name()?
+ .to_string_lossy()
+ .to_string()
+ .into(),
+ )
+ })
+ .unwrap_or("".into())
+ }
+
+ pub fn activate(&self, cx: &mut AppContext) {
+ let Some(git_state) = self.git_state.upgrade() else {
+ return;
+ };
+ git_state.update(cx, |git_state, cx| {
+ let Some((index, _)) = git_state
+ .repositories
+ .iter()
+ .enumerate()
+ .find(|(_, handle)| handle == &self)
+ else {
+ return;
+ };
+ git_state.active_index = Some(index);
+ cx.emit(Event::RepositoriesUpdated);
+ });
+ }
+
+ pub fn status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
+ self.repository_entry.status()
+ }
+
+ pub fn unrelativize(&self, path: &RepoPath) -> Option<ProjectPath> {
+ let path = self.repository_entry.unrelativize(path)?;
+ Some((self.worktree_id, path).into())
+ }
+
+ pub fn commit_message(&self) -> Model<Buffer> {
+ self.commit_message.clone()
}
pub fn stage_entries(
@@ -100,11 +248,8 @@ impl GitState {
if entries.is_empty() {
return Ok(());
}
- let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
- return Err(anyhow!("No active repository"));
- };
self.update_sender
- .unbounded_send((Message::Stage(git_repo.clone(), entries), err_sender))
+ .unbounded_send((Message::Stage(self.git_repo.clone(), entries), err_sender))
.map_err(|_| anyhow!("Failed to submit stage operation"))?;
Ok(())
}
@@ -117,20 +262,15 @@ impl GitState {
if entries.is_empty() {
return Ok(());
}
- let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
- return Err(anyhow!("No active repository"));
- };
self.update_sender
- .unbounded_send((Message::Unstage(git_repo.clone(), entries), err_sender))
+ .unbounded_send((Message::Unstage(self.git_repo.clone(), entries), err_sender))
.map_err(|_| anyhow!("Failed to submit unstage operation"))?;
Ok(())
}
pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
- let Some((_, entry, _)) = self.active_repository.as_ref() else {
- return Err(anyhow!("No active repository"));
- };
- let to_stage = entry
+ let to_stage = self
+ .repository_entry
.status()
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone())
@@ -140,10 +280,8 @@ impl GitState {
}
pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
- let Some((_, entry, _)) = self.active_repository.as_ref() else {
- return Err(anyhow!("No active repository"));
- };
- let to_unstage = entry
+ let to_unstage = self
+ .repository_entry
.status()
.filter(|entry| entry.status.is_staged().unwrap_or(true))
.map(|entry| entry.repo_path.clone())
@@ -155,23 +293,15 @@ impl GitState {
/// Get a count of all entries in the active repository, including
/// untracked files.
pub fn entry_count(&self) -> usize {
- self.active_repository
- .as_ref()
- .map_or(0, |(_, entry, _)| entry.status_len())
+ self.repository_entry.status_len()
}
fn have_changes(&self) -> bool {
- let Some((_, entry, _)) = self.active_repository.as_ref() else {
- return false;
- };
- entry.status_summary() != GitSummary::UNCHANGED
+ self.repository_entry.status_summary() != GitSummary::UNCHANGED
}
fn have_staged_changes(&self) -> bool {
- let Some((_, entry, _)) = self.active_repository.as_ref() else {
- return false;
- };
- entry.status_summary().index != TrackedSummary::UNCHANGED
+ self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
}
pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
@@ -185,36 +315,33 @@ impl GitState {
}
pub fn commit(
- &mut self,
+ &self,
err_sender: mpsc::Sender<anyhow::Error>,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> anyhow::Result<()> {
if !self.can_commit(false, cx) {
return Err(anyhow!("Unable to commit"));
}
- let Some((_, _, git_repo)) = self.active_repository() else {
- return Err(anyhow!("No active repository"));
- };
- let git_repo = git_repo.clone();
let message = self.commit_message.read(cx).as_rope().clone();
self.update_sender
- .unbounded_send((Message::Commit(git_repo, message), err_sender))
+ .unbounded_send((Message::Commit(self.git_repo.clone(), message), err_sender))
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
+ self.commit_message.update(cx, |commit_message, cx| {
+ commit_message.set_text("", cx);
+ });
Ok(())
}
pub fn commit_all(
- &mut self,
+ &self,
err_sender: mpsc::Sender<anyhow::Error>,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> anyhow::Result<()> {
if !self.can_commit(true, cx) {
return Err(anyhow!("Unable to commit"));
}
- let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
- return Err(anyhow!("No active repository"));
- };
- let to_stage = entry
+ let to_stage = self
+ .repository_entry
.status()
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone())
@@ -222,10 +349,13 @@ impl GitState {
let message = self.commit_message.read(cx).as_rope().clone();
self.update_sender
.unbounded_send((
- Message::StageAndCommit(git_repo.clone(), message, to_stage),
+ Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
err_sender,
))
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
+ self.commit_message.update(cx, |commit_message, cx| {
+ commit_message.set_text("", cx);
+ });
Ok(())
}
}
@@ -3128,12 +3128,15 @@ impl LspStore {
})
.detach()
}
- WorktreeStoreEvent::WorktreeReleased(..) => {}
WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx),
- WorktreeStoreEvent::WorktreeOrderChanged => {}
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree));
}
+ WorktreeStoreEvent::WorktreeReleased(..)
+ | WorktreeStoreEvent::WorktreeOrderChanged
+ | WorktreeStoreEvent::WorktreeUpdatedEntries(..)
+ | WorktreeStoreEvent::WorktreeUpdatedGitRepositories(..)
+ | WorktreeStoreEvent::WorktreeDeletedEntry(..) => {}
}
}
@@ -22,6 +22,7 @@ mod project_tests;
mod direnv;
mod environment;
pub use environment::EnvironmentErrorMessage;
+use git::RepositoryHandle;
pub mod search_history;
mod yarn;
@@ -691,7 +692,8 @@ impl Project {
)
});
- let git_state = Some(cx.new_model(|cx| GitState::new(languages.clone(), cx)));
+ let git_state =
+ Some(cx.new_model(|cx| GitState::new(&worktree_store, languages.clone(), cx)));
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@@ -2324,6 +2326,18 @@ impl Project {
}
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
+ WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, changes) => {
+ self.client()
+ .telemetry()
+ .report_discovered_project_events(*worktree_id, changes);
+ cx.emit(Event::WorktreeUpdatedEntries(*worktree_id, changes.clone()))
+ }
+ WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id) => {
+ cx.emit(Event::WorktreeUpdatedGitRepositories(*worktree_id))
+ }
+ WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, id) => {
+ cx.emit(Event::DeletedEntry(*worktree_id, *id))
+ }
}
}
@@ -2335,27 +2349,6 @@ impl Project {
}
}
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
- cx.subscribe(worktree, |project, worktree, event, cx| {
- let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
- match event {
- worktree::Event::UpdatedEntries(changes) => {
- cx.emit(Event::WorktreeUpdatedEntries(
- worktree.read(cx).id(),
- changes.clone(),
- ));
-
- project
- .client()
- .telemetry()
- .report_discovered_project_events(worktree_id, changes);
- }
- worktree::Event::UpdatedGitRepositories(_) => {
- cx.emit(Event::WorktreeUpdatedGitRepositories(worktree_id));
- }
- worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(worktree_id, *id)),
- }
- })
- .detach();
cx.notify();
}
@@ -4169,6 +4162,17 @@ impl Project {
pub fn git_state(&self) -> Option<&Model<GitState>> {
self.git_state.as_ref()
}
+
+ pub fn active_repository(&self, cx: &AppContext) -> Option<RepositoryHandle> {
+ self.git_state()
+ .and_then(|git_state| git_state.read(cx).active_repository())
+ }
+
+ pub fn all_repositories(&self, cx: &AppContext) -> Vec<RepositoryHandle> {
+ self.git_state()
+ .map(|git_state| git_state.read(cx).all_repositories())
+ .unwrap_or_default()
+ }
}
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {
@@ -25,7 +25,7 @@ use smol::{
};
use text::ReplicaId;
use util::{paths::SanitizedPath, ResultExt};
-use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
+use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
use crate::{search::SearchQuery, ProjectPath};
@@ -63,6 +63,9 @@ pub enum WorktreeStoreEvent {
WorktreeReleased(EntityId, WorktreeId),
WorktreeOrderChanged,
WorktreeUpdateSent(Model<Worktree>),
+ WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
+ WorktreeUpdatedGitRepositories(WorktreeId),
+ WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
}
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
@@ -364,6 +367,26 @@ impl WorktreeStore {
self.send_project_updates(cx);
let handle_id = worktree.entity_id();
+ cx.subscribe(worktree, |_, worktree, event, cx| {
+ let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
+ match event {
+ worktree::Event::UpdatedEntries(changes) => {
+ cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries(
+ worktree.read(cx).id(),
+ changes.clone(),
+ ));
+ }
+ worktree::Event::UpdatedGitRepositories(_) => {
+ cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(
+ worktree_id,
+ ));
+ }
+ worktree::Event::DeletedEntry(id) => {
+ cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id))
+ }
+ }
+ })
+ .detach();
cx.observe_release(worktree, move |this, worktree, cx| {
cx.emit(WorktreeStoreEvent::WorktreeReleased(
handle_id,
@@ -47,6 +47,7 @@ util.workspace = true
telemetry.workspace = true
workspace.workspace = true
zed_actions.workspace = true
+git_ui.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
@@ -16,6 +16,8 @@ use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, UserStore};
use feature_flags::{FeatureFlagAppExt, ZedPro};
+use git_ui::repository_selector::RepositorySelector;
+use git_ui::repository_selector::RepositorySelectorPopoverMenu;
use gpui::{
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
@@ -98,6 +100,7 @@ pub struct TitleBar {
platform_style: PlatformStyle,
content: Stateful<Div>,
children: SmallVec<[AnyElement; 2]>,
+ repository_selector: View<RepositorySelector>,
project: Model<Project>,
user_store: Model<UserStore>,
client: Arc<Client>,
@@ -181,6 +184,7 @@ impl Render for TitleBar {
title_bar
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
+ .children(self.render_current_repository(cx))
.children(self.render_project_branch(cx))
})
})
@@ -290,6 +294,7 @@ impl TitleBar {
content: div().id(id.into()),
children: SmallVec::new(),
application_menu,
+ repository_selector: cx.new_view(|cx| RepositorySelector::new(project.clone(), cx)),
workspace: workspace.weak_handle(),
should_move: false,
project,
@@ -474,6 +479,39 @@ impl TitleBar {
}))
}
+ // NOTE: Not sure we want to keep this in the titlebar, but for while we are working on Git it is helpful in the short term
+ pub fn render_current_repository(
+ &self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<impl IntoElement> {
+ // TODO what to render if no active repository?
+ let active_repository = self.project.read(cx).active_repository(cx)?;
+ let display_name = active_repository.display_name(self.project.read(cx), cx);
+ Some(RepositorySelectorPopoverMenu::new(
+ self.repository_selector.clone(),
+ ButtonLike::new("active-repository")
+ .style(ButtonStyle::Subtle)
+ .child(
+ h_flex().w_full().gap_0p5().child(
+ div()
+ .overflow_x_hidden()
+ .flex_grow()
+ .whitespace_nowrap()
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Label::new(display_name)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .into_any_element(),
+ ),
+ ),
+ ),
+ ))
+ }
+
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let entry = {
let mut names_and_branches =
@@ -32,7 +32,7 @@ rust-embed.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
-take-until = "0.2.0"
+take-until.workspace = true
tempfile = { workspace = true, optional = true }
unicase.workspace = true