git: Implement a basic repository selector (#23419)
This PR adds a rough-and-ready picker for selecting which of the project's repositories the git panel should display. Release Notes: - N/A --------- Co-authored-by: Nate Butler <iamnbutler@gmail.com> Co-authored-by: Nate <nate@zed.dev>
This commit is contained in:
parent
417760ade7
commit
31909bf334
13 changed files with 661 additions and 356 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -5245,6 +5245,7 @@ dependencies = [
|
||||||
"git",
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"menu",
|
"menu",
|
||||||
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -5256,7 +5257,6 @@ dependencies = [
|
||||||
"util",
|
"util",
|
||||||
"windows 0.58.0",
|
"windows 0.58.0",
|
||||||
"workspace",
|
"workspace",
|
||||||
"worktree",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7045,7 +7045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -9511,9 +9511,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.15"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-utils"
|
name = "pin-utils"
|
||||||
|
@ -13372,6 +13372,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
|
"git_ui",
|
||||||
"gpui",
|
"gpui",
|
||||||
"http_client",
|
"http_client",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
@ -15264,7 +15265,7 @@ version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -489,6 +489,7 @@ strum = { version = "0.26.0", features = ["derive"] }
|
||||||
subtle = "2.5.0"
|
subtle = "2.5.0"
|
||||||
sys-locale = "0.3.1"
|
sys-locale = "0.3.1"
|
||||||
sysinfo = "0.31.0"
|
sysinfo = "0.31.0"
|
||||||
|
take-until = "0.2.0"
|
||||||
tempfile = "3.9.0"
|
tempfile = "3.9.0"
|
||||||
thiserror = "1.0.29"
|
thiserror = "1.0.29"
|
||||||
tiktoken-rs = "0.6.0"
|
tiktoken-rs = "0.6.0"
|
||||||
|
|
|
@ -31,7 +31,7 @@ theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
worktree.workspace = true
|
picker.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
|
@ -7,13 +7,13 @@ use editor::scroll::ScrollbarAutoHide;
|
||||||
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
|
use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use git::repository::{GitRepository, RepoPath};
|
use git::repository::RepoPath;
|
||||||
use git::status::FileStatus;
|
use git::status::FileStatus;
|
||||||
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
|
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||||
use project::git::GitState;
|
use project::git::RepositoryHandle;
|
||||||
use project::{Fs, Project, ProjectPath, WorktreeId};
|
use project::{Fs, Project, ProjectPath};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
@ -22,14 +22,13 @@ use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
|
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::notifications::{DetachAndPromptErr, NotificationId};
|
use workspace::notifications::{DetachAndPromptErr, NotificationId};
|
||||||
use workspace::Toast;
|
use workspace::Toast;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
use worktree::RepositoryEntry;
|
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
git_panel,
|
git_panel,
|
||||||
|
@ -80,7 +79,6 @@ pub struct GitListEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GitPanel {
|
pub struct GitPanel {
|
||||||
weak_workspace: WeakView<Workspace>,
|
|
||||||
current_modifiers: Modifiers,
|
current_modifiers: Modifiers,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -88,6 +86,7 @@ pub struct GitPanel {
|
||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
|
active_repository: Option<RepositoryHandle>,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
selected_entry: Option<usize>,
|
selected_entry: Option<usize>,
|
||||||
|
@ -97,46 +96,46 @@ pub struct GitPanel {
|
||||||
visible_entries: Vec<GitListEntry>,
|
visible_entries: Vec<GitListEntry>,
|
||||||
all_staged: Option<bool>,
|
all_staged: Option<bool>,
|
||||||
width: Option<Pixels>,
|
width: Option<Pixels>,
|
||||||
reveal_in_editor: Task<()>,
|
|
||||||
err_sender: mpsc::Sender<anyhow::Error>,
|
err_sender: mpsc::Sender<anyhow::Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_worktree_repository(
|
fn commit_message_editor(
|
||||||
project: &Model<Project>,
|
active_repository: Option<&RepositoryHandle>,
|
||||||
worktree_id: WorktreeId,
|
cx: &mut ViewContext<'_, Editor>,
|
||||||
cx: &mut AppContext,
|
) -> Editor {
|
||||||
) -> Option<(RepositoryEntry, Arc<dyn GitRepository>)> {
|
let theme = ThemeSettings::get_global(cx);
|
||||||
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(
|
let mut text_style = cx.text_style();
|
||||||
project: &Model<Project>,
|
let refinement = TextStyleRefinement {
|
||||||
cx: &mut AppContext,
|
font_family: Some(theme.buffer_font.family.clone()),
|
||||||
) -> Option<(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
font_features: Some(FontFeatures::disable_ligatures()),
|
||||||
project.read(cx).worktrees(cx).next().and_then(|worktree| {
|
font_size: Some(px(12.).into()),
|
||||||
let snapshot = worktree.read(cx).snapshot();
|
color: Some(cx.theme().colors().editor_foreground),
|
||||||
let repo = snapshot.repositories().iter().next()?.clone();
|
background_color: Some(gpui::transparent_black()),
|
||||||
let git_repo = worktree
|
..Default::default()
|
||||||
.read(cx)
|
};
|
||||||
.as_local()?
|
text_style.refine(&refinement);
|
||||||
.get_local_repo(&repo)?
|
|
||||||
.repo()
|
let mut commit_editor = if let Some(active_repository) = active_repository.as_ref() {
|
||||||
.clone();
|
let buffer =
|
||||||
Some((snapshot.id(), repo, git_repo))
|
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 {
|
impl GitPanel {
|
||||||
|
@ -150,8 +149,8 @@ impl GitPanel {
|
||||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||||
let fs = workspace.app_state().fs.clone();
|
let fs = workspace.app_state().fs.clone();
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let weak_workspace = cx.view().downgrade();
|
|
||||||
let git_state = project.read(cx).git_state().cloned();
|
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 (err_sender, mut err_receiver) = mpsc::channel(1);
|
||||||
let workspace = cx.view().downgrade();
|
let workspace = cx.view().downgrade();
|
||||||
|
|
||||||
|
@ -162,143 +161,12 @@ impl GitPanel {
|
||||||
this.hide_scrollbar(cx);
|
this.hide_scrollbar(cx);
|
||||||
})
|
})
|
||||||
.detach();
|
.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 commit_editor =
|
||||||
let snapshot = worktree.read(cx).snapshot();
|
cx.new_view(|cx| commit_message_editor(active_repository.as_ref(), cx));
|
||||||
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 scroll_handle = UniformListScrollHandle::new();
|
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 rebuild_requested = Arc::new(AtomicBool::new(false));
|
||||||
let flag = rebuild_requested.clone();
|
let flag = rebuild_requested.clone();
|
||||||
let handle = cx.view().downgrade();
|
let handle = cx.view().downgrade();
|
||||||
|
@ -309,6 +177,9 @@ impl GitPanel {
|
||||||
if let Some(this) = handle.upgrade() {
|
if let Some(this) = handle.upgrade() {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.update_visible_entries(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();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -318,24 +189,33 @@ impl GitPanel {
|
||||||
})
|
})
|
||||||
.detach();
|
.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 {
|
let mut git_panel = Self {
|
||||||
weak_workspace,
|
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
fs,
|
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
visible_entries: Vec::new(),
|
visible_entries: Vec::new(),
|
||||||
all_staged: None,
|
all_staged: None,
|
||||||
current_modifiers: cx.modifiers(),
|
current_modifiers: cx.modifiers(),
|
||||||
width: Some(px(360.)),
|
width: Some(px(360.)),
|
||||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
|
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
|
||||||
scroll_handle,
|
|
||||||
selected_entry: None,
|
selected_entry: None,
|
||||||
show_scrollbar: false,
|
show_scrollbar: false,
|
||||||
hide_scrollbar_task: None,
|
hide_scrollbar_task: None,
|
||||||
|
active_repository,
|
||||||
|
scroll_handle,
|
||||||
|
fs,
|
||||||
rebuild_requested,
|
rebuild_requested,
|
||||||
commit_editor,
|
commit_editor,
|
||||||
project,
|
project,
|
||||||
reveal_in_editor: Task::ready(()),
|
|
||||||
err_sender,
|
err_sender,
|
||||||
workspace,
|
workspace,
|
||||||
};
|
};
|
||||||
|
@ -380,19 +260,6 @@ impl GitPanel {
|
||||||
git_panel
|
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>) {
|
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
// TODO: we can store stage status here
|
// TODO: we can store stage status here
|
||||||
let width = self.width;
|
let width = self.width;
|
||||||
|
@ -595,7 +462,13 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_first_entry_if_none(&mut self, cx: &mut ViewContext<Self>) {
|
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.selected_entry = Some(0);
|
||||||
self.scroll_to_selected_entry(cx);
|
self.scroll_to_selected_entry(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -624,16 +497,15 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_staged_for_entry(&mut self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
let result = git_state.update(cx, |git_state, _| {
|
let result = if entry.status.is_staged().unwrap_or(false) {
|
||||||
if entry.status.is_staged().unwrap_or(false) {
|
active_repository
|
||||||
git_state.unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
|
.unstage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
|
||||||
} else {
|
} else {
|
||||||
git_state.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
|
active_repository.stage_entries(vec![entry.repo_path.clone()], self.err_sender.clone())
|
||||||
}
|
};
|
||||||
});
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
self.show_err_toast("toggle staged error", e, cx);
|
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>) {
|
fn open_entry(&self, entry: &GitListEntry, cx: &mut ViewContext<Self>) {
|
||||||
let Some((worktree_id, path)) = maybe!({
|
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||||
let git_state = self.git_state(cx)?;
|
return;
|
||||||
let (id, repo, _) = git_state.read(cx).active_repository.as_ref()?;
|
};
|
||||||
let path = repo.work_directory.unrelativize(&entry.repo_path)?;
|
let Some(path) = active_repository.unrelativize(&entry.repo_path) else {
|
||||||
Some((*id, path))
|
|
||||||
}) else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let path = (worktree_id, path).into();
|
|
||||||
let path_exists = self.project.update(cx, |project, cx| {
|
let path_exists = self.project.update(cx, |project, cx| {
|
||||||
project.entry_for_path(&path, cx).is_some()
|
project.entry_for_path(&path, cx).is_some()
|
||||||
});
|
});
|
||||||
if !path_exists {
|
if !path_exists {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// TODO maybe move all of this into project?
|
||||||
cx.emit(Event::OpenedEntry { path });
|
cx.emit(Event::OpenedEntry { path });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_all(&mut self, _: &git::StageAll, cx: &mut ViewContext<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
for entry in &mut self.visible_entries {
|
for entry in &mut self.visible_entries {
|
||||||
|
@ -674,20 +544,20 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
self.all_staged = Some(true);
|
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);
|
self.show_err_toast("stage all error", e, cx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_all(&mut self, _: &git::UnstageAll, cx: &mut ViewContext<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
for entry in &mut self.visible_entries {
|
for entry in &mut self.visible_entries {
|
||||||
entry.is_staged = Some(false);
|
entry.is_staged = Some(false);
|
||||||
}
|
}
|
||||||
self.all_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);
|
self.show_err_toast("unstage all error", e, cx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -699,12 +569,10 @@ impl GitPanel {
|
||||||
|
|
||||||
/// Commit all staged changes
|
/// Commit all staged changes
|
||||||
fn commit_changes(&mut self, _: &git::CommitChanges, cx: &mut ViewContext<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(e) = git_state.update(cx, |git_state, cx| {
|
if let Err(e) = active_repository.commit(self.err_sender.clone(), cx) {
|
||||||
git_state.commit(self.err_sender.clone(), cx)
|
|
||||||
}) {
|
|
||||||
self.show_err_toast("commit error", e, cx);
|
self.show_err_toast("commit error", e, cx);
|
||||||
};
|
};
|
||||||
self.commit_editor
|
self.commit_editor
|
||||||
|
@ -713,12 +581,10 @@ impl GitPanel {
|
||||||
|
|
||||||
/// Commit all changes, regardless of whether they are staged or not
|
/// Commit all changes, regardless of whether they are staged or not
|
||||||
fn commit_all_changes(&mut self, _: &git::CommitAllChanges, cx: &mut ViewContext<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(e) = git_state.update(cx, |git_state, cx| {
|
if let Err(e) = active_repository.commit_all(self.err_sender.clone(), cx) {
|
||||||
git_state.commit_all(self.err_sender.clone(), cx)
|
|
||||||
}) {
|
|
||||||
self.show_err_toast("commit all error", e, cx);
|
self.show_err_toast("commit all error", e, cx);
|
||||||
};
|
};
|
||||||
self.commit_editor
|
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(
|
fn for_each_visible_entry(
|
||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
|
@ -832,11 +693,10 @@ impl GitPanel {
|
||||||
self.rebuild_requested.store(true, Ordering::Relaxed);
|
self.rebuild_requested.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.visible_entries.clear();
|
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.
|
// Just clear entries if no repository is active.
|
||||||
cx.notify();
|
cx.notify();
|
||||||
return;
|
return;
|
||||||
|
@ -882,7 +742,7 @@ impl GitPanel {
|
||||||
let entry = GitListEntry {
|
let entry = GitListEntry {
|
||||||
depth,
|
depth,
|
||||||
display_name,
|
display_name,
|
||||||
repo_path: entry.repo_path,
|
repo_path: entry.repo_path.clone(),
|
||||||
status: entry.status,
|
status: entry.status,
|
||||||
is_staged,
|
is_staged,
|
||||||
};
|
};
|
||||||
|
@ -901,7 +761,7 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
let notif_id = NotificationId::Named(id.into());
|
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 {
|
pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let focus_handle = self.focus_handle(cx).clone();
|
let focus_handle = self.focus_handle(cx).clone();
|
||||||
let entry_count = self
|
let entry_count = self
|
||||||
.git_state(cx)
|
.active_repository
|
||||||
.map_or(0, |git_state| git_state.read(cx).entry_count());
|
.as_ref()
|
||||||
|
.map_or(0, RepositoryHandle::entry_count);
|
||||||
|
|
||||||
let changes_string = match entry_count {
|
let changes_string = match entry_count {
|
||||||
0 => "No changes".to_string(),
|
0 => "No changes".to_string(),
|
||||||
|
@ -965,7 +826,7 @@ impl GitPanel {
|
||||||
.child(
|
.child(
|
||||||
Checkbox::new(
|
Checkbox::new(
|
||||||
"all-changes",
|
"all-changes",
|
||||||
if self.no_entries(cx) {
|
if entry_count == 0 {
|
||||||
ToggleState::Selected
|
ToggleState::Selected
|
||||||
} else {
|
} else {
|
||||||
self.all_staged
|
self.all_staged
|
||||||
|
@ -1056,13 +917,15 @@ impl GitPanel {
|
||||||
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||||
let editor = self.commit_editor.clone();
|
let editor = self.commit_editor.clone();
|
||||||
let editor_focus_handle = editor.read(cx).focus_handle(cx).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 (can_commit, can_commit_all) = self.active_repository.as_ref().map_or_else(
|
||||||
let git_state = git_state.read(cx);
|
|| (false, false),
|
||||||
(
|
|active_repository| {
|
||||||
git_state.can_commit(false, cx),
|
(
|
||||||
git_state.can_commit(true, cx),
|
active_repository.can_commit(false, cx),
|
||||||
)
|
active_repository.can_commit(true, cx),
|
||||||
});
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let focus_handle_1 = self.focus_handle(cx).clone();
|
let focus_handle_1 = self.focus_handle(cx).clone();
|
||||||
let focus_handle_2 = self.focus_handle(cx).clone();
|
let focus_handle_2 = self.focus_handle(cx).clone();
|
||||||
|
@ -1316,15 +1179,17 @@ impl GitPanel {
|
||||||
ToggleState::Indeterminate => None,
|
ToggleState::Indeterminate => None,
|
||||||
};
|
};
|
||||||
let repo_path = repo_path.clone();
|
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;
|
return;
|
||||||
};
|
};
|
||||||
let result = git_state.update(cx, |git_state, _| match toggle {
|
let result = match toggle {
|
||||||
ToggleState::Selected | ToggleState::Indeterminate => git_state
|
ToggleState::Selected | ToggleState::Indeterminate => {
|
||||||
.stage_entries(vec![repo_path], this.err_sender.clone()),
|
active_repository
|
||||||
ToggleState::Unselected => git_state
|
.stage_entries(vec![repo_path], this.err_sender.clone())
|
||||||
|
}
|
||||||
|
ToggleState::Unselected => active_repository
|
||||||
.unstage_entries(vec![repo_path], this.err_sender.clone()),
|
.unstage_entries(vec![repo_path], this.err_sender.clone()),
|
||||||
});
|
};
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
this.show_err_toast("toggle staged error", e, cx);
|
this.show_err_toast("toggle staged error", e, cx);
|
||||||
}
|
}
|
||||||
|
@ -1373,6 +1238,12 @@ impl GitPanel {
|
||||||
impl Render for GitPanel {
|
impl Render for GitPanel {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let project = self.project.read(cx);
|
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
|
let has_co_authors = self
|
||||||
.workspace
|
.workspace
|
||||||
.upgrade()
|
.upgrade()
|
||||||
|
@ -1437,7 +1308,7 @@ impl Render for GitPanel {
|
||||||
.bg(ElevationIndex::Surface.bg(cx))
|
.bg(ElevationIndex::Surface.bg(cx))
|
||||||
.child(self.render_panel_header(cx))
|
.child(self.render_panel_header(cx))
|
||||||
.child(self.render_divider(cx))
|
.child(self.render_divider(cx))
|
||||||
.child(if !self.no_entries(cx) {
|
.child(if has_entries {
|
||||||
self.render_entries(cx).into_any_element()
|
self.render_entries(cx).into_any_element()
|
||||||
} else {
|
} else {
|
||||||
self.render_empty_state(cx).into_any_element()
|
self.render_empty_state(cx).into_any_element()
|
||||||
|
|
|
@ -6,6 +6,7 @@ use ui::{Color, Icon, IconName, IntoElement};
|
||||||
|
|
||||||
pub mod git_panel;
|
pub mod git_panel;
|
||||||
mod git_panel_settings;
|
mod git_panel_settings;
|
||||||
|
pub mod repository_selector;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
GitPanelSettings::register(cx);
|
GitPanelSettings::register(cx);
|
||||||
|
|
232
crates/git_ui/src/repository_selector.rs
Normal file
232
crates/git_ui/src/repository_selector.rs
Normal file
|
@ -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,25 +1,55 @@
|
||||||
use anyhow::{anyhow, Context as _};
|
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
|
||||||
|
use crate::{Project, ProjectPath};
|
||||||
|
use anyhow::anyhow;
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use futures::{SinkExt as _, StreamExt as _};
|
use futures::{SinkExt as _, StreamExt as _};
|
||||||
use git::{
|
use git::{
|
||||||
repository::{GitRepository, RepoPath},
|
repository::{GitRepository, RepoPath},
|
||||||
status::{GitSummary, TrackedSummary},
|
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 language::{Buffer, LanguageRegistry};
|
||||||
use settings::WorktreeId;
|
use settings::WorktreeId;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use text::Rope;
|
use text::Rope;
|
||||||
use worktree::RepositoryEntry;
|
use util::maybe;
|
||||||
|
use worktree::{RepositoryEntry, StatusEntry};
|
||||||
|
|
||||||
pub struct GitState {
|
pub struct GitState {
|
||||||
pub commit_message: Model<Buffer>,
|
repositories: Vec<RepositoryHandle>,
|
||||||
|
active_index: Option<usize>,
|
||||||
/// 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>)>,
|
|
||||||
|
|
||||||
update_sender: mpsc::UnboundedSender<(Message, mpsc::Sender<anyhow::Error>)>,
|
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 {
|
enum Message {
|
||||||
|
@ -29,11 +59,21 @@ enum Message {
|
||||||
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
|
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
RepositoriesUpdated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<Event> for GitState {}
|
||||||
|
|
||||||
impl 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) =
|
let (update_sender, mut update_receiver) =
|
||||||
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
|
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 {
|
while let Some((msg, mut err_sender)) = update_receiver.next().await {
|
||||||
let result = cx
|
let result = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
|
@ -57,39 +97,147 @@ impl GitState {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let commit_message = cx.new_model(|cx| Buffer::local("", cx));
|
let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event);
|
||||||
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);
|
|
||||||
|
|
||||||
GitState {
|
GitState {
|
||||||
commit_message,
|
languages,
|
||||||
active_repository: None,
|
repositories: vec![],
|
||||||
|
active_index: None,
|
||||||
update_sender,
|
update_sender,
|
||||||
|
_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate_repository(
|
pub fn active_repository(&self) -> Option<RepositoryHandle> {
|
||||||
&mut self,
|
self.active_index
|
||||||
worktree_id: WorktreeId,
|
.map(|index| self.repositories[index].clone())
|
||||||
active_repository: RepositoryEntry,
|
|
||||||
git_repo: Arc<dyn GitRepository>,
|
|
||||||
) {
|
|
||||||
self.active_repository = Some((worktree_id, active_repository, git_repo));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_repository(
|
fn on_worktree_store_event(
|
||||||
&self,
|
&mut self,
|
||||||
) -> Option<&(WorktreeId, RepositoryEntry, Arc<dyn GitRepository>)> {
|
worktree_store: Model<WorktreeStore>,
|
||||||
self.active_repository.as_ref()
|
_event: &WorktreeStoreEvent,
|
||||||
|
cx: &mut ModelContext<'_, Self>,
|
||||||
|
) {
|
||||||
|
// 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 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(
|
pub fn stage_entries(
|
||||||
|
@ -100,11 +248,8 @@ impl GitState {
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
|
|
||||||
return Err(anyhow!("No active repository"));
|
|
||||||
};
|
|
||||||
self.update_sender
|
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"))?;
|
.map_err(|_| anyhow!("Failed to submit stage operation"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -117,20 +262,15 @@ impl GitState {
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let Some((_, _, git_repo)) = self.active_repository.as_ref() else {
|
|
||||||
return Err(anyhow!("No active repository"));
|
|
||||||
};
|
|
||||||
self.update_sender
|
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"))?;
|
.map_err(|_| anyhow!("Failed to submit unstage operation"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
pub fn stage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
let to_stage = self
|
||||||
return Err(anyhow!("No active repository"));
|
.repository_entry
|
||||||
};
|
|
||||||
let to_stage = entry
|
|
||||||
.status()
|
.status()
|
||||||
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||||
.map(|entry| entry.repo_path.clone())
|
.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<()> {
|
pub fn unstage_all(&self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
let to_unstage = self
|
||||||
return Err(anyhow!("No active repository"));
|
.repository_entry
|
||||||
};
|
|
||||||
let to_unstage = entry
|
|
||||||
.status()
|
.status()
|
||||||
.filter(|entry| entry.status.is_staged().unwrap_or(true))
|
.filter(|entry| entry.status.is_staged().unwrap_or(true))
|
||||||
.map(|entry| entry.repo_path.clone())
|
.map(|entry| entry.repo_path.clone())
|
||||||
|
@ -155,23 +293,15 @@ impl GitState {
|
||||||
/// Get a count of all entries in the active repository, including
|
/// Get a count of all entries in the active repository, including
|
||||||
/// untracked files.
|
/// untracked files.
|
||||||
pub fn entry_count(&self) -> usize {
|
pub fn entry_count(&self) -> usize {
|
||||||
self.active_repository
|
self.repository_entry.status_len()
|
||||||
.as_ref()
|
|
||||||
.map_or(0, |(_, entry, _)| entry.status_len())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn have_changes(&self) -> bool {
|
fn have_changes(&self) -> bool {
|
||||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
self.repository_entry.status_summary() != GitSummary::UNCHANGED
|
||||||
return false;
|
|
||||||
};
|
|
||||||
entry.status_summary() != GitSummary::UNCHANGED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn have_staged_changes(&self) -> bool {
|
fn have_staged_changes(&self) -> bool {
|
||||||
let Some((_, entry, _)) = self.active_repository.as_ref() else {
|
self.repository_entry.status_summary().index != TrackedSummary::UNCHANGED
|
||||||
return false;
|
|
||||||
};
|
|
||||||
entry.status_summary().index != TrackedSummary::UNCHANGED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
|
pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
|
||||||
|
@ -185,36 +315,33 @@ impl GitState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit(
|
pub fn commit(
|
||||||
&mut self,
|
&self,
|
||||||
err_sender: mpsc::Sender<anyhow::Error>,
|
err_sender: mpsc::Sender<anyhow::Error>,
|
||||||
cx: &AppContext,
|
cx: &mut AppContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if !self.can_commit(false, cx) {
|
if !self.can_commit(false, cx) {
|
||||||
return Err(anyhow!("Unable to commit"));
|
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();
|
let message = self.commit_message.read(cx).as_rope().clone();
|
||||||
self.update_sender
|
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"))?;
|
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
||||||
|
self.commit_message.update(cx, |commit_message, cx| {
|
||||||
|
commit_message.set_text("", cx);
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_all(
|
pub fn commit_all(
|
||||||
&mut self,
|
&self,
|
||||||
err_sender: mpsc::Sender<anyhow::Error>,
|
err_sender: mpsc::Sender<anyhow::Error>,
|
||||||
cx: &AppContext,
|
cx: &mut AppContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if !self.can_commit(true, cx) {
|
if !self.can_commit(true, cx) {
|
||||||
return Err(anyhow!("Unable to commit"));
|
return Err(anyhow!("Unable to commit"));
|
||||||
}
|
}
|
||||||
let Some((_, entry, git_repo)) = self.active_repository.as_ref() else {
|
let to_stage = self
|
||||||
return Err(anyhow!("No active repository"));
|
.repository_entry
|
||||||
};
|
|
||||||
let to_stage = entry
|
|
||||||
.status()
|
.status()
|
||||||
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||||
.map(|entry| entry.repo_path.clone())
|
.map(|entry| entry.repo_path.clone())
|
||||||
|
@ -222,10 +349,13 @@ impl GitState {
|
||||||
let message = self.commit_message.read(cx).as_rope().clone();
|
let message = self.commit_message.read(cx).as_rope().clone();
|
||||||
self.update_sender
|
self.update_sender
|
||||||
.unbounded_send((
|
.unbounded_send((
|
||||||
Message::StageAndCommit(git_repo.clone(), message, to_stage),
|
Message::StageAndCommit(self.git_repo.clone(), message, to_stage),
|
||||||
err_sender,
|
err_sender,
|
||||||
))
|
))
|
||||||
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
|
||||||
|
self.commit_message.update(cx, |commit_message, cx| {
|
||||||
|
commit_message.set_text("", cx);
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3128,12 +3128,15 @@ impl LspStore {
|
||||||
})
|
})
|
||||||
.detach()
|
.detach()
|
||||||
}
|
}
|
||||||
WorktreeStoreEvent::WorktreeReleased(..) => {}
|
|
||||||
WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx),
|
WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx),
|
||||||
WorktreeStoreEvent::WorktreeOrderChanged => {}
|
|
||||||
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
|
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
|
||||||
worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(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 direnv;
|
||||||
mod environment;
|
mod environment;
|
||||||
pub use environment::EnvironmentErrorMessage;
|
pub use environment::EnvironmentErrorMessage;
|
||||||
|
use git::RepositoryHandle;
|
||||||
pub mod search_history;
|
pub mod search_history;
|
||||||
mod yarn;
|
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();
|
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||||
|
|
||||||
|
@ -2324,6 +2326,18 @@ impl Project {
|
||||||
}
|
}
|
||||||
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
|
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
|
||||||
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
|
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.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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4169,6 +4162,17 @@ impl Project {
|
||||||
pub fn git_state(&self) -> Option<&Model<GitState>> {
|
pub fn git_state(&self) -> Option<&Model<GitState>> {
|
||||||
self.git_state.as_ref()
|
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> {
|
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {
|
||||||
|
|
|
@ -25,7 +25,7 @@ use smol::{
|
||||||
};
|
};
|
||||||
use text::ReplicaId;
|
use text::ReplicaId;
|
||||||
use util::{paths::SanitizedPath, ResultExt};
|
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};
|
use crate::{search::SearchQuery, ProjectPath};
|
||||||
|
|
||||||
|
@ -63,6 +63,9 @@ pub enum WorktreeStoreEvent {
|
||||||
WorktreeReleased(EntityId, WorktreeId),
|
WorktreeReleased(EntityId, WorktreeId),
|
||||||
WorktreeOrderChanged,
|
WorktreeOrderChanged,
|
||||||
WorktreeUpdateSent(Model<Worktree>),
|
WorktreeUpdateSent(Model<Worktree>),
|
||||||
|
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
|
||||||
|
WorktreeUpdatedGitRepositories(WorktreeId),
|
||||||
|
WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
|
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
|
||||||
|
@ -364,6 +367,26 @@ impl WorktreeStore {
|
||||||
self.send_project_updates(cx);
|
self.send_project_updates(cx);
|
||||||
|
|
||||||
let handle_id = worktree.entity_id();
|
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.observe_release(worktree, move |this, worktree, cx| {
|
||||||
cx.emit(WorktreeStoreEvent::WorktreeReleased(
|
cx.emit(WorktreeStoreEvent::WorktreeReleased(
|
||||||
handle_id,
|
handle_id,
|
||||||
|
|
|
@ -47,6 +47,7 @@ util.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
git_ui.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
|
@ -16,6 +16,8 @@ use auto_update::AutoUpdateStatus;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
use feature_flags::{FeatureFlagAppExt, ZedPro};
|
use feature_flags::{FeatureFlagAppExt, ZedPro};
|
||||||
|
use git_ui::repository_selector::RepositorySelector;
|
||||||
|
use git_ui::repository_selector::RepositorySelectorPopoverMenu;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
|
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
|
||||||
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
|
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
|
||||||
|
@ -98,6 +100,7 @@ pub struct TitleBar {
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
content: Stateful<Div>,
|
content: Stateful<Div>,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
repository_selector: View<RepositorySelector>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
|
@ -181,6 +184,7 @@ impl Render for TitleBar {
|
||||||
title_bar
|
title_bar
|
||||||
.children(self.render_project_host(cx))
|
.children(self.render_project_host(cx))
|
||||||
.child(self.render_project_name(cx))
|
.child(self.render_project_name(cx))
|
||||||
|
.children(self.render_current_repository(cx))
|
||||||
.children(self.render_project_branch(cx))
|
.children(self.render_project_branch(cx))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -290,6 +294,7 @@ impl TitleBar {
|
||||||
content: div().id(id.into()),
|
content: div().id(id.into()),
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
application_menu,
|
application_menu,
|
||||||
|
repository_selector: cx.new_view(|cx| RepositorySelector::new(project.clone(), cx)),
|
||||||
workspace: workspace.weak_handle(),
|
workspace: workspace.weak_handle(),
|
||||||
should_move: false,
|
should_move: false,
|
||||||
project,
|
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> {
|
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||||
let entry = {
|
let entry = {
|
||||||
let mut names_and_branches =
|
let mut names_and_branches =
|
||||||
|
|
|
@ -32,7 +32,7 @@ rust-embed.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
take-until = "0.2.0"
|
take-until.workspace = true
|
||||||
tempfile = { workspace = true, optional = true }
|
tempfile = { workspace = true, optional = true }
|
||||||
unicase.workspace = true
|
unicase.workspace = true
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue