diff --git a/Cargo.lock b/Cargo.lock index 35ac944cd07890e5ad1005b7c1577ee0c0ed13df..0278d21b437af8168631f86a1b065e90c3c384e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5196,7 +5196,6 @@ dependencies = [ "futures 0.3.31", "git", "gpui", - "language", "menu", "project", "schemars", diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c9353a8e45c08443978df45ba915d2bc11f7265c..6f789acce766e81c7cc12d912582c7ba75ab7e40 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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) }); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index a8cf91d9575e033cb24113f2e300b4c31a9832db..7223600d6f3e7e1e39484f4096e7d06ac15481e7 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -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 diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c08e05f11932f8f8b39701cef53c2a4d71e302b9..2a6c76f9ac4c2deadf2bf638271868f223595b8c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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, - event: &language::BufferEvent, - cx: &mut ViewContext, - ) { - 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) { 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(); diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index db569e3e28e8de3724b330795d7ab3b988d6a576..d699061bad85203e217c0d831e98d8f276daf3b3 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -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, /// 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, SharedString, Vec), - Commit(Arc, SharedString), + StageAndCommit(Arc, Rope, Vec), + Commit(Arc, Rope), Stage(Arc, Vec), Unstage(Arc, Vec), } impl GitState { - pub fn new(cx: &AppContext) -> Self { + pub fn new(languages: Arc, cx: &mut AppContext) -> Self { let (update_sender, mut update_receiver) = mpsc::unbounded::<(Message, mpsc::Sender)>(); 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::Result<()> { - if !self.can_commit(false) { + pub fn commit( + &mut self, + err_sender: mpsc::Sender, + 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::Result<()> { - if !self.can_commit(true) { + pub fn commit_all( + &mut self, + err_sender: mpsc::Sender, + 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::>(); - 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), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9c4b485ec25dbd9ad03272747d7caee078009667..51afd8d614746277dc71c8d091ab9f8038184ed7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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();