Cargo.lock 🔗
@@ -5196,7 +5196,6 @@ dependencies = [
"futures 0.3.31",
"git",
"gpui",
- "language",
"menu",
"project",
"schemars",
Cole Miller and Max created
This PR changes the `GitPanel` and `GitState` to use a
`language::Buffer` for the commit message. This is a small initial step
toward remote editing and collaboration support.
Release Notes:
- N/A
---------
Co-authored-by: Max <max@zed.dev>
Cargo.lock | 1
crates/editor/src/editor_tests.rs | 1
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/git_panel.rs | 224 ++++++++++++++------------------
crates/project/src/git.rs | 66 ++++++--
crates/project/src/project.rs | 2
6 files changed, 146 insertions(+), 149 deletions(-)
@@ -5196,7 +5196,6 @@ dependencies = [
"futures 0.3.31",
"git",
"gpui",
- "language",
"menu",
"project",
"schemars",
@@ -10488,6 +10488,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let mut assert = |before, after| {
let _state_context = cx.set_state(before);
+ cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
});
@@ -20,7 +20,6 @@ editor.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
-language.workspace = true
menu.workspace = true
project.workspace = true
schemars.workspace = true
@@ -1,17 +1,16 @@
use crate::git_panel_settings::StatusStyle;
use crate::{git_panel_settings::GitPanelSettings, git_status_icon};
-use anyhow::{Context as _, Result};
+use anyhow::Result;
use db::kvp::KEY_VALUE_STORE;
use editor::actions::MoveToEnd;
use editor::scroll::ScrollbarAutoHide;
-use editor::{Editor, EditorSettings, ShowScrollbar};
+use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
use futures::channel::mpsc;
use futures::StreamExt as _;
use git::repository::{GitRepository, RepoPath};
use git::status::FileStatus;
use git::{CommitAllChanges, CommitChanges, RevertAll, StageAll, ToggleStaged, UnstageAll};
use gpui::*;
-use language::Buffer;
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
use project::git::GitState;
use project::{Fs, Project, ProjectPath, WorktreeId};
@@ -153,10 +152,6 @@ impl GitPanel {
let project = workspace.project().clone();
let weak_workspace = cx.view().downgrade();
let git_state = project.read(cx).git_state().cloned();
- let language_registry = workspace.app_state().languages.clone();
- let current_commit_message = git_state
- .as_ref()
- .map(|git_state| git_state.read(cx).commit_message.clone());
let (err_sender, mut err_receiver) = mpsc::channel(1);
let workspace = cx.view().downgrade();
@@ -167,81 +162,85 @@ impl GitPanel {
this.hide_scrollbar(cx);
})
.detach();
- cx.subscribe(&project, 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);
+ 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) = project.read(cx).git_state().cloned() 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();
+ 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();
+ 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();
+ 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;
+ 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::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();
- }
- _ => {}
- };
- });
+ project::Event::Closed => {
+ this.reveal_in_editor = Task::ready(());
+ this.visible_entries.clear();
+ }
+ _ => {}
+ };
+ });
+ }
})
.detach();
@@ -257,15 +256,23 @@ impl GitPanel {
background_color: Some(gpui::transparent_black()),
..Default::default()
};
-
text_style.refine(&refinement);
- let mut commit_editor = Editor::auto_height(10, cx);
- if let Some(message) = current_commit_message {
- commit_editor.set_text(message, cx);
+ 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 {
- commit_editor.set_text("", cx);
- }
+ 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);
@@ -275,24 +282,6 @@ impl GitPanel {
commit_editor
});
- let buffer = commit_editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .expect("commit editor must be singleton");
-
- cx.subscribe(&buffer, Self::on_buffer_event).detach();
-
- let markdown = language_registry.language_for_name("Markdown");
- cx.spawn(|_, mut cx| async move {
- let markdown = markdown.await.context("failed to load Markdown language")?;
- buffer.update(&mut cx, |buffer, cx| {
- buffer.set_language(Some(markdown), cx)
- })
- })
- .detach_and_log_err(cx);
-
let scroll_handle = UniformListScrollHandle::new();
let mut visible_worktrees = project.read(cx).visible_worktrees(cx);
@@ -713,9 +702,9 @@ impl GitPanel {
let Some(git_state) = self.git_state(cx) else {
return;
};
- if let Err(e) =
- git_state.update(cx, |git_state, _| git_state.commit(self.err_sender.clone()))
- {
+ if let Err(e) = git_state.update(cx, |git_state, cx| {
+ git_state.commit(self.err_sender.clone(), cx)
+ }) {
self.show_err_toast("commit error", e, cx);
};
self.commit_editor
@@ -727,8 +716,8 @@ impl GitPanel {
let Some(git_state) = self.git_state(cx) else {
return;
};
- if let Err(e) = git_state.update(cx, |git_state, _| {
- git_state.commit_all(self.err_sender.clone())
+ if let Err(e) = git_state.update(cx, |git_state, cx| {
+ git_state.commit_all(self.err_sender.clone(), cx)
}) {
self.show_err_toast("commit all error", e, cx);
};
@@ -911,26 +900,6 @@ impl GitPanel {
cx.notify();
}
- fn on_buffer_event(
- &mut self,
- _buffer: Model<Buffer>,
- event: &language::BufferEvent,
- cx: &mut ViewContext<Self>,
- ) {
- if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
- let commit_message = self.commit_editor.update(cx, |editor, cx| editor.text(cx));
-
- let Some(git_state) = self.git_state(cx) else {
- return;
- };
- git_state.update(cx, |git_state, _| {
- git_state.commit_message = commit_message.into();
- });
-
- cx.notify();
- }
- }
-
fn show_err_toast(&self, id: &'static str, e: anyhow::Error, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.weak_workspace.upgrade() else {
return;
@@ -1089,7 +1058,10 @@ impl GitPanel {
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), git_state.can_commit(true))
+ (
+ git_state.can_commit(false, cx),
+ git_state.can_commit(true, cx),
+ )
});
let focus_handle_1 = self.focus_handle(cx).clone();
@@ -1,19 +1,19 @@
-use std::sync::Arc;
-
-use anyhow::anyhow;
+use anyhow::{anyhow, Context as _};
use futures::channel::mpsc;
use futures::{SinkExt as _, StreamExt as _};
use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
};
-use gpui::{AppContext, SharedString};
+use gpui::{AppContext, Context as _, Model};
+use language::{Buffer, LanguageRegistry};
use settings::WorktreeId;
+use std::sync::Arc;
+use text::Rope;
use worktree::RepositoryEntry;
pub struct GitState {
- /// The current commit message being composed.
- pub commit_message: SharedString,
+ 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.
@@ -23,14 +23,14 @@ pub struct GitState {
}
enum Message {
- StageAndCommit(Arc<dyn GitRepository>, SharedString, Vec<RepoPath>),
- Commit(Arc<dyn GitRepository>, SharedString),
+ StageAndCommit(Arc<dyn GitRepository>, Rope, Vec<RepoPath>),
+ Commit(Arc<dyn GitRepository>, Rope),
Stage(Arc<dyn GitRepository>, Vec<RepoPath>),
Unstage(Arc<dyn GitRepository>, Vec<RepoPath>),
}
impl GitState {
- pub fn new(cx: &AppContext) -> Self {
+ pub fn new(languages: Arc<LanguageRegistry>, cx: &mut AppContext) -> Self {
let (update_sender, mut update_receiver) =
mpsc::unbounded::<(Message, mpsc::Sender<anyhow::Error>)>();
cx.spawn(|cx| async move {
@@ -41,12 +41,12 @@ impl GitState {
match msg {
Message::StageAndCommit(repo, message, paths) => {
repo.stage_paths(&paths)?;
- repo.commit(&message)?;
+ repo.commit(&message.to_string())?;
Ok(())
}
Message::Stage(repo, paths) => repo.stage_paths(&paths),
Message::Unstage(repo, paths) => repo.unstage_paths(&paths),
- Message::Commit(repo, message) => repo.commit(&message),
+ Message::Commit(repo, message) => repo.commit(&message.to_string()),
}
})
.await;
@@ -56,8 +56,22 @@ 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);
+
GitState {
- commit_message: SharedString::default(),
+ commit_message,
active_repository: None,
update_sender,
}
@@ -160,29 +174,41 @@ impl GitState {
entry.status_summary().index != TrackedSummary::UNCHANGED
}
- pub fn can_commit(&self, commit_all: bool) -> bool {
- return !self.commit_message.trim().is_empty()
+ pub fn can_commit(&self, commit_all: bool, cx: &AppContext) -> bool {
+ return self
+ .commit_message
+ .read(cx)
+ .chars()
+ .any(|c| !c.is_ascii_whitespace())
&& self.have_changes()
&& (commit_all || self.have_staged_changes());
}
- pub fn commit(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
- if !self.can_commit(false) {
+ pub fn commit(
+ &mut self,
+ err_sender: mpsc::Sender<anyhow::Error>,
+ cx: &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 = std::mem::take(&mut self.commit_message);
+ let message = self.commit_message.read(cx).as_rope().clone();
self.update_sender
.unbounded_send((Message::Commit(git_repo, message), err_sender))
.map_err(|_| anyhow!("Failed to submit commit operation"))?;
Ok(())
}
- pub fn commit_all(&mut self, err_sender: mpsc::Sender<anyhow::Error>) -> anyhow::Result<()> {
- if !self.can_commit(true) {
+ pub fn commit_all(
+ &mut self,
+ err_sender: mpsc::Sender<anyhow::Error>,
+ cx: &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 {
@@ -193,7 +219,7 @@ impl GitState {
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
.map(|entry| entry.repo_path.clone())
.collect::<Vec<_>>();
- let message = std::mem::take(&mut self.commit_message);
+ let message = self.commit_message.read(cx).as_rope().clone();
self.update_sender
.unbounded_send((
Message::StageAndCommit(git_repo.clone(), message, to_stage),
@@ -691,7 +691,7 @@ impl Project {
)
});
- let git_state = Some(cx.new_model(|cx| GitState::new(cx)));
+ let git_state = Some(cx.new_model(|cx| GitState::new(languages.clone(), cx)));
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();