diff --git a/Cargo.lock b/Cargo.lock index 6b123f9061d4ba7b15d606e7896c65c22a3c8245..004214c9c85f0642e579c60672b40b04626ed1f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,7 +1222,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-broadcast", - "async-trait", "audio2", "client2", "collections", @@ -1242,9 +1241,7 @@ dependencies = [ "serde_json", "settings2", "smallvec", - "ui2", "util", - "workspace2", ] [[package]] @@ -6165,6 +6162,26 @@ dependencies = [ "workspace", ] +[[package]] +name = "outline2" +version = "0.1.0" +dependencies = [ + "editor2", + "fuzzy2", + "gpui2", + "language2", + "ordered-float 2.10.0", + "picker2", + "postage", + "settings2", + "smol", + "text2", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "overload" version = "0.1.1" @@ -7055,6 +7072,17 @@ dependencies = [ "workspace", ] +[[package]] +name = "quick_action_bar2" +version = "0.1.0" +dependencies = [ + "editor2", + "gpui2", + "search2", + "ui2", + "workspace2", +] + [[package]] name = "quote" version = "1.0.33" @@ -8216,6 +8244,57 @@ dependencies = [ "workspace", ] +[[package]] +name = "semantic_index2" +version = "0.1.0" +dependencies = [ + "ai2", + "anyhow", + "async-trait", + "client2", + "collections", + "ctor", + "env_logger 0.9.3", + "futures 0.3.28", + "globset", + "gpui2", + "language2", + "lazy_static", + "log", + "ndarray", + "node_runtime", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "postage", + "pretty_assertions", + "project2", + "rand 0.8.5", + "rpc2", + "rusqlite", + "rust-embed", + "schemars", + "serde", + "serde_json", + "settings2", + "sha1", + "smol", + "tempdir", + "tiktoken-rs", + "tree-sitter", + "tree-sitter-cpp", + "tree-sitter-elixir", + "tree-sitter-json 0.20.0", + "tree-sitter-lua", + "tree-sitter-php", + "tree-sitter-ruby", + "tree-sitter-rust", + "tree-sitter-toml", + "tree-sitter-typescript", + "unindent", + "util", + "workspace2", +] + [[package]] name = "semver" version = "1.0.18" @@ -11515,7 +11594,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion 1.0.5", - "async-trait", "bincode", "call2", "client2", @@ -11819,10 +11897,12 @@ dependencies = [ "menu2", "node_runtime", "num_cpus", + "outline2", "parking_lot 0.11.2", "postage", "project2", "project_panel2", + "quick_action_bar2", "rand 0.8.5", "regex", "rope2", diff --git a/Cargo.toml b/Cargo.toml index 0a7e4aa18f57e1d87c597392f086dcbc078815e2..ada0711e279b1911389d98de378e481dd535f8f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ members = [ "crates/notifications", "crates/notifications2", "crates/outline", + "crates/outline2", "crates/picker", "crates/picker2", "crates/plugin", @@ -88,12 +89,15 @@ members = [ "crates/project_panel", "crates/project_panel2", "crates/project_symbols", + "crates/quick_action_bar2", "crates/recent_projects", "crates/rope", "crates/rpc", "crates/rpc2", "crates/search", "crates/search2", + "crates/semantic_index", + "crates/semantic_index2", "crates/settings", "crates/settings2", "crates/snippet", @@ -113,7 +117,6 @@ members = [ "crates/theme_selector2", "crates/ui2", "crates/util", - "crates/semantic_index", "crates/story", "crates/vim", "crates/vcs_menu", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2a8d19f8829039d759c92e79b6acebe79e55b143..25fafa755e842359fd882b416f5e33e06c42a202 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -17,18 +17,8 @@ "cmd-enter": "menu::SecondaryConfirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "cmd-{": "pane::ActivatePrevItem", - "cmd-}": "pane::ActivateNextItem", - "alt-cmd-left": "pane::ActivatePrevItem", - "alt-cmd-right": "pane::ActivateNextItem", - "cmd-w": "pane::CloseActiveItem", - "alt-cmd-t": "pane::CloseInactiveItems", - "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", - "cmd-k u": "pane::CloseCleanItems", - "cmd-k cmd-w": "pane::CloseAllItems", "cmd-shift-w": "workspace::CloseWindow", - "cmd-s": "workspace::Save", - "cmd-shift-s": "workspace::SaveAs", + "cmd-o": "workspace::Open", "cmd-=": "zed::IncreaseBufferFontSize", "cmd-+": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", @@ -38,15 +28,7 @@ "cmd-h": "zed::Hide", "alt-cmd-h": "zed::HideOthers", "cmd-m": "zed::Minimize", - "ctrl-cmd-f": "zed::ToggleFullScreen", - "cmd-n": "workspace::NewFile", - "cmd-shift-n": "workspace::NewWindow", - "cmd-o": "workspace::Open", - "alt-cmd-o": "projects::OpenRecent", - "alt-cmd-b": "branches::OpenRecent", - "ctrl-~": "workspace::NewTerminal", - "ctrl-`": "terminal_panel::ToggleFocus", - "shift-escape": "workspace::ToggleZoom" + "ctrl-cmd-f": "zed::ToggleFullScreen" } }, { @@ -284,6 +266,15 @@ { "context": "Pane", "bindings": { + "cmd-{": "pane::ActivatePrevItem", + "cmd-}": "pane::ActivateNextItem", + "alt-cmd-left": "pane::ActivatePrevItem", + "alt-cmd-right": "pane::ActivateNextItem", + "cmd-w": "pane::CloseActiveItem", + "alt-cmd-t": "pane::CloseInactiveItems", + "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", + "cmd-k u": "pane::CloseCleanItems", + "cmd-k cmd-w": "pane::CloseAllItems", "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", @@ -389,6 +380,15 @@ { "context": "Workspace", "bindings": { + "alt-cmd-o": "projects::OpenRecent", + "alt-cmd-b": "branches::OpenRecent", + "ctrl-~": "workspace::NewTerminal", + "cmd-s": "workspace::Save", + "cmd-shift-s": "workspace::SaveAs", + "cmd-n": "workspace::NewFile", + "cmd-shift-n": "workspace::NewWindow", + "ctrl-`": "terminal_panel::ToggleFocus", + "shift-escape": "workspace::ToggleZoom", "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], "cmd-3": ["workspace::ActivatePane", 2], diff --git a/crates/ai2/src/auth.rs b/crates/ai2/src/auth.rs index baa1fe7b83299ef66db4ecf0d0403b1ac92dc5bc..1ea49bd615999a7f0318d3e205d3f86cee9c64a8 100644 --- a/crates/ai2/src/auth.rs +++ b/crates/ai2/src/auth.rs @@ -7,7 +7,7 @@ pub enum ProviderCredential { NotNeeded, } -pub trait CredentialProvider { +pub trait CredentialProvider: Send + Sync { fn has_credentials(&self) -> bool; fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential; fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential); diff --git a/crates/ai2/src/providers/open_ai/embedding.rs b/crates/ai2/src/providers/open_ai/embedding.rs index 8f62c8dc0d440675146cdcae6f1e4a803de15cec..d5fe4e8c5842709c587b9898862e0a2461461ed2 100644 --- a/crates/ai2/src/providers/open_ai/embedding.rs +++ b/crates/ai2/src/providers/open_ai/embedding.rs @@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider { model: OpenAILanguageModel, credential: Arc>, pub client: Arc, - pub executor: Arc, + pub executor: BackgroundExecutor, rate_limit_count_rx: watch::Receiver>, rate_limit_count_tx: Arc>>>, } @@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage { } impl OpenAIEmbeddingProvider { - pub fn new(client: Arc, executor: Arc) -> Self { + pub fn new(client: Arc, executor: BackgroundExecutor) -> Self { let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None); let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx)); diff --git a/crates/breadcrumbs2/src/breadcrumbs.rs b/crates/breadcrumbs2/src/breadcrumbs.rs index 75195a315930e2525ed1b01aea36f57e6d30b699..1c577fa3105ea2d5b3d1aa81aa070dce136d3d06 100644 --- a/crates/breadcrumbs2/src/breadcrumbs.rs +++ b/crates/breadcrumbs2/src/breadcrumbs.rs @@ -1,10 +1,10 @@ use gpui::{ - Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, + Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, ViewContext, WeakView, }; use itertools::Itertools; use theme::ActiveTheme; -use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label}; +use ui::{prelude::*, ButtonLike, ButtonStyle, Label}; use workspace::{ item::{ItemEvent, ItemHandle}, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -36,54 +36,51 @@ impl EventEmitter for Breadcrumbs {} impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { - type Element = Component; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let button = ButtonLike::new("breadcrumbs") - .style(ButtonStyle::Transparent) - .disabled(true); + let element = h_stack().text_ui(); + + let Some(active_item) = &self + .active_item + .as_ref() + .filter(|item| item.downcast::().is_some()) + else { + return element; + }; - let active_item = match &self.active_item { - Some(active_item) => active_item, - None => return button.into_element(), + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element; }; - let not_editor = active_item.downcast::().is_none(); - let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) { - Some(breadcrumbs) => breadcrumbs, - None => return button.into_element(), - } - .into_iter() - .map(|breadcrumb| { - StyledText::new(breadcrumb.text) - .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default()) + let highlighted_segments = segments.into_iter().map(|segment| { + StyledText::new(segment.text) + .with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default()) .into_any() }); + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").into_any_element() + }); - let button = button.children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" › ").into_any_element() - })); - - if not_editor || !self.pane_focused { - return button.into_element(); - } - - // let this = cx.view().downgrade(); - button - .style(ButtonStyle::Filled) - .disabled(false) - .on_click(move |_, _cx| { - todo!("outline::toggle"); - // this.update(cx, |this, cx| { - // if let Some(workspace) = this.workspace.upgrade() { - // workspace.update(cx, |_workspace, _cx| { - // outline::toggle(workspace, &Default::default(), cx) - // }) - // } - // }) - // .ok(); - }) - .into_element() + element.child( + ButtonLike::new("toggle outline view") + .style(ButtonStyle::Subtle) + .child(h_stack().gap_1().children(breadcrumbs)) + // We disable the button when it is not focused + // due to ... @julia what was the reason again? + .disabled(!self.pane_focused) + .on_click(move |_, _cx| { + todo!("outline::toggle"); + // this.update(cx, |this, cx| { + // if let Some(workspace) = this.workspace.upgrade() { + // workspace.update(cx, |_workspace, _cx| { + // outline::toggle(workspace, &Default::default(), cx) + // }) + // } + // }) + // .ok(); + }), + ) } } diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index 8dc37f68dd7bd9b91c1d1fab240448eb25119cbf..c2d95c8b52b2705763c537e71285f521aea3148d 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -31,9 +31,7 @@ media = { path = "../media" } project = { package = "project2", path = "../project2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } -ui = {package = "ui2", path = "../ui2"} -workspace = {package = "workspace2", path = "../workspace2"} -async-trait.workspace = true + anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index a93305772312cab3624f995e0dd49751554867e1..14cb28c32d6b932c8db9f2fa54b5c0125bbf8011 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -1,32 +1,25 @@ pub mod call_settings; pub mod participant; pub mod room; -mod shared_screen; use anyhow::{anyhow, Result}; -use async_trait::async_trait; use audio::Audio; use call_settings::CallSettings; -use client::{ - proto::{self, PeerId}, - Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, -}; +use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel, - Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle, + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, + WeakModel, }; -pub use participant::ParticipantLocation; use postage::watch; use project::Project; use room::Event; -pub use room::Room; use settings::Settings; -use shared_screen::SharedScreen; use std::sync::Arc; -use util::ResultExt; -use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; + +pub use participant::ParticipantLocation; +pub use room::Room; pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { CallSettings::register(cx); @@ -334,55 +327,12 @@ impl ActiveCall { pub fn join_channel( &mut self, channel_id: u64, - requesting_window: Option>, cx: &mut ModelContext, ) -> Task>>> { if let Some(room) = self.room().cloned() { if room.read(cx).channel_id() == Some(channel_id) { - return cx.spawn(|_, _| async move { - todo!(); - // let future = room.update(&mut cx, |room, cx| { - // room.most_active_project(cx).map(|(host, project)| { - // room.join_project(project, host, app_state.clone(), cx) - // }) - // }) - - // if let Some(future) = future { - // future.await?; - // } - - // Ok(Some(room)) - }); - } - - let should_prompt = room.update(cx, |room, _| { - room.channel_id().is_some() - && room.is_sharing_project() - && room.remote_participants().len() > 0 - }); - if should_prompt && requesting_window.is_some() { - return cx.spawn(|this, mut cx| async move { - let answer = requesting_window.unwrap().update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Warning, - "Leaving this call will unshare your current project.\nDo you want to switch channels?", - &["Yes, Join Channel", "Cancel"], - ) - })?; - if answer.await? == 1 { - return Ok(None); - } - - room.update(&mut cx, |room, cx| room.clear_state(cx))?; - - this.update(&mut cx, |this, cx| { - this.join_channel(channel_id, requesting_window, cx) - })? - .await - }); - } - - if room.read(cx).channel_id().is_some() { + return Task::ready(Ok(Some(room))); + } else { room.update(cx, |room, cx| room.clear_state(cx)); } } @@ -555,197 +505,6 @@ pub fn report_call_event_for_channel( ) } -pub struct Call { - active_call: Option<(Model, Vec)>, -} - -impl Call { - pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box { - let mut active_call = None; - if cx.has_global::>() { - let call = cx.global::>().clone(); - let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; - active_call = Some((call, subscriptions)); - } - Box::new(Self { active_call }) - } - fn on_active_call_event( - workspace: &mut Workspace, - _: Model, - event: &room::Event, - cx: &mut ViewContext, - ) { - match event { - room::Event::ParticipantLocationChanged { participant_id } - | room::Event::RemoteVideoTracksChanged { participant_id } => { - workspace.leader_updated(*participant_id, cx); - } - _ => {} - } - } -} - -#[async_trait(?Send)] -impl CallHandler for Call { - fn peer_state( - &mut self, - leader_id: PeerId, - project: &Model, - cx: &mut ViewContext, - ) -> Option<(bool, bool)> { - let (call, _) = self.active_call.as_ref()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(leader_id)?; - - let leader_in_this_app; - let leader_in_this_project; - match participant.location { - ParticipantLocation::SharedProject { project_id } => { - leader_in_this_app = true; - leader_in_this_project = Some(project_id) == project.read(cx).remote_id(); - } - ParticipantLocation::UnsharedProject => { - leader_in_this_app = true; - leader_in_this_project = false; - } - ParticipantLocation::External => { - leader_in_this_app = false; - leader_in_this_project = false; - } - }; - - Some((leader_in_this_project, leader_in_this_app)) - } - - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &View, - cx: &mut ViewContext, - ) -> Option> { - let (call, _) = self.active_call.as_ref()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; - let track = participant.video_tracks.values().next()?.clone(); - let user = participant.user.clone(); - for item in pane.read(cx).items_of_type::() { - if item.read(cx).peer_id == peer_id { - return Some(Box::new(item)); - } - } - - Some(Box::new(cx.build_view(|cx| { - SharedScreen::new(&track, peer_id, user.clone(), cx) - }))) - } - fn room_id(&self, cx: &AppContext) -> Option { - Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) - } - fn hang_up(&self, cx: &mut AppContext) -> Task> { - let Some((call, _)) = self.active_call.as_ref() else { - return Task::ready(Err(anyhow!("Cannot exit a call; not in a call"))); - }; - - call.update(cx, |this, cx| this.hang_up(cx)) - } - fn active_project(&self, cx: &AppContext) -> Option> { - ActiveCall::global(cx).read(cx).location().cloned() - } - fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut AppContext, - ) -> Task> { - ActiveCall::global(cx).update(cx, |this, cx| { - this.invite(called_user_id, initial_project, cx) - }) - } - fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { - self.active_call - .as_ref() - .map(|call| { - call.0.read(cx).room().map(|room| { - room.read(cx) - .remote_participants() - .iter() - .map(|participant| { - (participant.1.user.clone(), participant.1.peer_id.clone()) - }) - .collect() - }) - }) - .flatten() - } - fn is_muted(&self, cx: &AppContext) -> Option { - self.active_call - .as_ref() - .map(|call| { - call.0 - .read(cx) - .room() - .map(|room| room.read(cx).is_muted(cx)) - }) - .flatten() - } - fn toggle_mute(&self, cx: &mut AppContext) { - self.active_call.as_ref().map(|call| { - call.0.update(cx, |this, cx| { - this.room().map(|room| { - let room = room.clone(); - cx.spawn(|_, mut cx| async move { - room.update(&mut cx, |this, cx| this.toggle_mute(cx))?? - .await - }) - .detach_and_log_err(cx); - }) - }) - }); - } - fn toggle_screen_share(&self, cx: &mut AppContext) { - self.active_call.as_ref().map(|call| { - call.0.update(cx, |this, cx| { - this.room().map(|room| { - room.update(cx, |this, cx| { - if this.is_screen_sharing() { - this.unshare_screen(cx).log_err(); - } else { - let t = this.share_screen(cx); - cx.spawn(move |_, _| async move { - t.await.log_err(); - }) - .detach(); - } - }) - }) - }) - }); - } - fn toggle_deafen(&self, cx: &mut AppContext) { - self.active_call.as_ref().map(|call| { - call.0.update(cx, |this, cx| { - this.room().map(|room| { - room.update(cx, |this, cx| { - this.toggle_deafen(cx).log_err(); - }) - }) - }) - }); - } - fn is_deafened(&self, cx: &AppContext) -> Option { - self.active_call - .as_ref() - .map(|call| { - call.0 - .read(cx) - .room() - .map(|room| room.read(cx).is_deafened()) - }) - .flatten() - .flatten() - } -} - #[cfg(test)] mod test { use gpui::TestAppContext; diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs index 325a4f812b2f58c1b1bb0cc56f042e891df435d4..11a58b4b098cc6a255f8c1b061d76cf44c64684b 100644 --- a/crates/call2/src/participant.rs +++ b/crates/call2/src/participant.rs @@ -4,7 +4,7 @@ use client::{proto, User}; use collections::HashMap; use gpui::WeakModel; pub use live_kit_client::Frame; -pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; use std::sync::Arc; diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index a28f2ae87f0984241ca7df30fac0807d4e0fa31b..97509d730faf6f78cce728ce4e091985b297c430 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica use editor::{Editor, ExcerptRange, MultiBuffer}; use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; +use project::project_settings::ProjectSettings; use rpc::proto::PeerId; use serde_json::json; +use settings::SettingsStore; use std::{borrow::Cow, sync::Arc}; use workspace::{ dock::{test::TestPanel, DockPosition}, @@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces( }); } +#[gpui::test] +async fn test_following_into_excluded_file( + deterministic: Arc, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + for cx in [&mut cx_a, &mut cx_b] { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); + }); + }); + }); + } + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + ".git": { + "COMMIT_EDITMSG": "write your commit message here", + }, + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let window_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = window_a.root(cx_a); + let peer_id_a = client_a.peer_id().unwrap(); + let window_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = window_b.root(cx_b); + + // Client A opens editors for a regular file and an excluded file. + let editor_for_regular = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_for_excluded_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client A updates their selections in those editors + editor_for_regular.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_for_excluded_a.update(cx_a, |editor, cx| { + editor.select_all(&Default::default(), cx); + editor.handle_input("new commit message", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![18..17]); + }); + + // When client B starts following client A, currently visible file is replicated + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), + Some((worktree_id, ".git/COMMIT_EDITMSG").into()) + ); + assert_eq!( + editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![18..17] + ); + + // Changes from B to the excluded file are replicated in A's editor + editor_for_excluded_b.update(cx_b, |editor, cx| { + editor.handle_input("\nCo-Authored-By: B ", cx); + }); + deterministic.run_until_parked(); + editor_for_excluded_a.update(cx_a, |editor, cx| { + assert_eq!( + editor.text(cx), + "new commit messag\nCo-Authored-By: B " + ); + }); +} + fn visible_push_notifications( cx: &mut TestAppContext, ) -> Vec> { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index c5820b539526c94879edfd2a06412c7271ab3252..ad4c59e3773eb3de099d402ac736e6364daed688 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2981,11 +2981,10 @@ async fn test_fs_operations( let entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "c.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "c.txt"), false, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( @@ -3010,7 +3009,6 @@ async fn test_fs_operations( .update(cx_b, |project, cx| { project.rename_entry(entry.id, Path::new("d.txt"), cx) }) - .unwrap() .await .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -3034,11 +3032,10 @@ async fn test_fs_operations( let dir_entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR"), true, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { assert_eq!( @@ -3061,25 +3058,19 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/e.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/e.txt"), false, cx) }) .await .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx) }) .await .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) }) .await .unwrap(); @@ -3120,9 +3111,7 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .copy_entry(entry.id, Path::new("f.txt"), cx) - .unwrap() + project.copy_entry(entry.id, Path::new("f.txt"), cx) }) .await .unwrap(); diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 42a2b7927581f26a6d341ed9ed1d0683b43c89f6..f839333c95aedb94f55e24ce775acd23839e4a90 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest { ensure_project_shared(&project, client, cx).await; project .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) - .unwrap() .await?; } diff --git a/crates/collab2/src/tests/channel_tests.rs b/crates/collab2/src/tests/channel_tests.rs index 43d18ee7d13b850b634c67a0414831a64b455d5c..8ce5d99b80d3c630a81181e5f03f78d385186a10 100644 --- a/crates/collab2/src/tests/channel_tests.rs +++ b/crates/collab2/src/tests/channel_tests.rs @@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member( let active_call_b = cx_b.read(ActiveCall::global); assert!(active_call_b - .update(cx_b, |active_call, cx| active_call - .join_channel(sub_id, None, cx)) + .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx)) .await .is_ok()); } @@ -395,9 +394,7 @@ async fn test_channel_room( let active_call_b = cx_b.read(ActiveCall::global); active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -445,9 +442,7 @@ async fn test_channel_room( }); active_call_b - .update(cx_b, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -564,16 +559,12 @@ async fn test_channel_room( }); active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); active_call_b - .update(cx_b, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo let active_call_a = cx_a.read(ActiveCall::global); active_call_a - .update(cx_a, |active_call, cx| { - active_call.join_channel(zed_id, None, cx) - }) + .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); @@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo active_call_a .update(cx_a, |active_call, cx| { - active_call.join_channel(rust_id, None, cx) + active_call.join_channel(rust_id, cx) }) .await .unwrap(); @@ -804,7 +793,7 @@ async fn test_call_from_channel( let active_call_b = cx_b.read(ActiveCall::global); active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx)) + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) .await .unwrap(); @@ -1297,7 +1286,7 @@ async fn test_guest_access( // Non-members should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .is_err()); @@ -1319,7 +1308,7 @@ async fn test_guest_access( // Client B joins channel A as a guest active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .unwrap(); @@ -1352,7 +1341,7 @@ async fn test_guest_access( assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b, cx)) .await .unwrap(); @@ -1383,7 +1372,7 @@ async fn test_invite_access( // should not be allowed to join assert!(active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) .await .is_err()); @@ -1401,7 +1390,7 @@ async fn test_invite_access( .unwrap(); active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx)) + .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) .await .unwrap(); diff --git a/crates/collab2/src/tests/following_tests.rs b/crates/collab2/src/tests/following_tests.rs index 61d14c25c426cb93f4101aeb56ac2119d8efe0f7..5178df408f95b8809495836576c3f1c74159cf85 100644 --- a/crates/collab2/src/tests/following_tests.rs +++ b/crates/collab2/src/tests/following_tests.rs @@ -4,10 +4,12 @@ // use call::ActiveCall; // use collab_ui::notifications::project_shared_notification::ProjectSharedNotification; // use editor::{Editor, ExcerptRange, MultiBuffer}; -// use gpui::{BackgroundExecutor, TestAppContext, View}; +// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext}; // use live_kit_client::MacOSDisplay; +// use project::project_settings::ProjectSettings; // use rpc::proto::PeerId; // use serde_json::json; +// use settings::SettingsStore; // use std::borrow::Cow; // use workspace::{ // dock::{test::TestPanel, DockPosition}, @@ -24,7 +26,7 @@ // cx_c: &mut TestAppContext, // cx_d: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // let client_c = server.create_client(cx_c, "user_c").await; @@ -71,12 +73,22 @@ // .unwrap(); // let window_a = client_a.build_workspace(&project_a, cx_a); -// let workspace_a = window_a.root(cx_a); +// let workspace_a = window_a.root(cx_a).unwrap(); // let window_b = client_b.build_workspace(&project_b, cx_b); -// let workspace_b = window_b.root(cx_b); +// let workspace_b = window_b.root(cx_b).unwrap(); + +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; +// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c); +// let cx_c = &mut cx_c; +// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d); +// let cx_d = &mut cx_d; // // Client A opens some editors. -// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); +// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); // let editor_a1 = workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -132,8 +144,8 @@ // .await // .unwrap(); -// cx_c.foreground().run_until_parked(); -// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { +// cx_c.executor().run_until_parked(); +// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { // workspace // .active_item(cx) // .unwrap() @@ -145,19 +157,19 @@ // Some((worktree_id, "2.txt").into()) // ); // assert_eq!( -// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), +// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), // vec![2..1] // ); // assert_eq!( -// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), +// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), // vec![3..2] // ); -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // let active_call_c = cx_c.read(ActiveCall::global); // let project_c = client_c.build_remote_project(project_id, cx_c).await; // let window_c = client_c.build_workspace(&project_c, cx_c); -// let workspace_c = window_c.root(cx_c); +// let workspace_c = window_c.root(cx_c).unwrap(); // active_call_c // .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) // .await @@ -172,10 +184,13 @@ // .await // .unwrap(); -// cx_d.foreground().run_until_parked(); +// cx_d.executor().run_until_parked(); // let active_call_d = cx_d.read(ActiveCall::global); // let project_d = client_d.build_remote_project(project_id, cx_d).await; -// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); +// let workspace_d = client_d +// .build_workspace(&project_d, cx_d) +// .root(cx_d) +// .unwrap(); // active_call_d // .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) // .await @@ -183,7 +198,7 @@ // drop(project_d); // // All clients see that clients B and C are following client A. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -198,7 +213,7 @@ // }); // // All clients see that clients B is following client A. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -216,7 +231,7 @@ // .unwrap(); // // All clients see that clients B and C are following client A. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -240,7 +255,7 @@ // .unwrap(); // // All clients see that D is following C -// cx_d.foreground().run_until_parked(); +// cx_d.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -257,7 +272,7 @@ // cx_c.drop_last(workspace_c); // // Clients A and B see that client B is following A, and client C is not present in the followers. -// cx_c.foreground().run_until_parked(); +// cx_c.executor().run_until_parked(); // for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] { // assert_eq!( // followers_by_leader(project_id, cx), @@ -271,12 +286,15 @@ // workspace.activate_item(&editor_a1, cx) // }); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b1.item_id() +// ); // }); // // When client A opens a multibuffer, client B does so as well. -// let multibuffer_a = cx_a.add_model(|cx| { +// let multibuffer_a = cx_a.build_model(|cx| { // let buffer_a1 = project_a.update(cx, |project, cx| { // project // .get_open_buffer(&(worktree_id, "1.txt").into(), cx) @@ -308,12 +326,12 @@ // }); // let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { // let editor = -// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); +// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); // workspace.add_item(Box::new(editor.clone()), cx); // editor // }); // executor.run_until_parked(); -// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { +// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| { // workspace // .active_item(cx) // .unwrap() @@ -321,8 +339,8 @@ // .unwrap() // }); // assert_eq!( -// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), -// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), +// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)), +// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)), // ); // // When client A navigates back and forth, client B does so as well. @@ -333,8 +351,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b1.item_id() +// ); // }); // workspace_a @@ -344,8 +365,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b2.item_id() +// ); // }); // workspace_a @@ -355,8 +379,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_b.read_with(cx_b, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); +// workspace_b.update(cx_b, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_b1.item_id() +// ); // }); // // Changes to client A's editor are reflected on client B. @@ -364,20 +391,20 @@ // editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); // }); // executor.run_until_parked(); -// editor_b1.read_with(cx_b, |editor, cx| { +// editor_b1.update(cx_b, |editor, cx| { // assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); // }); // editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); // executor.run_until_parked(); -// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); +// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); // editor_a1.update(cx_a, |editor, cx| { // editor.change_selections(None, cx, |s| s.select_ranges([3..3])); -// editor.set_scroll_position(vec2f(0., 100.), cx); +// editor.set_scroll_position(point(0., 100.), cx); // }); // executor.run_until_parked(); -// editor_b1.read_with(cx_b, |editor, cx| { +// editor_b1.update(cx_b, |editor, cx| { // assert_eq!(editor.selections.ranges(cx), &[3..3]); // }); @@ -390,11 +417,11 @@ // }); // executor.run_until_parked(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, cx| workspace +// workspace_b.update(cx_b, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// editor_b1.id() +// .item_id()), +// editor_b1.item_id() // ); // // Client A starts following client B. @@ -405,15 +432,15 @@ // .await // .unwrap(); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), +// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), // Some(peer_id_b) // ); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, cx| workspace +// workspace_a.update(cx_a, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// editor_a1.id() +// .item_id()), +// editor_a1.item_id() // ); // // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. @@ -432,7 +459,7 @@ // .await // .unwrap(); // executor.run_until_parked(); -// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { +// let shared_screen = workspace_a.update(cx_a, |workspace, cx| { // workspace // .active_item(cx) // .expect("no active item") @@ -446,8 +473,11 @@ // .await // .unwrap(); // executor.run_until_parked(); -// workspace_a.read_with(cx_a, |workspace, cx| { -// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) +// workspace_a.update(cx_a, |workspace, cx| { +// assert_eq!( +// workspace.active_item(cx).unwrap().item_id(), +// editor_a1.item_id() +// ) // }); // // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. @@ -455,26 +485,26 @@ // workspace.activate_item(&multibuffer_editor_b, cx) // }); // executor.run_until_parked(); -// workspace_a.read_with(cx_a, |workspace, cx| { +// workspace_a.update(cx_a, |workspace, cx| { // assert_eq!( -// workspace.active_item(cx).unwrap().id(), -// multibuffer_editor_a.id() +// workspace.active_item(cx).unwrap().item_id(), +// multibuffer_editor_a.item_id() // ) // }); // // Client B activates a panel, and the previously-opened screen-sharing item gets activated. -// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); +// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left)); // workspace_b.update(cx_b, |workspace, cx| { // workspace.add_panel(panel, cx); // workspace.toggle_panel_focus::(cx); // }); // executor.run_until_parked(); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, cx| workspace +// workspace_a.update(cx_a, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// shared_screen.id() +// .item_id()), +// shared_screen.item_id() // ); // // Toggling the focus back to the pane causes client A to return to the multibuffer. @@ -482,16 +512,16 @@ // workspace.toggle_panel_focus::(cx); // }); // executor.run_until_parked(); -// workspace_a.read_with(cx_a, |workspace, cx| { +// workspace_a.update(cx_a, |workspace, cx| { // assert_eq!( -// workspace.active_item(cx).unwrap().id(), -// multibuffer_editor_a.id() +// workspace.active_item(cx).unwrap().item_id(), +// multibuffer_editor_a.item_id() // ) // }); // // Client B activates an item that doesn't implement following, // // so the previously-opened screen-sharing item gets activated. -// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); +// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new()); // workspace_b.update(cx_b, |workspace, cx| { // workspace.active_pane().update(cx, |pane, cx| { // pane.add_item(Box::new(unfollowable_item), true, true, None, cx) @@ -499,18 +529,18 @@ // }); // executor.run_until_parked(); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, cx| workspace +// workspace_a.update(cx_a, |workspace, cx| workspace // .active_item(cx) // .unwrap() -// .id()), -// shared_screen.id() +// .item_id()), +// shared_screen.item_id() // ); // // Following interrupts when client B disconnects. // client_b.disconnect(&cx_b.to_async()); // executor.advance_clock(RECONNECT_TIMEOUT); // assert_eq!( -// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), +// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), // None // ); // } @@ -521,7 +551,7 @@ // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // server @@ -560,13 +590,19 @@ // .await // .unwrap(); -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); +// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); +// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); -// let client_b_id = project_a.read_with(cx_a, |project, _| { +// let client_b_id = project_a.update(cx_a, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); @@ -584,7 +620,7 @@ // .await // .unwrap(); -// let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { +// let pane_paths = |pane: &View, cx: &mut TestAppContext| { // pane.update(cx, |pane, cx| { // pane.items() // .map(|item| { @@ -642,7 +678,7 @@ // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // server @@ -685,7 +721,10 @@ // .unwrap(); // // Client A opens a file. -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); // workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -696,7 +735,10 @@ // .unwrap(); // // Client B opens a different file. -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); // workspace_b // .update(cx_b, |workspace, cx| { // workspace.open_path((worktree_id, "2.txt"), None, true, cx) @@ -1167,7 +1209,7 @@ // cx_b: &mut TestAppContext, // ) { // // 2 clients connect to a server. -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // server @@ -1207,8 +1249,17 @@ // .await // .unwrap(); +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; + // // Client A opens some editors. -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); // let _editor_a1 = workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -1219,9 +1270,12 @@ // .unwrap(); // // Client B starts following client A. -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); -// let leader_id = project_b.read_with(cx_b, |project, _| { +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); +// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone()); +// let leader_id = project_b.update(cx_b, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); // workspace_b @@ -1231,10 +1285,10 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); -// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { +// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| { // workspace // .active_item(cx) // .unwrap() @@ -1245,7 +1299,7 @@ // // When client B moves, it automatically stops following client A. // editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); @@ -1256,14 +1310,14 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); // // When client B edits, it automatically stops following client A. // editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); @@ -1274,16 +1328,16 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); // // When client B scrolls, it automatically stops following client A. // editor_b2.update(cx_b, |editor, cx| { -// editor.set_scroll_position(vec2f(0., 3.), cx) +// editor.set_scroll_position(point(0., 3.), cx) // }); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); @@ -1294,7 +1348,7 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); @@ -1303,13 +1357,13 @@ // workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) // }); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); // workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // Some(leader_id) // ); @@ -1321,7 +1375,7 @@ // .await // .unwrap(); // assert_eq!( -// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), +// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), // None // ); // } @@ -1332,7 +1386,7 @@ // cx_a: &mut TestAppContext, // cx_b: &mut TestAppContext, // ) { -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // server @@ -1345,20 +1399,26 @@ // client_a.fs().insert_tree("/a", json!({})).await; // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); // let project_id = active_call_a // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) // .await // .unwrap(); // let project_b = client_b.build_remote_project(project_id, cx_b).await; -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); // executor.run_until_parked(); -// let client_a_id = project_b.read_with(cx_b, |project, _| { +// let client_a_id = project_b.update(cx_b, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); -// let client_b_id = project_a.read_with(cx_a, |project, _| { +// let client_b_id = project_a.update(cx_a, |project, _| { // project.collaborators().values().next().unwrap().peer_id // }); @@ -1370,13 +1430,13 @@ // }); // futures::try_join!(a_follow_b, b_follow_a).unwrap(); -// workspace_a.read_with(cx_a, |workspace, _| { +// workspace_a.update(cx_a, |workspace, _| { // assert_eq!( // workspace.leader_for_pane(workspace.active_pane()), // Some(client_b_id) // ); // }); -// workspace_b.read_with(cx_b, |workspace, _| { +// workspace_b.update(cx_b, |workspace, _| { // assert_eq!( // workspace.leader_for_pane(workspace.active_pane()), // Some(client_a_id) @@ -1398,7 +1458,7 @@ // // b opens a different file in project 2, a follows b // // b opens a different file in project 1, a cannot follow b // // b shares the project, a joins the project and follows b -// let mut server = TestServer::start(&executor).await; +// let mut server = TestServer::start(executor.clone()).await; // let client_a = server.create_client(cx_a, "user_a").await; // let client_b = server.create_client(cx_b, "user_b").await; // cx_a.update(editor::init); @@ -1435,8 +1495,14 @@ // let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; // let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; -// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); +// let workspace_a = client_a +// .build_workspace(&project_a, cx_a) +// .root(cx_a) +// .unwrap(); +// let workspace_b = client_b +// .build_workspace(&project_b, cx_b) +// .root(cx_b) +// .unwrap(); // cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); // cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); @@ -1455,6 +1521,12 @@ // .await // .unwrap(); +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; + // workspace_a // .update(cx_a, |workspace, cx| { // workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) @@ -1476,11 +1548,12 @@ // let workspace_b_project_a = cx_b // .windows() // .iter() -// .max_by_key(|window| window.id()) +// .max_by_key(|window| window.item_id()) // .unwrap() // .downcast::() // .unwrap() -// .root(cx_b); +// .root(cx_b) +// .unwrap(); // // assert that b is following a in project a in w.rs // workspace_b_project_a.update(cx_b, |workspace, cx| { @@ -1534,7 +1607,7 @@ // workspace.leader_for_pane(workspace.active_pane()) // ); // let item = workspace.active_pane().read(cx).active_item().unwrap(); -// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); +// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into()); // }); // // b moves to y.rs in b's project, a is still following but can't yet see @@ -1578,11 +1651,12 @@ // let workspace_a_project_b = cx_a // .windows() // .iter() -// .max_by_key(|window| window.id()) +// .max_by_key(|window| window.item_id()) // .unwrap() // .downcast::() // .unwrap() -// .root(cx_a); +// .root(cx_a) +// .unwrap(); // workspace_a_project_b.update(cx_a, |workspace, cx| { // assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); @@ -1596,12 +1670,151 @@ // }); // } +// #[gpui::test] +// async fn test_following_into_excluded_file( +// executor: BackgroundExecutor, +// mut cx_a: &mut TestAppContext, +// mut cx_b: &mut TestAppContext, +// ) { +// let mut server = TestServer::start(executor.clone()).await; +// let client_a = server.create_client(cx_a, "user_a").await; +// let client_b = server.create_client(cx_b, "user_b").await; +// for cx in [&mut cx_a, &mut cx_b] { +// cx.update(|cx| { +// cx.update_global::(|store, cx| { +// store.update_user_settings::(cx, |project_settings| { +// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]); +// }); +// }); +// }); +// } +// server +// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) +// .await; +// let active_call_a = cx_a.read(ActiveCall::global); +// let active_call_b = cx_b.read(ActiveCall::global); + +// cx_a.update(editor::init); +// cx_b.update(editor::init); + +// client_a +// .fs() +// .insert_tree( +// "/a", +// json!({ +// ".git": { +// "COMMIT_EDITMSG": "write your commit message here", +// }, +// "1.txt": "one\none\none", +// "2.txt": "two\ntwo\ntwo", +// "3.txt": "three\nthree\nthree", +// }), +// ) +// .await; +// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; +// active_call_a +// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) +// .await +// .unwrap(); + +// let project_id = active_call_a +// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) +// .await +// .unwrap(); +// let project_b = client_b.build_remote_project(project_id, cx_b).await; +// active_call_b +// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) +// .await +// .unwrap(); + +// let window_a = client_a.build_workspace(&project_a, cx_a); +// let workspace_a = window_a.root(cx_a).unwrap(); +// let peer_id_a = client_a.peer_id().unwrap(); +// let window_b = client_b.build_workspace(&project_b, cx_b); +// let workspace_b = window_b.root(cx_b).unwrap(); + +// todo!("could be wrong") +// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a); +// let cx_a = &mut cx_a; +// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b); +// let cx_b = &mut cx_b; + +// // Client A opens editors for a regular file and an excluded file. +// let editor_for_regular = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, "1.txt"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let editor_for_excluded_a = workspace_a +// .update(cx_a, |workspace, cx| { +// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx) +// }) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// // Client A updates their selections in those editors +// editor_for_regular.update(cx_a, |editor, cx| { +// editor.handle_input("a", cx); +// editor.handle_input("b", cx); +// editor.handle_input("c", cx); +// editor.select_left(&Default::default(), cx); +// assert_eq!(editor.selections.ranges(cx), vec![3..2]); +// }); +// editor_for_excluded_a.update(cx_a, |editor, cx| { +// editor.select_all(&Default::default(), cx); +// editor.handle_input("new commit message", cx); +// editor.select_left(&Default::default(), cx); +// assert_eq!(editor.selections.ranges(cx), vec![18..17]); +// }); + +// // When client B starts following client A, currently visible file is replicated +// workspace_b +// .update(cx_b, |workspace, cx| { +// workspace.follow(peer_id_a, cx).unwrap() +// }) +// .await +// .unwrap(); + +// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// assert_eq!( +// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)), +// Some((worktree_id, ".git/COMMIT_EDITMSG").into()) +// ); +// assert_eq!( +// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), +// vec![18..17] +// ); + +// // Changes from B to the excluded file are replicated in A's editor +// editor_for_excluded_b.update(cx_b, |editor, cx| { +// editor.handle_input("\nCo-Authored-By: B ", cx); +// }); +// executor.run_until_parked(); +// editor_for_excluded_a.update(cx_a, |editor, cx| { +// assert_eq!( +// editor.text(cx), +// "new commit messag\nCo-Authored-By: B " +// ); +// }); +// } + // fn visible_push_notifications( // cx: &mut TestAppContext, -// ) -> Vec> { +// ) -> Vec> { // let mut ret = Vec::new(); // for window in cx.windows() { -// window.read_with(cx, |window| { +// window.update(cx, |window| { // if let Some(handle) = window // .root_view() // .clone() @@ -1645,8 +1858,8 @@ // }) // } -// fn pane_summaries(workspace: &ViewHandle, cx: &mut TestAppContext) -> Vec { -// workspace.read_with(cx, |workspace, cx| { +// fn pane_summaries(workspace: &View, cx: &mut WindowContext<'_>) -> Vec { +// workspace.update(cx, |workspace, cx| { // let active_pane = workspace.active_pane(); // workspace // .panes() diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index 2268a51f2ba5f1671a02707101ecad8f65501d1c..823c8e9045eb02fe67fa605bc1cf2d21fb88a670 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( // Simultaneously join channel 1 and then channel 2 active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)) + .update(cx_a, |call, cx| call.join_channel(channel_1, cx)) .detach(); - let join_channel_2 = - active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx)); + let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx)); join_channel_2.await.unwrap(); @@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( call.invite(client_c.user_id().unwrap(), None, cx) }); - let join_channel = - active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)); + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); b_invite.await.unwrap(); c_invite.await.unwrap(); @@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously( .unwrap(); // Simultaneously join channel 1 and call user B and user C from client A. - let join_channel = - active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx)); + let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx)); let b_invite = active_call_a.update(cx_a, |call, cx| { call.invite(client_b.user_id().unwrap(), None, cx) @@ -2784,11 +2781,10 @@ async fn test_fs_operations( let entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "c.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "c.txt"), false, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2815,8 +2811,8 @@ async fn test_fs_operations( .update(cx_b, |project, cx| { project.rename_entry(entry.id, Path::new("d.txt"), cx) }) - .unwrap() .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2841,11 +2837,10 @@ async fn test_fs_operations( let dir_entry = project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR"), true, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2870,27 +2865,24 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/e.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/e.txt"), false, cx) }) .await + .unwrap() .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR"), true, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx) }) .await + .unwrap() .unwrap(); project_b .update(cx_b, |project, cx| { - project - .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) - .unwrap() + project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { @@ -2931,11 +2923,10 @@ async fn test_fs_operations( project_b .update(cx_b, |project, cx| { - project - .copy_entry(entry.id, Path::new("f.txt"), cx) - .unwrap() + project.copy_entry(entry.id, Path::new("f.txt"), cx) }) .await + .unwrap() .unwrap(); worktree_a.read_with(cx_a, |worktree, _| { diff --git a/crates/collab2/src/tests/random_project_collaboration_tests.rs b/crates/collab2/src/tests/random_project_collaboration_tests.rs index 47b936a6117df1873702cb1937614548aa03d796..f4194b98e8adbf41742a5aa279d766cf09c2477d 100644 --- a/crates/collab2/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab2/src/tests/random_project_collaboration_tests.rs @@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest { ensure_project_shared(&project, client, cx).await; project .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx)) - .unwrap() .await?; } diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 5f95f00d6fcd5c74d81d90f9b4b455ab531862d5..6bb57e11ab1d582031930f34b8bfe67b96a2581e 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -221,7 +221,6 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - call_factory: |_| Box::new(workspace::TestCallHandler), }); cx.update(|cx| { diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 9b2af2cfb1b77be0e200607c760e9cd11d6f7634..4ce04b131be561054f5d6cc5db83dc4c1710feb0 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -18,7 +18,7 @@ mod contact_finder; // }; use contact_finder::ContactFinder; use menu::{Cancel, Confirm, SelectNext, SelectPrev}; -use rpc::proto; +use rpc::proto::{self, PeerId}; use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; @@ -169,11 +169,12 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext, - AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, - ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString, - Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action, + AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, + FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model, + MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce, + ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -347,21 +348,21 @@ enum Section { #[derive(Clone, Debug)] enum ListEntry { Header(Section), - // CallParticipant { - // user: Arc, - // peer_id: Option, - // is_pending: bool, - // }, - // ParticipantProject { - // project_id: u64, - // worktree_root_names: Vec, - // host_user_id: u64, - // is_last: bool, - // }, - // ParticipantScreen { - // peer_id: Option, - // is_last: bool, - // }, + CallParticipant { + user: Arc, + peer_id: Option, + is_pending: bool, + }, + ParticipantProject { + project_id: u64, + worktree_root_names: Vec, + host_user_id: u64, + is_last: bool, + }, + ParticipantScreen { + peer_id: Option, + is_last: bool, + }, IncomingRequest(Arc), OutgoingRequest(Arc), // ChannelInvite(Arc), @@ -370,12 +371,12 @@ enum ListEntry { depth: usize, has_children: bool, }, - // ChannelNotes { - // channel_id: ChannelId, - // }, - // ChannelChat { - // channel_id: ChannelId, - // }, + ChannelNotes { + channel_id: ChannelId, + }, + ChannelChat { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -708,136 +709,136 @@ impl CollabPanel { let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); let old_entries = mem::take(&mut self.entries); - let scroll_to_top = false; - - // if let Some(room) = ActiveCall::global(cx).read(cx).room() { - // self.entries.push(ListEntry::Header(Section::ActiveCall)); - // if !old_entries - // .iter() - // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) - // { - // scroll_to_top = true; - // } + let mut scroll_to_top = false; - // if !self.collapsed_sections.contains(&Section::ActiveCall) { - // let room = room.read(cx); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + self.entries.push(ListEntry::Header(Section::ActiveCall)); + if !old_entries + .iter() + .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + { + scroll_to_top = true; + } - // if let Some(channel_id) = room.channel_id() { - // self.entries.push(ListEntry::ChannelNotes { channel_id }); - // self.entries.push(ListEntry::ChannelChat { channel_id }) - // } + if !self.collapsed_sections.contains(&Section::ActiveCall) { + let room = room.read(cx); - // // Populate the active user. - // if let Some(user) = user_store.current_user() { - // self.match_candidates.clear(); - // self.match_candidates.push(StringMatchCandidate { - // id: 0, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if !matches.is_empty() { - // let user_id = user.id; - // self.entries.push(ListEntry::CallParticipant { - // user, - // peer_id: None, - // is_pending: false, - // }); - // let mut projects = room.local_participant().projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: user_id, - // is_last: projects.peek().is_none() && !room.is_screen_sharing(), - // }); - // } - // if room.is_screen_sharing() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: None, - // is_last: true, - // }); - // } - // } - // } + if let Some(channel_id) = room.channel_id() { + self.entries.push(ListEntry::ChannelNotes { channel_id }); + self.entries.push(ListEntry::ChannelChat { channel_id }) + } - // // Populate remote participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.remote_participants().iter().map(|(_, participant)| { - // StringMatchCandidate { - // id: participant.user.id as usize, - // string: participant.user.github_login.clone(), - // char_bag: participant.user.github_login.chars().collect(), - // } - // })); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // for mat in matches { - // let user_id = mat.candidate_id as u64; - // let participant = &room.remote_participants()[&user_id]; - // self.entries.push(ListEntry::CallParticipant { - // user: participant.user.clone(), - // peer_id: Some(participant.peer_id), - // is_pending: false, - // }); - // let mut projects = participant.projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: participant.user.id, - // is_last: projects.peek().is_none() - // && participant.video_tracks.is_empty(), - // }); - // } - // if !participant.video_tracks.is_empty() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: Some(participant.peer_id), - // is_last: true, - // }); - // } - // } + // Populate the active user. + if let Some(user) = user_store.current_user() { + self.match_candidates.clear(); + self.match_candidates.push(StringMatchCandidate { + id: 0, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if !matches.is_empty() { + let user_id = user.id; + self.entries.push(ListEntry::CallParticipant { + user, + peer_id: None, + is_pending: false, + }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + self.entries.push(ListEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_last: projects.peek().is_none() && !room.is_screen_sharing(), + }); + } + if room.is_screen_sharing() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: None, + is_last: true, + }); + } + } + } - // // Populate pending participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.pending_participants().iter().enumerate().map( - // |(id, participant)| StringMatchCandidate { - // id, - // string: participant.github_login.clone(), - // char_bag: participant.github_login.chars().collect(), - // }, - // )); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // self.entries - // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { - // user: room.pending_participants()[mat.candidate_id].clone(), - // peer_id: None, - // is_pending: true, - // })); - // } - // } + // Populate remote participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.remote_participants().iter().map(|(_, participant)| { + StringMatchCandidate { + id: participant.user.id as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + } + })); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + for mat in matches { + let user_id = mat.candidate_id as u64; + let participant = &room.remote_participants()[&user_id]; + self.entries.push(ListEntry::CallParticipant { + user: participant.user.clone(), + peer_id: Some(participant.peer_id), + is_pending: false, + }); + let mut projects = participant.projects.iter().peekable(); + while let Some(project) = projects.next() { + self.entries.push(ListEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: participant.user.id, + is_last: projects.peek().is_none() + && participant.video_tracks.is_empty(), + }); + } + if !participant.video_tracks.is_empty() { + self.entries.push(ListEntry::ParticipantScreen { + peer_id: Some(participant.peer_id), + is_last: true, + }); + } + } + + // Populate pending participants. + self.match_candidates.clear(); + self.match_candidates + .extend(room.pending_participants().iter().enumerate().map( + |(id, participant)| StringMatchCandidate { + id, + string: participant.github_login.clone(), + char_bag: participant.github_login.chars().collect(), + }, + )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + self.entries + .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + peer_id: None, + is_pending: true, + })); + } + } let mut request_entries = Vec::new(); @@ -1135,290 +1136,179 @@ impl CollabPanel { cx.notify(); } - // fn render_call_participant( - // user: &User, - // peer_id: Option, - // user_store: ModelHandle, - // is_pending: bool, - // is_selected: bool, - // theme: &theme::Theme, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum CallParticipant {} - // enum CallParticipantTooltip {} - // enum LeaveCallButton {} - // enum LeaveCallTooltip {} - - // let collab_theme = &theme.collab_panel; - - // let is_current_user = - // user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); - - // let content = MouseEventHandler::new::( - // user.id as usize, - // cx, - // |mouse_state, cx| { - // let style = if is_current_user { - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(&mut Default::default()) - // } else { - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(mouse_state) - // }; - - // Flex::row() - // .with_children(user.avatar.clone().map(|avatar| { - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left() - // })) - // .with_child( - // Label::new( - // user.github_login.clone(), - // collab_theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(if is_pending { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned() - // .into_any(), - // ) - // } else if is_current_user { - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/exit.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // Self::leave_call(cx); - // }) - // .with_tooltip::( - // 0, - // "Leave call", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(style) - // }, - // ); - - // if is_current_user || is_pending || peer_id.is_none() { - // return content.into_any(); - // } - - // let tooltip = format!("Follow {}", user.github_login); - - // content - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // workspace - // .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx)) - // .map(|task| task.detach_and_log_err(cx)); - // } - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // user.id as usize, - // tooltip, - // Some(Box::new(FollowNextCollaborator)), - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } + fn render_call_participant( + &self, + user: Arc, + peer_id: Option, + is_pending: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let is_current_user = + self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + let tooltip = format!("Follow {}", user.github_login); - // fn render_participant_project( - // project_id: u64, - // worktree_root_names: &[String], - // host_user_id: u64, - // is_current: bool, - // is_last: bool, - // is_selected: bool, - // theme: &theme::Theme, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum JoinProject {} - // enum JoinProjectTooltip {} - - // let collab_theme = &theme.collab_panel; - // let host_avatar_width = collab_theme - // .contact_avatar - // .width - // .or(collab_theme.contact_avatar.height) - // .unwrap_or(0.); - // let tree_branch = collab_theme.tree_branch; - // let project_name = if worktree_root_names.is_empty() { - // "untitled".to_string() - // } else { - // worktree_root_names.join(", ") - // }; - - // let content = - // MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { - // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - // let row = if is_current { - // collab_theme - // .project_row - // .in_state(true) - // .style_for(&mut Default::default()) - // } else { - // collab_theme - // .project_row - // .in_state(is_selected) - // .style_for(mouse_state) - // }; - - // Flex::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // is_last, - // vec2f(host_avatar_width, collab_theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/file_icons/folder.svg") - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new(project_name.clone(), row.name.text.clone()) - // .aligned() - // .left() - // .contained() - // .with_style(row.name.container) - // .flex(1., false), - // ) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(row.container) - // }); - - // if is_current { - // return content.into_any(); - // } - - // content - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // let app_state = workspace.read(cx).app_state().clone(); - // workspace::join_remote_project(project_id, host_user_id, app_state, cx) - // .detach_and_log_err(cx); - // } - // }) - // .with_tooltip::( - // project_id as usize, - // format!("Open {}", project_name), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } + ListItem::new(SharedString::from(user.github_login.clone())) + .left_child(Avatar::data(user.avatar.clone().unwrap())) + .child( + h_stack() + .w_full() + .justify_between() + .child(Label::new(user.github_login.clone())) + .child(if is_pending { + Label::new("Calling").color(Color::Muted).into_any_element() + } else if is_current_user { + IconButton::new("leave-call", Icon::ArrowRight) + .on_click(cx.listener(move |this, _, cx| { + Self::leave_call(cx); + })) + .tooltip(|cx| Tooltip::text("Leave Call", cx)) + .into_any_element() + } else { + div().into_any_element() + }), + ) + .when_some(peer_id, |this, peer_id| { + this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + .on_click(cx.listener(move |this, _, cx| { + this.workspace + .update(cx, |workspace, cx| workspace.follow(peer_id, cx)); + })) + }) + } - // fn render_participant_screen( - // peer_id: Option, - // is_last: bool, - // is_selected: bool, - // theme: &theme::CollabPanel, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum OpenSharedScreen {} - - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - // let tree_branch = theme.tree_branch; - - // let handler = MouseEventHandler::new::( - // peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, - // cx, - // |mouse_state, cx| { - // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - // let row = theme - // .project_row - // .in_state(is_selected) - // .style_for(mouse_state); - - // Flex::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // is_last, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/desktop.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("Screen", row.name.text.clone()) - // .aligned() - // .left() - // .contained() - // .with_style(row.name.container) - // .flex(1., false), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(row.container) - // }, - // ); - // if peer_id.is_none() { - // return handler.into_any(); - // } - // handler - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.open_shared_screen(peer_id.unwrap(), cx) - // }); - // } - // }) - // .into_any() - // } + fn render_participant_project( + &self, + project_id: u64, + worktree_root_names: &[String], + host_user_id: u64, + // is_current: bool, + is_last: bool, + // is_selected: bool, + // theme: &theme::Theme, + cx: &mut ViewContext, + ) -> impl IntoElement { + let project_name: SharedString = if worktree_root_names.is_empty() { + "untitled".to_string() + } else { + worktree_root_names.join(", ") + } + .into(); + + let theme = cx.theme(); + + ListItem::new(project_id as usize) + .on_click(cx.listener(move |this, _, cx| { + this.workspace.update(cx, |workspace, cx| { + let app_state = workspace.app_state().clone(); + workspace::join_remote_project(project_id, host_user_id, app_state, cx) + .detach_and_log_err(cx); + }); + })) + .left_child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Folder)) + .child(Label::new(project_name.clone())) + .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) + + // enum JoinProject {} + // enum JoinProjectTooltip {} + + // let collab_theme = &theme.collab_panel; + // let host_avatar_width = collab_theme + // .contact_avatar + // .width + // .or(collab_theme.contact_avatar.height) + // .unwrap_or(0.); + // let tree_branch = collab_theme.tree_branch; + + // let content = + // MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { + // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + // let row = if is_current { + // collab_theme + // .project_row + // .in_state(true) + // .style_for(&mut Default::default()) + // } else { + // collab_theme + // .project_row + // .in_state(is_selected) + // .style_for(mouse_state) + // }; + + // Flex::row() + // .with_child(render_tree_branch( + // tree_branch, + // &row.name.text, + // is_last, + // vec2f(host_avatar_width, collab_theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/file_icons/folder.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(project_name.clone(), row.name.text.clone()) + // .aligned() + // .left() + // .contained() + // .with_style(row.name.container) + // .flex(1., false), + // ) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(row.container) + // }); + + // if is_current { + // return content.into_any(); + // } + + // content + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let app_state = workspace.read(cx).app_state().clone(); + // workspace::join_remote_project(project_id, host_user_id, app_state, cx) + // .detach_and_log_err(cx); + // } + // }) + // .with_tooltip::( + // project_id as usize, + // format!("Open {}", project_name), + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + } + + fn render_participant_screen( + &self, + peer_id: Option, + is_last: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); + + ListItem::new(("screen", id)) + .left_child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Screen)) + .child(Label::new("Screen")) + .when_some(peer_id, |this, _| { + this.on_click(cx.listener(move |this, _, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(peer_id.unwrap(), cx) + }); + })) + .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx)) + }) + } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { if let Some(_) = self.channel_editing_state.take() { @@ -1465,117 +1355,35 @@ impl CollabPanel { // .into_any() // } - // fn render_channel_notes( - // &self, - // channel_id: ChannelId, - // theme: &theme::CollabPanel, - // is_selected: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ChannelNotes {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // false, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/file.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("notes", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(*theme.channel_row.style_for(is_selected, state)) - // .with_padding_left(theme.channel_row.default_style().padding.left) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - // } + fn render_channel_notes( + &self, + channel_id: ChannelId, + cx: &mut ViewContext, + ) -> impl IntoElement { + ListItem::new("channel-notes") + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx); + })) + .left_child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::File)) + .child(Label::new("notes")) + .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) + } - // fn render_channel_chat( - // &self, - // channel_id: ChannelId, - // theme: &theme::CollabPanel, - // is_selected: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ChannelChat {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // true, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/conversations.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("chat", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(*theme.channel_row.style_for(is_selected, state)) - // .with_padding_left(theme.channel_row.default_style().padding.left) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - // } + fn render_channel_chat( + &self, + channel_id: ChannelId, + cx: &mut ViewContext, + ) -> impl IntoElement { + ListItem::new("channel-chat") + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx); + })) + .left_child(render_tree_branch(true, cx)) + .child(IconButton::new(0, Icon::MessageBubbles)) + .child(Label::new("chat")) + .tooltip(move |cx| Tooltip::text("Open Chat", cx)) + } // fn render_channel_invite( // channel: Arc, @@ -2273,20 +2081,19 @@ impl CollabPanel { } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; let Some(handle) = cx.window_handle().downcast::() else { return; }; - let active_call = ActiveCall::global(cx); - cx.spawn(|_, mut cx| async move { - active_call - .update(&mut cx, |active_call, cx| { - active_call.join_channel(channel_id, Some(handle), cx) - }) - .log_err()? - .await - .notify_async_err(&mut cx) - }) - .detach() + workspace::join_channel( + channel_id, + workspace.read(cx).app_state().clone(), + Some(handle), + cx, + ) + .detach_and_log_err(cx) } fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { @@ -2390,6 +2197,36 @@ impl CollabPanel { ListEntry::ChannelEditor { depth } => { self.render_channel_editor(depth, cx).into_any_element() } + ListEntry::CallParticipant { + user, + peer_id, + is_pending, + } => self + .render_call_participant(user, peer_id, is_pending, cx) + .into_any_element(), + ListEntry::ParticipantProject { + project_id, + worktree_root_names, + host_user_id, + is_last, + } => self + .render_participant_project( + project_id, + &worktree_root_names, + host_user_id, + is_last, + cx, + ) + .into_any_element(), + ListEntry::ParticipantScreen { peer_id, is_last } => self + .render_participant_screen(peer_id, is_last, cx) + .into_any_element(), + ListEntry::ChannelNotes { channel_id } => { + self.render_channel_notes(channel_id, cx).into_any_element() + } + ListEntry::ChannelChat { channel_id } => { + self.render_channel_chat(channel_id, cx).into_any_element() + } } }), ), @@ -2403,37 +2240,36 @@ impl CollabPanel { is_collapsed: bool, cx: &ViewContext, ) -> impl IntoElement { - // let mut channel_link = None; - // let mut channel_tooltip_text = None; - // let mut channel_icon = None; + let mut channel_link = None; + let mut channel_tooltip_text = None; + let mut channel_icon = None; // let mut is_dragged_over = false; let text = match section { Section::ActiveCall => { - // let channel_name = maybe!({ - // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - - // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - - // channel_link = Some(channel.link()); - // (channel_icon, channel_tooltip_text) = match channel.visibility { - // proto::ChannelVisibility::Public => { - // (Some("icons/public.svg"), Some("Copy public channel link.")) - // } - // proto::ChannelVisibility::Members => { - // (Some("icons/hash.svg"), Some("Copy private channel link.")) - // } - // }; - - // Some(channel.name.as_str()) - // }); - - // if let Some(name) = channel_name { - // SharedString::from(format!("{}", name)) - // } else { - // SharedString::from("Current Call") - // } - todo!() + let channel_name = maybe!({ + let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + + let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; + + channel_link = Some(channel.link()); + (channel_icon, channel_tooltip_text) = match channel.visibility { + proto::ChannelVisibility::Public => { + (Some("icons/public.svg"), Some("Copy public channel link.")) + } + proto::ChannelVisibility::Members => { + (Some("icons/hash.svg"), Some("Copy private channel link.")) + } + }; + + Some(channel.name.as_str()) + }); + + if let Some(name) = channel_name { + SharedString::from(format!("{}", name)) + } else { + SharedString::from("Current Call") + } } Section::ContactRequests => SharedString::from("Requests"), Section::Contacts => SharedString::from("Contacts"), @@ -2444,34 +2280,15 @@ impl CollabPanel { }; let button = match section { - Section::ActiveCall => - // channel_link.map(|channel_link| { - // let channel_link_copy = channel_link.clone(); - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/link.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // let item = ClipboardItem::new(channel_link_copy.clone()); - // cx.write_to_clipboard(item) - // }) - // .with_tooltip::( - // 0, - // channel_tooltip_text.unwrap(), - // None, - // tooltip_style.clone(), - // cx, - // ) - // }), - { - todo!() - } + Section::ActiveCall => channel_link.map(|channel_link| { + let channel_link_copy = channel_link.clone(); + IconButton::new("channel-link", Icon::Check) + .on_click(move |_, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) + }) + .tooltip(|cx| Tooltip::text("Copy channel link", cx)) + }), Section::Contacts => Some( IconButton::new("add-contact", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) @@ -2544,15 +2361,7 @@ impl CollabPanel { let user_id = contact.user.id; let github_login = SharedString::from(contact.user.github_login.clone()); let mut item = ListItem::new(github_login.clone()) - .on_click(cx.listener(move |this, _, cx| { - this.workspace - .update(cx, |this, cx| { - this.call_state() - .invite(user_id, None, cx) - .detach_and_log_err(cx) - }) - .log_err(); - })) + .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) .child( h_stack() .w_full() @@ -3175,50 +2984,43 @@ impl CollabPanel { } } -// fn render_tree_branch( -// branch_style: theme::TreeBranch, -// row_style: &TextStyle, -// is_last: bool, -// size: Vector2F, -// font_cache: &FontCache, -// ) -> gpui::elements::ConstrainedBox { -// let line_height = row_style.line_height(font_cache); -// let cap_height = row_style.cap_height(font_cache); -// let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.; - -// Canvas::new(move |bounds, _, _, cx| { -// cx.paint_layer(None, |cx| { -// let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.); -// let end_x = bounds.max_x(); -// let start_y = bounds.min_y(); -// let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - -// cx.scene().push_quad(gpui::Quad { -// bounds: RectF::from_points( -// vec2f(start_x, start_y), -// vec2f( -// start_x + branch_style.width, -// if is_last { end_y } else { bounds.max_y() }, -// ), -// ), -// background: Some(branch_style.color), -// border: gpui::Border::default(), -// corner_radii: (0.).into(), -// }); -// cx.scene().push_quad(gpui::Quad { -// bounds: RectF::from_points( -// vec2f(start_x, end_y), -// vec2f(end_x, end_y + branch_style.width), -// ), -// background: Some(branch_style.color), -// border: gpui::Border::default(), -// corner_radii: (0.).into(), -// }); -// }) -// }) -// .constrained() -// .with_width(size.x()) -// } +fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement { + let rem_size = cx.rem_size(); + let line_height = cx.text_style().line_height_in_pixels(rem_size); + let width = rem_size * 1.5; + let thickness = px(2.); + let color = cx.theme().colors().text; + + canvas(move |bounds, cx| { + let start_x = (bounds.left() + bounds.right() - thickness) / 2.; + let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; + let right = bounds.right(); + let top = bounds.top(); + + cx.paint_quad( + Bounds::from_corners( + point(start_x, top), + point( + start_x + thickness, + if is_last { start_y } else { bounds.bottom() }, + ), + ), + Default::default(), + color, + Default::default(), + Hsla::transparent_black(), + ); + cx.paint_quad( + Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), + Default::default(), + color, + Default::default(), + Hsla::transparent_black(), + ); + }) + .w(width) + .h(line_height) +} impl Render for CollabPanel { type Element = Focusable
; @@ -3425,33 +3227,33 @@ impl PartialEq for ListEntry { return section_1 == section_2; } } - // ListEntry::CallParticipant { user: user_1, .. } => { - // if let ListEntry::CallParticipant { user: user_2, .. } = other { - // return user_1.id == user_2.id; - // } - // } - // ListEntry::ParticipantProject { - // project_id: project_id_1, - // .. - // } => { - // if let ListEntry::ParticipantProject { - // project_id: project_id_2, - // .. - // } = other - // { - // return project_id_1 == project_id_2; - // } - // } - // ListEntry::ParticipantScreen { - // peer_id: peer_id_1, .. - // } => { - // if let ListEntry::ParticipantScreen { - // peer_id: peer_id_2, .. - // } = other - // { - // return peer_id_1 == peer_id_2; - // } - // } + ListEntry::CallParticipant { user: user_1, .. } => { + if let ListEntry::CallParticipant { user: user_2, .. } = other { + return user_1.id == user_2.id; + } + } + ListEntry::ParticipantProject { + project_id: project_id_1, + .. + } => { + if let ListEntry::ParticipantProject { + project_id: project_id_2, + .. + } = other + { + return project_id_1 == project_id_2; + } + } + ListEntry::ParticipantScreen { + peer_id: peer_id_1, .. + } => { + if let ListEntry::ParticipantScreen { + peer_id: peer_id_2, .. + } = other + { + return peer_id_1 == peer_id_2; + } + } ListEntry::Channel { channel: channel_1, .. } => { @@ -3462,22 +3264,22 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id; } } - // ListEntry::ChannelNotes { channel_id } => { - // if let ListEntry::ChannelNotes { - // channel_id: other_id, - // } = other - // { - // return channel_id == other_id; - // } - // } - // ListEntry::ChannelChat { channel_id } => { - // if let ListEntry::ChannelChat { - // channel_id: other_id, - // } = other - // { - // return channel_id == other_id; - // } - // } + ListEntry::ChannelNotes { channel_id } => { + if let ListEntry::ChannelNotes { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } + ListEntry::ChannelChat { channel_id } => { + if let ListEntry::ChannelChat { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } // ListEntry::ChannelInvite(channel_1) => { // if let ListEntry::ChannelInvite(channel_2) = other { // return channel_1.id == channel_2.id; diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2cdf32ca366b0fdeb8ed8fd44a84861df23daa84..7e5354c6015bf9764380d7637377fa2b91482da1 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,9 +31,9 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton, - ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, - ViewContext, VisualContext, WeakView, WindowBounds, + actions, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, + Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; use project::{Project, RepositoryEntry}; use theme::ActiveTheme; @@ -49,6 +49,14 @@ use crate::face_pile::FacePile; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; +actions!( + ShareProject, + UnshareProject, + ToggleUserMenu, + ToggleProjectMenu, + SwitchBranch +); + // actions!( // collab, // [ @@ -91,37 +99,23 @@ impl Render for CollabTitlebarItem { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let is_in_room = self - .workspace - .update(cx, |this, cx| this.call_state().is_in_room(cx)) - .unwrap_or_default(); + let room = ActiveCall::global(cx).read(cx).room(); + let is_in_room = room.is_some(); let is_shared = is_in_room && self.project.read(cx).is_shared(); let current_user = self.user_store.read(cx).current_user(); let client = self.client.clone(); - let users = self - .workspace - .update(cx, |this, cx| this.call_state().remote_participants(cx)) - .log_err() - .flatten(); - let is_muted = self - .workspace - .update(cx, |this, cx| this.call_state().is_muted(cx)) - .log_err() - .flatten() - .unwrap_or_default(); - let is_deafened = self - .workspace - .update(cx, |this, cx| this.call_state().is_deafened(cx)) - .log_err() - .flatten() - .unwrap_or_default(); - let speakers_icon = if self - .workspace - .update(cx, |this, cx| this.call_state().is_deafened(cx)) - .log_err() - .flatten() - .unwrap_or_default() - { + let remote_participants = room.map(|room| { + room.read(cx) + .remote_participants() + .values() + .map(|participant| (participant.user.clone(), participant.peer_id)) + .collect::>() + }); + let is_muted = room.map_or(false, |room| room.read(cx).is_muted(cx)); + let is_deafened = room + .and_then(|room| room.read(cx).is_deafened()) + .unwrap_or(false); + let speakers_icon = if is_deafened { ui::Icon::AudioOff } else { ui::Icon::AudioOn @@ -157,7 +151,7 @@ impl Render for CollabTitlebarItem { .children(self.render_project_branch(cx)), ) .when_some( - users.zip(current_user.clone()), + remote_participants.zip(current_user.clone()), |this, (remote_participants, current_user)| { let mut pile = FacePile::default(); pile.extend( @@ -168,25 +162,30 @@ impl Render for CollabTitlebarItem { div().child(Avatar::data(avatar.clone())).into_any_element() }) .into_iter() - .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| { - user.avatar.as_ref().map(|avatar| { - div() - .child( - Avatar::data(avatar.clone()).into_element().into_any(), - ) - .on_mouse_down(MouseButton::Left, { - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.open_shared_screen(peer_id, cx); - }) - .log_err(); - } - }) - .into_any_element() - }) - })), + .chain(remote_participants.into_iter().filter_map( + |(user, peer_id)| { + let avatar = user.avatar.as_ref()?; + Some( + div() + .child( + Avatar::data(avatar.clone()) + .into_element() + .into_any(), + ) + .on_mouse_down(MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.open_shared_screen(peer_id, cx); + }) + .log_err(); + } + }) + .into_any_element(), + ) + }, + )), ); this.child(pile.render(cx)) }, @@ -204,20 +203,24 @@ impl Render for CollabTitlebarItem { "toggle_sharing", if is_shared { "Unshare" } else { "Share" }, ) - .style(ButtonStyle::Subtle), + .style(ButtonStyle::Subtle) + .on_click(cx.listener( + move |this, _, cx| { + if is_shared { + this.unshare_project(&Default::default(), cx); + } else { + this.share_project(&Default::default(), cx); + } + }, + )), ) .child( IconButton::new("leave-call", ui::Icon::Exit) .style(ButtonStyle::Subtle) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().hang_up(cx).detach(); - }) - .log_err(); - } + .on_click(move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); }), ), ) @@ -235,15 +238,8 @@ impl Render for CollabTitlebarItem { ) .style(ButtonStyle::Subtle) .selected(is_muted) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_mute(cx); - }) - .log_err(); - } + .on_click(move |_, cx| { + crate::toggle_mute(&Default::default(), cx) }), ) .child( @@ -258,26 +254,15 @@ impl Render for CollabTitlebarItem { cx, ) }) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_deafen(cx); - }) - .log_err(); - } + .on_click(move |_, cx| { + crate::toggle_mute(&Default::default(), cx) }), ) .child( IconButton::new("screen-share", ui::Icon::Screen) .style(ButtonStyle::Subtle) .on_click(move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_screen_share(cx); - }) - .log_err(); + crate::toggle_screen_sharing(&Default::default(), cx) }), ) .pl_2(), @@ -451,46 +436,19 @@ impl CollabTitlebarItem { // render_project_owner -> resolve if you are in a room -> Option pub fn render_project_owner(&self, cx: &mut ViewContext) -> Option { - // TODO: We can't finish implementing this until project sharing works - // - [ ] Show the project owner when the project is remote (maybe done) - // - [x] Show the project owner when the project is local - // - [ ] Show the project owner with a lock icon when the project is local and unshared - - let remote_id = self.project.read(cx).remote_id(); - let is_local = remote_id.is_none(); - let is_shared = self.project.read(cx).is_shared(); - let (user_name, participant_index) = { - if let Some(host) = self.project.read(cx).host() { - debug_assert!(!is_local); - let (Some(host_user), Some(participant_index)) = ( - self.user_store.read(cx).get_cached_user(host.user_id), - self.user_store - .read(cx) - .participant_indices() - .get(&host.user_id), - ) else { - return None; - }; - (host_user.github_login.clone(), participant_index.0) - } else { - debug_assert!(is_local); - let name = self - .user_store - .read(cx) - .current_user() - .map(|user| user.github_login.clone())?; - (name, 0) - } - }; + let host = self.project.read(cx).host()?; + let host = self.user_store.read(cx).get_cached_user(host.user_id)?; + let participant_index = self + .user_store + .read(cx) + .participant_indices() + .get(&host.id)?; Some( div().border().border_color(gpui::red()).child( - Button::new( - "project_owner_trigger", - format!("{user_name} ({})", !is_shared), - ) - .color(Color::Player(participant_index)) - .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + Button::new("project_owner_trigger", host.github_login.clone()) + .color(Color::Player(participant_index.0)) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), ), ) } @@ -730,21 +688,21 @@ impl CollabTitlebarItem { cx.notify(); } - // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { - // let active_call = ActiveCall::global(cx); - // let project = self.project.clone(); - // active_call - // .update(cx, |call, cx| call.share_project(project, cx)) - // .detach_and_log_err(cx); - // } + fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { + let active_call = ActiveCall::global(cx); + let project = self.project.clone(); + active_call + .update(cx, |call, cx| call.share_project(project, cx)) + .detach_and_log_err(cx); + } - // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext) { - // let active_call = ActiveCall::global(cx); - // let project = self.project.clone(); - // active_call - // .update(cx, |call, cx| call.unshare_project(project, cx)) - // .log_err(); - // } + fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext) { + let active_call = ActiveCall::global(cx); + let project = self.project.clone(); + active_call + .update(cx, |call, cx| call.unshare_project(project, cx)) + .log_err(); + } // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { // self.user_menu.update(cx, |user_menu, cx| { diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index 57a33c6790868bcd97a597da5a68a2608d0a684a..efd3ff869225aced36002a3bdb4f1f5905579c5a 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -9,22 +9,21 @@ mod panel_settings; use std::{rc::Rc, sync::Arc}; +use call::{report_call_event_for_room, ActiveCall, Room}; pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; use gpui::{ - point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind, - WindowOptions, + actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds, + WindowKind, WindowOptions, }; pub use panel_settings::{ ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, }; use settings::Settings; +use util::ResultExt; use workspace::AppState; -// actions!( -// collab, -// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] -// ); +actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall); pub fn init(app_state: &Arc, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); @@ -42,61 +41,61 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { // cx.add_global_action(toggle_deafen); } -// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { -// let call = ActiveCall::global(cx).read(cx); -// if let Some(room) = call.room().cloned() { -// let client = call.client(); -// let toggle_screen_sharing = room.update(cx, |room, cx| { -// if room.is_screen_sharing() { -// report_call_event_for_room( -// "disable screen share", -// room.id(), -// room.channel_id(), -// &client, -// cx, -// ); -// Task::ready(room.unshare_screen(cx)) -// } else { -// report_call_event_for_room( -// "enable screen share", -// room.id(), -// room.channel_id(), -// &client, -// cx, -// ); -// room.share_screen(cx) -// } -// }); -// toggle_screen_sharing.detach_and_log_err(cx); -// } -// } +pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { + let call = ActiveCall::global(cx).read(cx); + if let Some(room) = call.room().cloned() { + let client = call.client(); + let toggle_screen_sharing = room.update(cx, |room, cx| { + if room.is_screen_sharing() { + report_call_event_for_room( + "disable screen share", + room.id(), + room.channel_id(), + &client, + cx, + ); + Task::ready(room.unshare_screen(cx)) + } else { + report_call_event_for_room( + "enable screen share", + room.id(), + room.channel_id(), + &client, + cx, + ); + room.share_screen(cx) + } + }); + toggle_screen_sharing.detach_and_log_err(cx); + } +} -// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { -// let call = ActiveCall::global(cx).read(cx); -// if let Some(room) = call.room().cloned() { -// let client = call.client(); -// room.update(cx, |room, cx| { -// let operation = if room.is_muted(cx) { -// "enable microphone" -// } else { -// "disable microphone" -// }; -// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); +pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { + let call = ActiveCall::global(cx).read(cx); + if let Some(room) = call.room().cloned() { + let client = call.client(); + room.update(cx, |room, cx| { + let operation = if room.is_muted(cx) { + "enable microphone" + } else { + "disable microphone" + }; + report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); -// room.toggle_mute(cx) -// }) -// .map(|task| task.detach_and_log_err(cx)) -// .log_err(); -// } -// } + room.toggle_mute(cx) + }) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); + } +} -// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { -// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { -// room.update(cx, Room::toggle_deafen) -// .map(|task| task.detach_and_log_err(cx)) -// .log_err(); -// } -// } +pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + room.update(cx, Room::toggle_deafen) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); + } +} fn notification_window_options( screen: Rc, diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index f94b5e77caa5cb3676073147555753471db13c98..bdd66fa569cb38b983abf2a4ac8418b80b734ff4 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -314,7 +314,11 @@ impl PickerDelegate for CommandPaletteDelegate { command.name.clone(), r#match.positions.clone(), )) - .children(KeyBinding::for_action(&*command.action, cx)), + .children(KeyBinding::for_action_in( + &*command.action, + &self.previous_focus_handle, + cx, + )), ), ) } diff --git a/crates/copilot_button2/src/copilot_button.rs b/crates/copilot_button2/src/copilot_button.rs index aab59a9cad500a97a93f6c12990ba43cdb25b528..dc6f8085339de18f40bd1c4262bc3b9e0d8c9a67 100644 --- a/crates/copilot_button2/src/copilot_button.rs +++ b/crates/copilot_button2/src/copilot_button.rs @@ -201,9 +201,8 @@ impl CopilotButton { url: COPILOT_SETTINGS_URL.to_string(), } .boxed_clone(), - cx, ) - .action("Sign Out", SignOut.boxed_clone(), cx) + .action("Sign Out", SignOut.boxed_clone()) }); } diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index dd01f90b9f0623b3658673304464229411e3b801..44acc285e8231a48e20c791f38f09f6619e99d08 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -88,7 +88,7 @@ struct DiagnosticGroupState { block_count: usize, } -impl EventEmitter for ProjectDiagnosticsEditor {} +impl EventEmitter for ProjectDiagnosticsEditor {} impl Render for ProjectDiagnosticsEditor { type Element = Focusable
; @@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor { }); let editor_event_subscription = cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { - Self::emit_item_event_for_editor_event(event, cx); + cx.emit(event.clone()); if event == &EditorEvent::Focused && this.path_states.is_empty() { cx.focus(&this.focus_handle); } @@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor { this } - fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext) { - match event { - EditorEvent::Closed => cx.emit(ItemEvent::CloseItem), - - EditorEvent::Saved | EditorEvent::TitleChanged => { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::Reparsed => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::SelectionsChanged { local } if *local => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::DirtyChanged => { - cx.emit(ItemEvent::UpdateTab); - } - - EditorEvent::BufferEdited => { - cx.emit(ItemEvent::Edit); - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { - cx.emit(ItemEvent::Edit); - } - - _ => {} - } - } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, cx); @@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor { this.update(&mut cx, |this, cx| { this.summary = this.project.read(cx).diagnostic_summary(false, cx); - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(EditorEvent::TitleChanged); })?; anyhow::Ok(()) } @@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor { } impl Item for ProjectDiagnosticsEditor { + type Event = EditorEvent; + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + fn deactivated(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| editor.deactivated(cx)); } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 529438648ab0a1a2495f60a261112ef73847d90b..a77e1dcc3b9621080e4e78ef94f80fff2c18112e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1675,8 +1675,7 @@ impl Editor { if let Some(project) = project.as_ref() { if buffer.read(cx).is_singleton() { project_subscriptions.push(cx.observe(project, |_, _, cx| { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(EditorEvent::TitleChanged); })); } project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { @@ -2141,10 +2140,6 @@ impl Editor { cx.emit(SearchEvent::ActiveMatchChanged) } - if local { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - cx.notify(); } @@ -8573,8 +8568,6 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } cx.emit(EditorEvent::BufferEdited); - cx.emit(ItemEvent::Edit); - cx.emit(ItemEvent::UpdateBreadcrumbs); cx.emit(SearchEvent::MatchesInvalidated); if *sigleton_buffer_edited { @@ -8622,20 +8615,14 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() }) } - multi_buffer::Event::Reparsed => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - multi_buffer::Event::DirtyChanged => { - cx.emit(ItemEvent::UpdateTab); - } - multi_buffer::Event::Saved - | multi_buffer::Event::FileHandleChanged - | multi_buffer::Event::Reloaded => { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed), + multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), + multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { + cx.emit(EditorEvent::TitleChanged) } multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged), - multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem), + multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 424da8987eb6d673f0e789d4b8ae8b1620967045..571cbd84bb179be0b1562dd07f5c7a0114e1b8e4 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -32,7 +32,7 @@ use util::{ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{ - item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle}, + item::{FollowEvent, FollowableItem, Item, ItemHandle}, NavigationEntry, ViewId, }; @@ -6478,7 +6478,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { cx.subscribe( &follower.root_view(cx).unwrap(), move |_, _, event: &EditorEvent, cx| { - if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { + if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) { *is_still_following.borrow_mut() = false; } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 5cfb72470ff26789e5231365304e5ebe0c902a70..d7b9d0bb40498cd8fb6c51f4cd33d5e6489f4ad1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1755,7 +1755,7 @@ impl EditorElement { let gutter_width; let gutter_margin; if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size).unwrap(); + let descent = cx.text_system().descent(font_id, font_size); let gutter_padding_factor = 3.5; gutter_padding = (em_width * gutter_padding_factor).round(); @@ -3757,7 +3757,7 @@ fn compute_auto_height_layout( let gutter_width; let gutter_margin; if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size).unwrap(); + let descent = cx.text_system().descent(font_id, font_size); let gutter_padding_factor = 3.5; gutter_padding = (em_width * gutter_padding_factor).round(); gutter_width = max_line_number_width + gutter_padding * 2.0; diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 93bb37c6222932f395d738e4a8bac9ec20d7076c..70d4d6bf255fad8b056a19ba5d5b41dc97a66178 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -32,10 +32,10 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Color, Label}; +use ui::{h_stack, Color, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ - item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, + item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, StatusItemView, }; use workspace::{ @@ -46,27 +46,7 @@ use workspace::{ pub const MAX_TAB_TITLE_LEN: usize = 24; -impl FollowableEvents for EditorEvent { - fn to_follow_event(&self) -> Option { - match self { - EditorEvent::Edited => Some(FollowEvent::Unfollow), - EditorEvent::SelectionsChanged { local } - | EditorEvent::ScrollPositionChanged { local, .. } => { - if *local { - Some(FollowEvent::Unfollow) - } else { - None - } - } - _ => None, - } - } -} - -impl EventEmitter for Editor {} - impl FollowableItem for Editor { - type FollowableEvent = EditorEvent; fn remote_id(&self) -> Option { self.remote_id } @@ -241,9 +221,24 @@ impl FollowableItem for Editor { })) } + fn to_follow_event(event: &EditorEvent) -> Option { + match event { + EditorEvent::Edited => Some(FollowEvent::Unfollow), + EditorEvent::SelectionsChanged { local } + | EditorEvent::ScrollPositionChanged { local, .. } => { + if *local { + Some(FollowEvent::Unfollow) + } else { + None + } + } + _ => None, + } + } + fn add_event_to_update_proto( &self, - event: &Self::FollowableEvent, + event: &EditorEvent, update: &mut Option, cx: &WindowContext, ) -> bool { @@ -528,6 +523,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) } impl Item for Editor { + type Event = EditorEvent; + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Ok(data) = data.downcast::() { let newest_selection = self.selections.newest::(cx); @@ -586,28 +583,25 @@ impl Item for Editor { fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { let theme = cx.theme(); - AnyElement::new( - div() - .flex() - .flex_row() - .items_center() - .gap_2() - .child(Label::new(self.title(cx).to_string())) - .children(detail.and_then(|detail| { - let path = path_for_buffer(&self.buffer, detail, false, cx)?; - let description = path.to_string_lossy(); - - Some( - div().child( - Label::new(util::truncate_and_trailoff( - &description, - MAX_TAB_TITLE_LEN, - )) - .color(Color::Muted), - ), - ) - })), - ) + let description = detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + let description = path.to_string_lossy(); + let description = description.trim(); + + if description.is_empty() { + return None; + } + + Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) + }); + + h_stack() + .gap_2() + .child(Label::new(self.title(cx).to_string())) + .when_some(description, |this, description| { + this.child(Label::new(description).color(Color::Muted)) + }) + .into_any_element() } fn for_each_project_item( @@ -841,6 +835,40 @@ impl Item for Editor { Some("Editor") } + fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { + match event { + EditorEvent::Closed => f(ItemEvent::CloseItem), + + EditorEvent::Saved | EditorEvent::TitleChanged => { + f(ItemEvent::UpdateTab); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::Reparsed => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::SelectionsChanged { local } if *local => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::DirtyChanged => { + f(ItemEvent::UpdateTab); + } + + EditorEvent::BufferEdited => { + f(ItemEvent::Edit); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { + f(ItemEvent::Edit); + } + + _ => {} + } + } + fn deserialize( project: Model, _workspace: WeakView, diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index ba989ab011714384627c47f5dc017aa3511dfa18..e6c0fc1111473541a285eaf6d4d41bf87c9952af 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -37,19 +37,18 @@ pub fn deploy_context_menu( }); let context_menu = ui::ContextMenu::build(cx, |menu, cx| { - menu.action("Rename Symbol", Box::new(Rename), cx) - .action("Go to Definition", Box::new(GoToDefinition), cx) - .action("Go to Type Definition", Box::new(GoToTypeDefinition), cx) - .action("Find All References", Box::new(FindAllReferences), cx) + menu.action("Rename Symbol", Box::new(Rename)) + .action("Go to Definition", Box::new(GoToDefinition)) + .action("Go to Type Definition", Box::new(GoToTypeDefinition)) + .action("Find All References", Box::new(FindAllReferences)) .action( "Code Actions", Box::new(ToggleCodeActions { deployed_from_indicator: false, }), - cx, ) .separator() - .action("Reveal in Finder", Box::new(RevealInFinder), cx) + .action("Reveal in Finder", Box::new(RevealInFinder)) }); let context_menu_focus = context_menu.focus_handle(cx); cx.focus(&context_menu_focus); diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index a34582f4f4024c5eeab0363d45896be7eaa2ee95..99d8542eba89cde981f8cbf966d298ec1d8af2af 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -482,10 +482,6 @@ impl WeakModel { /// Update the entity referenced by this model with the given function if /// the referenced entity still exists. Returns an error if the entity has /// been released. - /// - /// The update function receives a context appropriate for its environment. - /// When updating in an `AppContext`, it receives a `ModelContext`. - /// When updating an a `WindowContext`, it receives a `ViewContext`. pub fn update( &self, cx: &mut C, @@ -501,6 +497,21 @@ impl WeakModel { .map(|this| cx.update_model(&this, update)), ) } + + /// Reads the entity referenced by this model with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result + where + C: Context, + Result>: crate::Flatten, + { + crate::Flatten::flatten( + self.upgrade() + .ok_or_else(|| anyhow!("entity release")) + .map(|this| cx.read_model(&this, read)), + ) + } } impl Hash for WeakModel { diff --git a/crates/gpui2/src/elements/canvas.rs b/crates/gpui2/src/elements/canvas.rs new file mode 100644 index 0000000000000000000000000000000000000000..287a3b4b5a38fdc0c7c90c75763bb9a0921dfb7e --- /dev/null +++ b/crates/gpui2/src/elements/canvas.rs @@ -0,0 +1,52 @@ +use refineable::Refineable as _; + +use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext}; + +pub fn canvas(callback: impl 'static + FnOnce(Bounds, &mut WindowContext)) -> Canvas { + Canvas { + paint_callback: Box::new(callback), + style: StyleRefinement::default(), + } +} + +pub struct Canvas { + paint_callback: Box, &mut WindowContext)>, + style: StyleRefinement, +} + +impl IntoElement for Canvas { + type Element = Self; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Canvas { + type State = (); + + fn layout( + &mut self, + _: Option, + cx: &mut WindowContext, + ) -> (crate::LayoutId, Self::State) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = cx.request_layout(&style, []); + (layout_id, ()) + } + + fn paint(self, bounds: Bounds, _: &mut (), cx: &mut WindowContext) { + (self.paint_callback)(bounds, cx) + } +} + +impl Styled for Canvas { + fn style(&mut self) -> &mut crate::StyleRefinement { + &mut self.style + } +} diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 68dca4c9d144a571d894f63f382c2d9489f7251c..ce457fc6931246ee1e2e4d19a4a1639b37998395 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -221,20 +221,6 @@ pub trait InteractiveElement: Sized + Element { /// Add a listener for the given action, fires during the bubble event phase fn on_action(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { - // NOTE: this debug assert has the side-effect of working around - // a bug where a crate consisting only of action definitions does - // not register the actions in debug builds: - // - // https://github.com/rust-lang/rust/issues/47384 - // https://github.com/mmastrac/rust-ctor/issues/280 - // - // if we are relying on this side-effect still, removing the debug_assert! - // likely breaks the command_palette tests. - // debug_assert!( - // A::is_registered(), - // "{:?} is not registered as an action", - // A::qualified_name() - // ); self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |action, phase, cx| { @@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element { self } + fn on_boxed_action( + mut self, + action: &Box, + listener: impl Fn(&Box, &mut WindowContext) + 'static, + ) -> Self { + let action = action.boxed_clone(); + self.interactivity().action_listeners.push(( + (*action).type_id(), + Box::new(move |_, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(&action, cx) + } + }), + )); + self + } + fn on_key_down( mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, @@ -866,6 +869,7 @@ impl Interactivity { } if self.hover_style.is_some() + || self.base_style.mouse_cursor.is_some() || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() { let bounds = bounds.intersect(&cx.content_mask().bounds); diff --git a/crates/gpui2/src/elements/mod.rs b/crates/gpui2/src/elements/mod.rs index 12c57958eaaf1829664ee73500985d05037f9786..e986b0b3eaef37f65c55344a7a14ef234cb8539e 100644 --- a/crates/gpui2/src/elements/mod.rs +++ b/crates/gpui2/src/elements/mod.rs @@ -1,3 +1,4 @@ +mod canvas; mod div; mod img; mod overlay; @@ -5,6 +6,7 @@ mod svg; mod text; mod uniform_list; +pub use canvas::*; pub use div::*; pub use img::*; pub use overlay::*; diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 5fbf83bfbab8e45320ee856fd7542298abdc9649..4838b1a612ce65ba33c03ac25da878a752f716d3 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -16,7 +16,7 @@ pub struct DispatchNodeId(usize); pub(crate) struct DispatchTree { node_stack: Vec, - context_stack: Vec, + pub(crate) context_stack: Vec, nodes: Vec, focusable_node_ids: HashMap, keystroke_matchers: HashMap, KeystrokeMatcher>, @@ -163,11 +163,25 @@ impl DispatchTree { actions } - pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + pub fn bindings_for_action( + &self, + action: &dyn Action, + context_stack: &Vec, + ) -> Vec { self.keymap .lock() .bindings_for_action(action.type_id()) - .filter(|candidate| candidate.action.partial_eq(action)) + .filter(|candidate| { + if !candidate.action.partial_eq(action) { + return false; + } + for i in 1..context_stack.len() { + if candidate.matches_context(&context_stack[0..=i]) { + return true; + } + } + return false; + }) .cloned() .collect() } diff --git a/crates/gpui2/src/platform/mac/display_linker.rs b/crates/gpui2/src/platform/mac/display_linker.rs index b63cf24e2689d1d249016d86b82a62ffd2d3946b..d8f5a675a58f4204e644f5661dfc5078b9ae4295 100644 --- a/crates/gpui2/src/platform/mac/display_linker.rs +++ b/crates/gpui2/src/platform/mac/display_linker.rs @@ -7,6 +7,7 @@ use std::{ use crate::DisplayId; use collections::HashMap; use parking_lot::Mutex; +pub use sys::CVSMPTETime as SmtpeTime; pub use sys::CVTimeStamp as VideoTimestamp; pub(crate) struct MacDisplayLinker { @@ -153,7 +154,7 @@ mod sys { kCVTimeStampTopField | kCVTimeStampBottomField; #[repr(C)] - #[derive(Clone, Copy)] + #[derive(Clone, Copy, Default)] pub struct CVSMPTETime { pub subframes: i16, pub subframe_divisor: i16, diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index fa4b6e18c587521d88d8d4a4fd4041952c584f4a..2cbc228c72b2c51761982d0eb0e70f5a7450e5b4 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -147,18 +147,25 @@ impl Platform for TestPlatform { fn set_display_link_output_callback( &self, _display_id: DisplayId, - _callback: Box, + mut callback: Box, ) { - unimplemented!() - } - - fn start_display_link(&self, _display_id: DisplayId) { - unimplemented!() - } - - fn stop_display_link(&self, _display_id: DisplayId) { - unimplemented!() - } + let timestamp = crate::VideoTimestamp { + version: 0, + video_time_scale: 0, + video_time: 0, + host_time: 0, + rate_scalar: 0.0, + video_refresh_period: 0, + smpte_time: crate::SmtpeTime::default(), + flags: 0, + reserved: 0, + }; + callback(×tamp, ×tamp) + } + + fn start_display_link(&self, _display_id: DisplayId) {} + + fn stop_display_link(&self, _display_id: DisplayId) {} fn open_url(&self, _url: &str) { unimplemented!() diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index 440789dd472b35c02e1bbf3c2605e7b4c8ae3be3..b3f17bd057e2253fbba8d80439289cdc57f275d3 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -72,7 +72,7 @@ impl TextSystem { } } - pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result> { + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) } @@ -89,9 +89,9 @@ impl TextSystem { let bounds = self .platform_text_system .typographic_bounds(font_id, glyph_id)?; - self.read_metrics(font_id, |metrics| { + Ok(self.read_metrics(font_id, |metrics| { (bounds / metrics.units_per_em as f32 * font_size.0).map(px) - }) + })) } pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result> { @@ -100,28 +100,28 @@ impl TextSystem { .glyph_for_char(font_id, ch) .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?; let result = self.platform_text_system.advance(font_id, glyph_id)? - / self.units_per_em(font_id)? as f32; + / self.units_per_em(font_id) as f32; Ok(result * font_size) } - pub fn units_per_em(&self, font_id: FontId) -> Result { + pub fn units_per_em(&self, font_id: FontId) -> u32 { self.read_metrics(font_id, |metrics| metrics.units_per_em as u32) } - pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.cap_height(font_size)) } - pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.x_height(font_size)) } - pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.ascent(font_size)) } - pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result { + pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { self.read_metrics(font_id, |metrics| metrics.descent(font_size)) } @@ -130,24 +130,24 @@ impl TextSystem { font_id: FontId, font_size: Pixels, line_height: Pixels, - ) -> Result { - let ascent = self.ascent(font_id, font_size)?; - let descent = self.descent(font_id, font_size)?; + ) -> Pixels { + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); let padding_top = (line_height - ascent - descent) / 2.; - Ok(padding_top + ascent) + padding_top + ascent } - fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result { + fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T { let lock = self.font_metrics.upgradable_read(); if let Some(metrics) = lock.get(&font_id) { - Ok(read(metrics)) + read(metrics) } else { let mut lock = RwLockUpgradableReadGuard::upgrade(lock); let metrics = lock .entry(font_id) .or_insert_with(|| self.platform_text_system.font_metrics(font_id)); - Ok(read(metrics)) + read(metrics) } } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 0d15647b88fdfb112b72b150b0500d20ffac8b37..d62bee69c095139114ecf45561287d6ac218da2a 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -101,9 +101,7 @@ fn paint_line( let mut glyph_origin = origin; let mut prev_glyph_position = Point::default(); for (run_ix, run) in layout.runs.iter().enumerate() { - let max_glyph_size = text_system - .bounding_box(run.font_id, layout.font_size)? - .size; + let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { glyph_origin.x += glyph.position.x - prev_glyph_position.x; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 5ca35844d88ff36b6d230f84a51eb9f4ecc5ff65..271b09b8b950d777400f403fa59d5b81ad4e5ed6 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1350,6 +1350,8 @@ impl<'a> WindowContext<'a> { .dispatch_tree .dispatch_path(node_id); + let mut actions: Vec> = Vec::new(); + // Capture phase let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); self.propagate_event = true; @@ -1384,22 +1386,26 @@ impl<'a> WindowContext<'a> { let node = self.window.current_frame.dispatch_tree.node(*node_id); if !node.context.is_empty() { if let Some(key_down_event) = event.downcast_ref::() { - if let Some(action) = self + if let Some(found) = self .window .current_frame .dispatch_tree .dispatch_key(&key_down_event.keystroke, &context_stack) { - self.dispatch_action_on_node(*node_id, action); - if !self.propagate_event { - return; - } + actions.push(found.boxed_clone()) } } context_stack.pop(); } } + + for action in actions { + self.dispatch_action_on_node(node_id, action); + if !self.propagate_event { + return; + } + } } } @@ -1427,7 +1433,6 @@ impl<'a> WindowContext<'a> { } } } - // Bubble phase for node_id in dispatch_path.iter().rev() { let node = self.window.current_frame.dispatch_tree.node(*node_id); @@ -1505,9 +1510,30 @@ impl<'a> WindowContext<'a> { pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { self.window - .current_frame + .previous_frame .dispatch_tree - .bindings_for_action(action) + .bindings_for_action( + action, + &self.window.previous_frame.dispatch_tree.context_stack, + ) + } + + pub fn bindings_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Vec { + let dispatch_tree = &self.window.previous_frame.dispatch_tree; + + let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else { + return vec![]; + }; + let context_stack = dispatch_tree + .dispatch_path(node_id) + .into_iter() + .map(|node_id| dispatch_tree.node(node_id).context.clone()) + .collect(); + dispatch_tree.bindings_for_action(action, &context_stack) } pub fn listener_for( @@ -2742,6 +2768,7 @@ pub enum ElementId { Integer(usize), Name(SharedString), FocusHandle(FocusId), + NamedInteger(SharedString, usize), } impl ElementId { @@ -2791,3 +2818,15 @@ impl<'a> From<&'a FocusHandle> for ElementId { ElementId::FocusHandle(handle.id) } } + +impl From<(&'static str, EntityId)> for ElementId { + fn from((name, id): (&'static str, EntityId)) -> Self { + ElementId::NamedInteger(name.into(), id.as_u64() as usize) + } +} + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} diff --git a/crates/language2/src/outline.rs b/crates/language2/src/outline.rs index 4bcbdcd27fa7e4290560944e411047097da06425..df1a3c629e75e7695fcf9bd1f6c6a796df2c01f1 100644 --- a/crates/language2/src/outline.rs +++ b/crates/language2/src/outline.rs @@ -81,6 +81,7 @@ impl Outline { let mut prev_item_ix = 0; for mut string_match in matches { let outline_match = &self.items[string_match.candidate_id]; + string_match.string = outline_match.text.clone(); if is_path_query { let prefix_len = self.path_candidate_prefixes[string_match.candidate_id]; diff --git a/crates/language_selector2/src/language_selector.rs b/crates/language_selector2/src/language_selector.rs index 49be0c5418a826fbfc9a5623f3feb33770a571c9..e3970401d4c9201d631e0548ca306f6ca63e7a94 100644 --- a/crates/language_selector2/src/language_selector.rs +++ b/crates/language_selector2/src/language_selector.rs @@ -79,6 +79,7 @@ impl FocusableView for LanguageSelector { self.picker.focus_handle(cx) } } + impl EventEmitter for LanguageSelector {} pub struct LanguageSelectorDelegate { diff --git a/crates/outline2/Cargo.toml b/crates/outline2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7606fc46fe002d993b39a02fd151dee412ea14c7 --- /dev/null +++ b/crates/outline2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "outline2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/outline.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +language = { package = "language2", path = "../language2" } +picker = { package = "picker2", path = "../picker2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } + +ordered-float.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/outline2/src/outline.rs b/crates/outline2/src/outline.rs new file mode 100644 index 0000000000000000000000000000000000000000..8442d6480d4d011cbb745fa8de815118538dbf96 --- /dev/null +++ b/crates/outline2/src/outline.rs @@ -0,0 +1,276 @@ +use editor::{ + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, + DisplayPoint, Editor, ToPoint, +}; +use fuzzy::StringMatch; +use gpui::{ + actions, div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + FontWeight, ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, View, + ViewContext, VisualContext, WeakView, WindowContext, +}; +use language::Outline; +use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; +use theme::ActiveTheme; +use ui::{v_stack, ListItem, Selectable}; +use util::ResultExt; +use workspace::Workspace; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(OutlineView::register).detach(); +} + +pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let outline = editor + .read(cx) + .buffer() + .read(cx) + .snapshot(cx) + .outline(Some(&cx.theme().syntax())); + + if let Some(outline) = outline { + workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx)); + } + } +} + +pub struct OutlineView { + picker: View>, +} + +impl FocusableView for OutlineView { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for OutlineView {} + +impl Render for OutlineView { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().min_w_96().child(self.picker.clone()) + } +} + +impl OutlineView { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(toggle); + } + + fn new( + outline: Outline, + editor: View, + cx: &mut ViewContext, + ) -> OutlineView { + let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx); + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + OutlineView { picker } + } +} + +struct OutlineViewDelegate { + outline_view: WeakView, + active_editor: View, + outline: Outline, + selected_match_index: usize, + prev_scroll_position: Option>, + matches: Vec, + last_query: String, +} + +impl OutlineViewDelegate { + fn new( + outline_view: WeakView, + outline: Outline, + editor: View, + cx: &mut ViewContext, + ) -> Self { + Self { + outline_view, + last_query: Default::default(), + matches: Default::default(), + selected_match_index: 0, + prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), + active_editor: editor, + outline, + } + } + + fn restore_active_editor(&mut self, cx: &mut WindowContext) { + self.active_editor.update(cx, |editor, cx| { + editor.highlight_rows(None); + if let Some(scroll_position) = self.prev_scroll_position { + editor.set_scroll_position(scroll_position, cx); + } + }) + } + + fn set_selected_index( + &mut self, + ix: usize, + navigate: bool, + cx: &mut ViewContext>, + ) { + self.selected_match_index = ix; + + if navigate && !self.matches.is_empty() { + let selected_match = &self.matches[self.selected_match_index]; + let outline_item = &self.outline.items[selected_match.candidate_id]; + + self.active_editor.update(cx, |active_editor, cx| { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let buffer_snapshot = &snapshot.buffer_snapshot; + let start = outline_item.range.start.to_point(buffer_snapshot); + let end = outline_item.range.end.to_point(buffer_snapshot); + let display_rows = start.to_display_point(&snapshot).row() + ..end.to_display_point(&snapshot).row() + 1; + active_editor.highlight_rows(Some(display_rows)); + active_editor.request_autoscroll(Autoscroll::center(), cx); + }); + } + } +} + +impl PickerDelegate for OutlineViewDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self) -> Arc { + "Search buffer symbols...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.set_selected_index(ix, true, cx); + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> Task<()> { + let selected_index; + if query.is_empty() { + self.restore_active_editor(cx); + self.matches = self + .outline + .items + .iter() + .enumerate() + .map(|(index, _)| StringMatch { + candidate_id: index, + score: Default::default(), + positions: Default::default(), + string: Default::default(), + }) + .collect(); + + let editor = self.active_editor.read(cx); + let cursor_offset = editor.selections.newest::(cx).head(); + let buffer = editor.buffer().read(cx).snapshot(cx); + selected_index = self + .outline + .items + .iter() + .enumerate() + .map(|(ix, item)| { + let range = item.range.to_offset(&buffer); + let distance_to_closest_endpoint = cmp::min( + (range.start as isize - cursor_offset as isize).abs(), + (range.end as isize - cursor_offset as isize).abs(), + ); + let depth = if range.contains(&cursor_offset) { + Some(item.depth) + } else { + None + }; + (ix, depth, distance_to_closest_endpoint) + }) + .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) + .map(|(ix, _, _)| ix) + .unwrap_or(0); + } else { + self.matches = smol::block_on( + self.outline + .search(&query, cx.background_executor().clone()), + ); + selected_index = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(ix, _)| ix) + .unwrap_or(0); + } + self.last_query = query; + self.set_selected_index(selected_index, !self.last_query.is_empty(), cx); + Task::ready(()) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + self.prev_scroll_position.take(); + + self.active_editor.update(cx, |active_editor, cx| { + if let Some(rows) = active_editor.highlighted_rows() { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); + active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([position..position]) + }); + active_editor.highlight_rows(None); + } + }); + + self.dismissed(cx); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.outline_view + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + self.restore_active_editor(cx); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let mat = &self.matches[ix]; + let outline_item = &self.outline.items[mat.candidate_id]; + + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + outline_item.highlight_ranges.iter().cloned(), + ); + + let styled_text = StyledText::new(outline_item.text.clone()) + .with_highlights(&TextStyle::default(), highlights); + + Some( + ListItem::new(ix) + .inset(true) + .selected(selected) + .child(div().pl(rems(outline_item.depth as f32)).child(styled_text)), + ) + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3802039a81be6972ab67f0bc689dd6793f96e755..2e779b71b2c4c2765c2c73745ac6ebd24db44bc9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1121,20 +1121,22 @@ impl Project { project_path: impl Into, is_directory: bool, cx: &mut ModelContext, - ) -> Option>> { + ) -> Task>> { let project_path = project_path.into(); - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { + return Task::ready(Ok(None)); + }; if self.is_local() { - Some(worktree.update(cx, |worktree, cx| { + worktree.update(cx, |worktree, cx| { worktree .as_local_mut() .unwrap() .create_entry(project_path.path, is_directory, cx) - })) + }) } else { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn_weak(|_, mut cx| async move { + cx.spawn_weak(|_, mut cx| async move { let response = client .request(proto::CreateProjectEntry { worktree_id: project_path.worktree_id.to_proto(), @@ -1143,19 +1145,20 @@ impl Project { is_directory, }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - }) - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1164,8 +1167,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1178,7 +1183,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn_weak(|_, mut cx| async move { + cx.spawn_weak(|_, mut cx| async move { let response = client .request(proto::CopyProjectEntry { project_id, @@ -1186,19 +1191,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - }) - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1207,8 +1213,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1221,7 +1229,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn_weak(|_, mut cx| async move { + cx.spawn_weak(|_, mut cx| async move { let response = client .request(proto::RenameProjectEntry { project_id, @@ -1229,19 +1237,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - }) - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1658,19 +1667,15 @@ impl Project { pub fn open_path( &mut self, - path: impl Into, + path: ProjectPath, cx: &mut ModelContext, - ) -> Task> { - let project_path = path.into(); - let task = self.open_buffer(project_path.clone(), cx); + ) -> Task, AnyModelHandle)>> { + let task = self.open_buffer(path.clone(), cx); cx.spawn_weak(|_, cx| async move { let buffer = task.await?; - let project_entry_id = buffer - .read_with(&cx, |buffer, cx| { - File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) - }) - .with_context(|| format!("no project entry for {project_path:?}"))?; - + let project_entry_id = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + }); let buffer: &AnyModelHandle = &buffer; Ok((project_entry_id, buffer.clone())) }) @@ -1985,8 +1990,10 @@ impl Project { remote_id, ); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); + if let Some(entry_id) = file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } } @@ -2441,24 +2448,25 @@ impl Project { return None; }; - match self.local_buffer_ids_by_entry_id.get(&file.entry_id) { - Some(_) => { - return None; - } - None => { - let remote_id = buffer.read(cx).remote_id(); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); - - self.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - remote_id, - ); + let remote_id = buffer.read(cx).remote_id(); + if let Some(entry_id) = file.entry_id { + match self.local_buffer_ids_by_entry_id.get(&entry_id) { + Some(_) => { + return None; + } + None => { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } - } + }; + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); } _ => {} } @@ -5776,11 +5784,6 @@ impl Project { while let Some(ignored_abs_path) = ignored_paths_to_process.pop_front() { - if !query.file_matches(Some(&ignored_abs_path)) - || snapshot.is_path_excluded(&ignored_abs_path) - { - continue; - } if let Some(fs_metadata) = fs .metadata(&ignored_abs_path) .await @@ -5808,6 +5811,13 @@ impl Project { } } } else if !fs_metadata.is_symlink { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_path_excluded( + ignored_entry.path.to_path_buf(), + ) + { + continue; + } let matches = if let Some(file) = fs .open_sync(&ignored_abs_path) .await @@ -6208,10 +6218,13 @@ impl Project { return; } - let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + let new_file = if let Some(entry) = old_file + .entry_id + .and_then(|entry_id| snapshot.entry_for_id(entry_id)) + { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6220,7 +6233,7 @@ impl Project { } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6250,10 +6263,12 @@ impl Project { ); } - if new_file.entry_id != *entry_id { + if new_file.entry_id != Some(*entry_id) { self.local_buffer_ids_by_entry_id.remove(entry_id); - self.local_buffer_ids_by_entry_id - .insert(new_file.entry_id, buffer_id); + if let Some(entry_id) = new_file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, buffer_id); + } } if new_file != *old_file { @@ -6816,7 +6831,7 @@ impl Project { }) .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6840,11 +6855,10 @@ impl Project { .as_local_mut() .unwrap() .rename_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })? + }) .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6868,11 +6882,10 @@ impl Project { .as_local_mut() .unwrap() .copy_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })? + }) .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 5d061b868fb37dd730d09ea184fd3f7a91d447be..4fe6e1699b763cca151a7d8bc2e6a7b901c12c6b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4050,6 +4050,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex ); } +#[gpui::test] +async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + ".gitignore": "**/target\n/node_modules\n", + "target": { + "index.txt": "index_key:index_value" + }, + "node_modules": { + "eslint": { + "index.ts": "const eslint_key = 'eslint value'", + "package.json": r#"{ "some_key": "some value" }"#, + }, + "prettier": { + "index.ts": "const prettier_key = 'prettier value'", + "package.json": r#"{ "other_key": "other value" }"#, + }, + }, + "package.json": r#"{ "main_key": "main value" }"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let query = "key"; + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([("package.json".to_string(), vec![8..11])]), + "Only one non-ignored file should have the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("package.json".to_string(), vec![8..11]), + ("target/index.txt".to_string(), vec![6..9]), + ( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + ), + ("node_modules/prettier/index.ts".to_string(), vec![15..18]), + ("node_modules/eslint/index.ts".to_string(), vec![13..16]), + ("node_modules/eslint/package.json".to_string(), vec![8..11]), + ]), + "Unrestricted search with ignored directories should find every file with the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + query, + false, + false, + true, + vec![PathMatcher::new("node_modules/prettier/**").unwrap()], + vec![PathMatcher::new("*.ts").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + )]), + "With search including ignored prettier directory and excluding TS files, only one file should be found" + ); +} + #[test] fn test_glob_literal_prefix() { assert_eq!(glob_literal_prefix("**/*.js"), ""); diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index c673440326e82630bd34c8117665b3f3cc092b69..bfbc537b27e92821a02e401ccf05a7cd013fb2b7 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -371,15 +371,25 @@ impl SearchQuery { pub fn file_matches(&self, file_path: Option<&Path>) -> bool { match file_path { Some(file_path) => { - !self - .files_to_exclude() - .iter() - .any(|exclude_glob| exclude_glob.is_match(file_path)) - && (self.files_to_include().is_empty() + let mut path = file_path.to_path_buf(); + loop { + if self + .files_to_exclude() + .iter() + .any(|exclude_glob| exclude_glob.is_match(&path)) + { + return false; + } else if self.files_to_include().is_empty() || self .files_to_include() .iter() - .any(|include_glob| include_glob.is_match(file_path))) + .any(|include_glob| include_glob.is_match(&path)) + { + return true; + } else if !path.pop() { + return false; + } + } } None => self.files_to_include().is_empty(), } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index d5a046ba0dc4303c0acc95219c4846fb34136cec..c721d127add1344ee22df0a4224413c60c3fc9b2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -960,8 +960,6 @@ impl LocalWorktree { cx.spawn(|this, cx| async move { let text = fs.load(&abs_path).await?; - let entry = entry.await?; - let mut index_task = None; let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot()); if let Some(repo) = snapshot.repository_for_path(&path) { @@ -981,18 +979,43 @@ impl LocalWorktree { None }; - Ok(( - File { - entry_id: entry.id, - worktree: this, - path: entry.path, - mtime: entry.mtime, - is_local: true, - is_deleted: false, - }, - text, - diff_base, - )) + match entry.await? { + Some(entry) => Ok(( + File { + entry_id: Some(entry.id), + worktree: this, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!("Loading metadata for excluded file {abs_path:?}") + })? + .with_context(|| { + format!("Excluded file {abs_path:?} got removed during loading") + })?; + Ok(( + File { + entry_id: None, + worktree: this, + path, + mtime: metadata.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )) + } + } }) } @@ -1013,17 +1036,37 @@ impl LocalWorktree { let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); - let save = self.write_file(path, text, buffer.line_ending(), cx); + let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx); + let fs = Arc::clone(&self.fs); + let abs_path = self.absolutize(&path); cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; + let (entry_id, mtime, path) = match entry { + Some(entry) => (Some(entry.id), entry.mtime, entry.path), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!( + "Fetching metadata after saving the excluded buffer {abs_path:?}" + ) + })? + .with_context(|| { + format!("Excluded buffer {path:?} got removed during saving") + })?; + (None, metadata.mtime, path) + } + }; + if has_changed_file { let new_file = Arc::new(File { - entry_id: entry.id, + entry_id, worktree: handle, - path: entry.path, - mtime: entry.mtime, + path, + mtime, is_local: true, is_deleted: false, }); @@ -1049,13 +1092,13 @@ impl LocalWorktree { project_id, buffer_id, version: serialize_version(&version), - mtime: Some(entry.mtime.into()), + mtime: Some(mtime.into()), fingerprint: serialize_fingerprint(fingerprint), })?; } buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); + buffer.did_save(version.clone(), fingerprint, mtime, cx); }); Ok(()) @@ -1080,7 +1123,7 @@ impl LocalWorktree { path: impl Into>, is_dir: bool, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { let path = path.into(); let lowest_ancestor = self.lowest_ancestor(&path); let abs_path = self.absolutize(&path); @@ -1097,7 +1140,7 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; let (result, refreshes) = this.update(&mut cx, |this, cx| { - let mut refreshes = Vec::>>::new(); + let mut refreshes = Vec::new(); let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); for refresh_path in refresh_paths.ancestors() { if refresh_path == Path::new("") { @@ -1124,14 +1167,14 @@ impl LocalWorktree { }) } - pub fn write_file( + pub(crate) fn write_file( &self, path: impl Into>, text: Rope, line_ending: LineEnding, cx: &mut ModelContext, - ) -> Task> { - let path = path.into(); + ) -> Task>> { + let path: Arc = path.into(); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); let write = cx @@ -1190,8 +1233,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1201,7 +1247,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { rename.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1209,7 +1255,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), Some(old_path), cx) }) .await - })) + }) } pub fn copy_entry( @@ -1217,8 +1263,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1233,7 +1282,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { copy.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1241,7 +1290,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), None, cx) }) .await - })) + }) } pub fn expand_entry( @@ -1277,7 +1326,10 @@ impl LocalWorktree { path: Arc, old_path: Option>, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { + if self.is_path_excluded(path.to_path_buf()) { + return Task::ready(Ok(None)); + } let paths = if let Some(old_path) = old_path.as_ref() { vec![old_path.clone(), path.clone()] } else { @@ -1286,13 +1338,15 @@ impl LocalWorktree { let mut refresh = self.refresh_entries_for_paths(paths); cx.spawn_weak(move |this, mut cx| async move { refresh.recv().await; - this.upgrade(&cx) + let new_entry = this + .upgrade(&cx) .ok_or_else(|| anyhow!("worktree was dropped"))? .update(&mut cx, |this, _| { this.entry_for_path(path) .cloned() .ok_or_else(|| anyhow!("failed to read path after update")) - }) + })?; + Ok(Some(new_entry)) }) } @@ -2226,10 +2280,19 @@ impl LocalSnapshot { paths } - pub fn is_path_excluded(&self, abs_path: &Path) -> bool { - self.file_scan_exclusions - .iter() - .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) + pub fn is_path_excluded(&self, mut path: PathBuf) -> bool { + loop { + if self + .file_scan_exclusions + .iter() + .any(|exclude_matcher| exclude_matcher.is_match(&path)) + { + return true; + } + if !path.pop() { + return false; + } + } } } @@ -2458,8 +2521,7 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) - || snapshot.is_path_excluded(&git_dir_abs_path); + let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf()); if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { @@ -2666,7 +2728,7 @@ pub struct File { pub worktree: ModelHandle, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: ProjectEntryId, + pub(crate) entry_id: Option, pub(crate) is_local: bool, pub(crate) is_deleted: bool, } @@ -2735,7 +2797,7 @@ impl language::File for File { fn to_proto(&self) -> rpc::proto::File { rpc::proto::File { worktree_id: self.worktree.id() as u64, - entry_id: self.entry_id.to_proto(), + entry_id: self.entry_id.map(|id| id.to_proto()), path: self.path.to_string_lossy().into(), mtime: Some(self.mtime.into()), is_deleted: self.is_deleted, @@ -2793,7 +2855,7 @@ impl File { worktree, path: entry.path.clone(), mtime: entry.mtime, - entry_id: entry.id, + entry_id: Some(entry.id), is_local: true, is_deleted: false, }) @@ -2818,7 +2880,7 @@ impl File { worktree, path: Path::new(&proto.path).into(), mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), - entry_id: ProjectEntryId::from_proto(proto.entry_id), + entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, is_deleted: proto.is_deleted, }) @@ -2836,7 +2898,7 @@ impl File { if self.is_deleted { None } else { - Some(self.entry_id) + self.entry_id } } } @@ -3338,16 +3400,7 @@ impl BackgroundScanner { return false; } - // FS events may come for files which parent directory is excluded, need to check ignore those. - let mut path_to_test = abs_path.clone(); - let mut excluded_file_event = snapshot.is_path_excluded(abs_path) - || snapshot.is_path_excluded(&relative_path); - while !excluded_file_event && path_to_test.pop() { - if snapshot.is_path_excluded(&path_to_test) { - excluded_file_event = true; - } - } - if excluded_file_event { + if snapshot.is_path_excluded(relative_path.to_path_buf()) { if !is_git_related { log::debug!("ignoring FS event for excluded path {relative_path:?}"); } @@ -3531,7 +3584,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(job.path.to_path_buf()) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3603,8 +3656,8 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state.snapshot.is_path_excluded(&child_abs_path) { - let relative_path = job.path.join(child_name); + let relative_path = job.path.join(child_name); + if state.snapshot.is_path_excluded(relative_path.clone()) { log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); continue; diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index b4cf162d8f1e61dbf317b33917428ce79468bdea..e8865873277ecab09e0414529c299559f2bce3c1 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1052,11 +1052,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { &[ ".git/HEAD", ".git/foo", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", ], - &["target", "node_modules"], + &["target"], &[ ".DS_Store", "src/.DS_Store", @@ -1106,6 +1107,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { ".git/HEAD", ".git/foo", ".git/new_file", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", @@ -1114,7 +1116,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "build_output/new_file", "test_output/new_file", ], - &["target", "node_modules", "test_output"], + &["target", "test_output"], &[ ".DS_Store", "src/.DS_Store", @@ -1174,6 +1176,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { .create_entry("a/e".as_ref(), true, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_dir()); @@ -1222,6 +1225,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1257,6 +1261,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1275,6 +1280,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/e.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1291,6 +1297,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("d/e/f/g.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1616,14 +1623,14 @@ fn randomly_mutate_worktree( entry.id.0, new_path ); - let task = worktree.rename_entry(entry.id, new_path, cx).unwrap(); + let task = worktree.rename_entry(entry.id, new_path, cx); cx.foreground().spawn(async move { - task.await?; + task.await?.unwrap(); Ok(()) }) } _ => { - let task = if entry.is_dir() { + if entry.is_dir() { let child_path = entry.path.join(random_filename(rng)); let is_dir = rng.gen_bool(0.3); log::info!( @@ -1631,15 +1638,20 @@ fn randomly_mutate_worktree( if is_dir { "dir" } else { "file" }, child_path, ); - worktree.create_entry(child_path, is_dir, cx) + let task = worktree.create_entry(child_path, is_dir, cx); + cx.foreground().spawn(async move { + task.await?; + Ok(()) + }) } else { log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx) - }; - cx.foreground().spawn(async move { - task.await?; - Ok(()) - }) + let task = + worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); + cx.foreground().spawn(async move { + task.await?; + Ok(()) + }) + } } } } diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 9750fe053dbb0e1ff63f27a170376c0bd7bc6af0..243f896b0f14035e399f382b4b68e2f16ffd7438 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -1151,20 +1151,22 @@ impl Project { project_path: impl Into, is_directory: bool, cx: &mut ModelContext, - ) -> Option>> { + ) -> Task>> { let project_path = project_path.into(); - let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else { + return Task::ready(Ok(None)); + }; if self.is_local() { - Some(worktree.update(cx, |worktree, cx| { + worktree.update(cx, |worktree, cx| { worktree .as_local_mut() .unwrap() .create_entry(project_path.path, is_directory, cx) - })) + }) } else { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn(move |_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::CreateProjectEntry { worktree_id: project_path.worktree_id.to_proto(), @@ -1173,19 +1175,20 @@ impl Project { is_directory, }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + })? + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1194,8 +1197,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1208,7 +1213,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn(move |_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::CopyProjectEntry { project_id, @@ -1216,19 +1221,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + })? + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1237,8 +1243,10 @@ impl Project { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let worktree = self.worktree_for_entry(entry_id, cx)?; + ) -> Task>> { + let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + return Task::ready(Ok(None)); + }; let new_path = new_path.into(); if self.is_local() { worktree.update(cx, |worktree, cx| { @@ -1251,7 +1259,7 @@ impl Project { let client = self.client.clone(); let project_id = self.remote_id().unwrap(); - Some(cx.spawn(move |_, mut cx| async move { + cx.spawn(move |_, mut cx| async move { let response = client .request(proto::RenameProjectEntry { project_id, @@ -1259,19 +1267,20 @@ impl Project { new_path: new_path.to_string_lossy().into(), }) .await?; - let entry = response - .entry - .ok_or_else(|| anyhow!("missing entry in response"))?; - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - })) + match response.entry { + Some(entry) => worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + })? + .await + .map(Some), + None => Ok(None), + } + }) } } @@ -1688,18 +1697,15 @@ impl Project { pub fn open_path( &mut self, - path: impl Into, + path: ProjectPath, cx: &mut ModelContext, - ) -> Task> { - let project_path = path.into(); - let task = self.open_buffer(project_path.clone(), cx); - cx.spawn(move |_, mut cx| async move { + ) -> Task, AnyModel)>> { + let task = self.open_buffer(path.clone(), cx); + cx.spawn(move |_, cx| async move { let buffer = task.await?; - let project_entry_id = buffer - .update(&mut cx, |buffer, cx| { - File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) - })? - .with_context(|| format!("no project entry for {project_path:?}"))?; + let project_entry_id = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + })?; let buffer: &AnyModel = &buffer; Ok((project_entry_id, buffer.clone())) @@ -2018,8 +2024,10 @@ impl Project { remote_id, ); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); + if let Some(entry_id) = file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } } @@ -2474,24 +2482,25 @@ impl Project { return None; }; - match self.local_buffer_ids_by_entry_id.get(&file.entry_id) { - Some(_) => { - return None; - } - None => { - let remote_id = buffer.read(cx).remote_id(); - self.local_buffer_ids_by_entry_id - .insert(file.entry_id, remote_id); - - self.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - remote_id, - ); + let remote_id = buffer.read(cx).remote_id(); + if let Some(entry_id) = file.entry_id { + match self.local_buffer_ids_by_entry_id.get(&entry_id) { + Some(_) => { + return None; + } + None => { + self.local_buffer_ids_by_entry_id + .insert(entry_id, remote_id); + } } - } + }; + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); } _ => {} } @@ -5845,11 +5854,6 @@ impl Project { while let Some(ignored_abs_path) = ignored_paths_to_process.pop_front() { - if !query.file_matches(Some(&ignored_abs_path)) - || snapshot.is_path_excluded(&ignored_abs_path) - { - continue; - } if let Some(fs_metadata) = fs .metadata(&ignored_abs_path) .await @@ -5877,6 +5881,13 @@ impl Project { } } } else if !fs_metadata.is_symlink { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_path_excluded( + ignored_entry.path.to_path_buf(), + ) + { + continue; + } let matches = if let Some(file) = fs .open_sync(&ignored_abs_path) .await @@ -6278,10 +6289,13 @@ impl Project { return; } - let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + let new_file = if let Some(entry) = old_file + .entry_id + .and_then(|entry_id| snapshot.entry_for_id(entry_id)) + { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6290,7 +6304,7 @@ impl Project { } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { File { is_local: true, - entry_id: entry.id, + entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), worktree: worktree_handle.clone(), @@ -6320,10 +6334,12 @@ impl Project { ); } - if new_file.entry_id != *entry_id { + if new_file.entry_id != Some(*entry_id) { self.local_buffer_ids_by_entry_id.remove(entry_id); - self.local_buffer_ids_by_entry_id - .insert(new_file.entry_id, buffer_id); + if let Some(entry_id) = new_file.entry_id { + self.local_buffer_ids_by_entry_id + .insert(entry_id, buffer_id); + } } if new_file != *old_file { @@ -6890,7 +6906,7 @@ impl Project { })? .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6914,11 +6930,10 @@ impl Project { .as_local_mut() .unwrap() .rename_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })?? + })? .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } @@ -6942,11 +6957,10 @@ impl Project { .as_local_mut() .unwrap() .copy_entry(entry_id, new_path, cx) - .ok_or_else(|| anyhow!("invalid entry")) - })?? + })? .await?; Ok(proto::ProjectEntryResponse { - entry: Some((&entry).into()), + entry: entry.as_ref().map(|e| e.into()), worktree_scan_id: worktree_scan_id as u64, }) } diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 4dfb8004e3e644309b89ee31d99f5ac07e05f4b3..8f41c75fb4de0415089c1ce66c30cb93278c079c 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -4182,6 +4182,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex ); } +#[gpui::test] +async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + ".gitignore": "**/target\n/node_modules\n", + "target": { + "index.txt": "index_key:index_value" + }, + "node_modules": { + "eslint": { + "index.ts": "const eslint_key = 'eslint value'", + "package.json": r#"{ "some_key": "some value" }"#, + }, + "prettier": { + "index.ts": "const prettier_key = 'prettier value'", + "package.json": r#"{ "other_key": "other value" }"#, + }, + }, + "package.json": r#"{ "main_key": "main value" }"#, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let query = "key"; + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([("package.json".to_string(), vec![8..11])]), + "Only one non-ignored file should have the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([ + ("package.json".to_string(), vec![8..11]), + ("target/index.txt".to_string(), vec![6..9]), + ( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + ), + ("node_modules/prettier/index.ts".to_string(), vec![15..18]), + ("node_modules/eslint/index.ts".to_string(), vec![13..16]), + ("node_modules/eslint/package.json".to_string(), vec![8..11]), + ]), + "Unrestricted search with ignored directories should find every file with the query" + ); + + assert_eq!( + search( + &project, + SearchQuery::text( + query, + false, + false, + true, + vec![PathMatcher::new("node_modules/prettier/**").unwrap()], + vec![PathMatcher::new("*.ts").unwrap()], + ) + .unwrap(), + cx + ) + .await + .unwrap(), + HashMap::from_iter([( + "node_modules/prettier/package.json".to_string(), + vec![9..12] + )]), + "With search including ignored prettier directory and excluding TS files, only one file should be found" + ); +} + #[test] fn test_glob_literal_prefix() { assert_eq!(glob_literal_prefix("**/*.js"), ""); diff --git a/crates/project2/src/search.rs b/crates/project2/src/search.rs index c673440326e82630bd34c8117665b3f3cc092b69..bfbc537b27e92821a02e401ccf05a7cd013fb2b7 100644 --- a/crates/project2/src/search.rs +++ b/crates/project2/src/search.rs @@ -371,15 +371,25 @@ impl SearchQuery { pub fn file_matches(&self, file_path: Option<&Path>) -> bool { match file_path { Some(file_path) => { - !self - .files_to_exclude() - .iter() - .any(|exclude_glob| exclude_glob.is_match(file_path)) - && (self.files_to_include().is_empty() + let mut path = file_path.to_path_buf(); + loop { + if self + .files_to_exclude() + .iter() + .any(|exclude_glob| exclude_glob.is_match(&path)) + { + return false; + } else if self.files_to_include().is_empty() || self .files_to_include() .iter() - .any(|include_glob| include_glob.is_match(file_path))) + .any(|include_glob| include_glob.is_match(&path)) + { + return true; + } else if !path.pop() { + return false; + } + } } None => self.files_to_include().is_empty(), } diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index e424375220c1c7aa2292d9a4762d7581ea67222e..a5cb322cb5f52beb71c1b4200f58f098c2f6d88a 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -958,8 +958,6 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { let text = fs.load(&abs_path).await?; - let entry = entry.await?; - let mut index_task = None; let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?; if let Some(repo) = snapshot.repository_for_path(&path) { @@ -982,18 +980,43 @@ impl LocalWorktree { let worktree = this .upgrade() .ok_or_else(|| anyhow!("worktree was dropped"))?; - Ok(( - File { - entry_id: entry.id, - worktree, - path: entry.path, - mtime: entry.mtime, - is_local: true, - is_deleted: false, - }, - text, - diff_base, - )) + match entry.await? { + Some(entry) => Ok(( + File { + entry_id: Some(entry.id), + worktree, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!("Loading metadata for excluded file {abs_path:?}") + })? + .with_context(|| { + format!("Excluded file {abs_path:?} got removed during loading") + })?; + Ok(( + File { + entry_id: None, + worktree, + path, + mtime: metadata.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )) + } + } }) } @@ -1013,18 +1036,38 @@ impl LocalWorktree { let text = buffer.as_rope().clone(); let fingerprint = text.fingerprint(); let version = buffer.version(); - let save = self.write_file(path, text, buffer.line_ending(), cx); + let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx); + let fs = Arc::clone(&self.fs); + let abs_path = self.absolutize(&path); cx.spawn(move |this, mut cx| async move { let entry = save.await?; let this = this.upgrade().context("worktree dropped")?; + let (entry_id, mtime, path) = match entry { + Some(entry) => (Some(entry.id), entry.mtime, entry.path), + None => { + let metadata = fs + .metadata(&abs_path) + .await + .with_context(|| { + format!( + "Fetching metadata after saving the excluded buffer {abs_path:?}" + ) + })? + .with_context(|| { + format!("Excluded buffer {path:?} got removed during saving") + })?; + (None, metadata.mtime, path) + } + }; + if has_changed_file { let new_file = Arc::new(File { - entry_id: entry.id, + entry_id, worktree: this, - path: entry.path, - mtime: entry.mtime, + path, + mtime, is_local: true, is_deleted: false, }); @@ -1050,13 +1093,13 @@ impl LocalWorktree { project_id, buffer_id, version: serialize_version(&version), - mtime: Some(entry.mtime.into()), + mtime: Some(mtime.into()), fingerprint: serialize_fingerprint(fingerprint), })?; } buffer_handle.update(&mut cx, |buffer, cx| { - buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); + buffer.did_save(version.clone(), fingerprint, mtime, cx); })?; Ok(()) @@ -1081,7 +1124,7 @@ impl LocalWorktree { path: impl Into>, is_dir: bool, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { let path = path.into(); let lowest_ancestor = self.lowest_ancestor(&path); let abs_path = self.absolutize(&path); @@ -1098,7 +1141,7 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; let (result, refreshes) = this.update(&mut cx, |this, cx| { - let mut refreshes = Vec::>>::new(); + let mut refreshes = Vec::new(); let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); for refresh_path in refresh_paths.ancestors() { if refresh_path == Path::new("") { @@ -1125,14 +1168,14 @@ impl LocalWorktree { }) } - pub fn write_file( + pub(crate) fn write_file( &self, path: impl Into>, text: Rope, line_ending: LineEnding, cx: &mut ModelContext, - ) -> Task> { - let path = path.into(); + ) -> Task>> { + let path: Arc = path.into(); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); let write = cx @@ -1191,8 +1234,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1202,7 +1248,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { rename.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1210,7 +1256,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), Some(old_path), cx) })? .await - })) + }) } pub fn copy_entry( @@ -1218,8 +1264,11 @@ impl LocalWorktree { entry_id: ProjectEntryId, new_path: impl Into>, cx: &mut ModelContext, - ) -> Option>> { - let old_path = self.entry_for_id(entry_id)?.path.clone(); + ) -> Task>> { + let old_path = match self.entry_for_id(entry_id) { + Some(entry) => entry.path.clone(), + None => return Task::ready(Ok(None)), + }; let new_path = new_path.into(); let abs_old_path = self.absolutize(&old_path); let abs_new_path = self.absolutize(&new_path); @@ -1234,7 +1283,7 @@ impl LocalWorktree { .await }); - Some(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { copy.await?; this.update(&mut cx, |this, cx| { this.as_local_mut() @@ -1242,7 +1291,7 @@ impl LocalWorktree { .refresh_entry(new_path.clone(), None, cx) })? .await - })) + }) } pub fn expand_entry( @@ -1278,7 +1327,10 @@ impl LocalWorktree { path: Arc, old_path: Option>, cx: &mut ModelContext, - ) -> Task> { + ) -> Task>> { + if self.is_path_excluded(path.to_path_buf()) { + return Task::ready(Ok(None)); + } let paths = if let Some(old_path) = old_path.as_ref() { vec![old_path.clone(), path.clone()] } else { @@ -1287,11 +1339,12 @@ impl LocalWorktree { let mut refresh = self.refresh_entries_for_paths(paths); cx.spawn(move |this, mut cx| async move { refresh.recv().await; - this.update(&mut cx, |this, _| { + let new_entry = this.update(&mut cx, |this, _| { this.entry_for_path(path) .cloned() .ok_or_else(|| anyhow!("failed to read path after update")) - })? + })??; + Ok(Some(new_entry)) }) } @@ -2222,10 +2275,19 @@ impl LocalSnapshot { paths } - pub fn is_path_excluded(&self, abs_path: &Path) -> bool { - self.file_scan_exclusions - .iter() - .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) + pub fn is_path_excluded(&self, mut path: PathBuf) -> bool { + loop { + if self + .file_scan_exclusions + .iter() + .any(|exclude_matcher| exclude_matcher.is_match(&path)) + { + return true; + } + if !path.pop() { + return false; + } + } } } @@ -2455,8 +2517,7 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) - || snapshot.is_path_excluded(&git_dir_abs_path); + let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf()); if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { @@ -2663,7 +2724,7 @@ pub struct File { pub worktree: Model, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: ProjectEntryId, + pub(crate) entry_id: Option, pub(crate) is_local: bool, pub(crate) is_deleted: bool, } @@ -2732,7 +2793,7 @@ impl language::File for File { fn to_proto(&self) -> rpc::proto::File { rpc::proto::File { worktree_id: self.worktree.entity_id().as_u64(), - entry_id: self.entry_id.to_proto(), + entry_id: self.entry_id.map(|id| id.to_proto()), path: self.path.to_string_lossy().into(), mtime: Some(self.mtime.into()), is_deleted: self.is_deleted, @@ -2790,7 +2851,7 @@ impl File { worktree, path: entry.path.clone(), mtime: entry.mtime, - entry_id: entry.id, + entry_id: Some(entry.id), is_local: true, is_deleted: false, }) @@ -2815,7 +2876,7 @@ impl File { worktree, path: Path::new(&proto.path).into(), mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), - entry_id: ProjectEntryId::from_proto(proto.entry_id), + entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, is_deleted: proto.is_deleted, }) @@ -2833,7 +2894,7 @@ impl File { if self.is_deleted { None } else { - Some(self.entry_id) + self.entry_id } } } @@ -3329,16 +3390,7 @@ impl BackgroundScanner { return false; } - // FS events may come for files which parent directory is excluded, need to check ignore those. - let mut path_to_test = abs_path.clone(); - let mut excluded_file_event = snapshot.is_path_excluded(abs_path) - || snapshot.is_path_excluded(&relative_path); - while !excluded_file_event && path_to_test.pop() { - if snapshot.is_path_excluded(&path_to_test) { - excluded_file_event = true; - } - } - if excluded_file_event { + if snapshot.is_path_excluded(relative_path.to_path_buf()) { if !is_git_related { log::debug!("ignoring FS event for excluded path {relative_path:?}"); } @@ -3522,7 +3574,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(job.path.to_path_buf()) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3593,9 +3645,9 @@ impl BackgroundScanner { } { + let relative_path = job.path.join(child_name); let mut state = self.state.lock(); - if state.snapshot.is_path_excluded(&child_abs_path) { - let relative_path = job.path.join(child_name); + if state.snapshot.is_path_excluded(relative_path.clone()) { log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); continue; diff --git a/crates/project2/src/worktree_tests.rs b/crates/project2/src/worktree_tests.rs index 501a5f736f1934807984d90357593058ef1cde68..fbf8b74d624672c6e62b2a98d96ee8764b500957 100644 --- a/crates/project2/src/worktree_tests.rs +++ b/crates/project2/src/worktree_tests.rs @@ -1055,11 +1055,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { &[ ".git/HEAD", ".git/foo", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", ], - &["target", "node_modules"], + &["target"], &[ ".DS_Store", "src/.DS_Store", @@ -1109,6 +1110,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { ".git/HEAD", ".git/foo", ".git/new_file", + "node_modules", "node_modules/.DS_Store", "node_modules/prettier", "node_modules/prettier/package.json", @@ -1117,7 +1119,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "build_output/new_file", "test_output/new_file", ], - &["target", "node_modules", "test_output"], + &["target", "test_output"], &[ ".DS_Store", "src/.DS_Store", @@ -1177,6 +1179,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { .create_entry("a/e".as_ref(), true, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_dir()); @@ -1226,6 +1229,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1261,6 +1265,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/d.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1279,6 +1284,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("a/b/c/e.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1295,6 +1301,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { .create_entry("d/e/f/g.txt".as_ref(), false, cx) }) .await + .unwrap() .unwrap(); assert!(entry.is_file()); @@ -1620,14 +1627,14 @@ fn randomly_mutate_worktree( entry.id.0, new_path ); - let task = worktree.rename_entry(entry.id, new_path, cx).unwrap(); + let task = worktree.rename_entry(entry.id, new_path, cx); cx.background_executor().spawn(async move { - task.await?; + task.await?.unwrap(); Ok(()) }) } _ => { - let task = if entry.is_dir() { + if entry.is_dir() { let child_path = entry.path.join(random_filename(rng)); let is_dir = rng.gen_bool(0.3); log::info!( @@ -1635,15 +1642,20 @@ fn randomly_mutate_worktree( if is_dir { "dir" } else { "file" }, child_path, ); - worktree.create_entry(child_path, is_dir, cx) + let task = worktree.create_entry(child_path, is_dir, cx); + cx.background_executor().spawn(async move { + task.await?; + Ok(()) + }) } else { log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx) - }; - cx.background_executor().spawn(async move { - task.await?; - Ok(()) - }) + let task = + worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); + cx.background_executor().spawn(async move { + task.await?; + Ok(()) + }) + } } } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 875d4d4f83736aa6c768b6dda21e915e889afc06..c37d38804151585ca99bdc909cddbbbf0633f141 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -621,7 +621,7 @@ impl ProjectPanel { edited_entry_id = NEW_ENTRY_ID; edit_task = self.project.update(cx, |project, cx| { project.create_entry((worktree_id, &new_path), is_dir, cx) - })?; + }); } else { let new_path = if let Some(parent) = entry.path.clone().parent() { parent.join(&filename) @@ -635,7 +635,7 @@ impl ProjectPanel { edited_entry_id = entry.id; edit_task = self.project.update(cx, |project, cx| { project.rename_entry(entry.id, new_path.as_path(), cx) - })?; + }); }; edit_state.processing_filename = Some(filename); @@ -648,21 +648,22 @@ impl ProjectPanel { cx.notify(); })?; - let new_entry = new_entry?; - this.update(&mut cx, |this, cx| { - if let Some(selection) = &mut this.selection { - if selection.entry_id == edited_entry_id { - selection.worktree_id = worktree_id; - selection.entry_id = new_entry.id; - this.expand_to_selection(cx); + if let Some(new_entry) = new_entry? { + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } } - } - this.update_visible_entries(None, cx); - if is_new_entry && !is_dir { - this.open_entry(new_entry.id, true, cx); - } - cx.notify(); - })?; + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + } Ok(()) })) } @@ -935,15 +936,17 @@ impl ProjectPanel { } if clipboard_entry.is_cut() { - if let Some(task) = self.project.update(cx, |project, cx| { - project.rename_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx) - } - } else if let Some(task) = self.project.update(cx, |project, cx| { - project.copy_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx) + self.project + .update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) + } else { + self.project + .update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) } } None @@ -1026,7 +1029,7 @@ impl ProjectPanel { let mut new_path = destination_path.to_path_buf(); new_path.push(entry_path.path.file_name()?); if new_path != entry_path.path.as_ref() { - let task = project.rename_entry(entry_to_move, new_path, cx)?; + let task = project.rename_entry(entry_to_move, new_path, cx); cx.foreground().spawn(task).detach_and_log_err(cx); } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 0a5a63f14a289f5f8aeb44a20a454a35f8bea20b..81a7c779ca2380e62890f652ddd893660b6abc5f 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -397,7 +397,6 @@ impl ProjectPanel { menu = menu.action( "Add Folder to Project", Box::new(workspace::AddFolderToProject), - cx, ); if is_root { menu = menu.entry( @@ -412,35 +411,35 @@ impl ProjectPanel { } menu = menu - .action("New File", Box::new(NewFile), cx) - .action("New Folder", Box::new(NewDirectory), cx) + .action("New File", Box::new(NewFile)) + .action("New Folder", Box::new(NewDirectory)) .separator() - .action("Cut", Box::new(Cut), cx) - .action("Copy", Box::new(Copy), cx); + .action("Cut", Box::new(Cut)) + .action("Copy", Box::new(Copy)); if let Some(clipboard_entry) = self.clipboard_entry { if clipboard_entry.worktree_id() == worktree_id { - menu = menu.action("Paste", Box::new(Paste), cx); + menu = menu.action("Paste", Box::new(Paste)); } } menu = menu .separator() - .action("Copy Path", Box::new(CopyPath), cx) - .action("Copy Relative Path", Box::new(CopyRelativePath), cx) + .action("Copy Path", Box::new(CopyPath)) + .action("Copy Relative Path", Box::new(CopyRelativePath)) .separator() - .action("Reveal in Finder", Box::new(RevealInFinder), cx); + .action("Reveal in Finder", Box::new(RevealInFinder)); if is_dir { menu = menu - .action("Open in Terminal", Box::new(OpenInTerminal), cx) - .action("Search Inside", Box::new(NewSearchInDirectory), cx) + .action("Open in Terminal", Box::new(OpenInTerminal)) + .action("Search Inside", Box::new(NewSearchInDirectory)) } - menu = menu.separator().action("Rename", Box::new(Rename), cx); + menu = menu.separator().action("Rename", Box::new(Rename)); if !is_root { - menu = menu.action("Delete", Box::new(Delete), cx); + menu = menu.action("Delete", Box::new(Delete)); } menu @@ -611,7 +610,7 @@ impl ProjectPanel { edited_entry_id = NEW_ENTRY_ID; edit_task = self.project.update(cx, |project, cx| { project.create_entry((worktree_id, &new_path), is_dir, cx) - })?; + }); } else { let new_path = if let Some(parent) = entry.path.clone().parent() { parent.join(&filename) @@ -625,7 +624,7 @@ impl ProjectPanel { edited_entry_id = entry.id; edit_task = self.project.update(cx, |project, cx| { project.rename_entry(entry.id, new_path.as_path(), cx) - })?; + }); }; edit_state.processing_filename = Some(filename); @@ -638,21 +637,22 @@ impl ProjectPanel { cx.notify(); })?; - let new_entry = new_entry?; - this.update(&mut cx, |this, cx| { - if let Some(selection) = &mut this.selection { - if selection.entry_id == edited_entry_id { - selection.worktree_id = worktree_id; - selection.entry_id = new_entry.id; - this.expand_to_selection(cx); + if let Some(new_entry) = new_entry? { + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } } - } - this.update_visible_entries(None, cx); - if is_new_entry && !is_dir { - this.open_entry(new_entry.id, true, cx); - } - cx.notify(); - })?; + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + } Ok(()) })) } @@ -932,15 +932,17 @@ impl ProjectPanel { } if clipboard_entry.is_cut() { - if let Some(task) = self.project.update(cx, |project, cx| { - project.rename_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx); - } - } else if let Some(task) = self.project.update(cx, |project, cx| { - project.copy_entry(clipboard_entry.entry_id(), new_path, cx) - }) { - task.detach_and_log_err(cx); + self.project + .update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) + } else { + self.project + .update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) + .detach_and_log_err(cx) } Some(()) @@ -1026,7 +1028,7 @@ impl ProjectPanel { // let mut new_path = destination_path.to_path_buf(); // new_path.push(entry_path.path.file_name()?); // if new_path != entry_path.path.as_ref() { - // let task = project.rename_entry(entry_to_move, new_path, cx)?; + // let task = project.rename_entry(entry_to_move, new_path, cx); // cx.foreground_executor().spawn(task).detach_and_log_err(cx); // } diff --git a/crates/quick_action_bar2/Cargo.toml b/crates/quick_action_bar2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..32f440d202648b5c0dba8071d7aa5e49d4da18db --- /dev/null +++ b/crates/quick_action_bar2/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "quick_action_bar2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/quick_action_bar.rs" +doctest = false + +[dependencies] +#assistant = { path = "../assistant" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +search = { package = "search2", path = "../search2" } +workspace = { package = "workspace2", path = "../workspace2" } +ui = { package = "ui2", path = "../ui2" } + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs new file mode 100644 index 0000000000000000000000000000000000000000..3232de08adea814fcf33e7fc54b598a93bf18c25 --- /dev/null +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -0,0 +1,288 @@ +// use assistant::{assistant_panel::InlineAssist, AssistantPanel}; +use editor::Editor; + +use gpui::{ + Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, + Styled, Subscription, View, ViewContext, WeakView, +}; +use search::BufferSearchBar; +use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; +use workspace::{ + item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +pub struct QuickActionBar { + buffer_search_bar: View, + active_item: Option>, + _inlay_hints_enabled_subscription: Option, + #[allow(unused)] + workspace: WeakView, +} + +impl QuickActionBar { + pub fn new(buffer_search_bar: View, workspace: &Workspace) -> Self { + Self { + buffer_search_bar, + active_item: None, + _inlay_hints_enabled_subscription: None, + workspace: workspace.weak_handle(), + } + } + + #[allow(dead_code)] + fn active_editor(&self) -> Option> { + self.active_item + .as_ref() + .and_then(|item| item.downcast::()) + } +} + +impl Render for QuickActionBar { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let search_button = QuickActionBarButton::new( + "toggle buffer search", + Icon::MagnifyingGlass, + !self.buffer_search_bar.read(cx).is_dismissed(), + Box::new(search::buffer_search::Deploy { focus: false }), + "Buffer Search", + ); + let assistant_button = QuickActionBarButton::new( + "toggle inline assitant", + Icon::MagicWand, + false, + Box::new(gpui::NoAction), + "Inline assistant", + ); + h_stack() + .id("quick action bar") + .p_1() + .gap_2() + .child(search_button) + .child( + div() + .border() + .border_color(gpui::red()) + .child(assistant_button), + ) + } +} + +impl EventEmitter for QuickActionBar {} + +// impl View for QuickActionBar { +// fn ui_name() -> &'static str { +// "QuickActionsBar" +// } + +// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { +// let Some(editor) = self.active_editor() else { +// return div(); +// }; + +// let mut bar = Flex::row(); +// if editor.read(cx).supports_inlay_hints(cx) { +// bar = bar.with_child(render_quick_action_bar_button( +// 0, +// "icons/inlay_hint.svg", +// editor.read(cx).inlay_hints_enabled(), +// ( +// "Toggle Inlay Hints".to_string(), +// Some(Box::new(editor::ToggleInlayHints)), +// ), +// cx, +// |this, cx| { +// if let Some(editor) = this.active_editor() { +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); +// }); +// } +// }, +// )); +// } + +// if editor.read(cx).buffer().read(cx).is_singleton() { +// let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); +// let search_action = buffer_search::Deploy { focus: true }; + +// bar = bar.with_child(render_quick_action_bar_button( +// 1, +// "icons/magnifying_glass.svg", +// search_bar_shown, +// ( +// "Buffer Search".to_string(), +// Some(Box::new(search_action.clone())), +// ), +// cx, +// move |this, cx| { +// this.buffer_search_bar.update(cx, |buffer_search_bar, cx| { +// if search_bar_shown { +// buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); +// } else { +// buffer_search_bar.deploy(&search_action, cx); +// } +// }); +// }, +// )); +// } + +// bar.add_child(render_quick_action_bar_button( +// 2, +// "icons/magic-wand.svg", +// false, +// ("Inline Assist".into(), Some(Box::new(InlineAssist))), +// cx, +// move |this, cx| { +// if let Some(workspace) = this.workspace.upgrade(cx) { +// workspace.update(cx, |workspace, cx| { +// AssistantPanel::inline_assist(workspace, &Default::default(), cx); +// }); +// } +// }, +// )); + +// bar.into_any() +// } +// } + +#[derive(IntoElement)] +struct QuickActionBarButton { + id: ElementId, + icon: Icon, + toggled: bool, + action: Box, + tooltip: SharedString, + tooltip_meta: Option, +} + +impl QuickActionBarButton { + fn new( + id: impl Into, + icon: Icon, + toggled: bool, + action: Box, + tooltip: impl Into, + ) -> Self { + Self { + id: id.into(), + icon, + toggled, + action, + tooltip: tooltip.into(), + tooltip_meta: None, + } + } + + #[allow(dead_code)] + pub fn meta(mut self, meta: Option>) -> Self { + self.tooltip_meta = meta.map(|meta| meta.into()); + self + } +} + +impl RenderOnce for QuickActionBarButton { + type Rendered = IconButton; + + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let tooltip = self.tooltip.clone(); + let action = self.action.boxed_clone(); + let tooltip_meta = self.tooltip_meta.clone(); + + IconButton::new(self.id.clone(), self.icon) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected(self.toggled) + .tooltip(move |cx| { + if let Some(meta) = &tooltip_meta { + Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx) + } else { + Tooltip::for_action(tooltip.clone(), &*action, cx) + } + }) + .on_click({ + let action = self.action.boxed_clone(); + move |_, cx| cx.dispatch_action(action.boxed_clone()) + }) + } +} + +// fn render_quick_action_bar_button< +// F: 'static + Fn(&mut QuickActionBar, &mut ViewContext), +// >( +// index: usize, +// icon: &'static str, +// toggled: bool, +// tooltip: (String, Option>), +// cx: &mut ViewContext, +// on_click: F, +// ) -> AnyElement { +// enum QuickActionBarButton {} + +// let theme = theme::current(cx); +// let (tooltip_text, action) = tooltip; + +// MouseEventHandler::new::(index, cx, |mouse_state, _| { +// let style = theme +// .workspace +// .toolbar +// .toggleable_tool +// .in_state(toggled) +// .style_for(mouse_state); +// Svg::new(icon) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .constrained() +// .with_width(style.button_width) +// .with_height(style.button_width) +// .contained() +// .with_style(style.container) +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) +// .with_tooltip::(index, tooltip_text, action, theme.tooltip.clone(), cx) +// .into_any_named("quick action bar button") +// } + +impl ToolbarItemView for QuickActionBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + match active_pane_item { + Some(active_item) => { + self.active_item = Some(active_item.boxed_clone()); + self._inlay_hints_enabled_subscription.take(); + + if let Some(editor) = active_item.downcast::() { + let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); + self._inlay_hints_enabled_subscription = + Some(cx.observe(&editor, move |_, editor, cx| { + let editor = editor.read(cx); + let new_inlay_hints_enabled = editor.inlay_hints_enabled(); + let new_supports_inlay_hints = editor.supports_inlay_hints(cx); + let should_notify = inlay_hints_enabled != new_inlay_hints_enabled + || supports_inlay_hints != new_supports_inlay_hints; + inlay_hints_enabled = new_inlay_hints_enabled; + supports_inlay_hints = new_supports_inlay_hints; + if should_notify { + cx.notify() + } + })); + ToolbarItemLocation::PrimaryRight + } else { + ToolbarItemLocation::Hidden + } + } + None => { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a6d27fa57d4a0a9a063f4f0a30b634207ef8ac63..611514aacb44f9445674f2dca0b947eda8088ee3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -430,7 +430,7 @@ message ExpandProjectEntryResponse { } message ProjectEntryResponse { - Entry entry = 1; + optional Entry entry = 1; uint64 worktree_scan_id = 2; } @@ -1357,7 +1357,7 @@ message User { message File { uint64 worktree_id = 1; - uint64 entry_id = 2; + optional uint64 entry_id = 2; string path = 3; Timestamp mtime = 4; bool is_deleted = 5; diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 6f35bf64bc23c116c67000eabc29be07f5d6da8c..da0880377fb7ef4e381587a08c4ac3e0a9a2e4dc 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 66; +pub const PROTOCOL_VERSION: u32 = 67; diff --git a/crates/rpc2/proto/zed.proto b/crates/rpc2/proto/zed.proto index a6d27fa57d4a0a9a063f4f0a30b634207ef8ac63..611514aacb44f9445674f2dca0b947eda8088ee3 100644 --- a/crates/rpc2/proto/zed.proto +++ b/crates/rpc2/proto/zed.proto @@ -430,7 +430,7 @@ message ExpandProjectEntryResponse { } message ProjectEntryResponse { - Entry entry = 1; + optional Entry entry = 1; uint64 worktree_scan_id = 2; } @@ -1357,7 +1357,7 @@ message User { message File { uint64 worktree_id = 1; - uint64 entry_id = 2; + optional uint64 entry_id = 2; string path = 3; Timestamp mtime = 4; bool is_deleted = 5; diff --git a/crates/rpc2/src/rpc.rs b/crates/rpc2/src/rpc.rs index 6f35bf64bc23c116c67000eabc29be07f5d6da8c..da0880377fb7ef4e381587a08c4ac3e0a9a2e4dc 100644 --- a/crates/rpc2/src/rpc.rs +++ b/crates/rpc2/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 66; +pub const PROTOCOL_VERSION: u32 = 67; diff --git a/crates/semantic_index2/Cargo.toml b/crates/semantic_index2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..65ffb05ca5c8bc9f2d06a864f4c22acec3f34594 --- /dev/null +++ b/crates/semantic_index2/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "semantic_index2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/semantic_index.rs" +doctest = false + +[dependencies] +ai = { package = "ai2", path = "../ai2" } +collections = { path = "../collections" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +project = { package = "project2", path = "../project2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } +rpc = { package = "rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } +anyhow.workspace = true +postage.workspace = true +futures.workspace = true +ordered-float.workspace = true +smol.workspace = true +rusqlite.workspace = true +log.workspace = true +tree-sitter.workspace = true +lazy_static.workspace = true +serde.workspace = true +serde_json.workspace = true +async-trait.workspace = true +tiktoken-rs.workspace = true +parking_lot.workspace = true +rand.workspace = true +schemars.workspace = true +globset.workspace = true +sha1 = "0.10.5" +ndarray = { version = "0.15.0" } + +[dev-dependencies] +ai = { package = "ai2", path = "../ai2", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"]} +rust-embed = { version = "8.0", features = ["include-exclude"] } +client = { package = "client2", path = "../client2" } +node_runtime = { path = "../node_runtime"} + +pretty_assertions.workspace = true +rand.workspace = true +unindent.workspace = true +tempdir.workspace = true +ctor.workspace = true +env_logger.workspace = true + +tree-sitter-typescript.workspace = true +tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-toml.workspace = true +tree-sitter-cpp.workspace = true +tree-sitter-elixir.workspace = true +tree-sitter-lua.workspace = true +tree-sitter-ruby.workspace = true +tree-sitter-php.workspace = true diff --git a/crates/semantic_index2/README.md b/crates/semantic_index2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..85f83af121ed96a51ac84165c19cda3cd8aff7d4 --- /dev/null +++ b/crates/semantic_index2/README.md @@ -0,0 +1,20 @@ + +# Semantic Index + +## Evaluation + +### Metrics + +nDCG@k: +- "The value of NDCG is determined by comparing the relevance of the items returned by the search engine to the relevance of the item that a hypothetical "ideal" search engine would return. +- "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?" + +MRR@k: +- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list." + +MAP@k: +- "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list. + +Resources: +- [Evaluating recommendation metrics](https://www.shaped.ai/blog/evaluating-recommendation-systems-map-mmr-ndcg) +- [Math Walkthrough](https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0) diff --git a/crates/semantic_index2/eval/gpt-engineer.json b/crates/semantic_index2/eval/gpt-engineer.json new file mode 100644 index 0000000000000000000000000000000000000000..d008cc65d13b0c6a718beb57ece2393bb999029c --- /dev/null +++ b/crates/semantic_index2/eval/gpt-engineer.json @@ -0,0 +1,114 @@ +{ + "repo": "https://github.com/AntonOsika/gpt-engineer.git", + "commit": "7735a6445bae3611c62f521e6464c67c957f87c2", + "assertions": [ + { + "query": "How do I contribute to this project?", + "matches": [ + ".github/CONTRIBUTING.md:1", + "ROADMAP.md:48" + ] + }, + { + "query": "What version of the openai package is active?", + "matches": [ + "pyproject.toml:14" + ] + }, + { + "query": "Ask user for clarification", + "matches": [ + "gpt_engineer/steps.py:69" + ] + }, + { + "query": "generate tests for python code", + "matches": [ + "gpt_engineer/steps.py:153" + ] + }, + { + "query": "get item from database based on key", + "matches": [ + "gpt_engineer/db.py:42", + "gpt_engineer/db.py:68" + ] + }, + { + "query": "prompt user to select files", + "matches": [ + "gpt_engineer/file_selector.py:171", + "gpt_engineer/file_selector.py:306", + "gpt_engineer/file_selector.py:289", + "gpt_engineer/file_selector.py:234" + ] + }, + { + "query": "send to rudderstack", + "matches": [ + "gpt_engineer/collect.py:11", + "gpt_engineer/collect.py:38" + ] + }, + { + "query": "parse code blocks from chat messages", + "matches": [ + "gpt_engineer/chat_to_files.py:10", + "docs/intro/chat_parsing.md:1" + ] + }, + { + "query": "how do I use the docker cli?", + "matches": [ + "docker/README.md:1" + ] + }, + { + "query": "ask the user if the code ran successfully?", + "matches": [ + "gpt_engineer/learning.py:54" + ] + }, + { + "query": "how is consent granted by the user?", + "matches": [ + "gpt_engineer/learning.py:107", + "gpt_engineer/learning.py:130", + "gpt_engineer/learning.py:152" + ] + }, + { + "query": "what are all the different steps the agent can take?", + "matches": [ + "docs/intro/steps_module.md:1", + "gpt_engineer/steps.py:391" + ] + }, + { + "query": "ask the user for clarification?", + "matches": [ + "gpt_engineer/steps.py:69" + ] + }, + { + "query": "what models are available?", + "matches": [ + "gpt_engineer/ai.py:315", + "gpt_engineer/ai.py:341", + "docs/open-models.md:1" + ] + }, + { + "query": "what is the current focus of the project?", + "matches": [ + "ROADMAP.md:11" + ] + }, + { + "query": "does the agent know how to fix code?", + "matches": [ + "gpt_engineer/steps.py:367" + ] + } + ] +} diff --git a/crates/semantic_index2/eval/tree-sitter.json b/crates/semantic_index2/eval/tree-sitter.json new file mode 100644 index 0000000000000000000000000000000000000000..d3dcc86937d723e8a00e2b2bfc91e86163c1aac3 --- /dev/null +++ b/crates/semantic_index2/eval/tree-sitter.json @@ -0,0 +1,104 @@ +{ + "repo": "https://github.com/tree-sitter/tree-sitter.git", + "commit": "46af27796a76c72d8466627d499f2bca4af958ee", + "assertions": [ + { + "query": "What attributes are available for the tags configuration struct?", + "matches": [ + "tags/src/lib.rs:24" + ] + }, + { + "query": "create a new tag configuration", + "matches": [ + "tags/src/lib.rs:119" + ] + }, + { + "query": "generate tags based on config", + "matches": [ + "tags/src/lib.rs:261" + ] + }, + { + "query": "match on ts quantifier in rust", + "matches": [ + "lib/binding_rust/lib.rs:139" + ] + }, + { + "query": "cli command to generate tags", + "matches": [ + "cli/src/tags.rs:10" + ] + }, + { + "query": "what version of the tree-sitter-tags package is active?", + "matches": [ + "tags/Cargo.toml:4" + ] + }, + { + "query": "Insert a new parse state", + "matches": [ + "cli/src/generate/build_tables/build_parse_table.rs:153" + ] + }, + { + "query": "Handle conflict when numerous actions occur on the same symbol", + "matches": [ + "cli/src/generate/build_tables/build_parse_table.rs:363", + "cli/src/generate/build_tables/build_parse_table.rs:442" + ] + }, + { + "query": "Match based on associativity of actions", + "matches": [ + "cri/src/generate/build_tables/build_parse_table.rs:542" + ] + }, + { + "query": "Format token set display", + "matches": [ + "cli/src/generate/build_tables/item.rs:246" + ] + }, + { + "query": "extract choices from rule", + "matches": [ + "cli/src/generate/prepare_grammar/flatten_grammar.rs:124" + ] + }, + { + "query": "How do we identify if a symbol is being used?", + "matches": [ + "cli/src/generate/prepare_grammar/flatten_grammar.rs:175" + ] + }, + { + "query": "How do we launch the playground?", + "matches": [ + "cli/src/playground.rs:46" + ] + }, + { + "query": "How do we test treesitter query matches in rust?", + "matches": [ + "cli/src/query_testing.rs:152", + "cli/src/tests/query_test.rs:781", + "cli/src/tests/query_test.rs:2163", + "cli/src/tests/query_test.rs:3781", + "cli/src/tests/query_test.rs:887" + ] + }, + { + "query": "What does the CLI do?", + "matches": [ + "cli/README.md:10", + "cli/loader/README.md:3", + "docs/section-5-implementation.md:14", + "docs/section-5-implementation.md:18" + ] + } + ] +} diff --git a/crates/semantic_index2/src/db.rs b/crates/semantic_index2/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..f34baeaaae1373f4186e604689ea66df2094925d --- /dev/null +++ b/crates/semantic_index2/src/db.rs @@ -0,0 +1,603 @@ +use crate::{ + parsing::{Span, SpanDigest}, + SEMANTIC_INDEX_VERSION, +}; +use ai::embedding::Embedding; +use anyhow::{anyhow, Context, Result}; +use collections::HashMap; +use futures::channel::oneshot; +use gpui::BackgroundExecutor; +use ndarray::{Array1, Array2}; +use ordered_float::OrderedFloat; +use project::Fs; +use rpc::proto::Timestamp; +use rusqlite::params; +use rusqlite::types::Value; +use std::{ + future::Future, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::SystemTime, +}; +use util::{paths::PathMatcher, TryFutureExt}; + +pub fn argsort(data: &[T]) -> Vec { + let mut indices = (0..data.len()).collect::>(); + indices.sort_by_key(|&i| &data[i]); + indices.reverse(); + indices +} + +#[derive(Debug)] +pub struct FileRecord { + pub id: usize, + pub relative_path: String, + pub mtime: Timestamp, +} + +#[derive(Clone)] +pub struct VectorDatabase { + path: Arc, + transactions: + smol::channel::Sender>, +} + +impl VectorDatabase { + pub async fn new( + fs: Arc, + path: Arc, + executor: BackgroundExecutor, + ) -> Result { + if let Some(db_directory) = path.parent() { + fs.create_dir(db_directory).await?; + } + + let (transactions_tx, transactions_rx) = smol::channel::unbounded::< + Box, + >(); + executor + .spawn({ + let path = path.clone(); + async move { + let mut connection = rusqlite::Connection::open(&path)?; + + connection.pragma_update(None, "journal_mode", "wal")?; + connection.pragma_update(None, "synchronous", "normal")?; + connection.pragma_update(None, "cache_size", 1000000)?; + connection.pragma_update(None, "temp_store", "MEMORY")?; + + while let Ok(transaction) = transactions_rx.recv().await { + transaction(&mut connection); + } + + anyhow::Ok(()) + } + .log_err() + }) + .detach(); + let this = Self { + transactions: transactions_tx, + path, + }; + this.initialize_database().await?; + Ok(this) + } + + pub fn path(&self) -> &Arc { + &self.path + } + + fn transact(&self, f: F) -> impl Future> + where + F: 'static + Send + FnOnce(&rusqlite::Transaction) -> Result, + T: 'static + Send, + { + let (tx, rx) = oneshot::channel(); + let transactions = self.transactions.clone(); + async move { + if transactions + .send(Box::new(|connection| { + let result = connection + .transaction() + .map_err(|err| anyhow!(err)) + .and_then(|transaction| { + let result = f(&transaction)?; + transaction.commit()?; + Ok(result) + }); + let _ = tx.send(result); + })) + .await + .is_err() + { + return Err(anyhow!("connection was dropped"))?; + } + rx.await? + } + } + + fn initialize_database(&self) -> impl Future> { + self.transact(|db| { + rusqlite::vtab::array::load_module(&db)?; + + // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped + let version_query = db.prepare("SELECT version from semantic_index_config"); + let version = version_query + .and_then(|mut query| query.query_row([], |row| Ok(row.get::<_, i64>(0)?))); + if version.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64) { + log::trace!("vector database schema up to date"); + return Ok(()); + } + + log::trace!("vector database schema out of date. updating..."); + // We renamed the `documents` table to `spans`, so we want to drop + // `documents` without recreating it if it exists. + db.execute("DROP TABLE IF EXISTS documents", []) + .context("failed to drop 'documents' table")?; + db.execute("DROP TABLE IF EXISTS spans", []) + .context("failed to drop 'spans' table")?; + db.execute("DROP TABLE IF EXISTS files", []) + .context("failed to drop 'files' table")?; + db.execute("DROP TABLE IF EXISTS worktrees", []) + .context("failed to drop 'worktrees' table")?; + db.execute("DROP TABLE IF EXISTS semantic_index_config", []) + .context("failed to drop 'semantic_index_config' table")?; + + // Initialize Vector Databasing Tables + db.execute( + "CREATE TABLE semantic_index_config ( + version INTEGER NOT NULL + )", + [], + )?; + + db.execute( + "INSERT INTO semantic_index_config (version) VALUES (?1)", + params![SEMANTIC_INDEX_VERSION], + )?; + + db.execute( + "CREATE TABLE worktrees ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + absolute_path VARCHAR NOT NULL + ); + CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path); + ", + [], + )?; + + db.execute( + "CREATE TABLE files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + worktree_id INTEGER NOT NULL, + relative_path VARCHAR NOT NULL, + mtime_seconds INTEGER NOT NULL, + mtime_nanos INTEGER NOT NULL, + FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE + )", + [], + )?; + + db.execute( + "CREATE UNIQUE INDEX files_worktree_id_and_relative_path ON files (worktree_id, relative_path)", + [], + )?; + + db.execute( + "CREATE TABLE spans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id INTEGER NOT NULL, + start_byte INTEGER NOT NULL, + end_byte INTEGER NOT NULL, + name VARCHAR NOT NULL, + embedding BLOB NOT NULL, + digest BLOB NOT NULL, + FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE + )", + [], + )?; + db.execute( + "CREATE INDEX spans_digest ON spans (digest)", + [], + )?; + + log::trace!("vector database initialized with updated schema."); + Ok(()) + }) + } + + pub fn delete_file( + &self, + worktree_id: i64, + delete_path: Arc, + ) -> impl Future> { + self.transact(move |db| { + db.execute( + "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2", + params![worktree_id, delete_path.to_str()], + )?; + Ok(()) + }) + } + + pub fn insert_file( + &self, + worktree_id: i64, + path: Arc, + mtime: SystemTime, + spans: Vec, + ) -> impl Future> { + self.transact(move |db| { + // Return the existing ID, if both the file and mtime match + let mtime = Timestamp::from(mtime); + + db.execute( + " + REPLACE INTO files + (worktree_id, relative_path, mtime_seconds, mtime_nanos) + VALUES (?1, ?2, ?3, ?4) + ", + params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos], + )?; + + let file_id = db.last_insert_rowid(); + + let mut query = db.prepare( + " + INSERT INTO spans + (file_id, start_byte, end_byte, name, embedding, digest) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ", + )?; + + for span in spans { + query.execute(params![ + file_id, + span.range.start.to_string(), + span.range.end.to_string(), + span.name, + span.embedding, + span.digest + ])?; + } + + Ok(()) + }) + } + + pub fn worktree_previously_indexed( + &self, + worktree_root_path: &Path, + ) -> impl Future> { + let worktree_root_path = worktree_root_path.to_string_lossy().into_owned(); + self.transact(move |db| { + let mut worktree_query = + db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?; + let worktree_id = worktree_query + .query_row(params![worktree_root_path], |row| Ok(row.get::<_, i64>(0)?)); + + if worktree_id.is_ok() { + return Ok(true); + } else { + return Ok(false); + } + }) + } + + pub fn embeddings_for_digests( + &self, + digests: Vec, + ) -> impl Future>> { + self.transact(move |db| { + let mut query = db.prepare( + " + SELECT digest, embedding + FROM spans + WHERE digest IN rarray(?) + ", + )?; + let mut embeddings_by_digest = HashMap::default(); + let digests = Rc::new( + digests + .into_iter() + .map(|p| Value::Blob(p.0.to_vec())) + .collect::>(), + ); + let rows = query.query_map(params![digests], |row| { + Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) + })?; + + for row in rows { + if let Ok(row) = row { + embeddings_by_digest.insert(row.0, row.1); + } + } + + Ok(embeddings_by_digest) + }) + } + + pub fn embeddings_for_files( + &self, + worktree_id_file_paths: HashMap>>, + ) -> impl Future>> { + self.transact(move |db| { + let mut query = db.prepare( + " + SELECT digest, embedding + FROM spans + LEFT JOIN files ON files.id = spans.file_id + WHERE files.worktree_id = ? AND files.relative_path IN rarray(?) + ", + )?; + let mut embeddings_by_digest = HashMap::default(); + for (worktree_id, file_paths) in worktree_id_file_paths { + let file_paths = Rc::new( + file_paths + .into_iter() + .map(|p| Value::Text(p.to_string_lossy().into_owned())) + .collect::>(), + ); + let rows = query.query_map(params![worktree_id, file_paths], |row| { + Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) + })?; + + for row in rows { + if let Ok(row) = row { + embeddings_by_digest.insert(row.0, row.1); + } + } + } + + Ok(embeddings_by_digest) + }) + } + + pub fn find_or_create_worktree( + &self, + worktree_root_path: Arc, + ) -> impl Future> { + self.transact(move |db| { + let mut worktree_query = + db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?; + let worktree_id = worktree_query + .query_row(params![worktree_root_path.to_string_lossy()], |row| { + Ok(row.get::<_, i64>(0)?) + }); + + if worktree_id.is_ok() { + return Ok(worktree_id?); + } + + // If worktree_id is Err, insert new worktree + db.execute( + "INSERT into worktrees (absolute_path) VALUES (?1)", + params![worktree_root_path.to_string_lossy()], + )?; + Ok(db.last_insert_rowid()) + }) + } + + pub fn get_file_mtimes( + &self, + worktree_id: i64, + ) -> impl Future>> { + self.transact(move |db| { + let mut statement = db.prepare( + " + SELECT relative_path, mtime_seconds, mtime_nanos + FROM files + WHERE worktree_id = ?1 + ORDER BY relative_path", + )?; + let mut result: HashMap = HashMap::default(); + for row in statement.query_map(params![worktree_id], |row| { + Ok(( + row.get::<_, String>(0)?.into(), + Timestamp { + seconds: row.get(1)?, + nanos: row.get(2)?, + } + .into(), + )) + })? { + let row = row?; + result.insert(row.0, row.1); + } + Ok(result) + }) + } + + pub fn top_k_search( + &self, + query_embedding: &Embedding, + limit: usize, + file_ids: &[i64], + ) -> impl Future)>>> { + let file_ids = file_ids.to_vec(); + let query = query_embedding.clone().0; + let query = Array1::from_vec(query); + self.transact(move |db| { + let mut query_statement = db.prepare( + " + SELECT + id, embedding + FROM + spans + WHERE + file_id IN rarray(?) + ", + )?; + + let deserialized_rows = query_statement + .query_map(params![ids_to_sql(&file_ids)], |row| { + Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?)) + })? + .filter_map(|row| row.ok()) + .collect::>(); + + if deserialized_rows.len() == 0 { + return Ok(Vec::new()); + } + + // Get Length of Embeddings Returned + let embedding_len = deserialized_rows[0].1 .0.len(); + + let batch_n = 1000; + let mut batches = Vec::new(); + let mut batch_ids = Vec::new(); + let mut batch_embeddings: Vec = Vec::new(); + deserialized_rows.iter().for_each(|(id, embedding)| { + batch_ids.push(id); + batch_embeddings.extend(&embedding.0); + + if batch_ids.len() == batch_n { + let embeddings = std::mem::take(&mut batch_embeddings); + let ids = std::mem::take(&mut batch_ids); + let array = + Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings); + match array { + Ok(array) => { + batches.push((ids, array)); + } + Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err), + } + } + }); + + if batch_ids.len() > 0 { + let array = Array2::from_shape_vec( + (batch_ids.len(), embedding_len), + batch_embeddings.clone(), + ); + match array { + Ok(array) => { + batches.push((batch_ids.clone(), array)); + } + Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err), + } + } + + let mut ids: Vec = Vec::new(); + let mut results = Vec::new(); + for (batch_ids, array) in batches { + let scores = array + .dot(&query.t()) + .to_vec() + .iter() + .map(|score| OrderedFloat(*score)) + .collect::>>(); + results.extend(scores); + ids.extend(batch_ids); + } + + let sorted_idx = argsort(&results); + let mut sorted_results = Vec::new(); + let last_idx = limit.min(sorted_idx.len()); + for idx in &sorted_idx[0..last_idx] { + sorted_results.push((ids[*idx] as i64, results[*idx])) + } + + Ok(sorted_results) + }) + } + + pub fn retrieve_included_file_ids( + &self, + worktree_ids: &[i64], + includes: &[PathMatcher], + excludes: &[PathMatcher], + ) -> impl Future>> { + let worktree_ids = worktree_ids.to_vec(); + let includes = includes.to_vec(); + let excludes = excludes.to_vec(); + self.transact(move |db| { + let mut file_query = db.prepare( + " + SELECT + id, relative_path + FROM + files + WHERE + worktree_id IN rarray(?) + ", + )?; + + let mut file_ids = Vec::::new(); + let mut rows = file_query.query([ids_to_sql(&worktree_ids)])?; + + while let Some(row) = rows.next()? { + let file_id = row.get(0)?; + let relative_path = row.get_ref(1)?.as_str()?; + let included = + includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path)); + let excluded = excludes.iter().any(|glob| glob.is_match(relative_path)); + if included && !excluded { + file_ids.push(file_id); + } + } + + anyhow::Ok(file_ids) + }) + } + + pub fn spans_for_ids( + &self, + ids: &[i64], + ) -> impl Future)>>> { + let ids = ids.to_vec(); + self.transact(move |db| { + let mut statement = db.prepare( + " + SELECT + spans.id, + files.worktree_id, + files.relative_path, + spans.start_byte, + spans.end_byte + FROM + spans, files + WHERE + spans.file_id = files.id AND + spans.id in rarray(?) + ", + )?; + + let result_iter = statement.query_map(params![ids_to_sql(&ids)], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, i64>(1)?, + row.get::<_, String>(2)?.into(), + row.get(3)?..row.get(4)?, + )) + })?; + + let mut values_by_id = HashMap::)>::default(); + for row in result_iter { + let (id, worktree_id, path, range) = row?; + values_by_id.insert(id, (worktree_id, path, range)); + } + + let mut results = Vec::with_capacity(ids.len()); + for id in &ids { + let value = values_by_id + .remove(id) + .ok_or(anyhow!("missing span id {}", id))?; + results.push(value); + } + + Ok(results) + }) + } +} + +fn ids_to_sql(ids: &[i64]) -> Rc> { + Rc::new( + ids.iter() + .copied() + .map(|v| rusqlite::types::Value::from(v)) + .collect::>(), + ) +} diff --git a/crates/semantic_index2/src/embedding_queue.rs b/crates/semantic_index2/src/embedding_queue.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2371a1196b59834c0d5fcc034f3b0a364e4d38b --- /dev/null +++ b/crates/semantic_index2/src/embedding_queue.rs @@ -0,0 +1,169 @@ +use crate::{parsing::Span, JobHandle}; +use ai::embedding::EmbeddingProvider; +use gpui::BackgroundExecutor; +use parking_lot::Mutex; +use smol::channel; +use std::{mem, ops::Range, path::Path, sync::Arc, time::SystemTime}; + +#[derive(Clone)] +pub struct FileToEmbed { + pub worktree_id: i64, + pub path: Arc, + pub mtime: SystemTime, + pub spans: Vec, + pub job_handle: JobHandle, +} + +impl std::fmt::Debug for FileToEmbed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FileToEmbed") + .field("worktree_id", &self.worktree_id) + .field("path", &self.path) + .field("mtime", &self.mtime) + .field("spans", &self.spans) + .finish_non_exhaustive() + } +} + +impl PartialEq for FileToEmbed { + fn eq(&self, other: &Self) -> bool { + self.worktree_id == other.worktree_id + && self.path == other.path + && self.mtime == other.mtime + && self.spans == other.spans + } +} + +pub struct EmbeddingQueue { + embedding_provider: Arc, + pending_batch: Vec, + executor: BackgroundExecutor, + pending_batch_token_count: usize, + finished_files_tx: channel::Sender, + finished_files_rx: channel::Receiver, +} + +#[derive(Clone)] +pub struct FileFragmentToEmbed { + file: Arc>, + span_range: Range, +} + +impl EmbeddingQueue { + pub fn new( + embedding_provider: Arc, + executor: BackgroundExecutor, + ) -> Self { + let (finished_files_tx, finished_files_rx) = channel::unbounded(); + Self { + embedding_provider, + executor, + pending_batch: Vec::new(), + pending_batch_token_count: 0, + finished_files_tx, + finished_files_rx, + } + } + + pub fn push(&mut self, file: FileToEmbed) { + if file.spans.is_empty() { + self.finished_files_tx.try_send(file).unwrap(); + return; + } + + let file = Arc::new(Mutex::new(file)); + + self.pending_batch.push(FileFragmentToEmbed { + file: file.clone(), + span_range: 0..0, + }); + + let mut fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range; + for (ix, span) in file.lock().spans.iter().enumerate() { + let span_token_count = if span.embedding.is_none() { + span.token_count + } else { + 0 + }; + + let next_token_count = self.pending_batch_token_count + span_token_count; + if next_token_count > self.embedding_provider.max_tokens_per_batch() { + let range_end = fragment_range.end; + self.flush(); + self.pending_batch.push(FileFragmentToEmbed { + file: file.clone(), + span_range: range_end..range_end, + }); + fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range; + } + + fragment_range.end = ix + 1; + self.pending_batch_token_count += span_token_count; + } + } + + pub fn flush(&mut self) { + let batch = mem::take(&mut self.pending_batch); + self.pending_batch_token_count = 0; + if batch.is_empty() { + return; + } + + let finished_files_tx = self.finished_files_tx.clone(); + let embedding_provider = self.embedding_provider.clone(); + + self.executor + .spawn(async move { + let mut spans = Vec::new(); + for fragment in &batch { + let file = fragment.file.lock(); + spans.extend( + file.spans[fragment.span_range.clone()] + .iter() + .filter(|d| d.embedding.is_none()) + .map(|d| d.content.clone()), + ); + } + + // If spans is 0, just send the fragment to the finished files if its the last one. + if spans.is_empty() { + for fragment in batch.clone() { + if let Some(file) = Arc::into_inner(fragment.file) { + finished_files_tx.try_send(file.into_inner()).unwrap(); + } + } + return; + }; + + match embedding_provider.embed_batch(spans).await { + Ok(embeddings) => { + let mut embeddings = embeddings.into_iter(); + for fragment in batch { + for span in &mut fragment.file.lock().spans[fragment.span_range.clone()] + .iter_mut() + .filter(|d| d.embedding.is_none()) + { + if let Some(embedding) = embeddings.next() { + span.embedding = Some(embedding); + } else { + log::error!("number of embeddings != number of documents"); + } + } + + if let Some(file) = Arc::into_inner(fragment.file) { + finished_files_tx.try_send(file.into_inner()).unwrap(); + } + } + } + Err(error) => { + log::error!("{:?}", error); + } + } + }) + .detach(); + } + + pub fn finished_files(&self) -> channel::Receiver { + self.finished_files_rx.clone() + } +} diff --git a/crates/semantic_index2/src/parsing.rs b/crates/semantic_index2/src/parsing.rs new file mode 100644 index 0000000000000000000000000000000000000000..cb15ca453b2c0640739bd44a95482ca527b8d91b --- /dev/null +++ b/crates/semantic_index2/src/parsing.rs @@ -0,0 +1,414 @@ +use ai::{ + embedding::{Embedding, EmbeddingProvider}, + models::TruncationDirection, +}; +use anyhow::{anyhow, Result}; +use language::{Grammar, Language}; +use rusqlite::{ + types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}, + ToSql, +}; +use sha1::{Digest, Sha1}; +use std::{ + borrow::Cow, + cmp::{self, Reverse}, + collections::HashSet, + ops::Range, + path::Path, + sync::Arc, +}; +use tree_sitter::{Parser, QueryCursor}; + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct SpanDigest(pub [u8; 20]); + +impl FromSql for SpanDigest { + fn column_result(value: ValueRef) -> FromSqlResult { + let blob = value.as_blob()?; + let bytes = + blob.try_into() + .map_err(|_| rusqlite::types::FromSqlError::InvalidBlobSize { + expected_size: 20, + blob_size: blob.len(), + })?; + return Ok(SpanDigest(bytes)); + } +} + +impl ToSql for SpanDigest { + fn to_sql(&self) -> rusqlite::Result { + self.0.to_sql() + } +} + +impl From<&'_ str> for SpanDigest { + fn from(value: &'_ str) -> Self { + let mut sha1 = Sha1::new(); + sha1.update(value); + Self(sha1.finalize().into()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Span { + pub name: String, + pub range: Range, + pub content: String, + pub embedding: Option, + pub digest: SpanDigest, + pub token_count: usize, +} + +const CODE_CONTEXT_TEMPLATE: &str = + "The below code snippet is from file ''\n\n```\n\n```"; +const ENTIRE_FILE_TEMPLATE: &str = + "The below snippet is from file ''\n\n```\n\n```"; +const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file ''\n\n"; +pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[ + "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme", +]; + +pub struct CodeContextRetriever { + pub parser: Parser, + pub cursor: QueryCursor, + pub embedding_provider: Arc, +} + +// Every match has an item, this represents the fundamental treesitter symbol and anchors the search +// Every match has one or more 'name' captures. These indicate the display range of the item for deduplication. +// If there are preceeding comments, we track this with a context capture +// If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture +// If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture +#[derive(Debug, Clone)] +pub struct CodeContextMatch { + pub start_col: usize, + pub item_range: Option>, + pub name_range: Option>, + pub context_ranges: Vec>, + pub collapse_ranges: Vec>, +} + +impl CodeContextRetriever { + pub fn new(embedding_provider: Arc) -> Self { + Self { + parser: Parser::new(), + cursor: QueryCursor::new(), + embedding_provider, + } + } + + fn parse_entire_file( + &self, + relative_path: Option<&Path>, + language_name: Arc, + content: &str, + ) -> Result> { + let document_span = ENTIRE_FILE_TEMPLATE + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) + .replace("", language_name.as_ref()) + .replace("", &content); + let digest = SpanDigest::from(document_span.as_str()); + let model = self.embedding_provider.base_model(); + let document_span = model.truncate( + &document_span, + model.capacity()?, + ai::models::TruncationDirection::End, + )?; + let token_count = model.count_tokens(&document_span)?; + + Ok(vec![Span { + range: 0..content.len(), + content: document_span, + embedding: Default::default(), + name: language_name.to_string(), + digest, + token_count, + }]) + } + + fn parse_markdown_file( + &self, + relative_path: Option<&Path>, + content: &str, + ) -> Result> { + let document_span = MARKDOWN_CONTEXT_TEMPLATE + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) + .replace("", &content); + let digest = SpanDigest::from(document_span.as_str()); + + let model = self.embedding_provider.base_model(); + let document_span = model.truncate( + &document_span, + model.capacity()?, + ai::models::TruncationDirection::End, + )?; + let token_count = model.count_tokens(&document_span)?; + + Ok(vec![Span { + range: 0..content.len(), + content: document_span, + embedding: None, + name: "Markdown".to_string(), + digest, + token_count, + }]) + } + + fn get_matches_in_file( + &mut self, + content: &str, + grammar: &Arc, + ) -> Result> { + let embedding_config = grammar + .embedding_config + .as_ref() + .ok_or_else(|| anyhow!("no embedding queries"))?; + self.parser.set_language(grammar.ts_language).unwrap(); + + let tree = self + .parser + .parse(&content, None) + .ok_or_else(|| anyhow!("parsing failed"))?; + + let mut captures: Vec = Vec::new(); + let mut collapse_ranges: Vec> = Vec::new(); + let mut keep_ranges: Vec> = Vec::new(); + for mat in self.cursor.matches( + &embedding_config.query, + tree.root_node(), + content.as_bytes(), + ) { + let mut start_col = 0; + let mut item_range: Option> = None; + let mut name_range: Option> = None; + let mut context_ranges: Vec> = Vec::new(); + collapse_ranges.clear(); + keep_ranges.clear(); + for capture in mat.captures { + if capture.index == embedding_config.item_capture_ix { + item_range = Some(capture.node.byte_range()); + start_col = capture.node.start_position().column; + } else if Some(capture.index) == embedding_config.name_capture_ix { + name_range = Some(capture.node.byte_range()); + } else if Some(capture.index) == embedding_config.context_capture_ix { + context_ranges.push(capture.node.byte_range()); + } else if Some(capture.index) == embedding_config.collapse_capture_ix { + collapse_ranges.push(capture.node.byte_range()); + } else if Some(capture.index) == embedding_config.keep_capture_ix { + keep_ranges.push(capture.node.byte_range()); + } + } + + captures.push(CodeContextMatch { + start_col, + item_range, + name_range, + context_ranges, + collapse_ranges: subtract_ranges(&collapse_ranges, &keep_ranges), + }); + } + Ok(captures) + } + + pub fn parse_file_with_template( + &mut self, + relative_path: Option<&Path>, + content: &str, + language: Arc, + ) -> Result> { + let language_name = language.name(); + + if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) { + return self.parse_entire_file(relative_path, language_name, &content); + } else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) { + return self.parse_markdown_file(relative_path, &content); + } + + let mut spans = self.parse_file(content, language)?; + for span in &mut spans { + let document_content = CODE_CONTEXT_TEMPLATE + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) + .replace("", language_name.as_ref()) + .replace("item", &span.content); + + let model = self.embedding_provider.base_model(); + let document_content = model.truncate( + &document_content, + model.capacity()?, + TruncationDirection::End, + )?; + let token_count = model.count_tokens(&document_content)?; + + span.content = document_content; + span.token_count = token_count; + } + Ok(spans) + } + + pub fn parse_file(&mut self, content: &str, language: Arc) -> Result> { + let grammar = language + .grammar() + .ok_or_else(|| anyhow!("no grammar for language"))?; + + // Iterate through query matches + let matches = self.get_matches_in_file(content, grammar)?; + + let language_scope = language.default_scope(); + let placeholder = language_scope.collapsed_placeholder(); + + let mut spans = Vec::new(); + let mut collapsed_ranges_within = Vec::new(); + let mut parsed_name_ranges = HashSet::new(); + for (i, context_match) in matches.iter().enumerate() { + // Items which are collapsible but not embeddable have no item range + let item_range = if let Some(item_range) = context_match.item_range.clone() { + item_range + } else { + continue; + }; + + // Checks for deduplication + let name; + if let Some(name_range) = context_match.name_range.clone() { + name = content + .get(name_range.clone()) + .map_or(String::new(), |s| s.to_string()); + if parsed_name_ranges.contains(&name_range) { + continue; + } + parsed_name_ranges.insert(name_range); + } else { + name = String::new(); + } + + collapsed_ranges_within.clear(); + 'outer: for remaining_match in &matches[(i + 1)..] { + for collapsed_range in &remaining_match.collapse_ranges { + if item_range.start <= collapsed_range.start + && item_range.end >= collapsed_range.end + { + collapsed_ranges_within.push(collapsed_range.clone()); + } else { + break 'outer; + } + } + } + + collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end))); + + let mut span_content = String::new(); + for context_range in &context_match.context_ranges { + add_content_from_range( + &mut span_content, + content, + context_range.clone(), + context_match.start_col, + ); + span_content.push_str("\n"); + } + + let mut offset = item_range.start; + for collapsed_range in &collapsed_ranges_within { + if collapsed_range.start > offset { + add_content_from_range( + &mut span_content, + content, + offset..collapsed_range.start, + context_match.start_col, + ); + offset = collapsed_range.start; + } + + if collapsed_range.end > offset { + span_content.push_str(placeholder); + offset = collapsed_range.end; + } + } + + if offset < item_range.end { + add_content_from_range( + &mut span_content, + content, + offset..item_range.end, + context_match.start_col, + ); + } + + let sha1 = SpanDigest::from(span_content.as_str()); + spans.push(Span { + name, + content: span_content, + range: item_range.clone(), + embedding: None, + digest: sha1, + token_count: 0, + }) + } + + return Ok(spans); + } +} + +pub(crate) fn subtract_ranges( + ranges: &[Range], + ranges_to_subtract: &[Range], +) -> Vec> { + let mut result = Vec::new(); + + let mut ranges_to_subtract = ranges_to_subtract.iter().peekable(); + + for range in ranges { + let mut offset = range.start; + + while offset < range.end { + if let Some(range_to_subtract) = ranges_to_subtract.peek() { + if offset < range_to_subtract.start { + let next_offset = cmp::min(range_to_subtract.start, range.end); + result.push(offset..next_offset); + offset = next_offset; + } else { + let next_offset = cmp::min(range_to_subtract.end, range.end); + offset = next_offset; + } + + if offset >= range_to_subtract.end { + ranges_to_subtract.next(); + } + } else { + result.push(offset..range.end); + offset = range.end; + } + } + } + + result +} + +fn add_content_from_range( + output: &mut String, + content: &str, + range: Range, + start_col: usize, +) { + for mut line in content.get(range.clone()).unwrap_or("").lines() { + for _ in 0..start_col { + if line.starts_with(' ') { + line = &line[1..]; + } else { + break; + } + } + output.push_str(line); + output.push('\n'); + } + output.pop(); +} diff --git a/crates/semantic_index2/src/semantic_index.rs b/crates/semantic_index2/src/semantic_index.rs new file mode 100644 index 0000000000000000000000000000000000000000..0b207b0bf68b3c504050d62c7b60a99d5dbb5804 --- /dev/null +++ b/crates/semantic_index2/src/semantic_index.rs @@ -0,0 +1,1280 @@ +mod db; +mod embedding_queue; +mod parsing; +pub mod semantic_index_settings; + +#[cfg(test)] +mod semantic_index_tests; + +use crate::semantic_index_settings::SemanticIndexSettings; +use ai::embedding::{Embedding, EmbeddingProvider}; +use ai::providers::open_ai::OpenAIEmbeddingProvider; +use anyhow::{anyhow, Context as _, Result}; +use collections::{BTreeMap, HashMap, HashSet}; +use db::VectorDatabase; +use embedding_queue::{EmbeddingQueue, FileToEmbed}; +use futures::{future, FutureExt, StreamExt}; +use gpui::{ + AppContext, AsyncAppContext, BorrowWindow, Context, Model, ModelContext, Task, ViewContext, + WeakModel, +}; +use language::{Anchor, Bias, Buffer, Language, LanguageRegistry}; +use lazy_static::lazy_static; +use ordered_float::OrderedFloat; +use parking_lot::Mutex; +use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES}; +use postage::watch; +use project::{Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId}; +use settings::Settings; +use smol::channel; +use std::{ + cmp::Reverse, + env, + future::Future, + mem, + ops::Range, + path::{Path, PathBuf}, + sync::{Arc, Weak}, + time::{Duration, Instant, SystemTime}, +}; +use util::paths::PathMatcher; +use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt}; +use workspace::Workspace; + +const SEMANTIC_INDEX_VERSION: usize = 11; +const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60); +const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250); + +lazy_static! { + static ref OPENAI_API_KEY: Option = env::var("OPENAI_API_KEY").ok(); +} + +pub fn init( + fs: Arc, + http_client: Arc, + language_registry: Arc, + cx: &mut AppContext, +) { + SemanticIndexSettings::register(cx); + + let db_file_path = EMBEDDINGS_DIR + .join(Path::new(RELEASE_CHANNEL_NAME.as_str())) + .join("embeddings_db"); + + cx.observe_new_views( + |workspace: &mut Workspace, cx: &mut ViewContext| { + let Some(semantic_index) = SemanticIndex::global(cx) else { + return; + }; + let project = workspace.project().clone(); + + if project.read(cx).is_local() { + cx.app_mut() + .spawn(|mut cx| async move { + let previously_indexed = semantic_index + .update(&mut cx, |index, cx| { + index.project_previously_indexed(&project, cx) + })? + .await?; + if previously_indexed { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx))? + .await?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + }, + ) + .detach(); + + cx.spawn(move |cx| async move { + let semantic_index = SemanticIndex::new( + fs, + db_file_path, + Arc::new(OpenAIEmbeddingProvider::new( + http_client, + cx.background_executor().clone(), + )), + language_registry, + cx.clone(), + ) + .await?; + + cx.update(|cx| cx.set_global(semantic_index.clone()))?; + + anyhow::Ok(()) + }) + .detach(); +} + +#[derive(Copy, Clone, Debug)] +pub enum SemanticIndexStatus { + NotAuthenticated, + NotIndexed, + Indexed, + Indexing { + remaining_files: usize, + rate_limit_expiry: Option, + }, +} + +pub struct SemanticIndex { + fs: Arc, + db: VectorDatabase, + embedding_provider: Arc, + language_registry: Arc, + parsing_files_tx: channel::Sender<(Arc>, PendingFile)>, + _embedding_task: Task<()>, + _parsing_files_tasks: Vec>, + projects: HashMap, ProjectState>, +} + +struct ProjectState { + worktrees: HashMap, + pending_file_count_rx: watch::Receiver, + pending_file_count_tx: Arc>>, + pending_index: usize, + _subscription: gpui::Subscription, + _observe_pending_file_count: Task<()>, +} + +enum WorktreeState { + Registering(RegisteringWorktreeState), + Registered(RegisteredWorktreeState), +} + +impl WorktreeState { + fn is_registered(&self) -> bool { + matches!(self, Self::Registered(_)) + } + + fn paths_changed( + &mut self, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + worktree: &Worktree, + ) { + let changed_paths = match self { + Self::Registering(state) => &mut state.changed_paths, + Self::Registered(state) => &mut state.changed_paths, + }; + + for (path, entry_id, change) in changes.iter() { + let Some(entry) = worktree.entry_for_id(*entry_id) else { + continue; + }; + if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() { + continue; + } + changed_paths.insert( + path.clone(), + ChangedPathInfo { + mtime: entry.mtime, + is_deleted: *change == PathChange::Removed, + }, + ); + } + } +} + +struct RegisteringWorktreeState { + changed_paths: BTreeMap, ChangedPathInfo>, + done_rx: watch::Receiver>, + _registration: Task<()>, +} + +impl RegisteringWorktreeState { + fn done(&self) -> impl Future { + let mut done_rx = self.done_rx.clone(); + async move { + while let Some(result) = done_rx.next().await { + if result.is_some() { + break; + } + } + } + } +} + +struct RegisteredWorktreeState { + db_id: i64, + changed_paths: BTreeMap, ChangedPathInfo>, +} + +struct ChangedPathInfo { + mtime: SystemTime, + is_deleted: bool, +} + +#[derive(Clone)] +pub struct JobHandle { + /// The outer Arc is here to count the clones of a JobHandle instance; + /// when the last handle to a given job is dropped, we decrement a counter (just once). + tx: Arc>>>, +} + +impl JobHandle { + fn new(tx: &Arc>>) -> Self { + *tx.lock().borrow_mut() += 1; + Self { + tx: Arc::new(Arc::downgrade(&tx)), + } + } +} + +impl ProjectState { + fn new(subscription: gpui::Subscription, cx: &mut ModelContext) -> Self { + let (pending_file_count_tx, pending_file_count_rx) = watch::channel_with(0); + let pending_file_count_tx = Arc::new(Mutex::new(pending_file_count_tx)); + Self { + worktrees: Default::default(), + pending_file_count_rx: pending_file_count_rx.clone(), + pending_file_count_tx, + pending_index: 0, + _subscription: subscription, + _observe_pending_file_count: cx.spawn({ + let mut pending_file_count_rx = pending_file_count_rx.clone(); + |this, mut cx| async move { + while let Some(_) = pending_file_count_rx.next().await { + if this.update(&mut cx, |_, cx| cx.notify()).is_err() { + break; + } + } + } + }), + } + } + + fn worktree_id_for_db_id(&self, id: i64) -> Option { + self.worktrees + .iter() + .find_map(|(worktree_id, worktree_state)| match worktree_state { + WorktreeState::Registered(state) if state.db_id == id => Some(*worktree_id), + _ => None, + }) + } +} + +#[derive(Clone)] +pub struct PendingFile { + worktree_db_id: i64, + relative_path: Arc, + absolute_path: PathBuf, + language: Option>, + modified_time: SystemTime, + job_handle: JobHandle, +} + +#[derive(Clone)] +pub struct SearchResult { + pub buffer: Model, + pub range: Range, + pub similarity: OrderedFloat, +} + +impl SemanticIndex { + pub fn global(cx: &mut AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) + } else { + None + } + } + + pub fn authenticate(&mut self, cx: &mut AppContext) -> bool { + if !self.embedding_provider.has_credentials() { + self.embedding_provider.retrieve_credentials(cx); + } else { + return true; + } + + self.embedding_provider.has_credentials() + } + + pub fn is_authenticated(&self) -> bool { + self.embedding_provider.has_credentials() + } + + pub fn enabled(cx: &AppContext) -> bool { + SemanticIndexSettings::get_global(cx).enabled + } + + pub fn status(&self, project: &Model) -> SemanticIndexStatus { + if !self.is_authenticated() { + return SemanticIndexStatus::NotAuthenticated; + } + + if let Some(project_state) = self.projects.get(&project.downgrade()) { + if project_state + .worktrees + .values() + .all(|worktree| worktree.is_registered()) + && project_state.pending_index == 0 + { + SemanticIndexStatus::Indexed + } else { + SemanticIndexStatus::Indexing { + remaining_files: project_state.pending_file_count_rx.borrow().clone(), + rate_limit_expiry: self.embedding_provider.rate_limit_expiration(), + } + } + } else { + SemanticIndexStatus::NotIndexed + } + } + + pub async fn new( + fs: Arc, + database_path: PathBuf, + embedding_provider: Arc, + language_registry: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let t0 = Instant::now(); + let database_path = Arc::from(database_path); + let db = VectorDatabase::new(fs.clone(), database_path, cx.background_executor().clone()) + .await?; + + log::trace!( + "db initialization took {:?} milliseconds", + t0.elapsed().as_millis() + ); + + cx.build_model(|cx| { + let t0 = Instant::now(); + let embedding_queue = + EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor().clone()); + let _embedding_task = cx.background_executor().spawn({ + let embedded_files = embedding_queue.finished_files(); + let db = db.clone(); + async move { + while let Ok(file) = embedded_files.recv().await { + db.insert_file(file.worktree_id, file.path, file.mtime, file.spans) + .await + .log_err(); + } + } + }); + + // Parse files into embeddable spans. + let (parsing_files_tx, parsing_files_rx) = + channel::unbounded::<(Arc>, PendingFile)>(); + let embedding_queue = Arc::new(Mutex::new(embedding_queue)); + let mut _parsing_files_tasks = Vec::new(); + for _ in 0..cx.background_executor().num_cpus() { + let fs = fs.clone(); + let mut parsing_files_rx = parsing_files_rx.clone(); + let embedding_provider = embedding_provider.clone(); + let embedding_queue = embedding_queue.clone(); + let background = cx.background_executor().clone(); + _parsing_files_tasks.push(cx.background_executor().spawn(async move { + let mut retriever = CodeContextRetriever::new(embedding_provider.clone()); + loop { + let mut timer = background.timer(EMBEDDING_QUEUE_FLUSH_TIMEOUT).fuse(); + let mut next_file_to_parse = parsing_files_rx.next().fuse(); + futures::select_biased! { + next_file_to_parse = next_file_to_parse => { + if let Some((embeddings_for_digest, pending_file)) = next_file_to_parse { + Self::parse_file( + &fs, + pending_file, + &mut retriever, + &embedding_queue, + &embeddings_for_digest, + ) + .await + } else { + break; + } + }, + _ = timer => { + embedding_queue.lock().flush(); + } + } + } + })); + } + + log::trace!( + "semantic index task initialization took {:?} milliseconds", + t0.elapsed().as_millis() + ); + Self { + fs, + db, + embedding_provider, + language_registry, + parsing_files_tx, + _embedding_task, + _parsing_files_tasks, + projects: Default::default(), + } + }) + } + + async fn parse_file( + fs: &Arc, + pending_file: PendingFile, + retriever: &mut CodeContextRetriever, + embedding_queue: &Arc>, + embeddings_for_digest: &HashMap, + ) { + let Some(language) = pending_file.language else { + return; + }; + + if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() { + if let Some(mut spans) = retriever + .parse_file_with_template(Some(&pending_file.relative_path), &content, language) + .log_err() + { + log::trace!( + "parsed path {:?}: {} spans", + pending_file.relative_path, + spans.len() + ); + + for span in &mut spans { + if let Some(embedding) = embeddings_for_digest.get(&span.digest) { + span.embedding = Some(embedding.to_owned()); + } + } + + embedding_queue.lock().push(FileToEmbed { + worktree_id: pending_file.worktree_db_id, + path: pending_file.relative_path, + mtime: pending_file.modified_time, + job_handle: pending_file.job_handle, + spans, + }); + } + } + } + + pub fn project_previously_indexed( + &mut self, + project: &Model, + cx: &mut ModelContext, + ) -> Task> { + let worktrees_indexed_previously = project + .read(cx) + .worktrees() + .map(|worktree| { + self.db + .worktree_previously_indexed(&worktree.read(cx).abs_path()) + }) + .collect::>(); + cx.spawn(|_, _cx| async move { + let worktree_indexed_previously = + futures::future::join_all(worktrees_indexed_previously).await; + + Ok(worktree_indexed_previously + .iter() + .filter(|worktree| worktree.is_ok()) + .all(|v| v.as_ref().log_err().is_some_and(|v| v.to_owned()))) + }) + } + + fn project_entries_changed( + &mut self, + project: Model, + worktree_id: WorktreeId, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + cx: &mut ModelContext, + ) { + let Some(worktree) = project.read(cx).worktree_for_id(worktree_id.clone(), cx) else { + return; + }; + let project = project.downgrade(); + let Some(project_state) = self.projects.get_mut(&project) else { + return; + }; + + let worktree = worktree.read(cx); + let worktree_state = + if let Some(worktree_state) = project_state.worktrees.get_mut(&worktree_id) { + worktree_state + } else { + return; + }; + worktree_state.paths_changed(changes, worktree); + if let WorktreeState::Registered(_) = worktree_state { + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(BACKGROUND_INDEXING_DELAY) + .await; + if let Some((this, project)) = this.upgrade().zip(project.upgrade()) { + this.update(&mut cx, |this, cx| { + this.index_project(project, cx).detach_and_log_err(cx) + })?; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + + fn register_worktree( + &mut self, + project: Model, + worktree: Model, + cx: &mut ModelContext, + ) { + let project = project.downgrade(); + let project_state = if let Some(project_state) = self.projects.get_mut(&project) { + project_state + } else { + return; + }; + let worktree = if let Some(worktree) = worktree.read(cx).as_local() { + worktree + } else { + return; + }; + let worktree_abs_path = worktree.abs_path().clone(); + let scan_complete = worktree.scan_complete(); + let worktree_id = worktree.id(); + let db = self.db.clone(); + let language_registry = self.language_registry.clone(); + let (mut done_tx, done_rx) = watch::channel(); + let registration = cx.spawn(|this, mut cx| { + async move { + let register = async { + scan_complete.await; + let db_id = db.find_or_create_worktree(worktree_abs_path).await?; + let mut file_mtimes = db.get_file_mtimes(db_id).await?; + let worktree = if let Some(project) = project.upgrade() { + project + .read_with(&cx, |project, cx| project.worktree_for_id(worktree_id, cx)) + .ok() + .flatten() + .context("worktree not found")? + } else { + return anyhow::Ok(()); + }; + let worktree = worktree.read_with(&cx, |worktree, _| worktree.snapshot())?; + let mut changed_paths = cx + .background_executor() + .spawn(async move { + let mut changed_paths = BTreeMap::new(); + for file in worktree.files(false, 0) { + let absolute_path = worktree.absolutize(&file.path); + + if file.is_external || file.is_ignored || file.is_symlink { + continue; + } + + if let Ok(language) = language_registry + .language_for_file(&absolute_path, None) + .await + { + // Test if file is valid parseable file + if !PARSEABLE_ENTIRE_FILE_TYPES + .contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + + let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); + let already_stored = stored_mtime + .map_or(false, |existing_mtime| { + existing_mtime == file.mtime + }); + + if !already_stored { + changed_paths.insert( + file.path.clone(), + ChangedPathInfo { + mtime: file.mtime, + is_deleted: false, + }, + ); + } + } + } + + // Clean up entries from database that are no longer in the worktree. + for (path, mtime) in file_mtimes { + changed_paths.insert( + path.into(), + ChangedPathInfo { + mtime, + is_deleted: true, + }, + ); + } + + anyhow::Ok(changed_paths) + }) + .await?; + this.update(&mut cx, |this, cx| { + let project_state = this + .projects + .get_mut(&project) + .context("project not registered")?; + let project = project.upgrade().context("project was dropped")?; + + if let Some(WorktreeState::Registering(state)) = + project_state.worktrees.remove(&worktree_id) + { + changed_paths.extend(state.changed_paths); + } + project_state.worktrees.insert( + worktree_id, + WorktreeState::Registered(RegisteredWorktreeState { + db_id, + changed_paths, + }), + ); + this.index_project(project, cx).detach_and_log_err(cx); + + anyhow::Ok(()) + })??; + + anyhow::Ok(()) + }; + + if register.await.log_err().is_none() { + // Stop tracking this worktree if the registration failed. + this.update(&mut cx, |this, _| { + this.projects.get_mut(&project).map(|project_state| { + project_state.worktrees.remove(&worktree_id); + }); + }) + .ok(); + } + + *done_tx.borrow_mut() = Some(()); + } + }); + project_state.worktrees.insert( + worktree_id, + WorktreeState::Registering(RegisteringWorktreeState { + changed_paths: Default::default(), + done_rx, + _registration: registration, + }), + ); + } + + fn project_worktrees_changed(&mut self, project: Model, cx: &mut ModelContext) { + let project_state = if let Some(project_state) = self.projects.get_mut(&project.downgrade()) + { + project_state + } else { + return; + }; + + let mut worktrees = project + .read(cx) + .worktrees() + .filter(|worktree| worktree.read(cx).is_local()) + .collect::>(); + let worktree_ids = worktrees + .iter() + .map(|worktree| worktree.read(cx).id()) + .collect::>(); + + // Remove worktrees that are no longer present + project_state + .worktrees + .retain(|worktree_id, _| worktree_ids.contains(worktree_id)); + + // Register new worktrees + worktrees.retain(|worktree| { + let worktree_id = worktree.read(cx).id(); + !project_state.worktrees.contains_key(&worktree_id) + }); + for worktree in worktrees { + self.register_worktree(project.clone(), worktree, cx); + } + } + + pub fn pending_file_count(&self, project: &Model) -> Option> { + Some( + self.projects + .get(&project.downgrade())? + .pending_file_count_rx + .clone(), + ) + } + + pub fn search_project( + &mut self, + project: Model, + query: String, + limit: usize, + includes: Vec, + excludes: Vec, + cx: &mut ModelContext, + ) -> Task>> { + if query.is_empty() { + return Task::ready(Ok(Vec::new())); + } + + let index = self.index_project(project.clone(), cx); + let embedding_provider = self.embedding_provider.clone(); + + cx.spawn(|this, mut cx| async move { + index.await?; + let t0 = Instant::now(); + + let query = embedding_provider + .embed_batch(vec![query]) + .await? + .pop() + .context("could not embed query")?; + log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis()); + + let search_start = Instant::now(); + let modified_buffer_results = this.update(&mut cx, |this, cx| { + this.search_modified_buffers( + &project, + query.clone(), + limit, + &includes, + &excludes, + cx, + ) + })?; + let file_results = this.update(&mut cx, |this, cx| { + this.search_files(project, query, limit, includes, excludes, cx) + })?; + let (modified_buffer_results, file_results) = + futures::join!(modified_buffer_results, file_results); + + // Weave together the results from modified buffers and files. + let mut results = Vec::new(); + let mut modified_buffers = HashSet::default(); + for result in modified_buffer_results.log_err().unwrap_or_default() { + modified_buffers.insert(result.buffer.clone()); + results.push(result); + } + for result in file_results.log_err().unwrap_or_default() { + if !modified_buffers.contains(&result.buffer) { + results.push(result); + } + } + results.sort_by_key(|result| Reverse(result.similarity)); + results.truncate(limit); + log::trace!("Semantic search took {:?}", search_start.elapsed()); + Ok(results) + }) + } + + pub fn search_files( + &mut self, + project: Model, + query: Embedding, + limit: usize, + includes: Vec, + excludes: Vec, + cx: &mut ModelContext, + ) -> Task>> { + let db_path = self.db.path().clone(); + let fs = self.fs.clone(); + cx.spawn(|this, mut cx| async move { + let database = VectorDatabase::new( + fs.clone(), + db_path.clone(), + cx.background_executor().clone(), + ) + .await?; + + let worktree_db_ids = this.read_with(&cx, |this, _| { + let project_state = this + .projects + .get(&project.downgrade()) + .context("project was not indexed")?; + let worktree_db_ids = project_state + .worktrees + .values() + .filter_map(|worktree| { + if let WorktreeState::Registered(worktree) = worktree { + Some(worktree.db_id) + } else { + None + } + }) + .collect::>(); + anyhow::Ok(worktree_db_ids) + })??; + + let file_ids = database + .retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes) + .await?; + + let batch_n = cx.background_executor().num_cpus(); + let ids_len = file_ids.clone().len(); + let minimum_batch_size = 50; + + let batch_size = { + let size = ids_len / batch_n; + if size < minimum_batch_size { + minimum_batch_size + } else { + size + } + }; + + let mut batch_results = Vec::new(); + for batch in file_ids.chunks(batch_size) { + let batch = batch.into_iter().map(|v| *v).collect::>(); + let limit = limit.clone(); + let fs = fs.clone(); + let db_path = db_path.clone(); + let query = query.clone(); + if let Some(db) = + VectorDatabase::new(fs, db_path.clone(), cx.background_executor().clone()) + .await + .log_err() + { + batch_results.push(async move { + db.top_k_search(&query, limit, batch.as_slice()).await + }); + } + } + + let batch_results = futures::future::join_all(batch_results).await; + + let mut results = Vec::new(); + for batch_result in batch_results { + if batch_result.is_ok() { + for (id, similarity) in batch_result.unwrap() { + let ix = match results + .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s)) + { + Ok(ix) => ix, + Err(ix) => ix, + }; + + results.insert(ix, (id, similarity)); + results.truncate(limit); + } + } + } + + let ids = results.iter().map(|(id, _)| *id).collect::>(); + let scores = results + .into_iter() + .map(|(_, score)| score) + .collect::>(); + let spans = database.spans_for_ids(ids.as_slice()).await?; + + let mut tasks = Vec::new(); + let mut ranges = Vec::new(); + let weak_project = project.downgrade(); + project.update(&mut cx, |project, cx| { + let this = this.upgrade().context("index was dropped")?; + for (worktree_db_id, file_path, byte_range) in spans { + let project_state = + if let Some(state) = this.read(cx).projects.get(&weak_project) { + state + } else { + return Err(anyhow!("project not added")); + }; + if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) { + tasks.push(project.open_buffer((worktree_id, file_path), cx)); + ranges.push(byte_range); + } + } + + Ok(()) + })??; + + let buffers = futures::future::join_all(tasks).await; + Ok(buffers + .into_iter() + .zip(ranges) + .zip(scores) + .filter_map(|((buffer, range), similarity)| { + let buffer = buffer.log_err()?; + let range = buffer + .read_with(&cx, |buffer, _| { + let start = buffer.clip_offset(range.start, Bias::Left); + let end = buffer.clip_offset(range.end, Bias::Right); + buffer.anchor_before(start)..buffer.anchor_after(end) + }) + .log_err()?; + Some(SearchResult { + buffer, + range, + similarity, + }) + }) + .collect()) + }) + } + + fn search_modified_buffers( + &self, + project: &Model, + query: Embedding, + limit: usize, + includes: &[PathMatcher], + excludes: &[PathMatcher], + cx: &mut ModelContext, + ) -> Task>> { + let modified_buffers = project + .read(cx) + .opened_buffers() + .into_iter() + .filter_map(|buffer_handle| { + let buffer = buffer_handle.read(cx); + let snapshot = buffer.snapshot(); + let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| { + excludes.iter().any(|matcher| matcher.is_match(&path)) + }); + + let included = if includes.len() == 0 { + true + } else { + snapshot.resolve_file_path(cx, false).map_or(false, |path| { + includes.iter().any(|matcher| matcher.is_match(&path)) + }) + }; + + if buffer.is_dirty() && !excluded && included { + Some((buffer_handle, snapshot)) + } else { + None + } + }) + .collect::>(); + + let embedding_provider = self.embedding_provider.clone(); + let fs = self.fs.clone(); + let db_path = self.db.path().clone(); + let background = cx.background_executor().clone(); + cx.background_executor().spawn(async move { + let db = VectorDatabase::new(fs, db_path.clone(), background).await?; + let mut results = Vec::::new(); + + let mut retriever = CodeContextRetriever::new(embedding_provider.clone()); + for (buffer, snapshot) in modified_buffers { + let language = snapshot + .language_at(0) + .cloned() + .unwrap_or_else(|| language::PLAIN_TEXT.clone()); + let mut spans = retriever + .parse_file_with_template(None, &snapshot.text(), language) + .log_err() + .unwrap_or_default(); + if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db) + .await + .log_err() + .is_some() + { + for span in spans { + let similarity = span.embedding.unwrap().similarity(&query); + let ix = match results + .binary_search_by_key(&Reverse(similarity), |result| { + Reverse(result.similarity) + }) { + Ok(ix) => ix, + Err(ix) => ix, + }; + + let range = { + let start = snapshot.clip_offset(span.range.start, Bias::Left); + let end = snapshot.clip_offset(span.range.end, Bias::Right); + snapshot.anchor_before(start)..snapshot.anchor_after(end) + }; + + results.insert( + ix, + SearchResult { + buffer: buffer.clone(), + range, + similarity, + }, + ); + results.truncate(limit); + } + } + } + + Ok(results) + }) + } + + pub fn index_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Task> { + if !self.is_authenticated() { + if !self.authenticate(cx) { + return Task::ready(Err(anyhow!("user is not authenticated"))); + } + } + + if !self.projects.contains_key(&project.downgrade()) { + let subscription = cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => { + this.project_worktrees_changed(project.clone(), cx); + } + project::Event::WorktreeUpdatedEntries(worktree_id, changes) => { + this.project_entries_changed(project, *worktree_id, changes.clone(), cx); + } + _ => {} + }); + let project_state = ProjectState::new(subscription, cx); + self.projects.insert(project.downgrade(), project_state); + self.project_worktrees_changed(project.clone(), cx); + } + let project_state = self.projects.get_mut(&project.downgrade()).unwrap(); + project_state.pending_index += 1; + cx.notify(); + + let mut pending_file_count_rx = project_state.pending_file_count_rx.clone(); + let db = self.db.clone(); + let language_registry = self.language_registry.clone(); + let parsing_files_tx = self.parsing_files_tx.clone(); + let worktree_registration = self.wait_for_worktree_registration(&project, cx); + + cx.spawn(|this, mut cx| async move { + worktree_registration.await?; + + let mut pending_files = Vec::new(); + let mut files_to_delete = Vec::new(); + this.update(&mut cx, |this, cx| { + let project_state = this + .projects + .get_mut(&project.downgrade()) + .context("project was dropped")?; + let pending_file_count_tx = &project_state.pending_file_count_tx; + + project_state + .worktrees + .retain(|worktree_id, worktree_state| { + let worktree = if let Some(worktree) = + project.read(cx).worktree_for_id(*worktree_id, cx) + { + worktree + } else { + return false; + }; + let worktree_state = + if let WorktreeState::Registered(worktree_state) = worktree_state { + worktree_state + } else { + return true; + }; + + worktree_state.changed_paths.retain(|path, info| { + if info.is_deleted { + files_to_delete.push((worktree_state.db_id, path.clone())); + } else { + let absolute_path = worktree.read(cx).absolutize(path); + let job_handle = JobHandle::new(pending_file_count_tx); + pending_files.push(PendingFile { + absolute_path, + relative_path: path.clone(), + language: None, + job_handle, + modified_time: info.mtime, + worktree_db_id: worktree_state.db_id, + }); + } + + false + }); + true + }); + + anyhow::Ok(()) + })??; + + cx.background_executor() + .spawn(async move { + for (worktree_db_id, path) in files_to_delete { + db.delete_file(worktree_db_id, path).await.log_err(); + } + + let embeddings_for_digest = { + let mut files = HashMap::default(); + for pending_file in &pending_files { + files + .entry(pending_file.worktree_db_id) + .or_insert(Vec::new()) + .push(pending_file.relative_path.clone()); + } + Arc::new( + db.embeddings_for_files(files) + .await + .log_err() + .unwrap_or_default(), + ) + }; + + for mut pending_file in pending_files { + if let Ok(language) = language_registry + .language_for_file(&pending_file.relative_path, None) + .await + { + if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + pending_file.language = Some(language); + } + parsing_files_tx + .try_send((embeddings_for_digest.clone(), pending_file)) + .ok(); + } + + // Wait until we're done indexing. + while let Some(count) = pending_file_count_rx.next().await { + if count == 0 { + break; + } + } + }) + .await; + + this.update(&mut cx, |this, cx| { + let project_state = this + .projects + .get_mut(&project.downgrade()) + .context("project was dropped")?; + project_state.pending_index -= 1; + cx.notify(); + anyhow::Ok(()) + })??; + + Ok(()) + }) + } + + fn wait_for_worktree_registration( + &self, + project: &Model, + cx: &mut ModelContext, + ) -> Task> { + let project = project.downgrade(); + cx.spawn(|this, cx| async move { + loop { + let mut pending_worktrees = Vec::new(); + this.upgrade() + .context("semantic index dropped")? + .read_with(&cx, |this, _| { + if let Some(project) = this.projects.get(&project) { + for worktree in project.worktrees.values() { + if let WorktreeState::Registering(worktree) = worktree { + pending_worktrees.push(worktree.done()); + } + } + } + })?; + + if pending_worktrees.is_empty() { + break; + } else { + future::join_all(pending_worktrees).await; + } + } + Ok(()) + }) + } + + async fn embed_spans( + spans: &mut [Span], + embedding_provider: &dyn EmbeddingProvider, + db: &VectorDatabase, + ) -> Result<()> { + let mut batch = Vec::new(); + let mut batch_tokens = 0; + let mut embeddings = Vec::new(); + + let digests = spans + .iter() + .map(|span| span.digest.clone()) + .collect::>(); + let embeddings_for_digests = db + .embeddings_for_digests(digests) + .await + .log_err() + .unwrap_or_default(); + + for span in &*spans { + if embeddings_for_digests.contains_key(&span.digest) { + continue; + }; + + if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() { + let batch_embeddings = embedding_provider + .embed_batch(mem::take(&mut batch)) + .await?; + embeddings.extend(batch_embeddings); + batch_tokens = 0; + } + + batch_tokens += span.token_count; + batch.push(span.content.clone()); + } + + if !batch.is_empty() { + let batch_embeddings = embedding_provider + .embed_batch(mem::take(&mut batch)) + .await?; + + embeddings.extend(batch_embeddings); + } + + let mut embeddings = embeddings.into_iter(); + for span in spans { + let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) { + Some(embedding.clone()) + } else { + embeddings.next() + }; + let embedding = embedding.context("failed to embed spans")?; + span.embedding = Some(embedding); + } + Ok(()) + } +} + +impl Drop for JobHandle { + fn drop(&mut self) { + if let Some(inner) = Arc::get_mut(&mut self.tx) { + // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not) + if let Some(tx) = inner.upgrade() { + let mut tx = tx.lock(); + *tx.borrow_mut() -= 1; + } + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + #[test] + fn test_job_handle() { + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let tx = Arc::new(Mutex::new(job_count_tx)); + let job_handle = JobHandle::new(&tx); + + assert_eq!(1, *job_count_rx.borrow()); + let new_job_handle = job_handle.clone(); + assert_eq!(1, *job_count_rx.borrow()); + drop(job_handle); + assert_eq!(1, *job_count_rx.borrow()); + drop(new_job_handle); + assert_eq!(0, *job_count_rx.borrow()); + } +} diff --git a/crates/semantic_index2/src/semantic_index_settings.rs b/crates/semantic_index2/src/semantic_index_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..306a38fa9c2ec52f5a69d27898cc9fccc1af956c --- /dev/null +++ b/crates/semantic_index2/src/semantic_index_settings.rs @@ -0,0 +1,28 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Deserialize, Debug)] +pub struct SemanticIndexSettings { + pub enabled: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct SemanticIndexSettingsContent { + pub enabled: Option, +} + +impl Settings for SemanticIndexSettings { + const KEY: Option<&'static str> = Some("semantic_index"); + + type FileContent = SemanticIndexSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/semantic_index2/src/semantic_index_tests.rs b/crates/semantic_index2/src/semantic_index_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..ced08f4cbc30a991bfad0577af24f96c8ff81d8b --- /dev/null +++ b/crates/semantic_index2/src/semantic_index_tests.rs @@ -0,0 +1,1697 @@ +use crate::{ + embedding_queue::EmbeddingQueue, + parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest}, + semantic_index_settings::SemanticIndexSettings, + FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT, +}; +use ai::test::FakeEmbeddingProvider; + +use gpui::{Task, TestAppContext}; +use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; +use parking_lot::Mutex; +use pretty_assertions::assert_eq; +use project::{project_settings::ProjectSettings, FakeFs, Fs, Project}; +use rand::{rngs::StdRng, Rng}; +use serde_json::json; +use settings::{Settings, SettingsStore}; +use std::{path::Path, sync::Arc, time::SystemTime}; +use unindent::Unindent; +use util::{paths::PathMatcher, RandomCharIter}; + +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + +#[gpui::test] +async fn test_semantic_index(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/the-root", + json!({ + "src": { + "file1.rs": " + fn aaa() { + println!(\"aaaaaaaaaaaa!\"); + } + + fn zzzzz() { + println!(\"SLEEPING\"); + } + ".unindent(), + "file2.rs": " + fn bbb() { + println!(\"bbbbbbbbbbbbb!\"); + } + struct pqpqpqp {} + ".unindent(), + "file3.toml": " + ZZZZZZZZZZZZZZZZZZ = 5 + ".unindent(), + } + }), + ) + .await; + + let languages = Arc::new(LanguageRegistry::new(Task::ready(()))); + let rust_language = rust_lang(); + let toml_language = toml_lang(); + languages.add(rust_language); + languages.add(toml_language); + + let db_dir = tempdir::TempDir::new("vector-store").unwrap(); + let db_path = db_dir.path().join("db.sqlite"); + + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let semantic_index = SemanticIndex::new( + fs.clone(), + db_path, + embedding_provider.clone(), + languages, + cx.to_async(), + ) + .await + .unwrap(); + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + + let search_results = semantic_index.update(cx, |store, cx| { + store.search_project( + project.clone(), + "aaaaaabbbbzz".to_string(), + 5, + vec![], + vec![], + cx, + ) + }); + let pending_file_count = + semantic_index.read_with(cx, |index, _| index.pending_file_count(&project).unwrap()); + cx.background_executor.run_until_parked(); + assert_eq!(*pending_file_count.borrow(), 3); + cx.background_executor + .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT); + assert_eq!(*pending_file_count.borrow(), 0); + + let search_results = search_results.await.unwrap(); + assert_search_results( + &search_results, + &[ + (Path::new("src/file1.rs").into(), 0), + (Path::new("src/file2.rs").into(), 0), + (Path::new("src/file3.toml").into(), 0), + (Path::new("src/file1.rs").into(), 45), + (Path::new("src/file2.rs").into(), 45), + ], + cx, + ); + + // Test Include Files Functonality + let include_files = vec![PathMatcher::new("*.rs").unwrap()]; + let exclude_files = vec![PathMatcher::new("*.rs").unwrap()]; + let rust_only_search_results = semantic_index + .update(cx, |store, cx| { + store.search_project( + project.clone(), + "aaaaaabbbbzz".to_string(), + 5, + include_files, + vec![], + cx, + ) + }) + .await + .unwrap(); + + assert_search_results( + &rust_only_search_results, + &[ + (Path::new("src/file1.rs").into(), 0), + (Path::new("src/file2.rs").into(), 0), + (Path::new("src/file1.rs").into(), 45), + (Path::new("src/file2.rs").into(), 45), + ], + cx, + ); + + let no_rust_search_results = semantic_index + .update(cx, |store, cx| { + store.search_project( + project.clone(), + "aaaaaabbbbzz".to_string(), + 5, + vec![], + exclude_files, + cx, + ) + }) + .await + .unwrap(); + + assert_search_results( + &no_rust_search_results, + &[(Path::new("src/file3.toml").into(), 0)], + cx, + ); + + fs.save( + "/the-root/src/file2.rs".as_ref(), + &" + fn dddd() { println!(\"ddddd!\"); } + struct pqpqpqp {} + " + .unindent() + .into(), + Default::default(), + ) + .await + .unwrap(); + + cx.background_executor + .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT); + + let prev_embedding_count = embedding_provider.embedding_count(); + let index = semantic_index.update(cx, |store, cx| store.index_project(project.clone(), cx)); + cx.background_executor.run_until_parked(); + assert_eq!(*pending_file_count.borrow(), 1); + cx.background_executor + .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT); + assert_eq!(*pending_file_count.borrow(), 0); + index.await.unwrap(); + + assert_eq!( + embedding_provider.embedding_count() - prev_embedding_count, + 1 + ); +} + +#[gpui::test(iterations = 10)] +async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) { + let (outstanding_job_count, _) = postage::watch::channel_with(0); + let outstanding_job_count = Arc::new(Mutex::new(outstanding_job_count)); + + let files = (1..=3) + .map(|file_ix| FileToEmbed { + worktree_id: 5, + path: Path::new(&format!("path-{file_ix}")).into(), + mtime: SystemTime::now(), + spans: (0..rng.gen_range(4..22)) + .map(|document_ix| { + let content_len = rng.gen_range(10..100); + let content = RandomCharIter::new(&mut rng) + .with_simple_text() + .take(content_len) + .collect::(); + let digest = SpanDigest::from(content.as_str()); + Span { + range: 0..10, + embedding: None, + name: format!("document {document_ix}"), + content, + digest, + token_count: rng.gen_range(10..30), + } + }) + .collect(), + job_handle: JobHandle::new(&outstanding_job_count), + }) + .collect::>(); + + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + + let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor.clone()); + for file in &files { + queue.push(file.clone()); + } + queue.flush(); + + cx.background_executor.run_until_parked(); + let finished_files = queue.finished_files(); + let mut embedded_files: Vec<_> = files + .iter() + .map(|_| finished_files.try_recv().expect("no finished file")) + .collect(); + + let expected_files: Vec<_> = files + .iter() + .map(|file| { + let mut file = file.clone(); + for doc in &mut file.spans { + doc.embedding = Some(embedding_provider.embed_sync(doc.content.as_ref())); + } + file + }) + .collect(); + + embedded_files.sort_by_key(|f| f.path.clone()); + + assert_eq!(embedded_files, expected_files); +} + +#[track_caller] +fn assert_search_results( + actual: &[SearchResult], + expected: &[(Arc, usize)], + cx: &TestAppContext, +) { + let actual = actual + .iter() + .map(|search_result| { + search_result.buffer.read_with(cx, |buffer, _cx| { + ( + buffer.file().unwrap().path().clone(), + search_result.range.start.to_offset(buffer), + ) + }) + }) + .collect::>(); + assert_eq!(actual, expected); +} + +#[gpui::test] +async fn test_code_context_retrieval_rust() { + let language = rust_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = " + /// A doc comment + /// that spans multiple lines + #[gpui::test] + fn a() { + b + } + + impl C for D { + } + + impl E { + // This is also a preceding comment + pub fn function_1() -> Option<()> { + unimplemented!(); + } + + // This is a preceding comment + fn function_2() -> Result<()> { + unimplemented!(); + } + } + + #[derive(Clone)] + struct D { + name: String + } + " + .unindent(); + + let documents = retriever.parse_file(&text, language).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + " + /// A doc comment + /// that spans multiple lines + #[gpui::test] + fn a() { + b + }" + .unindent(), + text.find("fn a").unwrap(), + ), + ( + " + impl C for D { + }" + .unindent(), + text.find("impl C").unwrap(), + ), + ( + " + impl E { + // This is also a preceding comment + pub fn function_1() -> Option<()> { /* ... */ } + + // This is a preceding comment + fn function_2() -> Result<()> { /* ... */ } + }" + .unindent(), + text.find("impl E").unwrap(), + ), + ( + " + // This is also a preceding comment + pub fn function_1() -> Option<()> { + unimplemented!(); + }" + .unindent(), + text.find("pub fn function_1").unwrap(), + ), + ( + " + // This is a preceding comment + fn function_2() -> Result<()> { + unimplemented!(); + }" + .unindent(), + text.find("fn function_2").unwrap(), + ), + ( + " + #[derive(Clone)] + struct D { + name: String + }" + .unindent(), + text.find("struct D").unwrap(), + ), + ], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_json() { + let language = json_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + { + "array": [1, 2, 3, 4], + "string": "abcdefg", + "nested_object": { + "array_2": [5, 6, 7, 8], + "string_2": "hijklmnop", + "boolean": true, + "none": null + } + } + "# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[( + r#" + { + "array": [], + "string": "", + "nested_object": { + "array_2": [], + "string_2": "", + "boolean": true, + "none": null + } + }"# + .unindent(), + text.find("{").unwrap(), + )], + ); + + let text = r#" + [ + { + "name": "somebody", + "age": 42 + }, + { + "name": "somebody else", + "age": 43 + } + ] + "# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[( + r#" + [{ + "name": "", + "age": 42 + }]"# + .unindent(), + text.find("[").unwrap(), + )], + ); +} + +fn assert_documents_eq( + documents: &[Span], + expected_contents_and_start_offsets: &[(String, usize)], +) { + assert_eq!( + documents + .iter() + .map(|document| (document.content.clone(), document.range.start)) + .collect::>(), + expected_contents_and_start_offsets + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_javascript() { + let language = js_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = " + /* globals importScripts, backend */ + function _authorize() {} + + /** + * Sometimes the frontend build is way faster than backend. + */ + export async function authorizeBank() { + _authorize(pushModal, upgradingAccountId, {}); + } + + export class SettingsPage { + /* This is a test setting */ + constructor(page) { + this.page = page; + } + } + + /* This is a test comment */ + class TestClass {} + + /* Schema for editor_events in Clickhouse. */ + export interface ClickhouseEditorEvent { + installation_id: string + operation: string + } + " + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + " + /* globals importScripts, backend */ + function _authorize() {}" + .unindent(), + 37, + ), + ( + " + /** + * Sometimes the frontend build is way faster than backend. + */ + export async function authorizeBank() { + _authorize(pushModal, upgradingAccountId, {}); + }" + .unindent(), + 131, + ), + ( + " + export class SettingsPage { + /* This is a test setting */ + constructor(page) { + this.page = page; + } + }" + .unindent(), + 225, + ), + ( + " + /* This is a test setting */ + constructor(page) { + this.page = page; + }" + .unindent(), + 290, + ), + ( + " + /* This is a test comment */ + class TestClass {}" + .unindent(), + 374, + ), + ( + " + /* Schema for editor_events in Clickhouse. */ + export interface ClickhouseEditorEvent { + installation_id: string + operation: string + }" + .unindent(), + 440, + ), + ], + ) +} + +#[gpui::test] +async fn test_code_context_retrieval_lua() { + let language = lua_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + -- Creates a new class + -- @param baseclass The Baseclass of this class, or nil. + -- @return A new class reference. + function classes.class(baseclass) + -- Create the class definition and metatable. + local classdef = {} + -- Find the super class, either Object or user-defined. + baseclass = baseclass or classes.Object + -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable. + setmetatable(classdef, { __index = baseclass }) + -- All class instances have a reference to the class object. + classdef.class = classdef + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + -- All class instances have a reference to a superclass object. + local instance = { super = baseclass.alloc(mastertable) } + -- Any functions this instance does not know of will 'look up' to the superclass definition. + setmetatable(instance, { __index = classdef, __newindex = mastertable }) + return instance + end + end + "#.unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + (r#" + -- Creates a new class + -- @param baseclass The Baseclass of this class, or nil. + -- @return A new class reference. + function classes.class(baseclass) + -- Create the class definition and metatable. + local classdef = {} + -- Find the super class, either Object or user-defined. + baseclass = baseclass or classes.Object + -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable. + setmetatable(classdef, { __index = baseclass }) + -- All class instances have a reference to the class object. + classdef.class = classdef + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + --[ ... ]-- + --[ ... ]-- + end + end"#.unindent(), + 114), + (r#" + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + -- All class instances have a reference to a superclass object. + local instance = { super = baseclass.alloc(mastertable) } + -- Any functions this instance does not know of will 'look up' to the superclass definition. + setmetatable(instance, { __index = classdef, __newindex = mastertable }) + return instance + end"#.unindent(), 809), + ] + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_elixir() { + let language = elixir_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + defmodule File.Stream do + @moduledoc """ + Defines a `File.Stream` struct returned by `File.stream!/3`. + + The following fields are public: + + * `path` - the file path + * `modes` - the file modes + * `raw` - a boolean indicating if bin functions should be used + * `line_or_bytes` - if reading should read lines or a given number of bytes + * `node` - the node the file belongs to + + """ + + defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil + + @type t :: %__MODULE__{} + + @doc false + def __build__(path, modes, line_or_bytes) do + raw = :lists.keyfind(:encoding, 1, modes) == false + + modes = + case raw do + true -> + case :lists.keyfind(:read_ahead, 1, modes) do + {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)] + {:read_ahead, _} -> [:raw | modes] + false -> [:raw, :read_ahead | modes] + end + + false -> + modes + end + + %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()} + + end"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[( + r#" + defmodule File.Stream do + @moduledoc """ + Defines a `File.Stream` struct returned by `File.stream!/3`. + + The following fields are public: + + * `path` - the file path + * `modes` - the file modes + * `raw` - a boolean indicating if bin functions should be used + * `line_or_bytes` - if reading should read lines or a given number of bytes + * `node` - the node the file belongs to + + """ + + defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil + + @type t :: %__MODULE__{} + + @doc false + def __build__(path, modes, line_or_bytes) do + raw = :lists.keyfind(:encoding, 1, modes) == false + + modes = + case raw do + true -> + case :lists.keyfind(:read_ahead, 1, modes) do + {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)] + {:read_ahead, _} -> [:raw | modes] + false -> [:raw, :read_ahead | modes] + end + + false -> + modes + end + + %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()} + + end"# + .unindent(), + 0, + ),(r#" + @doc false + def __build__(path, modes, line_or_bytes) do + raw = :lists.keyfind(:encoding, 1, modes) == false + + modes = + case raw do + true -> + case :lists.keyfind(:read_ahead, 1, modes) do + {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)] + {:read_ahead, _} -> [:raw | modes] + false -> [:raw, :read_ahead | modes] + end + + false -> + modes + end + + %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()} + + end"#.unindent(), 574)], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_cpp() { + let language = cpp_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = " + /** + * @brief Main function + * @returns 0 on exit + */ + int main() { return 0; } + + /** + * This is a test comment + */ + class MyClass { // The class + public: // Access specifier + int myNum; // Attribute (int variable) + string myString; // Attribute (string variable) + }; + + // This is a test comment + enum Color { red, green, blue }; + + /** This is a preceding block comment + * This is the second line + */ + struct { // Structure declaration + int myNum; // Member (int variable) + string myString; // Member (string variable) + } myStructure; + + /** + * @brief Matrix class. + */ + template ::value || std::is_floating_point::value, + bool>::type> + class Matrix2 { + std::vector> _mat; + + public: + /** + * @brief Constructor + * @tparam Integer ensuring integers are being evaluated and not other + * data types. + * @param size denoting the size of Matrix as size x size + */ + template ::value, + Integer>::type> + explicit Matrix(const Integer size) { + for (size_t i = 0; i < size; ++i) { + _mat.emplace_back(std::vector(size, 0)); + } + } + }" + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + " + /** + * @brief Main function + * @returns 0 on exit + */ + int main() { return 0; }" + .unindent(), + 54, + ), + ( + " + /** + * This is a test comment + */ + class MyClass { // The class + public: // Access specifier + int myNum; // Attribute (int variable) + string myString; // Attribute (string variable) + }" + .unindent(), + 112, + ), + ( + " + // This is a test comment + enum Color { red, green, blue }" + .unindent(), + 322, + ), + ( + " + /** This is a preceding block comment + * This is the second line + */ + struct { // Structure declaration + int myNum; // Member (int variable) + string myString; // Member (string variable) + } myStructure;" + .unindent(), + 425, + ), + ( + " + /** + * @brief Matrix class. + */ + template ::value || std::is_floating_point::value, + bool>::type> + class Matrix2 { + std::vector> _mat; + + public: + /** + * @brief Constructor + * @tparam Integer ensuring integers are being evaluated and not other + * data types. + * @param size denoting the size of Matrix as size x size + */ + template ::value, + Integer>::type> + explicit Matrix(const Integer size) { + for (size_t i = 0; i < size; ++i) { + _mat.emplace_back(std::vector(size, 0)); + } + } + }" + .unindent(), + 612, + ), + ( + " + explicit Matrix(const Integer size) { + for (size_t i = 0; i < size; ++i) { + _mat.emplace_back(std::vector(size, 0)); + } + }" + .unindent(), + 1226, + ), + ], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_ruby() { + let language = ruby_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + # This concern is inspired by "sudo mode" on GitHub. It + # is a way to re-authenticate a user before allowing them + # to see or perform an action. + # + # Add `before_action :require_challenge!` to actions you + # want to protect. + # + # The user will be shown a page to enter the challenge (which + # is either the password, or just the username when no + # password exists). Upon passing, there is a grace period + # during which no challenge will be asked from the user. + # + # Accessing challenge-protected resources during the grace + # period will refresh the grace period. + module ChallengableConcern + extend ActiveSupport::Concern + + CHALLENGE_TIMEOUT = 1.hour.freeze + + def require_challenge! + return if skip_challenge? + + if challenge_passed_recently? + session[:challenge_passed_at] = Time.now.utc + return + end + + @challenge = Form::Challenge.new(return_to: request.url) + + if params.key?(:form_challenge) + if challenge_passed? + session[:challenge_passed_at] = Time.now.utc + else + flash.now[:alert] = I18n.t('challenge.invalid_password') + render_challenge + end + else + render_challenge + end + end + + def challenge_passed? + current_user.valid_password?(challenge_params[:current_password]) + end + end + + class Animal + include Comparable + + attr_reader :legs + + def initialize(name, legs) + @name, @legs = name, legs + end + + def <=>(other) + legs <=> other.legs + end + end + + # Singleton method for car object + def car.wheels + puts "There are four wheels" + end"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + r#" + # This concern is inspired by "sudo mode" on GitHub. It + # is a way to re-authenticate a user before allowing them + # to see or perform an action. + # + # Add `before_action :require_challenge!` to actions you + # want to protect. + # + # The user will be shown a page to enter the challenge (which + # is either the password, or just the username when no + # password exists). Upon passing, there is a grace period + # during which no challenge will be asked from the user. + # + # Accessing challenge-protected resources during the grace + # period will refresh the grace period. + module ChallengableConcern + extend ActiveSupport::Concern + + CHALLENGE_TIMEOUT = 1.hour.freeze + + def require_challenge! + # ... + end + + def challenge_passed? + # ... + end + end"# + .unindent(), + 558, + ), + ( + r#" + def require_challenge! + return if skip_challenge? + + if challenge_passed_recently? + session[:challenge_passed_at] = Time.now.utc + return + end + + @challenge = Form::Challenge.new(return_to: request.url) + + if params.key?(:form_challenge) + if challenge_passed? + session[:challenge_passed_at] = Time.now.utc + else + flash.now[:alert] = I18n.t('challenge.invalid_password') + render_challenge + end + else + render_challenge + end + end"# + .unindent(), + 663, + ), + ( + r#" + def challenge_passed? + current_user.valid_password?(challenge_params[:current_password]) + end"# + .unindent(), + 1254, + ), + ( + r#" + class Animal + include Comparable + + attr_reader :legs + + def initialize(name, legs) + # ... + end + + def <=>(other) + # ... + end + end"# + .unindent(), + 1363, + ), + ( + r#" + def initialize(name, legs) + @name, @legs = name, legs + end"# + .unindent(), + 1427, + ), + ( + r#" + def <=>(other) + legs <=> other.legs + end"# + .unindent(), + 1501, + ), + ( + r#" + # Singleton method for car object + def car.wheels + puts "There are four wheels" + end"# + .unindent(), + 1591, + ), + ], + ); +} + +#[gpui::test] +async fn test_code_context_retrieval_php() { + let language = php_lang(); + let embedding_provider = Arc::new(FakeEmbeddingProvider::default()); + let mut retriever = CodeContextRetriever::new(embedding_provider); + + let text = r#" + 100) { + throw new Exception(message: 'Progress cannot be greater than 100'); + } + + if ($this->achievements()->find($achievement->id)) { + throw new Exception(message: 'User already has this Achievement'); + } + + $this->achievements()->attach($achievement, [ + 'progress' => $progress ?? null, + ]); + + $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this))); + } + + public function achievements(): BelongsToMany + { + return $this->belongsToMany(related: Achievement::class) + ->withPivot(columns: 'progress') + ->where('is_secret', false) + ->using(AchievementUser::class); + } + } + + interface Multiplier + { + public function qualifies(array $data): bool; + + public function setMultiplier(): int; + } + + enum AuditType: string + { + case Add = 'add'; + case Remove = 'remove'; + case Reset = 'reset'; + case LevelUp = 'level_up'; + } + + ?>"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + r#" + /* + This is a multiple-lines comment block + that spans over multiple + lines + */ + function functionName() { + echo "Hello world!"; + }"# + .unindent(), + 123, + ), + ( + r#" + trait HasAchievements + { + /** + * @throws \Exception + */ + public function grantAchievement(Achievement $achievement, $progress = null): void + {/* ... */} + + public function achievements(): BelongsToMany + {/* ... */} + }"# + .unindent(), + 177, + ), + (r#" + /** + * @throws \Exception + */ + public function grantAchievement(Achievement $achievement, $progress = null): void + { + if ($progress > 100) { + throw new Exception(message: 'Progress cannot be greater than 100'); + } + + if ($this->achievements()->find($achievement->id)) { + throw new Exception(message: 'User already has this Achievement'); + } + + $this->achievements()->attach($achievement, [ + 'progress' => $progress ?? null, + ]); + + $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this))); + }"#.unindent(), 245), + (r#" + public function achievements(): BelongsToMany + { + return $this->belongsToMany(related: Achievement::class) + ->withPivot(columns: 'progress') + ->where('is_secret', false) + ->using(AchievementUser::class); + }"#.unindent(), 902), + (r#" + interface Multiplier + { + public function qualifies(array $data): bool; + + public function setMultiplier(): int; + }"#.unindent(), + 1146), + (r#" + enum AuditType: string + { + case Add = 'add'; + case Remove = 'remove'; + case Reset = 'reset'; + case LevelUp = 'level_up'; + }"#.unindent(), 1265) + ], + ); +} + +fn js_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Javascript".into(), + path_suffixes: vec!["js".into()], + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + ) + .with_embedding_query( + &r#" + + ( + (comment)* @context + . + [ + (export_statement + (function_declaration + "async"? @name + "function" @name + name: (_) @name)) + (function_declaration + "async"? @name + "function" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + [ + (export_statement + (class_declaration + "class" @name + name: (_) @name)) + (class_declaration + "class" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + [ + (export_statement + (interface_declaration + "interface" @name + name: (_) @name)) + (interface_declaration + "interface" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + [ + (export_statement + (enum_declaration + "enum" @name + name: (_) @name)) + (enum_declaration + "enum" @name + name: (_) @name) + ] @item + ) + + ( + (comment)* @context + . + (method_definition + [ + "get" + "set" + "async" + "*" + "static" + ]* @name + name: (_) @name) @item + ) + + "# + .unindent(), + ) + .unwrap(), + ) +} + +fn rust_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".into()], + collapsed_placeholder: " /* ... */ ".to_string(), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_embedding_query( + r#" + ( + [(line_comment) (attribute_item)]* @context + . + [ + (struct_item + name: (_) @name) + + (enum_item + name: (_) @name) + + (impl_item + trait: (_)? @name + "for"? @name + type: (_) @name) + + (trait_item + name: (_) @name) + + (function_item + name: (_) @name + body: (block + "{" @keep + "}" @keep) @collapse) + + (macro_definition + name: (_) @name) + ] @item + ) + + (attribute_item) @collapse + (use_declaration) @collapse + "#, + ) + .unwrap(), + ) +} + +fn json_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "JSON".into(), + path_suffixes: vec!["json".into()], + ..Default::default() + }, + Some(tree_sitter_json::language()), + ) + .with_embedding_query( + r#" + (document) @item + + (array + "[" @keep + . + (object)? @keep + "]" @keep) @collapse + + (pair value: (string + "\"" @keep + "\"" @keep) @collapse) + "#, + ) + .unwrap(), + ) +} + +fn toml_lang() -> Arc { + Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + path_suffixes: vec!["toml".into()], + ..Default::default() + }, + Some(tree_sitter_toml::language()), + )) +} + +fn cpp_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "CPP".into(), + path_suffixes: vec!["cpp".into()], + ..Default::default() + }, + Some(tree_sitter_cpp::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + (function_definition + (type_qualifier)? @name + type: (_)? @name + declarator: [ + (function_declarator + declarator: (_) @name) + (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name)) + (pointer_declarator + "*" @name + declarator: (pointer_declarator + "*" @name + declarator: (function_declarator + declarator: (_) @name))) + (reference_declarator + ["&" "&&"] @name + (function_declarator + declarator: (_) @name)) + ] + (type_qualifier)? @name) @item + ) + + ( + (comment)* @context + . + (template_declaration + (class_specifier + "class" @name + name: (_) @name) + ) @item + ) + + ( + (comment)* @context + . + (class_specifier + "class" @name + name: (_) @name) @item + ) + + ( + (comment)* @context + . + (enum_specifier + "enum" @name + name: (_) @name) @item + ) + + ( + (comment)* @context + . + (declaration + type: (struct_specifier + "struct" @name) + declarator: (_) @name) @item + ) + + "#, + ) + .unwrap(), + ) +} + +fn lua_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Lua".into(), + path_suffixes: vec!["lua".into()], + collapsed_placeholder: "--[ ... ]--".to_string(), + ..Default::default() + }, + Some(tree_sitter_lua::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + (function_declaration + "function" @name + name: (_) @name + (comment)* @collapse + body: (block) @collapse + ) @item + ) + "#, + ) + .unwrap(), + ) +} + +fn php_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "PHP".into(), + path_suffixes: vec!["php".into()], + collapsed_placeholder: "/* ... */".into(), + ..Default::default() + }, + Some(tree_sitter_php::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + [ + (function_definition + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (trait_declaration + "trait" @name + name: (_) @name) + + (method_declaration + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (interface_declaration + "interface" @name + name: (_) @name + ) + + (enum_declaration + "enum" @name + name: (_) @name + ) + + ] @item + ) + "#, + ) + .unwrap(), + ) +} + +fn ruby_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Ruby".into(), + path_suffixes: vec!["rb".into()], + collapsed_placeholder: "# ...".to_string(), + ..Default::default() + }, + Some(tree_sitter_ruby::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + [ + (module + "module" @name + name: (_) @name) + (method + "def" @name + name: (_) @name + body: (body_statement) @collapse) + (class + "class" @name + name: (_) @name) + (singleton_method + "def" @name + object: (_) @name + "." @name + name: (_) @name + body: (body_statement) @collapse) + ] @item + ) + "#, + ) + .unwrap(), + ) +} + +fn elixir_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Elixir".into(), + path_suffixes: vec!["rs".into()], + ..Default::default() + }, + Some(tree_sitter_elixir::language()), + ) + .with_embedding_query( + r#" + ( + (unary_operator + operator: "@" + operand: (call + target: (identifier) @unary + (#match? @unary "^(doc)$")) + ) @context + . + (call + target: (identifier) @name + (arguments + [ + (identifier) @name + (call + target: (identifier) @name) + (binary_operator + left: (call + target: (identifier) @name) + operator: "when") + ]) + (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + ) + + (call + target: (identifier) @name + (arguments (alias) @name) + (#any-match? @name "^(defmodule|defprotocol)$")) @item + "#, + ) + .unwrap(), + ) +} + +#[gpui::test] +fn test_subtract_ranges() { + // collapsed_ranges: Vec>, keep_ranges: Vec> + + assert_eq!( + subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]), + vec![1..4, 10..21] + ); + + assert_eq!(subtract_ranges(&[0..5], &[1..2]), &[0..1, 2..5]); +} + +fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + SemanticIndexSettings::register(cx); + ProjectSettings::register(cx); + }); +} diff --git a/crates/storybook2/src/stories/cursor.rs b/crates/storybook2/src/stories/cursor.rs index d160fa4f4a0c15e86e4a5b91c7f011d4b5005d6e..7d4cf8145a034906bb87bb944ae926fff5ab9f3a 100644 --- a/crates/storybook2/src/stories/cursor.rs +++ b/crates/storybook2/src/stories/cursor.rs @@ -102,7 +102,6 @@ impl Render for CursorStory { .w_64() .h_8() .bg(gpui::red()) - .hover(|style| style.bg(gpui::blue())) .active(|style| style.bg(gpui::green())) .text_sm() .child(Story::label(name)), diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5a13efd07a0da4b8490444b3322bfc4600b70e17..dda976b2cdf245d681b44493ce9e845f01e25291 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1132,6 +1132,7 @@ mod tests { }) }) .await + .unwrap() .unwrap(); (wt, entry) diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index b007d58c34bcb2163f42bd2b88e1979a18152f56..570b37ba098b86c159b7acce1e6941402336ec97 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -299,11 +299,8 @@ impl TerminalView { cx: &mut ViewContext, ) { self.context_menu = Some(ContextMenu::build(cx, |menu, cx| { - menu.action("Clear", Box::new(Clear), cx).action( - "Close", - Box::new(CloseActiveItem { save_intent: None }), - cx, - ) + menu.action("Clear", Box::new(Clear)) + .action("Close", Box::new(CloseActiveItem { save_intent: None })) })); dbg!(&position); // todo!() @@ -739,6 +736,8 @@ impl InputHandler for TerminalView { } impl Item for TerminalView { + type Event = ItemEvent; + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(self.terminal().read(cx).title().into()) } @@ -846,6 +845,10 @@ impl Item for TerminalView { // .detach(); self.workspace_id = workspace.database_id(); } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } } impl SearchableItem for TerminalView { @@ -1173,6 +1176,7 @@ mod tests { }) }) .await + .unwrap() .unwrap(); (wt, entry) diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 4a47bc05366c5c8a9063e1b4f8fd4be560b11195..b61e4792a4242199585e3a17bac493bfce7797e3 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -5,7 +5,7 @@ use crate::ColorScale; use crate::{SystemColors, ThemeColors}; pub(crate) fn neutral() -> ColorScaleSet { - slate() + sand() } impl ThemeColors { @@ -29,12 +29,12 @@ impl ThemeColors { element_disabled: neutral().light_alpha().step_3(), drop_target_background: blue().light_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().light_alpha().step_4(), - ghost_element_active: neutral().light_alpha().step_5(), + ghost_element_hover: neutral().light_alpha().step_3(), + ghost_element_active: neutral().light_alpha().step_4(), ghost_element_selected: neutral().light_alpha().step_5(), ghost_element_disabled: neutral().light_alpha().step_3(), - text: yellow().light().step_9(), - text_muted: neutral().light().step_11(), + text: neutral().light().step_12(), + text_muted: neutral().light().step_10(), text_placeholder: neutral().light().step_10(), text_disabled: neutral().light().step_9(), text_accent: blue().light().step_11(), @@ -53,13 +53,13 @@ impl ThemeColors { editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors") editor_subheader_background: neutral().light().step_2(), editor_active_line_background: neutral().light_alpha().step_3(), - editor_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors") - editor_active_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors") - editor_highlighted_line_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_invisible: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_active_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_document_highlight_read_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") + editor_line_number: neutral().light().step_10(), + editor_active_line_number: neutral().light().step_11(), + editor_highlighted_line_background: neutral().light_alpha().step_3(), + editor_invisible: neutral().light().step_10(), + editor_wrap_guide: neutral().light_alpha().step_7(), + editor_active_wrap_guide: neutral().light_alpha().step_8(), // todo!("pick the right colors") + editor_document_highlight_read_background: neutral().light_alpha().step_3(), // todo!("pick the right colors") editor_document_highlight_write_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") terminal_background: neutral().light().step_1(), terminal_ansi_black: black().light().step_12(), diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 8502f433f4a919d7d661f00e55d0dd353ff46fc5..ab953b121a714f1bb7454c096b777f1b742b4827 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,47 +1,51 @@ +use std::sync::Arc; + use crate::{ + default_color_scales, one_themes::{one_dark, one_family}, - Theme, ThemeFamily, + Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, + ThemeFamily, ThemeStyles, }; -// fn zed_pro_daylight() -> Theme { -// Theme { -// id: "zed_pro_daylight".to_string(), -// name: "Zed Pro Daylight".into(), -// appearance: Appearance::Light, -// styles: ThemeStyles { -// system: SystemColors::default(), -// colors: ThemeColors::light(), -// status: StatusColors::light(), -// player: PlayerColors::light(), -// syntax: Arc::new(SyntaxTheme::light()), -// }, -// } -// } +fn zed_pro_daylight() -> Theme { + Theme { + id: "zed_pro_daylight".to_string(), + name: "Zed Pro Daylight".into(), + appearance: Appearance::Light, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::light(), + status: StatusColors::light(), + player: PlayerColors::light(), + syntax: Arc::new(SyntaxTheme::light()), + }, + } +} -// pub(crate) fn zed_pro_moonlight() -> Theme { -// Theme { -// id: "zed_pro_moonlight".to_string(), -// name: "Zed Pro Moonlight".into(), -// appearance: Appearance::Dark, -// styles: ThemeStyles { -// system: SystemColors::default(), -// colors: ThemeColors::dark(), -// status: StatusColors::dark(), -// player: PlayerColors::dark(), -// syntax: Arc::new(SyntaxTheme::dark()), -// }, -// } -// } +pub(crate) fn zed_pro_moonlight() -> Theme { + Theme { + id: "zed_pro_moonlight".to_string(), + name: "Zed Pro Moonlight".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::dark(), + status: StatusColors::dark(), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), + }, + } +} -// pub fn zed_pro_family() -> ThemeFamily { -// ThemeFamily { -// id: "zed_pro".to_string(), -// name: "Zed Pro".into(), -// author: "Zed Team".into(), -// themes: vec![zed_pro_daylight(), zed_pro_moonlight()], -// scales: default_color_scales(), -// } -// } +pub fn zed_pro_family() -> ThemeFamily { + ThemeFamily { + id: "zed_pro".to_string(), + name: "Zed Pro".into(), + author: "Zed Team".into(), + themes: vec![zed_pro_daylight(), zed_pro_moonlight()], + scales: default_color_scales(), + } +} impl Default for ThemeFamily { fn default() -> Self { diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index b50eb831dda51b8357ad2b8c8ff9a7b6a86cfe81..cb7814cb6fb88886cb95490544c00f0dc7f5612e 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ - one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, - Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, + one_themes::one_family, zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, + SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, }; pub struct ThemeRegistry { @@ -117,7 +117,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_theme_families([one_family()]); + this.insert_theme_families([zed_pro_family(), one_family()]); this } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index 8675d30e3a00a94d3ea05efa018dfd7775dabace..cc73caa6dfca3c920cf79af89eb7d1993d670688 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -22,8 +22,8 @@ impl SyntaxTheme { highlights: vec![ ("attribute".into(), cyan().light().step_11().into()), ("boolean".into(), tomato().light().step_11().into()), - ("comment".into(), neutral().light().step_11().into()), - ("comment.doc".into(), iris().light().step_12().into()), + ("comment".into(), neutral().light().step_10().into()), + ("comment.doc".into(), iris().light().step_11().into()), ("constant".into(), red().light().step_9().into()), ("constructor".into(), red().light().step_9().into()), ("embedded".into(), red().light().step_9().into()), @@ -32,11 +32,11 @@ impl SyntaxTheme { ("enum".into(), red().light().step_9().into()), ("function".into(), red().light().step_9().into()), ("hint".into(), red().light().step_9().into()), - ("keyword".into(), orange().light().step_11().into()), + ("keyword".into(), orange().light().step_9().into()), ("label".into(), red().light().step_9().into()), ("link_text".into(), red().light().step_9().into()), ("link_uri".into(), red().light().step_9().into()), - ("number".into(), red().light().step_9().into()), + ("number".into(), purple().light().step_10().into()), ("operator".into(), red().light().step_9().into()), ("predictive".into(), red().light().step_9().into()), ("preproc".into(), red().light().step_9().into()), @@ -49,16 +49,16 @@ impl SyntaxTheme { ), ( "punctuation.delimiter".into(), - neutral().light().step_11().into(), + neutral().light().step_10().into(), ), ( "punctuation.list_marker".into(), blue().light().step_11().into(), ), ("punctuation.special".into(), red().light().step_9().into()), - ("string".into(), jade().light().step_11().into()), + ("string".into(), jade().light().step_9().into()), ("string.escape".into(), red().light().step_9().into()), - ("string.regex".into(), tomato().light().step_11().into()), + ("string.regex".into(), tomato().light().step_9().into()), ("string.special".into(), red().light().step_9().into()), ( "string.special.symbol".into(), @@ -67,7 +67,7 @@ impl SyntaxTheme { ("tag".into(), red().light().step_9().into()), ("text.literal".into(), red().light().step_9().into()), ("title".into(), red().light().step_9().into()), - ("type".into(), red().light().step_9().into()), + ("type".into(), cyan().light().step_9().into()), ("variable".into(), red().light().step_9().into()), ("variable.special".into(), red().light().step_9().into()), ("variant".into(), red().light().step_9().into()), diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index be55194e76ebdaedc0e72aa4a9f9d4d6314fb3eb..582ce43a88ddc8c197617693adbf0e05ab7b11e3 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View, - ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString, + View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; use theme::{Theme, ThemeRegistry, ThemeSettings}; -use ui::{prelude::*, ListItem}; +use ui::{prelude::*, v_stack, ListItem}; use util::ResultExt; use workspace::{ui::HighlightedLabel, Workspace}; @@ -65,10 +65,10 @@ impl FocusableView for ThemeSelector { } impl Render for ThemeSelector { - type Element = View>; + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - self.picker.clone() + v_stack().min_w_96().child(self.picker.clone()) } } @@ -98,7 +98,7 @@ impl ThemeSelectorDelegate { let original_theme = cx.theme().clone(); let staff_mode = cx.is_staff(); - let registry = cx.global::>(); + let registry = cx.global::(); let theme_names = registry.list(staff_mode).collect::>(); //todo!(theme sorting) // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); @@ -126,7 +126,7 @@ impl ThemeSelectorDelegate { fn show_selected_theme(&mut self, cx: &mut ViewContext>) { if let Some(mat) = self.matches.get(self.selected_index) { - let registry = cx.global::>(); + let registry = cx.global::(); match registry.get(&mat.string) { Ok(theme) => { Self::set_theme(theme, cx); diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 17271de48d4993c111c5cececd50499c6ef801b3..583b30a2e05fd647d7603d41ee4f0edbf8510713 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -5,6 +5,7 @@ mod context_menu; mod disclosure; mod divider; mod icon; +mod indicator; mod keybinding; mod label; mod list; @@ -24,6 +25,7 @@ pub use context_menu::*; pub use disclosure::*; pub use divider::*; pub use icon::*; +pub use indicator::*; pub use keybinding::*; pub use label::*; pub use list::*; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 54c8d9337574c914f3545ae9066279eeb6027936..0d6a632db58f3d750bcd8e60cf6a9a92b5405468 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -7,7 +7,7 @@ use gpui::{ IntoElement, Render, View, VisualContext, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use std::rc::Rc; +use std::{rc::Rc, time::Duration}; pub enum ContextMenuItem { Separator, @@ -16,7 +16,7 @@ pub enum ContextMenuItem { label: SharedString, icon: Option, handler: Rc, - key_binding: Option, + action: Option>, }, } @@ -24,6 +24,7 @@ pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, selected_index: Option, + delayed: bool, } impl FocusableView for ContextMenu { @@ -46,6 +47,7 @@ impl ContextMenu { items: Default::default(), focus_handle: cx.focus_handle(), selected_index: None, + delayed: false, }, cx, ) @@ -70,36 +72,26 @@ impl ContextMenu { self.items.push(ContextMenuItem::Entry { label: label.into(), handler: Rc::new(on_click), - key_binding: None, icon: None, + action: None, }); self } - pub fn action( - mut self, - label: impl Into, - action: Box, - cx: &mut WindowContext, - ) -> Self { + pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), - key_binding: KeyBinding::for_action(&*action, cx), + action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), icon: None, }); self } - pub fn link( - mut self, - label: impl Into, - action: Box, - cx: &mut WindowContext, - ) -> Self { + pub fn link(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), - key_binding: KeyBinding::for_action(&*action, cx), + action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), icon: Some(Icon::Link), }); @@ -161,6 +153,37 @@ impl ContextMenu { self.select_last(&Default::default(), cx); } } + + pub fn on_action_dispatch(&mut self, dispatched: &Box, cx: &mut ViewContext) { + if let Some(ix) = self.items.iter().position(|item| { + if let ContextMenuItem::Entry { + action: Some(action), + .. + } = item + { + action.partial_eq(&**dispatched) + } else { + false + } + }) { + self.selected_index = Some(ix); + self.delayed = true; + cx.notify(); + let action = dispatched.boxed_clone(); + cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + this.update(&mut cx, |this, cx| { + cx.dispatch_action(action); + this.cancel(&Default::default(), cx) + }) + }) + .detach_and_log_err(cx); + } else { + cx.propagate() + } + } } impl ContextMenuItem { @@ -185,6 +208,22 @@ impl Render for ContextMenu { .on_action(cx.listener(ContextMenu::select_prev)) .on_action(cx.listener(ContextMenu::confirm)) .on_action(cx.listener(ContextMenu::cancel)) + .when(!self.delayed, |mut el| { + for item in self.items.iter() { + if let ContextMenuItem::Entry { + action: Some(action), + .. + } = item + { + el = el.on_boxed_action( + action, + cx.listener(ContextMenu::on_action_dispatch), + ); + } + } + el + }) + .on_blur(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx))) .flex_none() .child( List::new().children(self.items.iter().enumerate().map( @@ -196,8 +235,8 @@ impl Render for ContextMenu { ContextMenuItem::Entry { label, handler, - key_binding, icon, + action, } => { let handler = handler.clone(); let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); @@ -218,11 +257,10 @@ impl Render for ContextMenu { .w_full() .justify_between() .child(label_element) - .children( - key_binding - .clone() - .map(|binding| div().ml_1().child(binding)), - ), + .children(action.as_ref().and_then(|action| { + KeyBinding::for_action(&**action, cx) + .map(|binding| div().ml_1().child(binding)) + })), ) .selected(Some(ix) == self.selected_index) .on_click(move |event, cx| { diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index e9a13bd009b0afd6b23e89e773f81d93684d8989..f3740800c27e561fb3a1d1a8904d70b786145088 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,15 +1,26 @@ -use gpui::{rems, svg, IntoElement, Svg}; +use gpui::{rems, svg, IntoElement, Rems, Svg}; use strum::EnumIter; use crate::prelude::*; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { + XSmall, Small, #[default] Medium, } +impl IconSize { + pub fn rems(self) -> Rems { + match self { + IconSize::XSmall => rems(12. / 16.), + IconSize::Small => rems(14. / 16.), + IconSize::Medium => rems(16. / 16.), + } + } +} + #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, @@ -173,13 +184,8 @@ impl RenderOnce for IconElement { type Rendered = Svg; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let svg_size = match self.size { - IconSize::Small => rems(14. / 16.), - IconSize::Medium => rems(16. / 16.), - }; - svg() - .size(svg_size) + .size(self.size.rems()) .flex_none() .path(self.path) .text_color(self.color.color(cx)) diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a94650dfc6cdd4a17290413587c2fd17898e20f --- /dev/null +++ b/crates/ui2/src/components/indicator.rs @@ -0,0 +1,60 @@ +use gpui::{Div, Position}; + +use crate::prelude::*; + +#[derive(Default)] +pub enum IndicatorStyle { + #[default] + Dot, + Bar, +} + +#[derive(IntoElement)] +pub struct Indicator { + position: Position, + style: IndicatorStyle, + color: Color, +} + +impl Indicator { + pub fn dot() -> Self { + Self { + position: Position::Relative, + style: IndicatorStyle::Dot, + color: Color::Default, + } + } + + pub fn bar() -> Self { + Self { + position: Position::Relative, + style: IndicatorStyle::Dot, + color: Color::Default, + } + } + + pub fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + pub fn absolute(mut self) -> Self { + self.position = Position::Absolute; + self + } +} + +impl RenderOnce for Indicator { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .flex_none() + .map(|this| match self.style { + IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(), + IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(), + }) + .when(self.position == Position::Absolute, |this| this.absolute()) + .bg(self.color.color(cx)) + } +} diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 993e2f323e7d2305cf4963f1fbd780a77441f208..c4054fa1a434e677c3480740c31ee55ec45cb419 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; -use gpui::{relative, rems, Action, Div, IntoElement, Keystroke}; +use gpui::{relative, rems, Action, Div, FocusHandle, IntoElement, Keystroke}; #[derive(IntoElement, Clone)] pub struct KeyBinding { @@ -49,12 +49,21 @@ impl RenderOnce for KeyBinding { impl KeyBinding { pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { - // todo! this last is arbitrary, we want to prefer users key bindings over defaults, - // and vim over normal (in vim mode), etc. let key_binding = cx.bindings_for_action(action).last().cloned()?; Some(Self::new(key_binding)) } + // like for_action(), but lets you specify the context from which keybindings + // are matched. + pub fn for_action_in( + action: &dyn Action, + focus: &FocusHandle, + cx: &mut WindowContext, + ) -> Option { + let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?; + Some(Self::new(key_binding)) + } + fn icon_for_key(keystroke: &Keystroke) -> Option { let mut icon: Option = None; diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 85198416cd94e7b498fa2c9283a175d390b77a82..529f2c2a58765caeeb20ce7ecf6f12080c2edc0b 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,7 +1,8 @@ use std::rc::Rc; use gpui::{ - px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful, + px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, + Stateful, }; use smallvec::SmallVec; @@ -21,6 +22,7 @@ pub struct ListItem { inset: bool, on_click: Option>, on_toggle: Option>, + tooltip: Option AnyView + 'static>>, on_secondary_mouse_down: Option>, children: SmallVec<[AnyElement; 2]>, } @@ -38,6 +40,7 @@ impl ListItem { on_click: None, on_secondary_mouse_down: None, on_toggle: None, + tooltip: None, children: SmallVec::new(), } } @@ -55,6 +58,11 @@ impl ListItem { self } + pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { + self.tooltip = Some(Box::new(tooltip)); + self + } + pub fn inset(mut self, inset: bool) -> Self { self.inset = inset; self @@ -149,6 +157,7 @@ impl RenderOnce for ListItem { (on_mouse_down)(event, cx) }) }) + .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) .child( div() .when(self.inset, |this| this.px_2()) diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 6fd0262c674ac6d13d9728b78f23a8db1f6e6ebd..38065b62754b5facb5ed9440ad2b74535f40d445 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -8,5 +8,6 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; +pub use crate::{h_stack, v_stack}; pub use crate::{ButtonCommon, Color, StyledExt}; pub use theme::ActiveTheme; diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index d0ba7957ec28c90aabacef8903a1544cd05e5a42..19b244383fdf2ab24cfec9381a6f8ddb87f6e977 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -219,9 +219,11 @@ impl PathMatcher { } pub fn is_match>(&self, other: P) -> bool { - other.as_ref().starts_with(&self.maybe_path) - || self.glob.is_match(&other) - || self.check_with_end_separator(other.as_ref()) + let other_path = other.as_ref(); + other_path.starts_with(&self.maybe_path) + || other_path.ends_with(&self.maybe_path) + || self.glob.is_match(other_path) + || self.check_with_end_separator(other_path) } fn check_with_end_separator(&self, path: &Path) -> bool { @@ -418,4 +420,14 @@ mod tests { "Path matcher {path_matcher} should match {path:?}" ); } + + #[test] + fn project_search() { + let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); + let path_matcher = PathMatcher::new("**/node_modules/**").unwrap(); + assert!( + path_matcher.is_match(&path), + "Path matcher {path_matcher} should match {path:?}" + ); + } } diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs index 441c2bf69663084e40d354eccefb8d7bbb66ce49..db348ab0a1a7115586f38ffb7acb37671c4b15a9 100644 --- a/crates/welcome2/src/welcome.rs +++ b/crates/welcome2/src/welcome.rs @@ -259,6 +259,8 @@ impl FocusableView for WelcomePage { } impl Item for WelcomePage { + type Event = ItemEvent; + fn tab_content(&self, _: Option, _: &WindowContext) -> AnyElement { "Welcome to Zed!".into_any() } @@ -278,4 +280,8 @@ impl Item for WelcomePage { _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), })) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ea70e4f7def8f35715bfbfdbda827a3b3c943d58..a50a109c83287997d87a71bcb4d396d43adb25aa 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -481,18 +481,21 @@ impl Pane { pub(crate) fn open_item( &mut self, - project_entry_id: ProjectEntryId, + project_entry_id: Option, focus_item: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut ViewContext) -> Box, ) -> Box { let mut existing_item = None; - for (index, item) in self.items.iter().enumerate() { - if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id] - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; + if let Some(project_entry_id) = project_entry_id { + for (index, item) in self.items.iter().enumerate() { + if item.is_singleton(cx) + && item.project_entry_ids(cx).as_slice() == [project_entry_id] + { + let item = item.boxed_clone(); + existing_item = Some((index, item)); + break; + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bea26e402ef41fea9b0acaa7d73a7459110be40a..455c27a57e1821f233a35a6eb6f9f3e223fa1bb0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2129,13 +2129,13 @@ impl Workspace { }) } - pub(crate) fn load_path( + fn load_path( &mut self, path: ProjectPath, cx: &mut ViewContext, ) -> Task< Result<( - ProjectEntryId, + Option, impl 'static + FnOnce(&mut ViewContext) -> Box, )>, > { diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml index 2dd7c6468eed0655fc082feaae89674db6f88bc5..a06ac6e3e09f4634795665c4c35f569ed70ff8d0 100644 --- a/crates/workspace2/Cargo.toml +++ b/crates/workspace2/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ [dependencies] db = { path = "../db2", package = "db2" } +call = { path = "../call2", package = "call2" } client = { path = "../client2", package = "client2" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } @@ -36,7 +37,6 @@ theme = { path = "../theme2", package = "theme2" } util = { path = "../util" } ui = { package = "ui2", path = "../ui2" } -async-trait.workspace = true async-recursion = "1.0.0" itertools = "0.10" bincode = "1.2.1" diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index e7cdb2f861b9f911019ed1b191653885d76b6402..536ebd980e6cc66fae6cb56d15f0bdead58fda1d 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -78,7 +78,7 @@ impl Settings for ItemSettings { } } -#[derive(Eq, PartialEq, Hash, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { CloseItem, UpdateTab, @@ -92,7 +92,9 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: FocusableView + EventEmitter { +pub trait Item: FocusableView + EventEmitter { + type Event; + fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { @@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter { unimplemented!("reload() must be implemented if can_save() returns true") } + fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent)); + fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -206,12 +210,12 @@ pub trait Item: FocusableView + EventEmitter { } pub trait ItemHandle: 'static + Send { - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; @@ -285,20 +289,20 @@ impl dyn ItemHandle { } impl ItemHandle for View { - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { - self.focus_handle(cx) - } - fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription { cx.subscribe(self, move |_, event, cx| { - handler(event, cx); + T::to_item_events(event, |item_event| handler(item_event, cx)); }) } + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.focus_handle(cx) + } + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { self.read(cx).tab_tooltip_text(cx) } @@ -461,7 +465,7 @@ impl ItemHandle for View { } } - match event { + T::to_item_events(event, |event| match event { ItemEvent::CloseItem => { pane.update(cx, |pane, cx| { pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx) @@ -489,7 +493,7 @@ impl ItemHandle for View { } _ => {} - } + }); })); cx.on_blur(&self.focus_handle(cx), move |workspace, cx| { @@ -655,12 +659,7 @@ pub enum FollowEvent { Unfollow, } -pub trait FollowableEvents { - fn to_follow_event(&self) -> Option; -} - pub trait FollowableItem: Item { - type FollowableEvent: FollowableEvents; fn remote_id(&self) -> Option; fn to_state_proto(&self, cx: &WindowContext) -> Option; fn from_state_proto( @@ -670,9 +669,10 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut WindowContext, ) -> Option>>>; + fn to_follow_event(event: &Self::Event) -> Option; fn add_event_to_update_proto( &self, - event: &Self::FollowableEvent, + event: &Self::Event, update: &mut Option, cx: &WindowContext, ) -> bool; @@ -683,7 +683,6 @@ pub trait FollowableItem: Item { cx: &mut ViewContext, ) -> Task>; fn is_project_item(&self, cx: &WindowContext) -> bool; - fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); } @@ -739,10 +738,7 @@ impl FollowableItemHandle for View { } fn to_follow_event(&self, event: &dyn Any) -> Option { - event - .downcast_ref() - .map(T::FollowableEvent::to_follow_event) - .flatten() + T::to_follow_event(event.downcast_ref()?) } fn apply_update_proto( @@ -929,6 +925,12 @@ pub mod test { } impl Item for TestItem { + type Event = ItemEvent; + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } + fn tab_description(&self, detail: usize, _: &AppContext) -> Option { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 438ad396936e740fe0c1cc3518e7bdca9f02e941..495819f6081745aaa243b4704e46ca84f10d8bda 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,5 +1,5 @@ use crate::{ - item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, + item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle}, toolbar::Toolbar, workspace_settings::{AutosaveSetting, WorkspaceSettings}, NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace, @@ -27,7 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip, + h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, + Indicator, Label, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -537,18 +538,21 @@ impl Pane { pub(crate) fn open_item( &mut self, - project_entry_id: ProjectEntryId, + project_entry_id: Option, focus_item: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut ViewContext) -> Box, ) -> Box { let mut existing_item = None; - for (index, item) in self.items.iter().enumerate() { - if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id] - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; + if let Some(project_entry_id) = project_entry_id { + for (index, item) in self.items.iter().enumerate() { + if item.is_singleton(cx) + && item.project_entry_ids(cx).as_slice() == [project_entry_id] + { + let item = item.boxed_clone(); + existing_item = Some((index, item)); + break; + } } } @@ -1415,22 +1419,7 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { let label = item.tab_content(Some(detail), cx); - let close_icon = || { - let id = item.item_id(); - - div() - .id(ix) - .invisible() - .group_hover("", |style| style.visible()) - .child( - IconButton::new("close_tab", Icon::Close).on_click(cx.listener( - move |pane, _, cx| { - pane.close_item_by_id(id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - }, - )), - ) - }; + let close_side = &ItemSettings::get_global(cx).close_position; let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { false => ( @@ -1447,109 +1436,129 @@ impl Pane { ), }; - let close_right = ItemSettings::get_global(cx).close_position.right(); let is_active = ix == self.active_item_index; + let indicator = { + let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + + h_stack() + .w_3() + .h_3() + .justify_center() + .absolute() + .map(|this| match close_side { + ClosePosition::Left => this.right_1(), + ClosePosition::Right => this.left_1(), + }) + .when_some(indicator_color, |this, indicator_color| { + this.child(Indicator::dot().color(indicator_color)) + }) + }; + + let close_button = { + let id = item.item_id(); + + h_stack() + .invisible() + .w_3() + .h_3() + .justify_center() + .absolute() + .map(|this| match close_side { + ClosePosition::Left => this.left_1(), + ClosePosition::Right => this.right_1(), + }) + .group_hover("", |style| style.visible()) + .child( + // TODO: Fix button size + IconButton::new("close tab", Icon::Close) + .icon_color(Color::Muted) + .size(ButtonSize::None) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(move |pane, _, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + })), + ) + }; + let tab = div() - .group("") - .id(ix) - .cursor_pointer() - .when_some(item.tab_tooltip_text(cx), |div, text| { - div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) - }) - .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))) - // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) - // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) - // .on_drop(|_view, state: View, cx| { - // eprintln!("{:?}", state.read(cx)); - // }) - .flex() - .items_center() - .justify_center() - // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") - .map(|this| { - if close_right { - this.pl_3().pr_1() - } else { - this.pr_1().pr_3() - } - }) - .py_1() - .bg(tab_bg) .border_color(cx.theme().colors().border) - .text_color(if is_active { - cx.theme().colors().text - } else { - cx.theme().colors().text_muted - }) + .bg(tab_bg) + // 30px @ 16px/rem + .h(rems(1.875)) .map(|this| { + let is_first_item = ix == 0; let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { - cmp::Ordering::Less => this.border_l().mr_px(), + cmp::Ordering::Less => { + if is_first_item { + this.pl_px().pr_px().border_b() + } else { + this.border_l().pr_px().border_b() + } + } cmp::Ordering::Greater => { if is_last_item { - this.mr_px().ml_px() + this.pr_px().pl_px().border_b() + } else { + this.border_r().pl_px().border_b() + } + } + cmp::Ordering::Equal => { + if is_first_item { + this.pl_px().border_r().pb_px() } else { - this.border_r().ml_px() + this.border_l().border_r().pb_px() } } - cmp::Ordering::Equal => this.border_l().border_r(), } }) - // .hover(|h| h.bg(tab_hover_bg)) - // .active(|a| a.bg(tab_active_bg)) .child( - div() - .flex() - .items_center() + h_stack() + .group("") + .id(ix) + .relative() + .h_full() + .cursor_pointer() + .when_some(item.tab_tooltip_text(cx), |div, text| { + div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) + }) + .on_click( + cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)), + ) + // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) + // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) + // .on_drop(|_view, state: View, cx| { + // eprintln!("{:?}", state.read(cx)); + // }) + .px_5() + // .hover(|h| h.bg(tab_hover_bg)) + // .active(|a| a.bg(tab_active_bg)) .gap_1() .text_color(text_color) - .children( - item.has_conflict(cx) - .then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Warning), - ) - }) - .or(item.is_dirty(cx).then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Info), - ) - })), - ) - .children((!close_right).then(|| close_icon())) - .child(label) - .children(close_right.then(|| close_icon())), + .child(indicator) + .child(close_button) + .child(label), ); right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { - menu.action( - "Close Active Item", - CloseActiveItem { save_intent: None }.boxed_clone(), - cx, - ) - .action("Close Inactive Items", CloseInactiveItems.boxed_clone(), cx) - .action("Close Clean Items", CloseCleanItems.boxed_clone(), cx) - .action( - "Close Items To The Left", - CloseItemsToTheLeft.boxed_clone(), - cx, - ) - .action( - "Close Items To The Right", - CloseItemsToTheRight.boxed_clone(), - cx, - ) - .action( - "Close All Items", - CloseAllItems { save_intent: None }.boxed_clone(), - cx, - ) + menu.action("Close", CloseActiveItem { save_intent: None }.boxed_clone()) + .action("Close Others", CloseInactiveItems.boxed_clone()) + .separator() + .action("Close Left", CloseItemsToTheLeft.boxed_clone()) + .action("Close Right", CloseItemsToTheRight.boxed_clone()) + .separator() + .action("Close Clean", CloseCleanItems.boxed_clone()) + .action( + "Close All", + CloseAllItems { save_intent: None }.boxed_clone(), + ) }) }) } @@ -1569,125 +1578,118 @@ impl Pane { // Left Side .child( h_stack() - .px_2() .flex() .flex_none() .gap_1() + .px_1() + .border_b() + .border_r() + .border_color(cx.theme().colors().border) // Nav Buttons .child( - div().border().border_color(gpui::red()).child( - IconButton::new("navigate_backward", Icon::ArrowLeft) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_backward()), - ), + IconButton::new("navigate_backward", Icon::ArrowLeft) + .icon_size(IconSize::Small) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_backward()), ) .child( - div().border().border_color(gpui::red()).child( - IconButton::new("navigate_forward", Icon::ArrowRight) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_forward()), - ), + IconButton::new("navigate_forward", Icon::ArrowRight) + .icon_size(IconSize::Small) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_forward()), ), ) .child( - div().flex_1().h_full().child( - div().id("tabs").flex().overflow_x_scroll().children( - self.items - .iter() - .enumerate() - .zip(self.tab_details(cx)) - .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + div() + .relative() + .flex_1() + .h_full() + .overflow_hidden_x() + .child( + div() + .absolute() + .top_0() + .left_0() + .z_index(1) + .size_full() + .border_b() + .border_color(cx.theme().colors().border), + ) + .child( + h_stack().id("tabs").z_index(2).children( + self.items + .iter() + .enumerate() + .zip(self.tab_details(cx)) + .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + ), ), - ), ) // Right Side .child( - div() - .px_1() + h_stack() .flex() .flex_none() - .gap_2() - // Nav Buttons + .gap_1() + .px_1() + .border_b() + .border_l() + .border_color(cx.theme().colors().border) .child( div() .flex() .items_center() .gap_px() .child( - div() - .bg(gpui::blue()) - .border() - .border_color(gpui::red()) - .child(IconButton::new("plus", Icon::Plus).on_click( - cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("New File", NewFile.boxed_clone(), cx) - .action( - "New Terminal", - NewCenterTerminal.boxed_clone(), - cx, - ) - .action( - "New Search", - NewSearch.boxed_clone(), - cx, - ) - }); - cx.subscribe( - &menu, - |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.new_item_menu = None; - }, - ) - .detach(); - this.new_item_menu = Some(menu); - }), - )) - .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { - el.child(Self::render_menu_overlay(new_item_menu)) - }), + IconButton::new("plus", Icon::Plus) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("New File", NewFile.boxed_clone()) + .action( + "New Terminal", + NewCenterTerminal.boxed_clone(), + ) + .action("New Search", NewSearch.boxed_clone()) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.new_item_menu = None; + }) + .detach(); + this.new_item_menu = Some(menu); + })), ) + .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { + el.child(Self::render_menu_overlay(new_item_menu)) + }) .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("split", Icon::Split).on_click( - cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action( - "Split Right", - SplitRight.boxed_clone(), - cx, - ) - .action("Split Left", SplitLeft.boxed_clone(), cx) - .action("Split Up", SplitUp.boxed_clone(), cx) - .action("Split Down", SplitDown.boxed_clone(), cx) - }); - cx.subscribe( - &menu, - |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.split_item_menu = None; - }, - ) - .detach(); - this.split_item_menu = Some(menu); - }), - )) - .when_some( - self.split_item_menu.as_ref(), - |el, split_item_menu| { - el.child(Self::render_menu_overlay(split_item_menu)) - }, - ), - ), + IconButton::new("split", Icon::Split) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("Split Right", SplitRight.boxed_clone()) + .action("Split Left", SplitLeft.boxed_clone()) + .action("Split Up", SplitUp.boxed_clone()) + .action("Split Down", SplitDown.boxed_clone()) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.split_item_menu = None; + }) + .detach(); + this.split_item_menu = Some(menu); + })), + ) + .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { + el.child(Self::render_menu_overlay(split_item_menu)) + }), ), ) } @@ -2105,6 +2107,8 @@ impl Render for Pane { v_stack() .key_context("Pane") .track_focus(&self.focus_handle) + .size_full() + .overflow_hidden() .on_focus_in({ let this = this.clone(); move |event, cx| { @@ -2172,7 +2176,6 @@ impl Render for Pane { pane.close_all_items(action, cx) .map(|task| task.detach_and_log_err(cx)); })) - .size_full() .on_action( cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index c98fac00c6773a85955b05c2114a2c7b59e6cec9..66465a4982895e3ed22b8b76d288464b1e54e100 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -1,18 +1,20 @@ use crate::{AppState, FollowerState, Pane, Workspace}; use anyhow::{anyhow, bail, Result}; +use call::{ActiveCall, ParticipantLocation}; use collections::HashMap; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{ - point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext, + point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View, + ViewContext, }; use parking_lot::Mutex; use project::Project; use serde::Deserialize; use std::sync::Arc; -use ui::prelude::*; +use ui::{prelude::*, Button}; const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; @@ -126,6 +128,7 @@ impl PaneGroup { &self, project: &Model, follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, @@ -135,6 +138,7 @@ impl PaneGroup { project, 0, follower_states, + active_call, active_pane, zoomed, app_state, @@ -196,6 +200,7 @@ impl Member { project: &Model, basis: usize, follower_states: &HashMap, FollowerState>, + active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, app_state: &Arc, @@ -203,19 +208,89 @@ impl Member { ) -> impl IntoElement { match self { Member::Pane(pane) => { - // todo!() - // let pane_element = if Some(pane.into()) == zoomed { - // None - // } else { - // Some(pane) - // }; - - div().size_full().child(pane.clone()).into_any() - - // Stack::new() - // .with_child(pane_element.contained().with_border(leader_border)) - // .with_children(leader_status_box) - // .into_any() + let leader = follower_states.get(pane).and_then(|state| { + let room = active_call?.read(cx).room()?.read(cx); + room.remote_participant_for_peer_id(state.leader_id) + }); + + let mut leader_border = None; + let mut leader_status_box = None; + if let Some(leader) = &leader { + let mut leader_color = cx + .theme() + .players() + .color_for_participant(leader.participant_index.0) + .cursor; + leader_color.fade_out(0.3); + leader_border = Some(leader_color); + + leader_status_box = match leader.location { + ParticipantLocation::SharedProject { + project_id: leader_project_id, + } => { + if Some(leader_project_id) == project.read(cx).remote_id() { + None + } else { + let leader_user = leader.user.clone(); + let leader_user_id = leader.user.id; + Some( + Button::new( + ("leader-status", pane.entity_id()), + format!( + "Follow {} to their active project", + leader_user.github_login, + ), + ) + .on_click(cx.listener( + move |this, _, cx| { + crate::join_remote_project( + leader_project_id, + leader_user_id, + this.app_state().clone(), + cx, + ) + .detach_and_log_err(cx); + }, + )), + ) + } + } + ParticipantLocation::UnsharedProject => Some(Button::new( + ("leader-status", pane.entity_id()), + format!( + "{} is viewing an unshared Zed project", + leader.user.github_login + ), + )), + ParticipantLocation::External => Some(Button::new( + ("leader-status", pane.entity_id()), + format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ), + )), + }; + } + + div() + .relative() + .size_full() + .child(pane.clone()) + .when_some(leader_border, |this, color| { + this.border_2().border_color(color) + }) + .when_some(leader_status_box, |this, status_box| { + this.child( + div() + .absolute() + .w_96() + .bottom_3() + .right_3() + .z_index(1) + .child(status_box), + ) + }) + .into_any() // let el = div() // .flex() diff --git a/crates/call2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs similarity index 90% rename from crates/call2/src/shared_screen.rs rename to crates/workspace2/src/shared_screen.rs index c38ebeac021d59c810fc27ff528ddc773f9642f4..134dfc66bb82a42867c7fdb9d32b4cca359a0337 100644 --- a/crates/call2/src/shared_screen.rs +++ b/crates/workspace2/src/shared_screen.rs @@ -1,5 +1,9 @@ -use crate::participant::{Frame, RemoteVideoTrack}; +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; use anyhow::Result; +use call::participant::{Frame, RemoteVideoTrack}; use client::{proto::PeerId, User}; use futures::StreamExt; use gpui::{ @@ -9,7 +13,6 @@ use gpui::{ }; use std::sync::{Arc, Weak}; use ui::{h_stack, Icon, IconElement}; -use workspace::{item::Item, ItemNavHistory, WorkspaceId}; pub enum Event { Close, @@ -56,7 +59,6 @@ impl SharedScreen { } impl EventEmitter for SharedScreen {} -impl EventEmitter for SharedScreen {} impl FocusableView for SharedScreen { fn focus_handle(&self, _: &AppContext) -> FocusHandle { @@ -76,9 +78,12 @@ impl Render for SharedScreen { } impl Item for SharedScreen { + type Event = Event; + fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some(format!("{}'s screen", self.user.github_login).into()) } + fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(nav_history) = self.nav_history.as_mut() { nav_history.push::<()>(None, cx); @@ -108,4 +113,10 @@ impl Item for SharedScreen { let track = self.track.upgrade()?; Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } + } } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 8c554dcd6744d94fb7c3b0dfb30e40fad5afeadc..1cc71e4d849e0d43bc94782d547e9289da180011 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,10 +1,10 @@ use crate::ItemHandle; use gpui::{ - div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, + AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext, WindowContext, }; use ui::prelude::*; -use ui::{h_stack, v_stack, Icon, IconButton}; +use ui::{h_stack, v_stack}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), @@ -87,25 +87,7 @@ impl Render for Toolbar { .child( h_stack() .justify_between() - // Toolbar left side - .children(self.items.iter().map(|(child, _)| child.to_any())) - // Toolbar right side - .child( - h_stack() - .p_1() - .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)), - ) - .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("inline-assist", Icon::MagicWand)), - ), - ), + .children(self.items.iter().map(|(child, _)| child.to_any())), ) } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5dcec2cabd5392a5d65695602a1ae6c05aabe63c..2439e19d04ebe3f258cd4975babec928c88d6a1c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -10,15 +10,16 @@ mod persistence; pub mod searchable; // todo!() mod modal_layer; +pub mod shared_screen; mod status_bar; mod toolbar; mod workspace_settings; use anyhow::{anyhow, Context as _, Result}; -use async_trait::async_trait; +use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, User, UserStore, + Client, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; @@ -28,11 +29,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, - ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext, + actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext, + AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, + FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, + ModelContext, ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; @@ -52,6 +53,7 @@ use postage::stream::Stream; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; use settings::Settings; +use shared_screen::SharedScreen; use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ @@ -209,27 +211,32 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &Open, cx: &mut AppContext| { - // let mut paths = cx.prompt_for_paths(PathPromptOptions { - // files: true, - // directories: true, - // multiple: true, - // }); - // if let Some(app_state) = app_state.upgrade() { - // cx.spawn(move |mut cx| async move { - // if let Some(paths) = paths.recv().await.flatten() { - // cx.update(|cx| { - // open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) - // }); - // } - // }) - // .detach(); - // } - // } - // }); + cx.on_action(Workspace::close_global); + cx.on_action(restart); + + cx.on_action({ + let app_state = Arc::downgrade(&app_state); + move |_: &Open, cx: &mut AppContext| { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); + + if let Some(app_state) = app_state.upgrade() { + cx.spawn(move |mut cx| async move { + if let Some(paths) = paths.await.log_err().flatten() { + cx.update(|cx| { + open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) + }) + .ok(); + } + }) + .detach(); + } + } + }); } type ProjectItemBuilders = @@ -302,7 +309,6 @@ pub struct AppState { pub user_store: Model, pub workspace_store: Model, pub fs: Arc, - pub call_factory: CallFactory, pub build_window_options: fn(Option, Option, &mut AppContext) -> WindowOptions, pub node_runtime: Arc, @@ -321,69 +327,6 @@ struct Follower { peer_id: PeerId, } -#[cfg(any(test, feature = "test-support"))] -pub struct TestCallHandler; - -#[cfg(any(test, feature = "test-support"))] -impl CallHandler for TestCallHandler { - fn peer_state( - &mut self, - id: PeerId, - project: &Model, - cx: &mut ViewContext, - ) -> Option<(bool, bool)> { - None - } - - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &View, - cx: &mut ViewContext, - ) -> Option> { - None - } - - fn room_id(&self, cx: &AppContext) -> Option { - None - } - - fn hang_up(&self, cx: &mut AppContext) -> Task> { - Task::ready(Err(anyhow!("TestCallHandler should not be hanging up"))) - } - - fn active_project(&self, cx: &AppContext) -> Option> { - None - } - - fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut AppContext, - ) -> Task> { - unimplemented!() - } - - fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { - None - } - - fn is_muted(&self, cx: &AppContext) -> Option { - None - } - - fn toggle_mute(&self, cx: &mut AppContext) {} - - fn toggle_screen_share(&self, cx: &mut AppContext) {} - - fn toggle_deafen(&self, cx: &mut AppContext) {} - - fn is_deafened(&self, cx: &AppContext) -> Option { - None - } -} - impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { @@ -414,7 +357,6 @@ impl AppState { workspace_store, node_runtime: FakeNodeRuntime::new(), build_window_options: |_, _, _| Default::default(), - call_factory: |_| Box::new(TestCallHandler), }) } } @@ -471,40 +413,6 @@ pub enum Event { WorkspaceCreated(WeakView), } -#[async_trait(?Send)] -pub trait CallHandler { - fn peer_state( - &mut self, - id: PeerId, - project: &Model, - cx: &mut ViewContext, - ) -> Option<(bool, bool)>; - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &View, - cx: &mut ViewContext, - ) -> Option>; - fn room_id(&self, cx: &AppContext) -> Option; - fn is_in_room(&self, cx: &mut ViewContext) -> bool { - self.room_id(cx).is_some() - } - fn hang_up(&self, cx: &mut AppContext) -> Task>; - fn active_project(&self, cx: &AppContext) -> Option>; - fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut AppContext, - ) -> Task>; - fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>>; - fn is_muted(&self, cx: &AppContext) -> Option; - fn is_deafened(&self, cx: &AppContext) -> Option; - fn toggle_mute(&self, cx: &mut AppContext); - fn toggle_deafen(&self, cx: &mut AppContext); - fn toggle_screen_share(&self, cx: &mut AppContext); -} - pub struct Workspace { window_self: WindowHandle, weak_self: WeakView, @@ -525,10 +433,10 @@ pub struct Workspace { titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, - call_handler: Box, follower_states: HashMap, FollowerState>, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, + active_call: Option<(Model, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, @@ -556,7 +464,6 @@ struct FollowerState { enum WorkspaceBounds {} -type CallFactory = fn(&mut ViewContext) -> Box; impl Workspace { pub fn new( workspace_id: WorkspaceId, @@ -648,19 +555,9 @@ impl Workspace { mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); let _apply_leader_updates = cx.spawn(|this, mut cx| async move { while let Some((leader_id, update)) = leader_updates_rx.next().await { - let mut cx2 = cx.clone(); - let t = this.clone(); - - Workspace::process_leader_update(&this, leader_id, update, &mut cx) + Self::process_leader_update(&this, leader_id, update, &mut cx) .await .log_err(); - - // this.update(&mut cx, |this, cxx| { - // this.call_handler - // .process_leader_update(leader_id, update, cx2) - // })? - // .await - // .log_err(); } Ok(()) @@ -693,6 +590,14 @@ impl Workspace { // drag_and_drop.register_container(weak_handle.clone()); // }); + let mut active_call = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + let mut subscriptions = Vec::new(); + subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); + active_call = Some((call, subscriptions)); + } + let subscriptions = vec![ cx.observe_window_activation(Self::on_window_activation_changed), cx.observe_window_bounds(move |_, cx| { @@ -769,8 +674,7 @@ impl Workspace { follower_states: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, - - call_handler: (app_state.call_factory)(cx), + active_call, database_id: workspace_id, app_state, _observe_current_user, @@ -1176,7 +1080,6 @@ impl Workspace { } } - // todo!(Non-window-actions) pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { cx.windows().iter().find(|window| { window @@ -1194,21 +1097,18 @@ impl Workspace { }); } - pub fn close( - &mut self, - _: &CloseWindow, - cx: &mut ViewContext, - ) -> Option>> { + pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext) { let window = cx.window_handle(); let prepare = self.prepare_to_close(false, cx); - Some(cx.spawn(|_, mut cx| async move { + cx.spawn(|_, mut cx| async move { if prepare.await? { window.update(&mut cx, |_, cx| { cx.remove_window(); })?; } - Ok(()) - })) + anyhow::Ok(()) + }) + .detach_and_log_err(cx) } pub fn prepare_to_close( @@ -1217,7 +1117,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { //todo!(saveing) - + let active_call = self.active_call().cloned(); let window = cx.window_handle(); cx.spawn(|this, mut cx| async move { @@ -1228,27 +1128,27 @@ impl Workspace { .count() })?; - if !quitting - && workspace_count == 1 - && this - .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx)) - .log_err() - .unwrap_or_default() - { - let answer = window.update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - &["Close window and hang up", "Cancel"], - ) - })?; + if let Some(active_call) = active_call { + if !quitting + && workspace_count == 1 + && active_call.read_with(&cx, |call, _| call.room().is_some())? + { + let answer = window.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Close window and hang up", "Cancel"], + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))? - .await - .log_err(); + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(&mut cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); + } } } @@ -1953,13 +1853,13 @@ impl Workspace { }) } - pub(crate) fn load_path( + fn load_path( &mut self, path: ProjectPath, cx: &mut ViewContext, ) -> Task< Result<( - ProjectEntryId, + Option, impl 'static + Send + FnOnce(&mut ViewContext) -> Box, )>, > { @@ -2032,7 +1932,7 @@ impl Workspace { pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext) { if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) { self.active_pane.update(cx, |pane, cx| { - pane.add_item(shared_screen, false, true, None, cx) + pane.add_item(Box::new(shared_screen), false, true, None, cx) }); } } @@ -2370,159 +2270,173 @@ impl Workspace { cx.notify(); } - // fn start_following( - // &mut self, - // leader_id: PeerId, - // cx: &mut ViewContext, - // ) -> Option>> { - // let pane = self.active_pane().clone(); - - // self.last_leaders_by_pane - // .insert(pane.downgrade(), leader_id); - // self.unfollow(&pane, cx); - // self.follower_states.insert( - // pane.clone(), - // FollowerState { - // leader_id, - // active_view_id: None, - // items_by_leader_view_id: Default::default(), - // }, - // ); - // cx.notify(); - - // let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - // let project_id = self.project.read(cx).remote_id(); - // let request = self.app_state.client.request(proto::Follow { - // room_id, - // project_id, - // leader_id: Some(leader_id), - // }); + fn start_following( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option>> { + let pane = self.active_pane().clone(); + + self.last_leaders_by_pane + .insert(pane.downgrade(), leader_id); + self.unfollow(&pane, cx); + self.follower_states.insert( + pane.clone(), + FollowerState { + leader_id, + active_view_id: None, + items_by_leader_view_id: Default::default(), + }, + ); + cx.notify(); - // Some(cx.spawn(|this, mut cx| async move { - // let response = request.await?; - // this.update(&mut cx, |this, _| { - // let state = this - // .follower_states - // .get_mut(&pane) - // .ok_or_else(|| anyhow!("following interrupted"))?; - // state.active_view_id = if let Some(active_view_id) = response.active_view_id { - // Some(ViewId::from_proto(active_view_id)?) - // } else { - // None - // }; - // Ok::<_, anyhow::Error>(()) - // })??; - // Self::add_views_from_leader( - // this.clone(), - // leader_id, - // vec![pane], - // response.views, - // &mut cx, - // ) - // .await?; - // this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?; - // Ok(()) - // })) - // } + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + let project_id = self.project.read(cx).remote_id(); + let request = self.app_state.client.request(proto::Follow { + room_id, + project_id, + leader_id: Some(leader_id), + }); - // pub fn follow_next_collaborator( - // &mut self, - // _: &FollowNextCollaborator, - // cx: &mut ViewContext, - // ) -> Option>> { - // let collaborators = self.project.read(cx).collaborators(); - // let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { - // let mut collaborators = collaborators.keys().copied(); - // for peer_id in collaborators.by_ref() { - // if peer_id == leader_id { - // break; - // } - // } - // collaborators.next() - // } else if let Some(last_leader_id) = - // self.last_leaders_by_pane.get(&self.active_pane.downgrade()) - // { - // if collaborators.contains_key(last_leader_id) { - // Some(*last_leader_id) - // } else { - // None + Some(cx.spawn(|this, mut cx| async move { + let response = request.await?; + this.update(&mut cx, |this, _| { + let state = this + .follower_states + .get_mut(&pane) + .ok_or_else(|| anyhow!("following interrupted"))?; + state.active_view_id = if let Some(active_view_id) = response.active_view_id { + Some(ViewId::from_proto(active_view_id)?) + } else { + None + }; + Ok::<_, anyhow::Error>(()) + })??; + Self::add_views_from_leader( + this.clone(), + leader_id, + vec![pane], + response.views, + &mut cx, + ) + .await?; + this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?; + Ok(()) + })) + } + + // pub fn follow_next_collaborator( + // &mut self, + // _: &FollowNextCollaborator, + // cx: &mut ViewContext, + // ) { + // let collaborators = self.project.read(cx).collaborators(); + // let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { + // let mut collaborators = collaborators.keys().copied(); + // for peer_id in collaborators.by_ref() { + // if peer_id == leader_id { + // break; // } + // } + // collaborators.next() + // } else if let Some(last_leader_id) = + // self.last_leaders_by_pane.get(&self.active_pane.downgrade()) + // { + // if collaborators.contains_key(last_leader_id) { + // Some(*last_leader_id) // } else { // None - // }; - - // let pane = self.active_pane.clone(); - // let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) - // else { - // return None; - // }; - // if Some(leader_id) == self.unfollow(&pane, cx) { - // return None; // } - // self.follow(leader_id, cx) + // } else { + // None + // }; + + // let pane = self.active_pane.clone(); + // let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) + // else { + // return; + // }; + // if Some(leader_id) == self.unfollow(&pane, cx) { + // return; + // } + // if let Some(task) = self.follow(leader_id, cx) { + // task.detach(); // } + // } - // pub fn follow( - // &mut self, - // leader_id: PeerId, - // cx: &mut ViewContext, - // ) -> Option>> { - // let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - // let project = self.project.read(cx); + pub fn follow( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option>> { + let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + let project = self.project.read(cx); - // let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { - // return None; - // }; + let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { + return None; + }; - // let other_project_id = match remote_participant.location { - // call::ParticipantLocation::External => None, - // call::ParticipantLocation::UnsharedProject => None, - // call::ParticipantLocation::SharedProject { project_id } => { - // if Some(project_id) == project.remote_id() { - // None - // } else { - // Some(project_id) - // } - // } - // }; + let other_project_id = match remote_participant.location { + call::ParticipantLocation::External => None, + call::ParticipantLocation::UnsharedProject => None, + call::ParticipantLocation::SharedProject { project_id } => { + if Some(project_id) == project.remote_id() { + None + } else { + Some(project_id) + } + } + }; - // // if they are active in another project, follow there. - // if let Some(project_id) = other_project_id { - // let app_state = self.app_state.clone(); - // return Some(crate::join_remote_project( - // project_id, - // remote_participant.user.id, - // app_state, - // cx, - // )); - // } + // if they are active in another project, follow there. + if let Some(project_id) = other_project_id { + let app_state = self.app_state.clone(); + return Some(crate::join_remote_project( + project_id, + remote_participant.user.id, + app_state, + cx, + )); + } - // // if you're already following, find the right pane and focus it. - // for (pane, state) in &self.follower_states { - // if leader_id == state.leader_id { - // cx.focus(pane); - // return None; - // } - // } + // if you're already following, find the right pane and focus it. + for (pane, state) in &self.follower_states { + if leader_id == state.leader_id { + cx.focus_view(pane); + return None; + } + } - // // Otherwise, follow. - // self.start_following(leader_id, cx) + // Otherwise, follow. + self.start_following(leader_id, cx) + } + + // // if you're already following, find the right pane and focus it. + // for (pane, state) in &self.follower_states { + // if leader_id == state.leader_id { + // cx.focus(pane); + // return None; + // } // } + // // Otherwise, follow. + // self.start_following(leader_id, cx) + // } + pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { - let follower_states = &mut self.follower_states; - let state = follower_states.remove(pane)?; + let state = self.follower_states.remove(pane)?; let leader_id = state.leader_id; for (_, item) in state.items_by_leader_view_id { item.set_leader_peer_id(None, cx); } - if follower_states + if self + .follower_states .values() .all(|state| state.leader_id != state.leader_id) { let project_id = self.project.read(cx).remote_id(); - let room_id = self.call_handler.room_id(cx)?; + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); self.app_state .client .send(proto::Unfollow { @@ -2657,57 +2571,55 @@ impl Workspace { } } - // // RPC handlers + // RPC handlers fn handle_follow( &mut self, - _follower_project_id: Option, - _cx: &mut ViewContext, + follower_project_id: Option, + cx: &mut ViewContext, ) -> proto::FollowResponse { - todo!() + let client = &self.app_state.client; + let project_id = self.project.read(cx).remote_id(); - // let client = &self.app_state.client; - // let project_id = self.project.read(cx).remote_id(); + let active_view_id = self.active_item(cx).and_then(|i| { + Some( + i.to_followable_item_handle(cx)? + .remote_id(client, cx)? + .to_proto(), + ) + }); - // let active_view_id = self.active_item(cx).and_then(|i| { - // Some( - // i.to_followable_item_handle(cx)? - // .remote_id(client, cx)? - // .to_proto(), - // ) - // }); + cx.notify(); - // cx.notify(); - - // self.last_active_view_id = active_view_id.clone(); - // proto::FollowResponse { - // active_view_id, - // views: self - // .panes() - // .iter() - // .flat_map(|pane| { - // let leader_id = self.leader_for_pane(pane); - // pane.read(cx).items().filter_map({ - // let cx = &cx; - // move |item| { - // let item = item.to_followable_item_handle(cx)?; - // if (project_id.is_none() || project_id != follower_project_id) - // && item.is_project_item(cx) - // { - // return None; - // } - // let id = item.remote_id(client, cx)?.to_proto(); - // let variant = item.to_state_proto(cx)?; - // Some(proto::View { - // id: Some(id), - // leader_id, - // variant: Some(variant), - // }) - // } - // }) - // }) - // .collect(), - // } + self.last_active_view_id = active_view_id.clone(); + proto::FollowResponse { + active_view_id, + views: self + .panes() + .iter() + .flat_map(|pane| { + let leader_id = self.leader_for_pane(pane); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let item = item.to_followable_item_handle(cx)?; + if (project_id.is_none() || project_id != follower_project_id) + && item.is_project_item(cx) + { + return None; + } + let id = item.remote_id(client, cx)?.to_proto(); + let variant = item.to_state_proto(cx)?; + Some(proto::View { + id: Some(id), + leader_id, + variant: Some(variant), + }) + } + }) + }) + .collect(), + } } fn handle_update_followers( @@ -2878,9 +2790,8 @@ impl Workspace { } else { None }; - let room_id = self.call_handler.room_id(cx)?; self.app_state().workspace_store.update(cx, |store, cx| { - store.update_followers(project_id, room_id, update, cx) + store.update_followers(project_id, update, cx) }) } @@ -2888,12 +2799,31 @@ impl Workspace { self.follower_states.get(pane).map(|state| state.leader_id) } - pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { cx.notify(); - let (leader_in_this_project, leader_in_this_app) = - self.call_handler.peer_state(leader_id, &self.project, cx)?; + let call = self.active_call()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(leader_id)?; let mut items_to_activate = Vec::new(); + + let leader_in_this_app; + let leader_in_this_project; + match participant.location { + call::ParticipantLocation::SharedProject { project_id } => { + leader_in_this_app = true; + leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); + } + call::ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; + } + call::ParticipantLocation::External => { + leader_in_this_app = false; + leader_in_this_project = false; + } + }; + for (pane, state) in &self.follower_states { if state.leader_id != leader_id { continue; @@ -2914,7 +2844,7 @@ impl Workspace { } if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - items_to_activate.push((pane.clone(), shared_screen)); + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } @@ -2923,8 +2853,8 @@ impl Workspace { if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); } else { - pane.update(cx, |pane, mut cx| { - pane.add_item(item.boxed_clone(), false, false, None, &mut cx) + pane.update(cx, |pane, cx| { + pane.add_item(item.boxed_clone(), false, false, None, cx) }); } @@ -2941,21 +2871,20 @@ impl Workspace { peer_id: PeerId, pane: &View, cx: &mut ViewContext, - ) -> Option> { - self.call_handler.shared_screen_for_peer(peer_id, pane, cx) - // let call = self.active_call()?; - // let room = call.read(cx).room()?.read(cx); - // let participant = room.remote_participant_for_peer_id(peer_id)?; - // let track = participant.video_tracks.values().next()?.clone(); - // let user = participant.user.clone(); - - // for item in pane.read(cx).items_of_type::() { - // if item.read(cx).peer_id == peer_id { - // return Some(item); - // } - // } + ) -> Option> { + let call = self.active_call()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + let track = participant.video_tracks.values().next()?.clone(); + let user = participant.user.clone(); + + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == peer_id { + return Some(item); + } + } - // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) } pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { @@ -2984,6 +2913,25 @@ impl Workspace { } } + fn active_call(&self) -> Option<&Model> { + self.active_call.as_ref().map(|(call, _)| call) + } + + fn on_active_call_event( + &mut self, + _: Model, + event: &call::room::Event, + cx: &mut ViewContext, + ) { + match event { + call::room::Event::ParticipantLocationChanged { participant_id } + | call::room::Event::RemoteVideoTracksChanged { participant_id } => { + self.leader_updated(*participant_id, cx); + } + _ => {} + } + } + pub fn database_id(&self) -> WorkspaceId { self.database_id } @@ -3285,13 +3233,8 @@ impl Workspace { fn actions(&self, div: Div, cx: &mut ViewContext) -> Div { self.add_workspace_actions_listeners(div, cx) - // cx.add_async_action(Workspace::open); - // cx.add_async_action(Workspace::follow_next_collaborator); - // cx.add_async_action(Workspace::close); .on_action(cx.listener(Self::close_inactive_items_and_panes)) .on_action(cx.listener(Self::close_all_items_and_panes)) - // cx.add_global_action(Workspace::close_global); - // cx.add_global_action(restart); .on_action(cx.listener(Self::save_all)) .on_action(cx.listener(Self::add_folder_to_project)) .on_action(cx.listener(|workspace, _: &Unfollow, cx| { @@ -3340,6 +3283,9 @@ impl Workspace { workspace.close_all_docks(cx); }), ) + .on_action(cx.listener(Workspace::open)) + .on_action(cx.listener(Workspace::close_window)) + // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); @@ -3393,7 +3339,6 @@ impl Workspace { fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - call_factory: |_| Box::new(TestCallHandler), }); let workspace = Self::new(0, project, app_state, cx); workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); @@ -3472,10 +3417,6 @@ impl Workspace { self.modal_layer .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) } - - pub fn call_state(&mut self) -> &mut dyn CallHandler { - &mut *self.call_handler - } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { @@ -3676,6 +3617,7 @@ impl Render for Workspace { .child(self.center.render( &self.project, &self.follower_states, + self.active_call(), &self.active_pane, self.zoomed.as_ref(), &self.app_state, @@ -3830,15 +3772,15 @@ impl Render for Workspace { // } impl WorkspaceStore { - pub fn new(client: Arc, _cx: &mut ModelContext) -> Self { + pub fn new(client: Arc, cx: &mut ModelContext) -> Self { Self { workspaces: Default::default(), followers: Default::default(), - _subscriptions: vec![], - // client.add_request_handler(cx.weak_model(), Self::handle_follow), - // client.add_message_handler(cx.weak_model(), Self::handle_unfollow), - // client.add_message_handler(cx.weak_model(), Self::handle_update_followers), - // ], + _subscriptions: vec![ + client.add_request_handler(cx.weak_model(), Self::handle_follow), + client.add_message_handler(cx.weak_model(), Self::handle_unfollow), + client.add_message_handler(cx.weak_model(), Self::handle_update_followers), + ], client, } } @@ -3846,10 +3788,14 @@ impl WorkspaceStore { pub fn update_followers( &self, project_id: Option, - room_id: u64, update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { + if !cx.has_global::>() { + return None; + } + + let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); let follower_ids: Vec<_> = self .followers .iter() @@ -3885,17 +3831,9 @@ impl WorkspaceStore { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; + let active_project = ActiveCall::global(cx).read(cx).location().cloned(); + let mut response = proto::FollowResponse::default(); - let active_project = this - .workspaces - .iter() - .next() - .and_then(|workspace| { - workspace - .read_with(cx, |this, cx| this.call_handler.active_project(cx)) - .log_err() - }) - .flatten(); for workspace in &this.workspaces { workspace .update(cx, |workspace, cx| { @@ -3947,7 +3885,7 @@ impl WorkspaceStore { this: Model, envelope: TypedEnvelope, _: Arc, - mut cx: AsyncWindowContext, + mut cx: AsyncAppContext, ) -> Result<()> { let leader_id = envelope.original_sender_id()?; let update = envelope.payload; @@ -4012,8 +3950,6 @@ impl std::fmt::Debug for OpenPaths { } } -pub struct WorkspaceCreated(pub WeakView); - pub fn activate_workspace_for_project( cx: &mut AppContext, predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static, @@ -4048,187 +3984,184 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } -// async fn join_channel_internal( -// channel_id: u64, -// app_state: &Arc, -// requesting_window: Option>, -// active_call: &ModelHandle, -// cx: &mut AsyncAppContext, -// ) -> Result { -// let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { -// let Some(room) = active_call.room().map(|room| room.read(cx)) else { -// return (false, None); -// }; - -// let already_in_channel = room.channel_id() == Some(channel_id); -// let should_prompt = room.is_sharing_project() -// && room.remote_participants().len() > 0 -// && !already_in_channel; -// let open_room = if already_in_channel { -// active_call.room().cloned() -// } else { -// None -// }; -// (should_prompt, open_room) -// }); - -// if let Some(room) = open_room { -// let task = room.update(cx, |room, cx| { -// if let Some((project, host)) = room.most_active_project(cx) { -// return Some(join_remote_project(project, host, app_state.clone(), cx)); -// } - -// None -// }); -// if let Some(task) = task { -// task.await?; -// } -// return anyhow::Ok(true); -// } +async fn join_channel_internal( + channel_id: u64, + app_state: &Arc, + requesting_window: Option>, + active_call: &Model, + cx: &mut AsyncAppContext, +) -> Result { + let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| { + let Some(room) = active_call.room().map(|room| room.read(cx)) else { + return (false, None); + }; -// if should_prompt { -// if let Some(workspace) = requesting_window { -// if let Some(window) = workspace.update(cx, |cx| cx.window()) { -// let answer = window.prompt( -// PromptLevel::Warning, -// "Leaving this call will unshare your current project.\nDo you want to switch channels?", -// &["Yes, Join Channel", "Cancel"], -// cx, -// ); - -// if let Some(mut answer) = answer { -// if answer.next().await == Some(1) { -// return Ok(false); -// } -// } -// } else { -// return Ok(false); // unreachable!() hopefully -// } -// } else { -// return Ok(false); // unreachable!() hopefully -// } -// } + let already_in_channel = room.channel_id() == Some(channel_id); + let should_prompt = room.is_sharing_project() + && room.remote_participants().len() > 0 + && !already_in_channel; + let open_room = if already_in_channel { + active_call.room().cloned() + } else { + None + }; + (should_prompt, open_room) + })?; -// let client = cx.read(|cx| active_call.read(cx).client()); - -// let mut client_status = client.status(); - -// // this loop will terminate within client::CONNECTION_TIMEOUT seconds. -// 'outer: loop { -// let Some(status) = client_status.recv().await else { -// return Err(anyhow!("error connecting")); -// }; - -// match status { -// Status::Connecting -// | Status::Authenticating -// | Status::Reconnecting -// | Status::Reauthenticating => continue, -// Status::Connected { .. } => break 'outer, -// Status::SignedOut => return Err(anyhow!("not signed in")), -// Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), -// Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { -// return Err(anyhow!("zed is offline")) -// } -// } -// } + if let Some(room) = open_room { + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } -// let room = active_call -// .update(cx, |active_call, cx| { -// active_call.join_channel(channel_id, cx) -// }) -// .await?; - -// room.update(cx, |room, _| room.room_update_completed()) -// .await; - -// let task = room.update(cx, |room, cx| { -// if let Some((project, host)) = room.most_active_project(cx) { -// return Some(join_remote_project(project, host, app_state.clone(), cx)); -// } - -// None -// }); -// if let Some(task) = task { -// task.await?; -// return anyhow::Ok(true); -// } -// anyhow::Ok(false) -// } + None + })?; + if let Some(task) = task { + task.await?; + } + return anyhow::Ok(true); + } -// pub fn join_channel( -// channel_id: u64, -// app_state: Arc, -// requesting_window: Option>, -// cx: &mut AppContext, -// ) -> Task> { -// let active_call = ActiveCall::global(cx); -// cx.spawn(|mut cx| async move { -// let result = join_channel_internal( -// channel_id, -// &app_state, -// requesting_window, -// &active_call, -// &mut cx, -// ) -// .await; - -// // join channel succeeded, and opened a window -// if matches!(result, Ok(true)) { -// return anyhow::Ok(()); -// } - -// if requesting_window.is_some() { -// return anyhow::Ok(()); -// } - -// // find an existing workspace to focus and show call controls -// let mut active_window = activate_any_workspace_window(&mut cx); -// if active_window.is_none() { -// // no open workspaces, make one to show the error in (blergh) -// cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx)) -// .await; -// } - -// active_window = activate_any_workspace_window(&mut cx); -// if active_window.is_none() { -// return result.map(|_| ()); // unreachable!() assuming new_local always opens a window -// } - -// if let Err(err) = result { -// let prompt = active_window.unwrap().prompt( -// PromptLevel::Critical, -// &format!("Failed to join channel: {}", err), -// &["Ok"], -// &mut cx, -// ); -// if let Some(mut prompt) = prompt { -// prompt.next().await; -// } else { -// return Err(err); -// } -// } - -// // return ok, we showed the error to the user. -// return anyhow::Ok(()); -// }) -// } + if should_prompt { + if let Some(workspace) = requesting_window { + let answer = workspace.update(cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + ) + })?.await; -// pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { -// for window in cx.windows() { -// let found = window.update(cx, |cx| { -// let is_workspace = cx.root_view().clone().downcast::().is_some(); -// if is_workspace { -// cx.activate_window(); -// } -// is_workspace -// }); -// if found == Some(true) { -// return Some(window); -// } -// } -// None -// } + if answer == Ok(1) { + return Ok(false); + } + } else { + return Ok(false); // unreachable!() hopefully + } + } + + let client = cx.update(|cx| active_call.read(cx).client())?; + + let mut client_status = client.status(); + + // this loop will terminate within client::CONNECTION_TIMEOUT seconds. + 'outer: loop { + let Some(status) = client_status.recv().await else { + return Err(anyhow!("error connecting")); + }; + + match status { + Status::Connecting + | Status::Authenticating + | Status::Reconnecting + | Status::Reauthenticating => continue, + Status::Connected { .. } => break 'outer, + Status::SignedOut => return Err(anyhow!("not signed in")), + Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), + Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { + return Err(anyhow!("zed is offline")) + } + } + } + + let room = active_call + .update(cx, |active_call, cx| { + active_call.join_channel(channel_id, cx) + })? + .await?; + + let Some(room) = room else { + return anyhow::Ok(true); + }; + + room.update(cx, |room, _| room.room_update_completed())? + .await; + + let task = room.update(cx, |room, cx| { + if let Some((project, host)) = room.most_active_project(cx) { + return Some(join_remote_project(project, host, app_state.clone(), cx)); + } + + None + })?; + if let Some(task) = task { + task.await?; + return anyhow::Ok(true); + } + anyhow::Ok(false) +} + +pub fn join_channel( + channel_id: u64, + app_state: Arc, + requesting_window: Option>, + cx: &mut AppContext, +) -> Task> { + let active_call = ActiveCall::global(cx); + cx.spawn(|mut cx| async move { + let result = join_channel_internal( + channel_id, + &app_state, + requesting_window, + &active_call, + &mut cx, + ) + .await; + + // join channel succeeded, and opened a window + if matches!(result, Ok(true)) { + return anyhow::Ok(()); + } + + if requesting_window.is_some() { + return anyhow::Ok(()); + } + + // find an existing workspace to focus and show call controls + let mut active_window = activate_any_workspace_window(&mut cx); + if active_window.is_none() { + // no open workspaces, make one to show the error in (blergh) + cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))? + .await?; + } + + active_window = activate_any_workspace_window(&mut cx); + let Some(active_window) = active_window else { + return anyhow::Ok(()); + }; + + if let Err(err) = result { + active_window + .update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + ) + })? + .await + .ok(); + } + + // return ok, we showed the error to the user. + return anyhow::Ok(()); + }) +} + +pub fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { + cx.update(|cx| { + for window in cx.windows() { + let is_workspace = window.downcast::().is_some(); + if is_workspace { + window.update(cx, |_, cx| cx.activate_window()).ok(); + return Some(window); + } + } + None + }) + .ok() + .flatten() +} #[allow(clippy::type_complexity)] pub fn open_paths( @@ -4314,101 +4247,88 @@ pub fn create_and_open_local_file( }) } -// pub fn join_remote_project( -// project_id: u64, -// follow_user_id: u64, -// app_state: Arc, -// cx: &mut AppContext, -// ) -> Task> { -// cx.spawn(|mut cx| async move { -// let windows = cx.windows(); -// let existing_workspace = windows.into_iter().find_map(|window| { -// window.downcast::().and_then(|window| { -// window -// .read_root_with(&cx, |workspace, cx| { -// if workspace.project().read(cx).remote_id() == Some(project_id) { -// Some(cx.handle().downgrade()) -// } else { -// None -// } -// }) -// .unwrap_or(None) -// }) -// }); - -// let workspace = if let Some(existing_workspace) = existing_workspace { -// existing_workspace -// } else { -// let active_call = cx.read(ActiveCall::global); -// let room = active_call -// .read_with(&cx, |call, _| call.room().cloned()) -// .ok_or_else(|| anyhow!("not in a call"))?; -// let project = room -// .update(&mut cx, |room, cx| { -// room.join_project( -// project_id, -// app_state.languages.clone(), -// app_state.fs.clone(), -// cx, -// ) -// }) -// .await?; - -// let window_bounds_override = window_bounds_env_override(&cx); -// let window = cx.add_window( -// (app_state.build_window_options)( -// window_bounds_override, -// None, -// cx.platform().as_ref(), -// ), -// |cx| Workspace::new(0, project, app_state.clone(), cx), -// ); -// let workspace = window.root(&cx).unwrap(); -// (app_state.initialize_workspace)( -// workspace.downgrade(), -// false, -// app_state.clone(), -// cx.clone(), -// ) -// .await -// .log_err(); - -// workspace.downgrade() -// }; - -// workspace.window().activate(&mut cx); -// cx.platform().activate(true); - -// workspace.update(&mut cx, |workspace, cx| { -// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { -// let follow_peer_id = room -// .read(cx) -// .remote_participants() -// .iter() -// .find(|(_, participant)| participant.user.id == follow_user_id) -// .map(|(_, p)| p.peer_id) -// .or_else(|| { -// // If we couldn't follow the given user, follow the host instead. -// let collaborator = workspace -// .project() -// .read(cx) -// .collaborators() -// .values() -// .find(|collaborator| collaborator.replica_id == 0)?; -// Some(collaborator.peer_id) -// }); - -// if let Some(follow_peer_id) = follow_peer_id { -// workspace -// .follow(follow_peer_id, cx) -// .map(|follow| follow.detach_and_log_err(cx)); -// } -// } -// })?; - -// anyhow::Ok(()) -// }) -// } +pub fn join_remote_project( + project_id: u64, + follow_user_id: u64, + app_state: Arc, + cx: &mut AppContext, +) -> Task> { + let windows = cx.windows(); + cx.spawn(|mut cx| async move { + let existing_workspace = windows.into_iter().find_map(|window| { + window.downcast::().and_then(|window| { + window + .update(&mut cx, |workspace, cx| { + if workspace.project().read(cx).remote_id() == Some(project_id) { + Some(window) + } else { + None + } + }) + .unwrap_or(None) + }) + }); + + let workspace = if let Some(existing_workspace) = existing_workspace { + existing_workspace + } else { + let active_call = cx.update(|cx| ActiveCall::global(cx))?; + let room = active_call + .read_with(&cx, |call, _| call.room().cloned())? + .ok_or_else(|| anyhow!("not in a call"))?; + let project = room + .update(&mut cx, |room, cx| { + room.join_project( + project_id, + app_state.languages.clone(), + app_state.fs.clone(), + cx, + ) + })? + .await?; + + let window_bounds_override = window_bounds_env_override(&cx); + cx.update(|cx| { + let options = (app_state.build_window_options)(window_bounds_override, None, cx); + cx.open_window(options, |cx| { + cx.build_view(|cx| Workspace::new(0, project, app_state.clone(), cx)) + }) + })? + }; + + workspace.update(&mut cx, |workspace, cx| { + cx.activate(true); + cx.activate_window(); + + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + let follow_peer_id = room + .read(cx) + .remote_participants() + .iter() + .find(|(_, participant)| participant.user.id == follow_user_id) + .map(|(_, p)| p.peer_id) + .or_else(|| { + // If we couldn't follow the given user, follow the host instead. + let collaborator = workspace + .project() + .read(cx) + .collaborators() + .values() + .find(|collaborator| collaborator.replica_id == 0)?; + Some(collaborator.peer_id) + }); + + if let Some(follow_peer_id) = follow_peer_id { + workspace + .follow(follow_peer_id, cx) + .map(|follow| follow.detach_and_log_err(cx)); + } + } + })?; + + anyhow::Ok(()) + }) +} pub fn restart(_: &Restart, cx: &mut AppContext) { let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d202c2aaed1b2af57afad12fe206e44ce42479ea..d0a526748327f5deace2de7e78b47affd8d147e0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -615,8 +615,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, dir_path), true, cx) }) - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } } @@ -625,8 +625,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, file_path), false, cx) }) - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } let editor = workspace @@ -763,7 +763,7 @@ mod tests { AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, }; use language::LanguageRegistry; - use project::{Project, ProjectPath}; + use project::{project_settings::ProjectSettings, Project, ProjectPath}; use serde_json::json; use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; use std::{ @@ -1308,6 +1308,122 @@ mod tests { }); } + #[gpui::test] + async fn test_opening_excluded_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = + Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); + }); + }); + }); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + ".gitignore": "ignored_dir\n", + ".git": { + "HEAD": "ref: refs/heads/main", + }, + "regular_dir": { + "file": "regular file contents", + }, + "ignored_dir": { + "ignored_subdir": { + "file": "ignored subfile contents", + }, + "file": "ignored file contents", + }, + "excluded_dir": { + "file": "excluded file contents", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + + let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); + let paths_to_open = [ + Path::new("/root/excluded_dir/file").to_path_buf(), + Path::new("/root/.git/HEAD").to_path_buf(), + Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), + ]; + let (opened_workspace, new_items) = cx + .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) + .await + .unwrap(); + + assert_eq!( + opened_workspace.id(), + workspace.id(), + "Excluded files in subfolders of a workspace root should be opened in the workspace" + ); + let mut opened_paths = cx.read(|cx| { + assert_eq!( + new_items.len(), + paths_to_open.len(), + "Expect to get the same number of opened items as submitted paths to open" + ); + new_items + .iter() + .zip(paths_to_open.iter()) + .map(|(i, path)| { + match i { + Some(Ok(i)) => { + Some(i.project_path(cx).map(|p| p.path.display().to_string())) + } + Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), + None => None, + } + .flatten() + }) + .collect::>() + }); + opened_paths.sort(); + assert_eq!( + opened_paths, + vec![ + None, + Some(".git/HEAD".to_string()), + Some("excluded_dir/file".to_string()), + ], + "Excluded files should get opened, excluded dir should not get opened" + ); + + let entries = cx.read(|cx| workspace.file_project_paths(cx)); + assert_eq!( + initial_entries, entries, + "Workspace entries should not change after opening excluded files and directories paths" + ); + + cx.read(|cx| { + let pane = workspace.read(cx).active_pane().read(cx); + let mut opened_buffer_paths = pane + .items() + .map(|i| { + i.project_path(cx) + .expect("all excluded files that got open should have a path") + .path + .display() + .to_string() + }) + .collect::>(); + opened_buffer_paths.sort(); + assert_eq!( + opened_buffer_paths, + vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], + "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" + ); + }); + } + #[gpui::test] async fn test_save_conflicting_item(cx: &mut TestAppContext) { let app_state = init_test(cx); diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index ab4b724a0676d6998cfe3455226d1489a58a005e..cc6150676438fef760aa70913651695968380e2d 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -50,12 +50,12 @@ menu = { package = "menu2", path = "../menu2" } # language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } # assistant = { path = "../assistant" } -# outline = { path = "../outline" } +outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } -# quick_action_bar = { path = "../quick_action_bar" } +quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } # recent_projects = { path = "../recent_projects" } rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 1cf3793fe12ea73881a49d7999549845915417ac..6ca5d1a805b53ab1dabd43591b7353c0231aa365 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -191,7 +191,6 @@ fn main() { user_store: user_store.clone(), fs, build_window_options, - call_factory: call::Call::new, workspace_store, node_runtime, }); @@ -205,7 +204,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); - // outline::init(cx); + outline::init(cx); // project_symbols::init(cx); project_panel::init(Assets, cx); channel::init(&client, user_store.clone(), cx); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 1b9f1cc719bc889377ba113a78b09a72d91656a1..5f2099154cc8f298baedceeee3ab282fe8eca079 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -19,6 +19,7 @@ pub use open_listener::*; use anyhow::{anyhow, Context as _}; use project_panel::ProjectPanel; +use quick_action_bar::QuickActionBar; use settings::{initial_local_settings_content, Settings}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; @@ -100,11 +101,10 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - // todo!() - // let quick_action_bar = cx.add_view(|_| { - // QuickActionBar::new(buffer_search_bar, workspace) - // }); - // toolbar.add_item(quick_action_bar, cx); + + let quick_action_bar = cx + .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); + toolbar.add_item(quick_action_bar, cx); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); @@ -168,9 +168,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.on_window_should_close(move |cx| { handle .update(cx, |workspace, cx| { - if let Some(task) = workspace.close(&Default::default(), cx) { - task.detach_and_log_err(cx); - } + workspace.close_window(&Default::default(), cx); false }) .unwrap_or(true) @@ -582,8 +580,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, dir_path), true, cx) })? - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } } @@ -592,8 +590,8 @@ fn open_local_settings_file( .update(&mut cx, |project, cx| { project.create_entry((tree_id, file_path), false, cx) })? - .ok_or_else(|| anyhow!("worktree was removed"))? - .await?; + .await + .context("worktree was removed")?; } let editor = workspace @@ -718,3 +716,1846 @@ fn open_bundled_file( }) .detach_and_log_err(cx); } + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use assets::Assets; +// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; +// use fs::{FakeFs, Fs}; +// use gpui::{ +// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle, +// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, +// }; +// use language::LanguageRegistry; +// use project::{project_settings::ProjectSettings, Project, ProjectPath}; +// use serde_json::json; +// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; +// use std::{ +// collections::HashSet, +// path::{Path, PathBuf}, +// }; +// use theme::{ThemeRegistry, ThemeSettings}; +// use workspace::{ +// item::{Item, ItemHandle}, +// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle, +// }; + +// #[gpui::test] +// async fn test_open_paths_action(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "aa": null, +// "ab": null, +// }, +// "b": { +// "ba": null, +// "bb": null, +// }, +// "c": { +// "ca": null, +// "cb": null, +// }, +// "d": { +// "da": null, +// "db": null, +// }, +// }), +// ) +// .await; + +// cx.update(|cx| { +// open_paths( +// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], +// &app_state, +// None, +// cx, +// ) +// }) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); + +// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); +// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); +// workspace_1.update(cx, |workspace, cx| { +// assert_eq!(workspace.worktrees(cx).count(), 2); +// assert!(workspace.left_dock().read(cx).is_open()); +// assert!(workspace.active_pane().is_focused(cx)); +// }); + +// cx.update(|cx| { +// open_paths( +// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], +// &app_state, +// None, +// cx, +// ) +// }) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 2); + +// // Replace existing windows +// let window = cx.windows()[0].downcast::().unwrap(); +// cx.update(|cx| { +// open_paths( +// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], +// &app_state, +// Some(window), +// cx, +// ) +// }) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 2); +// let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); +// workspace_1.update(cx, |workspace, cx| { +// assert_eq!( +// workspace +// .worktrees(cx) +// .map(|w| w.read(cx).abs_path()) +// .collect::>(), +// &[Path::new("/root/c").into(), Path::new("/root/d").into()] +// ); +// assert!(workspace.left_dock().read(cx).is_open()); +// assert!(workspace.active_pane().is_focused(cx)); +// }); +// } + +// #[gpui::test] +// async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({"a": "hey"})) +// .await; + +// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); + +// // When opening the workspace, the window is not in a edited state. +// let window = cx.windows()[0].downcast::().unwrap(); +// let workspace = window.root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// assert!(!window.is_edited(cx)); + +// // Editing a buffer marks the window as edited. +// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); +// assert!(window.is_edited(cx)); + +// // Undoing the edit restores the window's edited state. +// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); +// assert!(!window.is_edited(cx)); + +// // Redoing the edit marks the window as edited again. +// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); +// assert!(window.is_edited(cx)); + +// // Closing the item restores the window's edited state. +// let close = pane.update(cx, |pane, cx| { +// drop(editor); +// pane.close_active_item(&Default::default(), cx).unwrap() +// }); +// executor.run_until_parked(); + +// window.simulate_prompt_answer(1, cx); +// close.await.unwrap(); +// assert!(!window.is_edited(cx)); + +// // Opening the buffer again doesn't impact the window's edited state. +// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) +// .await +// .unwrap(); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// assert!(!window.is_edited(cx)); + +// // Editing the buffer marks the window as edited. +// editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); +// assert!(window.is_edited(cx)); + +// // Ensure closing the window via the mouse gets preempted due to the +// // buffer having unsaved changes. +// assert!(!window.simulate_close(cx)); +// executor.run_until_parked(); +// assert_eq!(cx.windows().len(), 1); + +// // The window is successfully closed after the user dismisses the prompt. +// window.simulate_prompt_answer(1, cx); +// executor.run_until_parked(); +// assert_eq!(cx.windows().len(), 0); +// } + +// #[gpui::test] +// async fn test_new_empty_workspace(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// cx.update(|cx| { +// open_new(&app_state, cx, |workspace, cx| { +// Editor::new_file(workspace, &Default::default(), cx) +// }) +// }) +// .await; + +// let window = cx +// .windows() +// .first() +// .unwrap() +// .downcast::() +// .unwrap(); +// let workspace = window.root(cx); + +// let editor = workspace.update(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); + +// editor.update(cx, |editor, cx| { +// assert!(editor.text(cx).is_empty()); +// assert!(!editor.is_dirty(cx)); +// }); + +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); +// save_task.await.unwrap(); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "the-new-name"); +// }); +// } + +// #[gpui::test] +// async fn test_open_entry(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "contents 1", +// "file2": "contents 2", +// "file3": "contents 3", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); +// let file2 = entries[1].clone(); +// let file3 = entries[2].clone(); + +// // Open the first entry +// let entry_1 = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap(); +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file1.clone()) +// ); +// assert_eq!(pane.items_len(), 1); +// }); + +// // Open the second entry +// workspace +// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) +// .await +// .unwrap(); +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file2.clone()) +// ); +// assert_eq!(pane.items_len(), 2); +// }); + +// // Open the first entry again. The existing pane item is activated. +// let entry_1b = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap(); +// assert_eq!(entry_1.id(), entry_1b.id()); + +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file1.clone()) +// ); +// assert_eq!(pane.items_len(), 2); +// }); + +// // Split the pane with the first entry, then open the second entry again. +// workspace +// .update(cx, |w, cx| { +// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx); +// w.open_path(file2.clone(), None, true, cx) +// }) +// .await +// .unwrap(); + +// workspace.read_with(cx, |w, cx| { +// assert_eq!( +// w.active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .project_path(cx), +// Some(file2.clone()) +// ); +// }); + +// // Open the third entry twice concurrently. Only one pane item is added. +// let (t1, t2) = workspace.update(cx, |w, cx| { +// ( +// w.open_path(file3.clone(), None, true, cx), +// w.open_path(file3.clone(), None, true, cx), +// ) +// }); +// t1.await.unwrap(); +// t2.await.unwrap(); +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// assert_eq!( +// pane.active_item().unwrap().project_path(cx), +// Some(file3.clone()) +// ); +// let pane_entries = pane +// .items() +// .map(|i| i.project_path(cx).unwrap()) +// .collect::>(); +// assert_eq!(pane_entries, &[file1, file2, file3]); +// }); +// } + +// #[gpui::test] +// async fn test_open_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/", +// json!({ +// "dir1": { +// "a.txt": "" +// }, +// "dir2": { +// "b.txt": "" +// }, +// "dir3": { +// "c.txt": "" +// }, +// "d.txt": "" +// }), +// ) +// .await; + +// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) +// .await +// .unwrap(); +// assert_eq!(cx.windows().len(), 1); +// let workspace = cx.windows()[0].downcast::().unwrap().root(cx); + +// #[track_caller] +// fn assert_project_panel_selection( +// workspace: &Workspace, +// expected_worktree_path: &Path, +// expected_entry_path: &Path, +// cx: &AppContext, +// ) { +// let project_panel = [ +// workspace.left_dock().read(cx).panel::(), +// workspace.right_dock().read(cx).panel::(), +// workspace.bottom_dock().read(cx).panel::(), +// ] +// .into_iter() +// .find_map(std::convert::identity) +// .expect("found no project panels") +// .read(cx); +// let (selected_worktree, selected_entry) = project_panel +// .selected_entry(cx) +// .expect("project panel should have a selected entry"); +// assert_eq!( +// selected_worktree.abs_path().as_ref(), +// expected_worktree_path, +// "Unexpected project panel selected worktree path" +// ); +// assert_eq!( +// selected_entry.path.as_ref(), +// expected_entry_path, +// "Unexpected project panel selected entry path" +// ); +// } + +// // Open a file within an existing worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/dir1/a.txt".into()], true, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "a.txt" +// ); +// }); + +// // Open a file outside of any existing worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/dir2/b.txt".into()], true, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); +// let worktree_roots = workspace +// .worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// worktree_roots, +// vec!["/dir1", "/dir2/b.txt"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "b.txt" +// ); +// }); + +// // Ensure opening a directory and one of its children only adds one worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); +// let worktree_roots = workspace +// .worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// worktree_roots, +// vec!["/dir1", "/dir2/b.txt", "/dir3"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "c.txt" +// ); +// }); + +// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec!["/d.txt".into()], false, cx) +// }) +// .await; +// cx.read(|cx| { +// let workspace = workspace.read(cx); +// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); +// let worktree_roots = workspace +// .worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// worktree_roots, +// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); + +// let visible_worktree_roots = workspace +// .visible_worktrees(cx) +// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) +// .collect::>(); +// assert_eq!( +// visible_worktree_roots, +// vec!["/dir1", "/dir2/b.txt", "/dir3"] +// .into_iter() +// .map(Path::new) +// .collect(), +// ); + +// assert_eq!( +// workspace +// .active_pane() +// .read(cx) +// .active_item() +// .unwrap() +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "d.txt" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_opening_excluded_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// cx.update(|cx| { +// cx.update_global::(|store, cx| { +// store.update_user_settings::(cx, |project_settings| { +// project_settings.file_scan_exclusions = +// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); +// }); +// }); +// }); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// ".gitignore": "ignored_dir\n", +// ".git": { +// "HEAD": "ref: refs/heads/main", +// }, +// "regular_dir": { +// "file": "regular file contents", +// }, +// "ignored_dir": { +// "ignored_subdir": { +// "file": "ignored subfile contents", +// }, +// "file": "ignored file contents", +// }, +// "excluded_dir": { +// "file": "excluded file contents", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let paths_to_open = [ +// Path::new("/root/excluded_dir/file").to_path_buf(), +// Path::new("/root/.git/HEAD").to_path_buf(), +// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(), +// ]; +// let (opened_workspace, new_items) = cx +// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx)) +// .await +// .unwrap(); + +// assert_eq!( +// opened_workspace.id(), +// workspace.id(), +// "Excluded files in subfolders of a workspace root should be opened in the workspace" +// ); +// let mut opened_paths = cx.read(|cx| { +// assert_eq!( +// new_items.len(), +// paths_to_open.len(), +// "Expect to get the same number of opened items as submitted paths to open" +// ); +// new_items +// .iter() +// .zip(paths_to_open.iter()) +// .map(|(i, path)| { +// match i { +// Some(Ok(i)) => { +// Some(i.project_path(cx).map(|p| p.path.display().to_string())) +// } +// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), +// None => None, +// } +// .flatten() +// }) +// .collect::>() +// }); +// opened_paths.sort(); +// assert_eq!( +// opened_paths, +// vec![ +// None, +// Some(".git/HEAD".to_string()), +// Some("excluded_dir/file".to_string()), +// ], +// "Excluded files should get opened, excluded dir should not get opened" +// ); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// assert_eq!( +// initial_entries, entries, +// "Workspace entries should not change after opening excluded files and directories paths" +// ); + +// cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// let mut opened_buffer_paths = pane +// .items() +// .map(|i| { +// i.project_path(cx) +// .expect("all excluded files that got open should have a path") +// .path +// .display() +// .to_string() +// }) +// .collect::>(); +// opened_buffer_paths.sort(); +// assert_eq!( +// opened_buffer_paths, +// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()], +// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_save_conflicting_item(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({ "a.txt": "" })) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // Open a file within an existing worktree. +// workspace +// .update(cx, |view, cx| { +// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx) +// }) +// .await; +// let editor = cx.read(|cx| { +// let pane = workspace.read(cx).active_pane().read(cx); +// let item = pane.active_item().unwrap(); +// item.downcast::().unwrap() +// }); + +// editor.update(cx, |editor, cx| editor.handle_input("x", cx)); +// app_state +// .fs +// .as_fake() +// .insert_file("/root/a.txt", "changed".to_string()) +// .await; +// editor +// .condition(cx, |editor, cx| editor.has_conflict(cx)) +// .await; +// cx.read(|cx| assert!(editor.is_dirty(cx))); + +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// cx.foreground().run_until_parked(); +// window.simulate_prompt_answer(0, cx); +// save_task.await.unwrap(); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert!(!editor.has_conflict(cx)); +// }); +// } + +// #[gpui::test] +// async fn test_open_and_save_new_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages().add(rust_lang())); +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); + +// // Create a new untitled buffer +// cx.dispatch_action(window.into(), NewFile); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); + +// editor.update(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "untitled"); +// assert!(Arc::ptr_eq( +// &editor.language_at(0, cx).unwrap(), +// &languages::PLAIN_TEXT +// )); +// editor.handle_input("hi", cx); +// assert!(editor.is_dirty(cx)); +// }); + +// // Save the buffer. This prompts for a filename. +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|parent_dir| { +// assert_eq!(parent_dir, Path::new("/root")); +// Some(parent_dir.join("the-new-name.rs")) +// }); +// cx.read(|cx| { +// assert!(editor.is_dirty(cx)); +// assert_eq!(editor.read(cx).title(cx), "untitled"); +// }); + +// // When the save completes, the buffer's title is updated and the language is assigned based +// // on the path. +// save_task.await.unwrap(); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "the-new-name.rs"); +// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust"); +// }); + +// // Edit the file and save it again. This time, there is no filename prompt. +// editor.update(cx, |editor, cx| { +// editor.handle_input(" there", cx); +// assert!(editor.is_dirty(cx)); +// }); +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// save_task.await.unwrap(); +// assert!(!cx.did_prompt_for_new_path()); +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.title(cx), "the-new-name.rs") +// }); + +// // Open the same newly-created file in another pane item. The new editor should reuse +// // the same buffer. +// cx.dispatch_action(window.into(), NewFile); +// workspace +// .update(cx, |workspace, cx| { +// workspace.split_and_clone( +// workspace.active_pane().clone(), +// SplitDirection::Right, +// cx, +// ); +// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx) +// }) +// .await +// .unwrap(); +// let editor2 = workspace.update(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); +// cx.read(|cx| { +// assert_eq!( +// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), +// editor.read(cx).buffer().read(cx).as_singleton().unwrap() +// ); +// }) +// } + +// #[gpui::test] +// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + +// let project = Project::test(app_state.fs.clone(), [], cx).await; +// project.update(cx, |project, _| project.languages().add(rust_lang())); +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // Create a new untitled buffer +// cx.dispatch_action(window.into(), NewFile); +// let editor = workspace.read_with(cx, |workspace, cx| { +// workspace +// .active_item(cx) +// .unwrap() +// .downcast::() +// .unwrap() +// }); + +// editor.update(cx, |editor, cx| { +// assert!(Arc::ptr_eq( +// &editor.language_at(0, cx).unwrap(), +// &languages::PLAIN_TEXT +// )); +// editor.handle_input("hi", cx); +// assert!(editor.is_dirty(cx)); +// }); + +// // Save the buffer. This prompts for a filename. +// let save_task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(SaveIntent::Save, cx) +// }); +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); +// save_task.await.unwrap(); +// // The buffer is not dirty anymore and the language is assigned based on the path. +// editor.read_with(cx, |editor, cx| { +// assert!(!editor.is_dirty(cx)); +// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust") +// }); +// } + +// #[gpui::test] +// async fn test_pane_actions(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "contents 1", +// "file2": "contents 2", +// "file3": "contents 3", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); + +// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); + +// workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap(); + +// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| { +// let editor = pane_1.active_item().unwrap().downcast::().unwrap(); +// assert_eq!(editor.project_path(cx), Some(file1.clone())); +// let buffer = editor.update(cx, |editor, cx| { +// editor.insert("dirt", cx); +// editor.buffer().downgrade() +// }); +// (editor.downgrade(), buffer) +// }); + +// cx.dispatch_action(window.into(), pane::SplitRight); +// let editor_2 = cx.update(|cx| { +// let pane_2 = workspace.read(cx).active_pane().clone(); +// assert_ne!(pane_1, pane_2); + +// let pane2_item = pane_2.read(cx).active_item().unwrap(); +// assert_eq!(pane2_item.project_path(cx), Some(file1.clone())); + +// pane2_item.downcast::().unwrap().downgrade() +// }); +// cx.dispatch_action( +// window.into(), +// workspace::CloseActiveItem { save_intent: None }, +// ); + +// cx.foreground().run_until_parked(); +// workspace.read_with(cx, |workspace, _| { +// assert_eq!(workspace.panes().len(), 1); +// assert_eq!(workspace.active_pane(), &pane_1); +// }); + +// cx.dispatch_action( +// window.into(), +// workspace::CloseActiveItem { save_intent: None }, +// ); +// cx.foreground().run_until_parked(); +// window.simulate_prompt_answer(1, cx); +// cx.foreground().run_until_parked(); + +// workspace.read_with(cx, |workspace, cx| { +// assert_eq!(workspace.panes().len(), 1); +// assert!(workspace.active_item(cx).is_none()); +// }); + +// cx.assert_dropped(editor_1); +// cx.assert_dropped(editor_2); +// cx.assert_dropped(buffer); +// } + +// #[gpui::test] +// async fn test_navigation(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "contents 1\n".repeat(20), +// "file2": "contents 2\n".repeat(20), +// "file3": "contents 3\n".repeat(20), +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); +// let file2 = entries[1].clone(); +// let file3 = entries[2].clone(); + +// let editor1 = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// editor1.update(cx, |editor, cx| { +// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { +// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)]) +// }); +// }); +// let editor2 = workspace +// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); +// let editor3 = workspace +// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) +// .await +// .unwrap() +// .downcast::() +// .unwrap(); + +// editor3 +// .update(cx, |editor, cx| { +// editor.change_selections(Some(Autoscroll::fit()), cx, |s| { +// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)]) +// }); +// editor.newline(&Default::default(), cx); +// editor.newline(&Default::default(), cx); +// editor.move_down(&Default::default(), cx); +// editor.move_down(&Default::default(), cx); +// editor.save(project.clone(), cx) +// }) +// .await +// .unwrap(); +// editor3.update(cx, |editor, cx| { +// editor.set_scroll_position(vec2f(0., 12.5), cx) +// }); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(16, 0), 12.5) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file2.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(10, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Go back one more time and ensure we don't navigate past the first item in the history. +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(10, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file2.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Go forward to an item that has been closed, ensuring it gets re-opened at the same +// // location. +// pane.update(cx, |pane, cx| { +// let editor3_id = editor3.id(); +// drop(editor3); +// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(16, 0), 12.5) +// ); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. +// pane.update(cx, |pane, cx| { +// let editor2_id = editor2.id(); +// drop(editor2); +// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// app_state +// .fs +// .remove_file(Path::new("/root/a/file2"), Default::default()) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); + +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(10, 0), 0.) +// ); +// workspace +// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file3.clone(), DisplayPoint::new(0, 0), 0.) +// ); + +// // Modify file to collapse multiple nav history entries into the same location. +// // Ensure we don't visit the same location twice when navigating. +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]) +// }) +// }); + +// for _ in 0..5 { +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) +// }); +// }); +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)]) +// }) +// }); +// } + +// editor1.update(cx, |editor, cx| { +// editor.transact(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)]) +// }); +// editor.insert("", cx); +// }) +// }); + +// editor1.update(cx, |editor, cx| { +// editor.change_selections(None, cx, |s| { +// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) +// }) +// }); +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(2, 0), 0.) +// ); +// workspace +// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) +// .await +// .unwrap(); +// assert_eq!( +// active_location(&workspace, cx), +// (file1.clone(), DisplayPoint::new(3, 0), 0.) +// ); + +// fn active_location( +// workspace: &ViewHandle, +// cx: &mut TestAppContext, +// ) -> (ProjectPath, DisplayPoint, f32) { +// workspace.update(cx, |workspace, cx| { +// let item = workspace.active_item(cx).unwrap(); +// let editor = item.downcast::().unwrap(); +// let (selections, scroll_position) = editor.update(cx, |editor, cx| { +// ( +// editor.selections.display_ranges(cx), +// editor.scroll_position(cx), +// ) +// }); +// ( +// item.project_path(cx).unwrap(), +// selections[0].start, +// scroll_position.y(), +// ) +// }) +// } +// } + +// #[gpui::test] +// async fn test_reopening_closed_items(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "file1": "", +// "file2": "", +// "file3": "", +// "file4": "", +// }, +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + +// let entries = cx.read(|cx| workspace.file_project_paths(cx)); +// let file1 = entries[0].clone(); +// let file2 = entries[1].clone(); +// let file3 = entries[2].clone(); +// let file4 = entries[3].clone(); + +// let file1_item_id = workspace +// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// let file2_item_id = workspace +// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// let file3_item_id = workspace +// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// let file4_item_id = workspace +// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx)) +// .await +// .unwrap() +// .id(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// // Close all the pane items in some arbitrary order. +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// pane.update(cx, |pane, cx| { +// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), None); + +// // Reopen all the closed items, ensuring they are reopened in the same order +// // in which they were closed. +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// // Reopening past the last closed item is a no-op. +// workspace +// .update(cx, Workspace::reopen_closed_item) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// // Reopening closed items doesn't interfere with navigation history. +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file4.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file3.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file2.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// workspace +// .update(cx, |workspace, cx| { +// workspace.go_back(workspace.active_pane().downgrade(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(active_path(&workspace, cx), Some(file1.clone())); + +// fn active_path( +// workspace: &ViewHandle, +// cx: &TestAppContext, +// ) -> Option { +// workspace.read_with(cx, |workspace, cx| { +// let item = workspace.active_item(cx)?; +// item.project_path(cx) +// }) +// } +// } + +// #[gpui::test] +// async fn test_base_keymap(cx: &mut gpui::TestAppContext) { +// struct TestView; + +// impl Entity for TestView { +// type Event = (); +// } + +// impl View for TestView { +// fn ui_name() -> &'static str { +// "TestView" +// } + +// fn render(&mut self, _: &mut ViewContext) -> AnyElement { +// Empty::new().into_any() +// } +// } + +// let executor = cx.background(); +// let fs = FakeFs::new(executor.clone()); + +// actions!(test, [A, B]); +// // From the Atom keymap +// actions!(workspace, [ActivatePreviousPane]); +// // From the JetBrains keymap +// actions!(pane, [ActivatePrevItem]); + +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "Atom" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": "test::A" +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init(Assets, cx); +// welcome::init(cx); + +// cx.add_global_action(|_: &A, _cx| {}); +// cx.add_global_action(|_: &B, _cx| {}); +// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); +// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + +// let settings_rx = watch_config_file( +// executor.clone(), +// fs.clone(), +// PathBuf::from("/settings.json"), +// ); +// let keymap_rx = +// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + +// handle_keymap_file_changes(keymap_rx, cx); +// handle_settings_file_changes(settings_rx, cx); +// }); + +// cx.foreground().run_until_parked(); + +// let window = cx.add_window(|_| TestView); + +// // Test loading the keymap base at all +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &A), ("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test modifying the users keymap, while retaining the base keymap +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": "test::B" +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &B), ("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test modifying the base, while retaining the users keymap +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "JetBrains" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &B), ("[", &ActivatePrevItem)], +// line!(), +// ); + +// #[track_caller] +// fn assert_key_bindings_for<'a>( +// window: AnyWindowHandle, +// cx: &TestAppContext, +// actions: Vec<(&'static str, &'a dyn Action)>, +// line: u32, +// ) { +// for (key, action) in actions { +// // assert that... +// assert!( +// cx.available_actions(window, 0) +// .into_iter() +// .any(|(_, bound_action, b)| { +// // action names match... +// bound_action.name() == action.name() +// && bound_action.namespace() == action.namespace() +// // and key strokes contain the given key +// && b.iter() +// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) +// }), +// "On {} Failed to find {} with key binding {}", +// line, +// action.name(), +// key +// ); +// } +// } +// } + +// #[gpui::test] +// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { +// struct TestView; + +// impl Entity for TestView { +// type Event = (); +// } + +// impl View for TestView { +// fn ui_name() -> &'static str { +// "TestView" +// } + +// fn render(&mut self, _: &mut ViewContext) -> AnyElement { +// Empty::new().into_any() +// } +// } + +// let executor = cx.background(); +// let fs = FakeFs::new(executor.clone()); + +// actions!(test, [A, B]); +// // From the Atom keymap +// actions!(workspace, [ActivatePreviousPane]); +// // From the JetBrains keymap +// actions!(pane, [ActivatePrevItem]); + +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "Atom" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": "test::A" +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init(Assets, cx); +// welcome::init(cx); + +// cx.add_global_action(|_: &A, _cx| {}); +// cx.add_global_action(|_: &B, _cx| {}); +// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {}); +// cx.add_global_action(|_: &ActivatePrevItem, _cx| {}); + +// let settings_rx = watch_config_file( +// executor.clone(), +// fs.clone(), +// PathBuf::from("/settings.json"), +// ); +// let keymap_rx = +// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json")); + +// handle_keymap_file_changes(keymap_rx, cx); +// handle_settings_file_changes(settings_rx, cx); +// }); + +// cx.foreground().run_until_parked(); + +// let window = cx.add_window(|_| TestView); + +// // Test loading the keymap base at all +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("backspace", &A), ("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test disabling the key binding for the base keymap +// fs.save( +// "/keymap.json".as_ref(), +// &r#" +// [ +// { +// "bindings": { +// "backspace": null +// } +// } +// ] +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for( +// window.into(), +// cx, +// vec![("k", &ActivatePreviousPane)], +// line!(), +// ); + +// // Test modifying the base, while retaining the users keymap +// fs.save( +// "/settings.json".as_ref(), +// &r#" +// { +// "base_keymap": "JetBrains" +// } +// "# +// .into(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); + +// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!()); + +// #[track_caller] +// fn assert_key_bindings_for<'a>( +// window: AnyWindowHandle, +// cx: &TestAppContext, +// actions: Vec<(&'static str, &'a dyn Action)>, +// line: u32, +// ) { +// for (key, action) in actions { +// // assert that... +// assert!( +// cx.available_actions(window, 0) +// .into_iter() +// .any(|(_, bound_action, b)| { +// // action names match... +// bound_action.name() == action.name() +// && bound_action.namespace() == action.namespace() +// // and key strokes contain the given key +// && b.iter() +// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)) +// }), +// "On {} Failed to find {} with key binding {}", +// line, +// action.name(), +// key +// ); +// } +// } +// } + +// #[gpui::test] +// fn test_bundled_settings_and_themes(cx: &mut AppContext) { +// cx.platform() +// .fonts() +// .add_fonts(&[ +// Assets +// .load("fonts/zed-sans/zed-sans-extended.ttf") +// .unwrap() +// .to_vec() +// .into(), +// Assets +// .load("fonts/zed-mono/zed-mono-extended.ttf") +// .unwrap() +// .to_vec() +// .into(), +// Assets +// .load("fonts/plex/IBMPlexSans-Regular.ttf") +// .unwrap() +// .to_vec() +// .into(), +// ]) +// .unwrap(); +// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); +// let mut settings = SettingsStore::default(); +// settings +// .set_default_settings(&settings::default_settings(), cx) +// .unwrap(); +// cx.set_global(settings); +// theme::init(Assets, cx); + +// let mut has_default_theme = false; +// for theme_name in themes.list(false).map(|meta| meta.name) { +// let theme = themes.get(&theme_name).unwrap(); +// assert_eq!(theme.meta.name, theme_name); +// if theme.meta.name == settings::get::(cx).theme.meta.name { +// has_default_theme = true; +// } +// } +// assert!(has_default_theme); +// } + +// #[gpui::test] +// fn test_bundled_languages(cx: &mut AppContext) { +// cx.set_global(SettingsStore::test(cx)); +// let mut languages = LanguageRegistry::test(); +// languages.set_executor(cx.background().clone()); +// let languages = Arc::new(languages); +// let node_runtime = node_runtime::FakeNodeRuntime::new(); +// languages::init(languages.clone(), node_runtime, cx); +// for name in languages.language_names() { +// languages.language_for_name(&name); +// } +// cx.foreground().run_until_parked(); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// let mut app_state = AppState::test(cx); +// let state = Arc::get_mut(&mut app_state).unwrap(); +// state.initialize_workspace = initialize_workspace; +// state.build_window_options = build_window_options; +// theme::init((), cx); +// audio::init((), cx); +// channel::init(&app_state.client, app_state.user_store.clone(), cx); +// call::init(app_state.client.clone(), app_state.user_store.clone(), cx); +// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// language::init(cx); +// editor::init(cx); +// project_panel::init_settings(cx); +// collab_ui::init(&app_state, cx); +// pane::init(cx); +// project_panel::init((), cx); +// terminal_view::init(cx); +// assistant::init(cx); +// app_state +// }) +// } + +// fn rust_lang() -> Arc { +// Arc::new(language::Language::new( +// language::LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// )) +// } +// }