Detailed changes
@@ -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",
@@ -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",
@@ -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],
@@ -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);
@@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider {
model: OpenAILanguageModel,
credential: Arc<RwLock<ProviderCredential>>,
pub client: Arc<dyn HttpClient>,
- pub executor: Arc<BackgroundExecutor>,
+ pub executor: BackgroundExecutor,
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
}
@@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage {
}
impl OpenAIEmbeddingProvider {
- pub fn new(client: Arc<dyn HttpClient>, executor: Arc<BackgroundExecutor>) -> Self {
+ pub fn new(client: Arc<dyn HttpClient>, 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));
@@ -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<Event> for Breadcrumbs {}
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
- type Element = Component<ButtonLike>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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::<editor::Editor>().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::<editor::Editor>().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();
+ }),
+ )
}
}
@@ -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
@@ -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<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
@@ -334,55 +327,12 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: u64,
- requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
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<ActiveCall>, Vec<Subscription>)>,
-}
-
-impl Call {
- pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box<dyn CallHandler> {
- let mut active_call = None;
- if cx.has_global::<Model<ActiveCall>>() {
- let call = cx.global::<Model<ActiveCall>>().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<ActiveCall>,
- event: &room::Event,
- cx: &mut ViewContext<Workspace>,
- ) {
- 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<Project>,
- cx: &mut ViewContext<Workspace>,
- ) -> 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<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>> {
- 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::<SharedScreen>() {
- 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<u64> {
- Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
- }
- fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
- 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<WeakModel<Project>> {
- ActiveCall::global(cx).read(cx).location().cloned()
- }
- fn invite(
- &mut self,
- called_user_id: u64,
- initial_project: Option<Model<Project>>,
- cx: &mut AppContext,
- ) -> Task<Result<()>> {
- ActiveCall::global(cx).update(cx, |this, cx| {
- this.invite(called_user_id, initial_project, cx)
- })
- }
- fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, 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<bool> {
- 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<bool> {
- 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;
@@ -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;
@@ -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<Deterministic>,
+ 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::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(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::<Editor>()
+ .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::<Editor>()
+ .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::<Editor>()
+ .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 <b@b.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 <b@b.b>"
+ );
+ });
+}
+
fn visible_push_notifications(
cx: &mut TestAppContext,
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
@@ -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();
@@ -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?;
}
@@ -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();
@@ -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::<TestPanel>(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::<TestPanel>(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<workspace::Pane>, cx: &mut TestAppContext| {
+// let pane_paths = |pane: &View<workspace::Pane>, 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::<Workspace>()
// .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::<Workspace>()
// .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::<SettingsStore, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(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::<Editor>()
+// .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::<Editor>()
+// .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::<Editor>()
+// .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 <b@b.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 <b@b.b>"
+// );
+// });
+// }
+
// fn visible_push_notifications(
// cx: &mut TestAppContext,
-// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+// ) -> Vec<gpui::View<ProjectSharedNotification>> {
// 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<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
-// workspace.read_with(cx, |workspace, cx| {
+// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
+// workspace.update(cx, |workspace, cx| {
// let active_pane = workspace.active_pane();
// workspace
// .panes()
@@ -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, _| {
@@ -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?;
}
@@ -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| {
@@ -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<User>,
- // peer_id: Option<PeerId>,
- // is_pending: bool,
- // },
- // ParticipantProject {
- // project_id: u64,
- // worktree_root_names: Vec<String>,
- // host_user_id: u64,
- // is_last: bool,
- // },
- // ParticipantScreen {
- // peer_id: Option<PeerId>,
- // is_last: bool,
- // },
+ CallParticipant {
+ user: Arc<User>,
+ peer_id: Option<PeerId>,
+ is_pending: bool,
+ },
+ ParticipantProject {
+ project_id: u64,
+ worktree_root_names: Vec<String>,
+ host_user_id: u64,
+ is_last: bool,
+ },
+ ParticipantScreen {
+ peer_id: Option<PeerId>,
+ is_last: bool,
+ },
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
// ChannelInvite(Arc<Channel>),
@@ -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<PeerId>,
- // user_store: ModelHandle<UserStore>,
- // is_pending: bool,
- // is_selected: bool,
- // theme: &theme::Theme,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // 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::<CallParticipant, _>(
- // 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::<LeaveCallButton, _>(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::<LeaveCallTooltip>(
- // 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::<CallParticipantTooltip>(
- // user.id as usize,
- // tooltip,
- // Some(Box::new(FollowNextCollaborator)),
- // theme.tooltip.clone(),
- // cx,
- // )
- // .into_any()
- // }
+ fn render_call_participant(
+ &self,
+ user: Arc<User>,
+ peer_id: Option<PeerId>,
+ is_pending: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>,
- // ) -> AnyElement<Self> {
- // 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::<JoinProject, _>(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::<JoinProjectTooltip>(
- // 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<PeerId>,
- // is_last: bool,
- // is_selected: bool,
- // theme: &theme::CollabPanel,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // 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::<OpenSharedScreen, _>(
- // 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<Self>,
+ ) -> 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::<JoinProject, _>(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::<JoinProjectTooltip>(
+ // project_id as usize,
+ // format!("Open {}", project_name),
+ // None,
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .into_any()
+ }
+
+ fn render_participant_screen(
+ &self,
+ peer_id: Option<PeerId>,
+ is_last: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> 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<Self>,
- // ) -> AnyElement<Self> {
- // enum ChannelNotes {}
- // let host_avatar_width = theme
- // .contact_avatar
- // .width
- // .or(theme.contact_avatar.height)
- // .unwrap_or(0.);
-
- // MouseEventHandler::new::<ChannelNotes, _>(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::<Self>::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<Self>,
+ ) -> 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<Self>,
- // ) -> AnyElement<Self> {
- // enum ChannelChat {}
- // let host_avatar_width = theme
- // .contact_avatar
- // .width
- // .or(theme.contact_avatar.height)
- // .unwrap_or(0.);
-
- // MouseEventHandler::new::<ChannelChat, _>(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::<Self>::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<Self>,
+ ) -> 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<Channel>,
@@ -2273,20 +2081,19 @@ impl CollabPanel {
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
let Some(handle) = cx.window_handle().downcast::<Workspace>() 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<Self>) {
@@ -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()
+ }
}
}),
),
@@ -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<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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::<Vec<_>>()
+ });
+ 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<foo>
pub fn render_project_owner(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
- // 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<Self>) {
- // 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<Self>) {
+ 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<Self>) {
- // 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<Self>) {
+ 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>) {
// self.user_menu.update(cx, |user_menu, cx| {
@@ -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<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
@@ -42,61 +41,61 @@ pub fn init(app_state: &Arc<AppState>, 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<dyn PlatformDisplay>,
@@ -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,
+ )),
),
)
}
@@ -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())
});
}
@@ -88,7 +88,7 @@ struct DiagnosticGroupState {
block_count: usize,
}
-impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
+impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
impl Render for ProjectDiagnosticsEditor {
type Element = Focusable<Div>;
@@ -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<Self>) {
- 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<Workspace>) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(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>) {
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
}
@@ -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);
}
@@ -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;
}
@@ -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;
@@ -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<workspace::item::FollowEvent> {
- match self {
- EditorEvent::Edited => Some(FollowEvent::Unfollow),
- EditorEvent::SelectionsChanged { local }
- | EditorEvent::ScrollPositionChanged { local, .. } => {
- if *local {
- Some(FollowEvent::Unfollow)
- } else {
- None
- }
- }
- _ => None,
- }
- }
-}
-
-impl EventEmitter<ItemEvent> for Editor {}
-
impl FollowableItem for Editor {
- type FollowableEvent = EditorEvent;
fn remote_id(&self) -> Option<ViewId> {
self.remote_id
}
@@ -241,9 +221,24 @@ impl FollowableItem for Editor {
}))
}
+ fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
+ 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<proto::update_view::Variant>,
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<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() {
let newest_selection = self.selections.newest::<Point>(cx);
@@ -586,28 +583,25 @@ impl Item for Editor {
fn tab_content(&self, detail: Option<usize>, 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<Project>,
_workspace: WeakView<Workspace>,
@@ -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);
@@ -482,10 +482,6 @@ impl<T: 'static> WeakModel<T> {
/// 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<C, R>(
&self,
cx: &mut C,
@@ -501,6 +497,21 @@ impl<T: 'static> WeakModel<T> {
.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<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
+ where
+ C: Context,
+ Result<C::Result<R>>: crate::Flatten<R>,
+ {
+ crate::Flatten::flatten(
+ self.upgrade()
+ .ok_or_else(|| anyhow!("entity release"))
+ .map(|this| cx.read_model(&this, read)),
+ )
+ }
}
impl<T> Hash for WeakModel<T> {
@@ -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<Pixels>, &mut WindowContext)) -> Canvas {
+ Canvas {
+ paint_callback: Box::new(callback),
+ style: StyleRefinement::default(),
+ }
+}
+
+pub struct Canvas {
+ paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+ style: StyleRefinement,
+}
+
+impl IntoElement for Canvas {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for Canvas {
+ type State = ();
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ 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<Pixels>, _: &mut (), cx: &mut WindowContext) {
+ (self.paint_callback)(bounds, cx)
+ }
+}
+
+impl Styled for Canvas {
+ fn style(&mut self) -> &mut crate::StyleRefinement {
+ &mut self.style
+ }
+}
@@ -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<A: 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::<A>(),
Box::new(move |action, phase, cx| {
@@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element {
self
}
+ fn on_boxed_action(
+ mut self,
+ action: &Box<dyn Action>,
+ listener: impl Fn(&Box<dyn Action>, &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);
@@ -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::*;
@@ -16,7 +16,7 @@ pub struct DispatchNodeId(usize);
pub(crate) struct DispatchTree {
node_stack: Vec<DispatchNodeId>,
- context_stack: Vec<KeyContext>,
+ pub(crate) context_stack: Vec<KeyContext>,
nodes: Vec<DispatchNode>,
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
@@ -163,11 +163,25 @@ impl DispatchTree {
actions
}
- pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
+ pub fn bindings_for_action(
+ &self,
+ action: &dyn Action,
+ context_stack: &Vec<KeyContext>,
+ ) -> Vec<KeyBinding> {
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()
}
@@ -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,
@@ -147,18 +147,25 @@ impl Platform for TestPlatform {
fn set_display_link_output_callback(
&self,
_display_id: DisplayId,
- _callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
+ mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
) {
- 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!()
@@ -72,7 +72,7 @@ impl TextSystem {
}
}
- pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result<Bounds<Pixels>> {
+ pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
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<Size<Pixels>> {
@@ -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<u32> {
+ 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<Pixels> {
+ 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<Pixels> {
+ 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<Pixels> {
+ 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<Pixels> {
+ 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<Pixels> {
- 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<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+ fn read_metrics<T>(&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)
}
}
@@ -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;
@@ -1350,6 +1350,8 @@ impl<'a> WindowContext<'a> {
.dispatch_tree
.dispatch_path(node_id);
+ let mut actions: Vec<Box<dyn Action>> = 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::<KeyDownEvent>() {
- 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<KeyBinding> {
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<KeyBinding> {
+ 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<V: Render, E>(
@@ -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)
+ }
+}
@@ -81,6 +81,7 @@ impl<T> Outline<T> {
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];
@@ -79,6 +79,7 @@ impl FocusableView for LanguageSelector {
self.picker.focus_handle(cx)
}
}
+
impl EventEmitter<DismissEvent> for LanguageSelector {}
pub struct LanguageSelectorDelegate {
@@ -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"] }
@@ -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<Workspace>) {
+ if let Some(editor) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<Editor>())
+ {
+ 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<Picker<OutlineViewDelegate>>,
+}
+
+impl FocusableView for OutlineView {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for OutlineView {}
+
+impl Render for OutlineView {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ }
+}
+
+impl OutlineView {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(toggle);
+ }
+
+ fn new(
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<OutlineView>,
+ active_editor: View<Editor>,
+ outline: Outline<Anchor>,
+ selected_match_index: usize,
+ prev_scroll_position: Option<Point<f32>>,
+ matches: Vec<StringMatch>,
+ last_query: String,
+}
+
+impl OutlineViewDelegate {
+ fn new(
+ outline_view: WeakView<OutlineView>,
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<OutlineView>,
+ ) -> 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<Picker<OutlineViewDelegate>>,
+ ) {
+ 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<str> {
+ "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<Picker<OutlineViewDelegate>>) {
+ self.set_selected_index(ix, true, cx);
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) -> 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::<usize>(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<Picker<OutlineViewDelegate>>) {
+ 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<Picker<OutlineViewDelegate>>) {
+ 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<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ 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)),
+ )
+ }
+}
@@ -1121,20 +1121,22 @@ impl Project {
project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
+ ) -> Task<Result<Option<Entry>>> {
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<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ 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<ProjectPath>,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
- let project_path = path.into();
- let task = self.open_buffer(project_path.clone(), cx);
+ ) -> Task<Result<(Option<ProjectEntryId>, 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,
})
}
@@ -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"), "");
@@ -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(),
}
@@ -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<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
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::<Task<anyhow::Result<Entry>>>::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<Arc<Path>>,
text: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
+ ) -> Task<Result<Option<Entry>>> {
+ let path: Arc<Path> = 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<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Path>,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: ProjectEntryId,
+ pub(crate) entry_id: Option<ProjectEntryId>,
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;
@@ -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(())
+ })
+ }
}
}
}
@@ -1151,20 +1151,22 @@ impl Project {
project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
+ ) -> Task<Result<Option<Entry>>> {
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<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ 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<ProjectPath>,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<(ProjectEntryId, AnyModel)>> {
- let project_path = path.into();
- let task = self.open_buffer(project_path.clone(), cx);
- cx.spawn(move |_, mut cx| async move {
+ ) -> Task<Result<(Option<ProjectEntryId>, 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,
})
}
@@ -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"), "");
@@ -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(),
}
@@ -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<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
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::<Task<anyhow::Result<Entry>>>::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<Arc<Path>>,
text: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
+ ) -> Task<Result<Option<Entry>>> {
+ let path: Arc<Path> = 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<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Path>,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
+ 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<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: ProjectEntryId,
+ pub(crate) entry_id: Option<ProjectEntryId>,
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;
@@ -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(())
+ })
+ }
}
}
}
@@ -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);
}
@@ -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);
// }
@@ -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"] }
@@ -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<BufferSearchBar>,
+ active_item: Option<Box<dyn ItemHandle>>,
+ _inlay_hints_enabled_subscription: Option<Subscription>,
+ #[allow(unused)]
+ workspace: WeakView<Workspace>,
+}
+
+impl QuickActionBar {
+ pub fn new(buffer_search_bar: View<BufferSearchBar>, 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<View<Editor>> {
+ self.active_item
+ .as_ref()
+ .and_then(|item| item.downcast::<Editor>())
+ }
+}
+
+impl Render for QuickActionBar {
+ type Element = Stateful<Div>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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<ToolbarItemEvent> for QuickActionBar {}
+
+// impl View for QuickActionBar {
+// fn ui_name() -> &'static str {
+// "QuickActionsBar"
+// }
+
+// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+// 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<dyn Action>,
+ tooltip: SharedString,
+ tooltip_meta: Option<SharedString>,
+}
+
+impl QuickActionBarButton {
+ fn new(
+ id: impl Into<ElementId>,
+ icon: Icon,
+ toggled: bool,
+ action: Box<dyn Action>,
+ tooltip: impl Into<SharedString>,
+ ) -> Self {
+ Self {
+ id: id.into(),
+ icon,
+ toggled,
+ action,
+ tooltip: tooltip.into(),
+ tooltip_meta: None,
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn meta(mut self, meta: Option<impl Into<SharedString>>) -> 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<QuickActionBar>),
+// >(
+// index: usize,
+// icon: &'static str,
+// toggled: bool,
+// tooltip: (String, Option<Box<dyn Action>>),
+// cx: &mut ViewContext<QuickActionBar>,
+// on_click: F,
+// ) -> AnyElement<QuickActionBar> {
+// enum QuickActionBarButton {}
+
+// let theme = theme::current(cx);
+// let (tooltip_text, action) = tooltip;
+
+// MouseEventHandler::new::<QuickActionBarButton, _>(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::<QuickActionBarButton>(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<Self>,
+ ) -> 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::<Editor>() {
+ 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
+ }
+ }
+ }
+}
@@ -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;
@@ -9,4 +9,4 @@ pub use notification::*;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;
@@ -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;
@@ -9,4 +9,4 @@ pub use notification::*;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;
@@ -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
@@ -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)
@@ -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"
+ ]
+ }
+ ]
+}
@@ -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"
+ ]
+ }
+ ]
+}
@@ -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<T: Ord>(data: &[T]) -> Vec<usize> {
+ let mut indices = (0..data.len()).collect::<Vec<_>>();
+ 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<Path>,
+ transactions:
+ smol::channel::Sender<Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>>,
+}
+
+impl VectorDatabase {
+ pub async fn new(
+ fs: Arc<dyn Fs>,
+ path: Arc<Path>,
+ executor: BackgroundExecutor,
+ ) -> Result<Self> {
+ if let Some(db_directory) = path.parent() {
+ fs.create_dir(db_directory).await?;
+ }
+
+ let (transactions_tx, transactions_rx) = smol::channel::unbounded::<
+ Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>,
+ >();
+ 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<Path> {
+ &self.path
+ }
+
+ fn transact<F, T>(&self, f: F) -> impl Future<Output = Result<T>>
+ where
+ F: 'static + Send + FnOnce(&rusqlite::Transaction) -> Result<T>,
+ 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<Output = Result<()>> {
+ 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<Path>,
+ ) -> impl Future<Output = Result<()>> {
+ 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<Path>,
+ mtime: SystemTime,
+ spans: Vec<Span>,
+ ) -> impl Future<Output = Result<()>> {
+ 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<Output = Result<bool>> {
+ 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<SpanDigest>,
+ ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+ 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::<Vec<_>>(),
+ );
+ 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<i64, Vec<Arc<Path>>>,
+ ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+ 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::<Vec<_>>(),
+ );
+ 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<Path>,
+ ) -> impl Future<Output = Result<i64>> {
+ 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<Output = Result<HashMap<PathBuf, SystemTime>>> {
+ 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<PathBuf, SystemTime> = 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<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
+ 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::<Vec<(usize, Embedding)>>();
+
+ 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<f32> = 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<usize> = 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::<Vec<OrderedFloat<f32>>>();
+ 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<Output = Result<Vec<i64>>> {
+ 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::<i64>::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<Output = Result<Vec<(i64, PathBuf, Range<usize>)>>> {
+ 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::<i64, (i64, PathBuf, Range<usize>)>::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<Vec<rusqlite::types::Value>> {
+ Rc::new(
+ ids.iter()
+ .copied()
+ .map(|v| rusqlite::types::Value::from(v))
+ .collect::<Vec<_>>(),
+ )
+}
@@ -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<Path>,
+ pub mtime: SystemTime,
+ pub spans: Vec<Span>,
+ 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<dyn EmbeddingProvider>,
+ pending_batch: Vec<FileFragmentToEmbed>,
+ executor: BackgroundExecutor,
+ pending_batch_token_count: usize,
+ finished_files_tx: channel::Sender<FileToEmbed>,
+ finished_files_rx: channel::Receiver<FileToEmbed>,
+}
+
+#[derive(Clone)]
+pub struct FileFragmentToEmbed {
+ file: Arc<Mutex<FileToEmbed>>,
+ span_range: Range<usize>,
+}
+
+impl EmbeddingQueue {
+ pub fn new(
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ 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<FileToEmbed> {
+ self.finished_files_rx.clone()
+ }
+}
@@ -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<Self> {
+ 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<ToSqlOutput> {
+ 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<usize>,
+ pub content: String,
+ pub embedding: Option<Embedding>,
+ pub digest: SpanDigest,
+ pub token_count: usize,
+}
+
+const CODE_CONTEXT_TEMPLATE: &str =
+ "The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const ENTIRE_FILE_TEMPLATE: &str =
+ "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
+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<dyn EmbeddingProvider>,
+}
+
+// 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<Range<usize>>,
+ pub name_range: Option<Range<usize>>,
+ pub context_ranges: Vec<Range<usize>>,
+ pub collapse_ranges: Vec<Range<usize>>,
+}
+
+impl CodeContextRetriever {
+ pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>) -> Self {
+ Self {
+ parser: Parser::new(),
+ cursor: QueryCursor::new(),
+ embedding_provider,
+ }
+ }
+
+ fn parse_entire_file(
+ &self,
+ relative_path: Option<&Path>,
+ language_name: Arc<str>,
+ content: &str,
+ ) -> Result<Vec<Span>> {
+ let document_span = ENTIRE_FILE_TEMPLATE
+ .replace(
+ "<path>",
+ &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+ )
+ .replace("<language>", language_name.as_ref())
+ .replace("<item>", &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<Vec<Span>> {
+ let document_span = MARKDOWN_CONTEXT_TEMPLATE
+ .replace(
+ "<path>",
+ &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+ )
+ .replace("<item>", &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<Grammar>,
+ ) -> Result<Vec<CodeContextMatch>> {
+ 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<CodeContextMatch> = Vec::new();
+ let mut collapse_ranges: Vec<Range<usize>> = Vec::new();
+ let mut keep_ranges: Vec<Range<usize>> = 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<Range<usize>> = None;
+ let mut name_range: Option<Range<usize>> = None;
+ let mut context_ranges: Vec<Range<usize>> = 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<Language>,
+ ) -> Result<Vec<Span>> {
+ 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(
+ "<path>",
+ &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+ )
+ .replace("<language>", 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<Language>) -> Result<Vec<Span>> {
+ 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<usize>],
+ ranges_to_subtract: &[Range<usize>],
+) -> Vec<Range<usize>> {
+ 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<usize>,
+ 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();
+}
@@ -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<String> = env::var("OPENAI_API_KEY").ok();
+}
+
+pub fn init(
+ fs: Arc<dyn Fs>,
+ http_client: Arc<dyn HttpClient>,
+ language_registry: Arc<LanguageRegistry>,
+ 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<Workspace>| {
+ 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<Instant>,
+ },
+}
+
+pub struct SemanticIndex {
+ fs: Arc<dyn Fs>,
+ db: VectorDatabase,
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ language_registry: Arc<LanguageRegistry>,
+ parsing_files_tx: channel::Sender<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>,
+ _embedding_task: Task<()>,
+ _parsing_files_tasks: Vec<Task<()>>,
+ projects: HashMap<WeakModel<Project>, ProjectState>,
+}
+
+struct ProjectState {
+ worktrees: HashMap<WorktreeId, WorktreeState>,
+ pending_file_count_rx: watch::Receiver<usize>,
+ pending_file_count_tx: Arc<Mutex<watch::Sender<usize>>>,
+ 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<Path>, 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<Arc<Path>, ChangedPathInfo>,
+ done_rx: watch::Receiver<Option<()>>,
+ _registration: Task<()>,
+}
+
+impl RegisteringWorktreeState {
+ fn done(&self) -> impl Future<Output = ()> {
+ 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<Arc<Path>, 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<Weak<Mutex<watch::Sender<usize>>>>,
+}
+
+impl JobHandle {
+ fn new(tx: &Arc<Mutex<watch::Sender<usize>>>) -> Self {
+ *tx.lock().borrow_mut() += 1;
+ Self {
+ tx: Arc::new(Arc::downgrade(&tx)),
+ }
+ }
+}
+
+impl ProjectState {
+ fn new(subscription: gpui::Subscription, cx: &mut ModelContext<SemanticIndex>) -> 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<WorktreeId> {
+ 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<Path>,
+ absolute_path: PathBuf,
+ language: Option<Arc<Language>>,
+ modified_time: SystemTime,
+ job_handle: JobHandle,
+}
+
+#[derive(Clone)]
+pub struct SearchResult {
+ pub buffer: Model<Buffer>,
+ pub range: Range<Anchor>,
+ pub similarity: OrderedFloat<f32>,
+}
+
+impl SemanticIndex {
+ pub fn global(cx: &mut AppContext) -> Option<Model<SemanticIndex>> {
+ if cx.has_global::<Model<Self>>() {
+ Some(cx.global::<Model<SemanticIndex>>().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<Project>) -> 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<dyn Fs>,
+ database_path: PathBuf,
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ language_registry: Arc<LanguageRegistry>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Model<Self>> {
+ 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<HashMap<SpanDigest, Embedding>>, 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<dyn Fs>,
+ pending_file: PendingFile,
+ retriever: &mut CodeContextRetriever,
+ embedding_queue: &Arc<Mutex<EmbeddingQueue>>,
+ embeddings_for_digest: &HashMap<SpanDigest, Embedding>,
+ ) {
+ 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<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<bool>> {
+ let worktrees_indexed_previously = project
+ .read(cx)
+ .worktrees()
+ .map(|worktree| {
+ self.db
+ .worktree_previously_indexed(&worktree.read(cx).abs_path())
+ })
+ .collect::<Vec<_>>();
+ 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<Project>,
+ worktree_id: WorktreeId,
+ changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<Project>,
+ worktree: Model<Worktree>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<Project>, cx: &mut ModelContext<Self>) {
+ 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::<Vec<_>>();
+ let worktree_ids = worktrees
+ .iter()
+ .map(|worktree| worktree.read(cx).id())
+ .collect::<HashSet<_>>();
+
+ // 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<Project>) -> Option<watch::Receiver<usize>> {
+ Some(
+ self.projects
+ .get(&project.downgrade())?
+ .pending_file_count_rx
+ .clone(),
+ )
+ }
+
+ pub fn search_project(
+ &mut self,
+ project: Model<Project>,
+ query: String,
+ limit: usize,
+ includes: Vec<PathMatcher>,
+ excludes: Vec<PathMatcher>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<SearchResult>>> {
+ 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<Project>,
+ query: Embedding,
+ limit: usize,
+ includes: Vec<PathMatcher>,
+ excludes: Vec<PathMatcher>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<SearchResult>>> {
+ 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::<Vec<i64>>();
+ 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::<Vec<i64>>();
+ 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::<Vec<i64>>();
+ let scores = results
+ .into_iter()
+ .map(|(_, score)| score)
+ .collect::<Vec<_>>();
+ 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<Project>,
+ query: Embedding,
+ limit: usize,
+ includes: &[PathMatcher],
+ excludes: &[PathMatcher],
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<SearchResult>>> {
+ 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::<HashMap<_, _>>();
+
+ 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::<SearchResult>::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<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ 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<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ 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::<Vec<_>>();
+ 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());
+ }
+}
@@ -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<bool>,
+}
+
+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> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -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::<String>();
+ 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::<Vec<_>>();
+
+ 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<Path>, 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::<Vec<_>>();
+ 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::<Vec<_>>(),
+ 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 <typename T,
+ typename = typename std::enable_if<
+ std::is_integral<T>::value || std::is_floating_point<T>::value,
+ bool>::type>
+ class Matrix2 {
+ std::vector<std::vector<T>> _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 <typename Integer,
+ typename = typename std::enable_if<std::is_integral<Integer>::value,
+ Integer>::type>
+ explicit Matrix(const Integer size) {
+ for (size_t i = 0; i < size; ++i) {
+ _mat.emplace_back(std::vector<T>(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 <typename T,
+ typename = typename std::enable_if<
+ std::is_integral<T>::value || std::is_floating_point<T>::value,
+ bool>::type>
+ class Matrix2 {
+ std::vector<std::vector<T>> _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 <typename Integer,
+ typename = typename std::enable_if<std::is_integral<Integer>::value,
+ Integer>::type>
+ explicit Matrix(const Integer size) {
+ for (size_t i = 0; i < size; ++i) {
+ _mat.emplace_back(std::vector<T>(size, 0));
+ }
+ }
+ }"
+ .unindent(),
+ 612,
+ ),
+ (
+ "
+ explicit Matrix(const Integer size) {
+ for (size_t i = 0; i < size; ++i) {
+ _mat.emplace_back(std::vector<T>(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#"
+ <?php
+
+ namespace LevelUp\Experience\Concerns;
+
+ /*
+ This is a multiple-lines comment block
+ that spans over multiple
+ lines
+ */
+ function functionName() {
+ echo "Hello world!";
+ }
+
+ trait HasAchievements
+ {
+ /**
+ * @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)));
+ }
+
+ 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<Language> {
+ 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<Language> {
+ 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<Language> {
+ 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<Language> {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: "TOML".into(),
+ path_suffixes: vec!["toml".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_toml::language()),
+ ))
+}
+
+fn cpp_lang() -> Arc<Language> {
+ 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<Language> {
+ 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<Language> {
+ 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<Language> {
+ 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<Language> {
+ 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<Range<usize>>, keep_ranges: Vec<Range<usize>>
+
+ 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);
+ });
+}
@@ -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)),
@@ -1132,6 +1132,7 @@ mod tests {
})
})
.await
+ .unwrap()
.unwrap();
(wt, entry)
@@ -299,11 +299,8 @@ impl TerminalView {
cx: &mut ViewContext<Self>,
) {
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<SharedString> {
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)
@@ -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(),
@@ -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 {
@@ -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
}
@@ -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()),
@@ -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<Picker<ThemeSelectorDelegate>>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> 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::<Arc<ThemeRegistry>>();
+ let registry = cx.global::<ThemeRegistry>();
let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
//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<Picker<ThemeSelectorDelegate>>) {
if let Some(mat) = self.matches.get(self.selected_index) {
- let registry = cx.global::<Arc<ThemeRegistry>>();
+ let registry = cx.global::<ThemeRegistry>();
match registry.get(&mat.string) {
Ok(theme) => {
Self::set_theme(theme, cx);
@@ -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::*;
@@ -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<Icon>,
handler: Rc<dyn Fn(&mut WindowContext)>,
- key_binding: Option<KeyBinding>,
+ action: Option<Box<dyn Action>>,
},
}
@@ -24,6 +24,7 @@ pub struct ContextMenu {
items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
selected_index: Option<usize>,
+ 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<SharedString>,
- action: Box<dyn Action>,
- cx: &mut WindowContext,
- ) -> Self {
+ pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> 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<SharedString>,
- action: Box<dyn Action>,
- cx: &mut WindowContext,
- ) -> Self {
+ pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> 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<dyn Action>, cx: &mut ViewContext<Self>) {
+ 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| {
@@ -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))
@@ -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))
+ }
+}
@@ -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<Self> {
- // 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<Self> {
+ let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
+ Some(Self::new(key_binding))
+ }
+
fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
let mut icon: Option<Icon> = None;
@@ -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<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
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())
@@ -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;
@@ -219,9 +219,11 @@ impl PathMatcher {
}
pub fn is_match<P: AsRef<Path>>(&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:?}"
+ );
+ }
}
@@ -259,6 +259,8 @@ impl FocusableView for WelcomePage {
}
impl Item for WelcomePage {
+ type Event = ItemEvent;
+
fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
"Welcome to Zed!".into_any()
}
@@ -278,4 +280,8 @@ impl Item for WelcomePage {
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
}))
}
+
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+ f(*event)
+ }
}
@@ -481,18 +481,21 @@ impl Pane {
pub(crate) fn open_item(
&mut self,
- project_entry_id: ProjectEntryId,
+ project_entry_id: Option<ProjectEntryId>,
focus_item: bool,
cx: &mut ViewContext<Self>,
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
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;
+ }
}
}
@@ -2129,13 +2129,13 @@ impl Workspace {
})
}
- pub(crate) fn load_path(
+ fn load_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<
Result<(
- ProjectEntryId,
+ Option<ProjectEntryId>,
impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
@@ -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"
@@ -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<Vec<(Range<usize>, HighlightStyle)>>,
}
-pub trait Item: FocusableView + EventEmitter<ItemEvent> {
+pub trait Item: FocusableView + EventEmitter<Self::Event> {
+ type Event;
+
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
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<ItemEvent> {
}
pub trait ItemHandle: 'static + Send {
- fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
fn subscribe_to_item_events(
&self,
cx: &mut WindowContext,
- handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
+ handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
) -> gpui::Subscription;
+ fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
@@ -285,20 +289,20 @@ impl dyn ItemHandle {
}
impl<T: Item> ItemHandle for View<T> {
- fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
- self.focus_handle(cx)
- }
-
fn subscribe_to_item_events(
&self,
cx: &mut WindowContext,
- handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
+ handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
) -> 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<SharedString> {
self.read(cx).tab_tooltip_text(cx)
}
@@ -461,7 +465,7 @@ impl<T: Item> ItemHandle for View<T> {
}
}
- 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<T: Item> ItemHandle for View<T> {
}
_ => {}
- }
+ });
}));
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<FollowEvent>;
-}
-
pub trait FollowableItem: Item {
- type FollowableEvent: FollowableEvents;
fn remote_id(&self) -> Option<ViewId>;
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn from_state_proto(
@@ -670,9 +669,10 @@ pub trait FollowableItem: Item {
state: &mut Option<proto::view::Variant>,
cx: &mut WindowContext,
) -> Option<Task<Result<View<Self>>>>;
+ fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
fn add_event_to_update_proto(
&self,
- event: &Self::FollowableEvent,
+ event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
cx: &WindowContext,
) -> bool;
@@ -683,7 +683,6 @@ pub trait FollowableItem: Item {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>>;
fn is_project_item(&self, cx: &WindowContext) -> bool;
-
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
}
@@ -739,10 +738,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
}
fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
- 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<SharedString> {
self.tab_descriptions.as_ref().and_then(|descriptions| {
let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
@@ -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<ProjectEntryId>,
focus_item: bool,
cx: &mut ViewContext<Self>,
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
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::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
- // .on_drop(|_view, state: View<DraggedTab>, 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::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
+ // .on_drop(|_view, state: View<DraggedTab>, 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)
@@ -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<Project>,
follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
@@ -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<Project>,
basis: usize,
follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
@@ -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()
@@ -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<Event> for SharedScreen {}
-impl EventEmitter<workspace::item::ItemEvent> 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<SharedString> {
Some(format!("{}'s screen", self.user.github_login).into())
}
+
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
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),
+ }
+ }
}
@@ -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())),
)
}
}
@@ -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<AppState>, 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<UserStore>,
pub workspace_store: Model<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
- pub call_factory: CallFactory,
pub build_window_options:
fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
pub node_runtime: Arc<dyn NodeRuntime>,
@@ -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<Project>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<(bool, bool)> {
- None
- }
-
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- pane: &View<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>> {
- None
- }
-
- fn room_id(&self, cx: &AppContext) -> Option<u64> {
- None
- }
-
- fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
- Task::ready(Err(anyhow!("TestCallHandler should not be hanging up")))
- }
-
- fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
- None
- }
-
- fn invite(
- &mut self,
- called_user_id: u64,
- initial_project: Option<Model<Project>>,
- cx: &mut AppContext,
- ) -> Task<Result<()>> {
- unimplemented!()
- }
-
- fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
- None
- }
-
- fn is_muted(&self, cx: &AppContext) -> Option<bool> {
- 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<bool> {
- None
- }
-}
-
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -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<Workspace>),
}
-#[async_trait(?Send)]
-pub trait CallHandler {
- fn peer_state(
- &mut self,
- id: PeerId,
- project: &Model<Project>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<(bool, bool)>;
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- pane: &View<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>>;
- fn room_id(&self, cx: &AppContext) -> Option<u64>;
- fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
- self.room_id(cx).is_some()
- }
- fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>>;
- fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
- fn invite(
- &mut self,
- called_user_id: u64,
- initial_project: Option<Model<Project>>,
- cx: &mut AppContext,
- ) -> Task<Result<()>>;
- fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>>;
- fn is_muted(&self, cx: &AppContext) -> Option<bool>;
- fn is_deafened(&self, cx: &AppContext) -> Option<bool>;
- 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<Self>,
weak_self: WeakView<Self>,
@@ -525,10 +433,10 @@ pub struct Workspace {
titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
- call_handler: Box<dyn CallHandler>,
follower_states: HashMap<View<Pane>, FollowerState>,
last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
window_edited: bool,
+ active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
@@ -556,7 +464,6 @@ struct FollowerState {
enum WorkspaceBounds {}
-type CallFactory = fn(&mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
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::<Model<ActiveCall>>() {
+ let call = cx.global::<Model<ActiveCall>>().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<Self>,
- ) -> Option<Task<Result<()>>> {
+ pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
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<Self>,
) -> Task<Result<bool>> {
//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<Self>,
) -> Task<
Result<(
- ProjectEntryId,
+ Option<ProjectEntryId>,
impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
@@ -2032,7 +1932,7 @@ impl Workspace {
pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
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<Self>,
- // ) -> Option<Task<Result<()>>> {
- // 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Self>,
- // ) -> Option<Task<Result<()>>> {
- // 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<Self>,
+ // ) {
+ // 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<Self>,
- // ) -> Option<Task<Result<()>>> {
- // 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<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
- 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<u64>,
- _cx: &mut ViewContext<Self>,
+ follower_project_id: Option<u64>,
+ cx: &mut ViewContext<Self>,
) -> 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<Self>) -> Option<()> {
+ fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> 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<Pane>,
cx: &mut ViewContext<Self>,
- ) -> Option<Box<dyn ItemHandle>> {
- 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::<SharedScreen>() {
- // if item.read(cx).peer_id == peer_id {
- // return Some(item);
- // }
- // }
+ ) -> Option<View<SharedScreen>> {
+ 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::<SharedScreen>() {
+ 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<Self>) {
@@ -2984,6 +2913,25 @@ impl Workspace {
}
}
+ fn active_call(&self) -> Option<&Model<ActiveCall>> {
+ self.active_call.as_ref().map(|(call, _)| call)
+ }
+
+ fn on_active_call_event(
+ &mut self,
+ _: Model<ActiveCall>,
+ event: &call::room::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<Self>) -> 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<WindowBounds> {
@@ -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<Client>, _cx: &mut ModelContext<Self>) -> Self {
+ pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> 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,
}
}
@@ -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::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(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::<Vec<_>>()
+ });
+ 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::<Vec<_>>();
+ 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);
@@ -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" }
@@ -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);
@@ -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<AppState>, 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<AppState>, 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::<Workspace>().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::<Workspace>().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::<Workspace>().unwrap().root(cx);
+// workspace_1.update(cx, |workspace, cx| {
+// assert_eq!(
+// workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).abs_path())
+// .collect::<Vec<_>>(),
+// &[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<Deterministic>, 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::<Workspace>().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::<Editor>()
+// .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::<Editor>()
+// .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::<Workspace>()
+// .unwrap();
+// let workspace = window.root(cx);
+
+// let editor = workspace.update(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<editor::Editor>()
+// .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::<Vec<_>>();
+// 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::<Workspace>().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::<ProjectPanel>(),
+// workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+// workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+// ]
+// .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::<Editor>()
+// .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::<HashSet<_>>();
+// 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::<Editor>()
+// .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::<HashSet<_>>();
+// 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::<Editor>()
+// .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::<HashSet<_>>();
+// 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::<HashSet<_>>();
+// 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::<Editor>()
+// .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::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(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::<Vec<_>>()
+// });
+// 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::<Vec<_>>();
+// 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::<Editor>().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::<Editor>()
+// .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::<Editor>()
+// .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::<Editor>()
+// .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::<Editor>().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::<Editor>().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::<Editor>()
+// .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::<Editor>()
+// .unwrap();
+// let editor3 = workspace
+// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .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<Workspace>,
+// cx: &mut TestAppContext,
+// ) -> (ProjectPath, DisplayPoint, f32) {
+// workspace.update(cx, |workspace, cx| {
+// let item = workspace.active_item(cx).unwrap();
+// let editor = item.downcast::<Editor>().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<Workspace>,
+// cx: &TestAppContext,
+// ) -> Option<ProjectPath> {
+// 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<Self>) -> AnyElement<Self> {
+// 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<Self>) -> AnyElement<Self> {
+// 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::<ThemeSettings>(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<AppState> {
+// 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<language::Language> {
+// Arc::new(language::Language::new(
+// language::LanguageConfig {
+// name: "Rust".into(),
+// path_suffixes: vec!["rs".to_string()],
+// ..Default::default()
+// },
+// Some(tree_sitter_rust::language()),
+// ))
+// }
+// }