diff --git a/Cargo.lock b/Cargo.lock index 5ec386837baa43369f46bd685d8ff3fc181b3f2e..3110a9ff43e75bc698b782d3ec4d79383851fca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9970,7 +9970,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=b5f461a69bf3df7298b1903574d506179e6390b0#b5f461a69bf3df7298b1903574d506179e6390b0" dependencies = [ "cc", "regex", diff --git a/Cargo.toml b/Cargo.toml index 674bfbd6065b244903e46b8641cad5b7fbbde0de..5dc30ca40b1687200ce69dded3fdf8e54ca03b5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -201,7 +201,7 @@ tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", re tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index ef6a655bdcead3cd64f29e9aa25b90d0d4e4d626..2a8d19f8829039d759c92e79b6acebe79e55b143 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -530,12 +530,17 @@ "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", "enter": "project_panel::Rename", - "space": "project_panel::Open", "backspace": "project_panel::Delete", "alt-cmd-r": "project_panel::RevealInFinder", "alt-shift-f": "project_panel::NewSearchInDirectory" } }, + { + "context": "ProjectPanel && not_editing", + "bindings": { + "space": "project_panel::Open" + } + }, { "context": "CollabPanel && not_editing", "bindings": { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index cac8bf6c54f8b335df756c13b08fb056b6378dd5..e472e8c8dfc7f3f7f2dc072825efea4fd89b41de 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1218,6 +1218,31 @@ impl View for AssistantPanel { let style = &theme.assistant; if let Some(api_key_editor) = self.api_key_editor.as_ref() { Flex::column() + .with_child( + Text::new( + "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", + style.api_key_prompt.text.clone(), + ), + ) + .with_child( + Text::new( + " - Having a subscription for another service like GitHub Copilot won't work.", + style.api_key_prompt.text.clone(), + ), + ) + .with_child( + Text::new( + " - You can create a api key at: platform.openai.com/api-keys", + style.api_key_prompt.text.clone(), + ), + ) + .with_child( + Text::new( + " ", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) .with_child( Text::new( "Paste your OpenAI API key and press Enter to use the assistant", @@ -1231,6 +1256,20 @@ impl View for AssistantPanel { .with_style(style.api_key_editor.container) .aligned(), ) + .with_child( + Text::new( + " ", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) + .with_child( + Text::new( + "Click on the Z button in the status bar to close this panel.", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) .contained() .with_style(style.api_key_prompt.container) .aligned() diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index df7dd847cf3413a410ce17b87f1c89efa2e2905e..a93305772312cab3624f995e0dd49751554867e1 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -15,7 +15,7 @@ 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, WeakView, WindowHandle, + Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle, }; pub use participant::ParticipantLocation; use postage::watch; @@ -557,24 +557,17 @@ pub fn report_call_event_for_channel( pub struct Call { active_call: Option<(Model, Vec)>, - parent_workspace: WeakView, } impl Call { - pub fn new( - parent_workspace: WeakView, - cx: &mut ViewContext<'_, Workspace>, - ) -> Box { + pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box { let mut active_call = None; if cx.has_global::>() { let call = cx.global::>().clone(); let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; active_call = Some((call, subscriptions)); } - Box::new(Self { - active_call, - parent_workspace, - }) + Box::new(Self { active_call }) } fn on_active_call_event( workspace: &mut Workspace, @@ -597,6 +590,7 @@ impl CallHandler for Call { fn peer_state( &mut self, leader_id: PeerId, + project: &Model, cx: &mut ViewContext, ) -> Option<(bool, bool)> { let (call, _) = self.active_call.as_ref()?; @@ -608,12 +602,7 @@ impl CallHandler for Call { match participant.location { ParticipantLocation::SharedProject { project_id } => { leader_in_this_app = true; - leader_in_this_project = Some(project_id) - == self - .parent_workspace - .update(cx, |this, cx| this.project().read(cx).remote_id()) - .log_err() - .flatten(); + leader_in_this_project = Some(project_id) == project.read(cx).remote_id(); } ParticipantLocation::UnsharedProject => { leader_in_this_app = true; diff --git a/crates/call2/src/shared_screen.rs b/crates/call2/src/shared_screen.rs index 7b7cd7d9df33045759ae9bda2f16e016f959c9d6..c38ebeac021d59c810fc27ff528ddc773f9642f4 100644 --- a/crates/call2/src/shared_screen.rs +++ b/crates/call2/src/shared_screen.rs @@ -3,10 +3,12 @@ use anyhow::Result; use client::{proto::PeerId, User}; use futures::StreamExt; use gpui::{ - div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render, - SharedString, Task, View, ViewContext, VisualContext, WindowContext, + div, img, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, FocusableView, + InteractiveElement, ParentElement, Render, SharedString, Styled, Task, View, ViewContext, + VisualContext, WindowContext, }; use std::sync::{Arc, Weak}; +use ui::{h_stack, Icon, IconElement}; use workspace::{item::Item, ItemNavHistory, WorkspaceId}; pub enum Event { @@ -16,8 +18,6 @@ pub enum Event { pub struct SharedScreen { track: Weak, frame: Option, - // temporary addition just to render something interactive. - current_frame_id: usize, pub peer_id: PeerId, user: Arc, nav_history: Option, @@ -51,7 +51,6 @@ impl SharedScreen { Ok(()) }), focus: cx.focus_handle(), - current_frame_id: 0, } } } @@ -65,50 +64,16 @@ impl FocusableView for SharedScreen { } } impl Render for SharedScreen { - type Element = Div; + type Element = Focusable
; + fn render(&mut self, _: &mut ViewContext) -> Self::Element { - let frame = self.frame.clone(); - let frame_id = self.current_frame_id; - self.current_frame_id = self.current_frame_id.wrapping_add(1); - div().children(frame.map(|_| { - ui::Label::new(frame_id.to_string()).color(ui::Color::Error) - // img().data(Arc::new(ImageData::new(image::ImageBuffer::new( - // frame.width() as u32, - // frame.height() as u32, - // )))) - })) + div().track_focus(&self.focus).size_full().children( + self.frame + .as_ref() + .map(|frame| img(frame.image()).size_full()), + ) } } -// impl View for SharedScreen { -// fn ui_name() -> &'static str { -// "SharedScreen" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// enum Focus {} - -// let frame = self.frame.clone(); -// MouseEventHandler::new::(0, cx, |_, cx| { -// Canvas::new(move |bounds, _, _, cx| { -// if let Some(frame) = frame.clone() { -// let size = constrain_size_preserving_aspect_ratio( -// bounds.size(), -// vec2f(frame.width() as f32, frame.height() as f32), -// ); -// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; -// cx.scene().push_surface(gpui::platform::mac::Surface { -// bounds: RectF::new(origin, size), -// image_buffer: frame.image(), -// }); -// } -// }) -// .contained() -// .with_style(theme::current(cx).shared_screen) -// }) -// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) -// .into_any() -// } -// } impl Item for SharedScreen { fn tab_tooltip_text(&self, _: &AppContext) -> Option { @@ -121,25 +86,14 @@ impl Item for SharedScreen { } fn tab_content(&self, _: Option, _: &WindowContext<'_>) -> gpui::AnyElement { - div().child("Shared screen").into_any() - // Flex::row() - // .with_child( - // Svg::new("icons/desktop.svg") - // .with_color(style.label.text.color) - // .constrained() - // .with_width(style.type_icon_width) - // .aligned() - // .contained() - // .with_margin_right(style.spacing), - // ) - // .with_child( - // Label::new( - // format!("{}'s screen", self.user.github_login), - // style.label.clone(), - // ) - // .aligned(), - // ) - // .into_any() + h_stack() + .gap_1() + .child(IconElement::new(Icon::Screen)) + .child(SharedString::from(format!( + "{}'s screen", + self.user.github_login + ))) + .into_any() } fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index fa7c4fe67df4fed4645e8c6552e242b3d7662276..c5820b539526c94879edfd2a06412c7271ab3252 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3941,7 +3941,7 @@ async fn test_collaborating_with_diagnostics( // Ensure client B observes the new diagnostics. project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), &[( ProjectPath { worktree_id, @@ -3961,14 +3961,14 @@ async fn test_collaborating_with_diagnostics( let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c_diagnostic_summaries = Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { - project.diagnostic_summaries(cx).collect::>() + project.diagnostic_summaries(false, cx).collect::>() }))); project_c.update(cx_c, |_, cx| { let summaries = project_c_diagnostic_summaries.clone(); cx.subscribe(&project_c, { move |p, _, event, cx| { if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { - *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect(); + *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect(); } } }) @@ -4018,7 +4018,7 @@ async fn test_collaborating_with_diagnostics( deterministic.run_until_parked(); project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -4034,7 +4034,7 @@ async fn test_collaborating_with_diagnostics( }); project_c.read_with(cx_c, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -4097,13 +4097,22 @@ async fn test_collaborating_with_diagnostics( ); deterministic.run_until_parked(); project_a.read_with(cx_a, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_b.read_with(cx_b, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_c.read_with(cx_c, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); } diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index e579c384e36a0e20beccbc812c7866bcbe6a6991..2268a51f2ba5f1671a02707101ecad8f65501d1c 100644 --- a/crates/collab2/src/tests/integration_tests.rs +++ b/crates/collab2/src/tests/integration_tests.rs @@ -3688,7 +3688,7 @@ async fn test_collaborating_with_diagnostics( project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), &[( ProjectPath { worktree_id, @@ -3708,14 +3708,14 @@ async fn test_collaborating_with_diagnostics( let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c_diagnostic_summaries = Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { - project.diagnostic_summaries(cx).collect::>() + project.diagnostic_summaries(false, cx).collect::>() }))); project_c.update(cx_c, |_, cx| { let summaries = project_c_diagnostic_summaries.clone(); cx.subscribe(&project_c, { move |p, _, event, cx| { if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { - *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect(); + *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect(); } } }) @@ -3766,7 +3766,7 @@ async fn test_collaborating_with_diagnostics( project_b.read_with(cx_b, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -3783,7 +3783,7 @@ async fn test_collaborating_with_diagnostics( project_c.read_with(cx_c, |project, cx| { assert_eq!( - project.diagnostic_summaries(cx).collect::>(), + project.diagnostic_summaries(false, cx).collect::>(), [( ProjectPath { worktree_id, @@ -3844,15 +3844,24 @@ async fn test_collaborating_with_diagnostics( executor.run_until_parked(); project_a.read_with(cx_a, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_b.read_with(cx_b, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); project_c.read_with(cx_c, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + assert_eq!( + project.diagnostic_summaries(false, cx).collect::>(), + [] + ) }); } diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 969869599b33f4d9e4ec19d8df14fda875bc9ddf..5f95f00d6fcd5c74d81d90f9b4b455ab531862d5 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -221,7 +221,7 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - call_factory: |_, _| Box::new(workspace::TestCallHandler), + call_factory: |_| Box::new(workspace::TestCallHandler), }); cx.update(|cx| { diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 3c3e125108bbb2dd70791926515bff68e41e2ea7..b90df68c2a8ac08ed3e7a2fdf0ff31fe0f920998 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -17,7 +17,7 @@ mod contact_finder; // Client, Contact, User, UserStore, // }; use contact_finder::ContactFinder; -use menu::Confirm; +use menu::{Cancel, Confirm, SelectNext, SelectPrev}; use rpc::proto; use theme::{ActiveTheme, ThemeSettings}; // use context_menu::{ContextMenu, ContextMenuItem}; @@ -170,10 +170,10 @@ 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, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, - FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Pixels, - Point, PromptLevel, Render, RenderOnce, SharedString, Stateful, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, + 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, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -302,7 +302,7 @@ pub struct CollabPanel { client: Arc, project: Model, match_candidates: Vec, - // list_state: ListState, + scroll_handle: ScrollHandle, subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, @@ -384,10 +384,6 @@ enum ListEntry { ContactPlaceholder, } -// impl Entity for CollabPanel { -// type Event = Event; -// } - impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { cx.build_view(|cx| { @@ -399,28 +395,28 @@ impl CollabPanel { editor }); - // cx.subscribe(&filter_editor, |this, _, event, cx| { - // if let editor::Event::BufferEdited = event { - // let query = this.filter_editor.read(cx).text(cx); - // if !query.is_empty() { - // this.selection.take(); - // } - // this.update_entries(true, cx); - // if !query.is_empty() { - // this.selection = this - // .entries - // .iter() - // .position(|entry| !matches!(entry, ListEntry::Header(_))); - // } - // } else if let editor::Event::Blurred = event { - // let query = this.filter_editor.read(cx).text(cx); - // if query.is_empty() { - // this.selection.take(); - // this.update_entries(true, cx); - // } - // } - // }) - // .detach(); + cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { + if let editor::EditorEvent::BufferEdited = event { + let query = this.filter_editor.read(cx).text(cx); + if !query.is_empty() { + this.selection.take(); + } + this.update_entries(true, cx); + if !query.is_empty() { + this.selection = this + .entries + .iter() + .position(|entry| !matches!(entry, ListEntry::Header(_))); + } + } else if let editor::EditorEvent::Blurred = event { + let query = this.filter_editor.read(cx).text(cx); + if query.is_empty() { + this.selection.take(); + this.update_entries(true, cx); + } + } + }) + .detach(); let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx)); @@ -586,13 +582,13 @@ impl CollabPanel { project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), + scroll_handle: ScrollHandle::new(), collapsed_sections: vec![Section::Offline], collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), // context_menu_on_selected: true, drag_target_channel: ChannelDragTarget::None, - // list_state, }; this.update_entries(false, cx); @@ -708,9 +704,9 @@ impl CollabPanel { let query = self.filter_editor.read(cx).text(cx); let executor = cx.background_executor().clone(); - // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - let _old_entries = mem::take(&mut self.entries); - // let mut scroll_to_top = false; + 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)); @@ -1077,71 +1073,62 @@ impl CollabPanel { self.entries.push(ListEntry::ContactPlaceholder); } - // if select_same_item { - // if let Some(prev_selected_entry) = prev_selected_entry { - // self.selection.take(); - // for (ix, entry) in self.entries.iter().enumerate() { - // if *entry == prev_selected_entry { - // self.selection = Some(ix); - // break; - // } - // } - // } - // } else { - // self.selection = self.selection.and_then(|prev_selection| { - // if self.entries.is_empty() { - // None - // } else { - // Some(prev_selection.min(self.entries.len() - 1)) - // } - // }); - // } - - // let old_scroll_top = self.list_state.logical_scroll_top(); - - // self.list_state.reset(self.entries.len()); + if select_same_item { + if let Some(prev_selected_entry) = prev_selected_entry { + self.selection.take(); + for (ix, entry) in self.entries.iter().enumerate() { + if *entry == prev_selected_entry { + self.selection = Some(ix); + self.scroll_handle.scroll_to_item(ix); + break; + } + } + } + } else { + self.selection = self.selection.and_then(|prev_selection| { + if self.entries.is_empty() { + None + } else { + let ix = prev_selection.min(self.entries.len() - 1); + self.scroll_handle.scroll_to_item(ix); + Some(ix) + } + }); + } - // if scroll_to_top { - // self.list_state.scroll_to(ListOffset::default()); - // } else { - // // Attempt to maintain the same scroll position. - // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { - // let new_scroll_top = self - // .entries - // .iter() - // .position(|entry| entry == old_top_entry) - // .map(|item_ix| ListOffset { - // item_ix, - // offset_in_item: old_scroll_top.offset_in_item, - // }) - // .or_else(|| { - // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; - // let item_ix = self - // .entries - // .iter() - // .position(|entry| entry == entry_after_old_top)?; - // Some(ListOffset { - // item_ix, - // offset_in_item: 0., - // }) - // }) - // .or_else(|| { - // let entry_before_old_top = - // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; - // let item_ix = self - // .entries - // .iter() - // .position(|entry| entry == entry_before_old_top)?; - // Some(ListOffset { - // item_ix, - // offset_in_item: 0., - // }) - // }); + if scroll_to_top { + self.scroll_handle.scroll_to_item(0) + } else { + let (old_index, old_offset) = self.scroll_handle.logical_scroll_top(); + // Attempt to maintain the same scroll position. + if let Some(old_top_entry) = old_entries.get(old_index) { + let (new_index, new_offset) = self + .entries + .iter() + .position(|entry| entry == old_top_entry) + .map(|item_ix| (item_ix, old_offset)) + .or_else(|| { + let entry_after_old_top = old_entries.get(old_index + 1)?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_after_old_top)?; + Some((item_ix, px(0.))) + }) + .or_else(|| { + let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?; + let item_ix = self + .entries + .iter() + .position(|entry| entry == entry_before_old_top)?; + Some((item_ix, px(0.))) + }) + .unwrap_or_else(|| (old_index, old_offset)); - // self.list_state - // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); - // } - // } + self.scroll_handle + .set_logical_scroll_top(new_index, new_offset); + } + } cx.notify(); } @@ -1685,8 +1672,6 @@ impl CollabPanel { ix: usize, cx: &mut ViewContext, ) { - // self.context_menu_on_selected = position.is_none(); - let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { self.channel_store .read(cx) @@ -1777,138 +1762,131 @@ impl CollabPanel { }); cx.focus_view(&context_menu); - let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { - this.context_menu.take(); - cx.notify(); - }); + let subscription = + cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + if this.context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(cx) + }) { + cx.focus_self(); + } + this.context_menu.take(); + cx.notify(); + }); self.context_menu = Some((context_menu, position, subscription)); cx.notify(); } - // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - // if self.take_editing_state(cx) { - // cx.focus(&self.filter_editor); - // } else { - // self.filter_editor.update(cx, |editor, cx| { - // if editor.buffer().read(cx).len(cx) > 0 { - // editor.set_text("", cx); - // } - // }); - // } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_editing_state(cx) { + cx.focus_view(&self.filter_editor); + } else { + self.filter_editor.update(cx, |editor, cx| { + if editor.buffer().read(cx).len(cx) > 0 { + editor.set_text("", cx); + } + }); + } - // self.update_entries(false, cx); - // } + self.update_entries(false, cx); + } - // fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - // let ix = self.selection.map_or(0, |ix| ix + 1); - // if ix < self.entries.len() { - // self.selection = Some(ix); - // } + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + let ix = self.selection.map_or(0, |ix| ix + 1); + if ix < self.entries.len() { + self.selection = Some(ix); + } - // self.list_state.reset(self.entries.len()); - // if let Some(ix) = self.selection { - // self.list_state.scroll_to(ListOffset { - // item_ix: ix, - // offset_in_item: 0., - // }); - // } - // cx.notify(); - // } + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } - // fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - // let ix = self.selection.take().unwrap_or(0); - // if ix > 0 { - // self.selection = Some(ix - 1); - // } + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + let ix = self.selection.take().unwrap_or(0); + if ix > 0 { + self.selection = Some(ix - 1); + } - // self.list_state.reset(self.entries.len()); - // if let Some(ix) = self.selection { - // self.list_state.scroll_to(ListOffset { - // item_ix: ix, - // offset_in_item: 0., - // }); - // } - // cx.notify(); - // } + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if self.confirm_channel_edit(cx) { return; } - // if let Some(selection) = self.selection { - // if let Some(entry) = self.entries.get(selection) { - // match entry { - // ListEntry::Header(section) => match section { - // Section::ActiveCall => Self::leave_call(cx), - // Section::Channels => self.new_root_channel(cx), - // Section::Contacts => self.toggle_contact_finder(cx), - // Section::ContactRequests - // | Section::Online - // | Section::Offline - // | Section::ChannelInvites => { - // self.toggle_section_expanded(*section, cx); - // } - // }, - // ListEntry::Contact { contact, calling } => { - // if contact.online && !contact.busy && !calling { - // self.call(contact.user.id, Some(self.project.clone()), cx); - // } - // } - // ListEntry::ParticipantProject { - // project_id, - // host_user_id, - // .. - // } => { - // if let Some(workspace) = self.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); - // } - // } - // ListEntry::ParticipantScreen { peer_id, .. } => { - // let Some(peer_id) = peer_id else { - // return; - // }; - // if let Some(workspace) = self.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.open_shared_screen(*peer_id, cx) - // }); - // } - // } - // ListEntry::Channel { channel, .. } => { - // let is_active = maybe!({ - // let call_channel = ActiveCall::global(cx) - // .read(cx) - // .room()? - // .read(cx) - // .channel_id()?; - - // Some(call_channel == channel.id) - // }) - // .unwrap_or(false); - // if is_active { - // self.open_channel_notes( - // &OpenChannelNotes { - // channel_id: channel.id, - // }, - // cx, - // ) - // } else { - // self.join_channel(channel.id, cx) - // } - // } - // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), - // _ => {} - // } - // } - // } + if let Some(selection) = self.selection { + if let Some(entry) = self.entries.get(selection) { + match entry { + ListEntry::Header(section) => match section { + Section::ActiveCall => Self::leave_call(cx), + Section::Channels => self.new_root_channel(cx), + Section::Contacts => self.toggle_contact_finder(cx), + Section::ContactRequests + | Section::Online + | Section::Offline + | Section::ChannelInvites => { + self.toggle_section_expanded(*section, cx); + } + }, + ListEntry::Contact { contact, calling } => { + if contact.online && !contact.busy && !calling { + self.call(contact.user.id, cx); + } + } + // ListEntry::ParticipantProject { + // project_id, + // host_user_id, + // .. + // } => { + // if let Some(workspace) = self.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); + // } + // } + // ListEntry::ParticipantScreen { peer_id, .. } => { + // let Some(peer_id) = peer_id else { + // return; + // }; + // if let Some(workspace) = self.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // workspace.open_shared_screen(*peer_id, cx) + // }); + // } + // } + ListEntry::Channel { channel, .. } => { + let is_active = maybe!({ + let call_channel = ActiveCall::global(cx) + .read(cx) + .room()? + .read(cx) + .channel_id()?; + + Some(call_channel == channel.id) + }) + .unwrap_or(false); + if is_active { + self.open_channel_notes(channel.id, cx) + } else { + self.join_channel(channel.id, cx) + } + } + ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), + _ => {} + } + } + } } fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { @@ -1975,33 +1953,33 @@ impl CollabPanel { self.update_entries(false, cx); } - // fn collapse_selected_channel( - // &mut self, - // _: &CollapseSelectedChannel, - // cx: &mut ViewContext, - // ) { - // let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { - // return; - // }; + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; - // if self.is_channel_collapsed(channel_id) { - // return; - // } + if self.is_channel_collapsed(channel_id) { + return; + } - // self.toggle_channel_collapsed(channel_id, cx); - // } + self.toggle_channel_collapsed(channel_id, cx); + } - // fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { - // let Some(id) = self.selected_channel().map(|channel| channel.id) else { - // return; - // }; + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; - // if !self.is_channel_collapsed(id) { - // return; - // } + if !self.is_channel_collapsed(id) { + return; + } - // self.toggle_channel_collapsed(id, cx) - // } + self.toggle_channel_collapsed(id, cx) + } // fn toggle_channel_collapsed_action( // &mut self, @@ -2030,11 +2008,11 @@ impl CollabPanel { self.collapsed_channels.binary_search(&channel_id).is_ok() } - // fn leave_call(cx: &mut ViewContext) { - // ActiveCall::global(cx) - // .update(cx, |call, cx| call.hang_up(cx)) - // .detach_and_log_err(cx); - // } + fn leave_call(cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + } fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { @@ -2154,13 +2132,20 @@ impl CollabPanel { } } - // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { - // let Some(channel) = self.selected_channel() else { - // return; - // }; + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { + let Some(channel) = self.selected_channel() else { + return; + }; + let Some(bounds) = self + .selection + .and_then(|ix| self.scroll_handle.bounds_for_item(ix)) + else { + return; + }; - // self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); - // } + self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); + cx.stop_propagation(); + } fn selected_channel(&self) -> Option<&Arc> { self.selection @@ -2350,44 +2335,67 @@ impl CollabPanel { ) } - fn render_signed_in(&mut self, cx: &mut ViewContext) -> List { - let is_selected = false; // todo!() this.selection == Some(ix); - - List::new().children( - self.entries - .clone() - .into_iter() - .enumerate() - .map(|(ix, entry)| match entry { - ListEntry::Header(section) => { - let is_collapsed = self.collapsed_sections.contains(§ion); - self.render_header(section, is_selected, is_collapsed, cx) - .into_any_element() - } - ListEntry::Contact { contact, calling } => self - .render_contact(&*contact, calling, is_selected, cx) - .into_any_element(), - ListEntry::ContactPlaceholder => self - .render_contact_placeholder(is_selected, cx) - .into_any_element(), - ListEntry::IncomingRequest(user) => self - .render_contact_request(user, true, is_selected, cx) - .into_any_element(), - ListEntry::OutgoingRequest(user) => self - .render_contact_request(user, false, is_selected, cx) - .into_any_element(), - ListEntry::Channel { - channel, - depth, - has_children, - } => self - .render_channel(&*channel, depth, has_children, is_selected, ix, cx) - .into_any_element(), - ListEntry::ChannelEditor { depth } => { - self.render_channel_editor(depth, cx).into_any_element() - } - }), - ) + fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { + v_stack() + .size_full() + .child( + div() + .p_2() + .child(div().rounded(px(2.0)).child(self.filter_editor.clone())), + ) + .child( + v_stack() + .size_full() + .id("scroll") + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .children( + self.entries + .clone() + .into_iter() + .enumerate() + .map(|(ix, entry)| { + let is_selected = self.selection == Some(ix); + match entry { + ListEntry::Header(section) => { + let is_collapsed = + self.collapsed_sections.contains(§ion); + self.render_header(section, is_selected, is_collapsed, cx) + .into_any_element() + } + ListEntry::Contact { contact, calling } => self + .render_contact(&*contact, calling, is_selected, cx) + .into_any_element(), + ListEntry::ContactPlaceholder => self + .render_contact_placeholder(is_selected, cx) + .into_any_element(), + ListEntry::IncomingRequest(user) => self + .render_contact_request(user, true, is_selected, cx) + .into_any_element(), + ListEntry::OutgoingRequest(user) => self + .render_contact_request(user, false, is_selected, cx) + .into_any_element(), + ListEntry::Channel { + channel, + depth, + has_children, + } => self + .render_channel( + &*channel, + depth, + has_children, + is_selected, + ix, + cx, + ) + .into_any_element(), + ListEntry::ChannelEditor { depth } => { + self.render_channel_editor(depth, cx).into_any_element() + } + } + }), + ), + ) } fn render_header( @@ -2494,6 +2502,7 @@ impl CollabPanel { el.child( ListItem::new(text.clone()) .child(div().w_full().child(Label::new(text))) + .selected(is_selected) .toggle(Some(!is_collapsed)) .on_click(cx.listener(move |this, _, cx| { this.toggle_section_expanded(section, cx) @@ -2502,7 +2511,7 @@ impl CollabPanel { } else { el.child( ListHeader::new(text) - .when_some(button, |el, button| el.right_button(button)) + .when_some(button, |el, button| el.meta(button)) .selected(is_selected), ) } @@ -3214,23 +3223,36 @@ impl CollabPanel { // } impl Render for CollabPanel { - type Element = Focusable>; + type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() - .id("collab-panel") + v_stack() .key_context("CollabPanel") + .on_action(cx.listener(CollabPanel::cancel)) + .on_action(cx.listener(CollabPanel::select_next)) + .on_action(cx.listener(CollabPanel::select_prev)) + .on_action(cx.listener(CollabPanel::confirm)) + .on_action(cx.listener(CollabPanel::insert_space)) + // .on_action(cx.listener(CollabPanel::remove)) + .on_action(cx.listener(CollabPanel::remove_selected_channel)) + .on_action(cx.listener(CollabPanel::show_inline_context_menu)) + // .on_action(cx.listener(CollabPanel::new_subchannel)) + // .on_action(cx.listener(CollabPanel::invite_members)) + // .on_action(cx.listener(CollabPanel::manage_members)) + .on_action(cx.listener(CollabPanel::rename_selected_channel)) + // .on_action(cx.listener(CollabPanel::rename_channel)) + // .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action)) + .on_action(cx.listener(CollabPanel::collapse_selected_channel)) + .on_action(cx.listener(CollabPanel::expand_selected_channel)) + // .on_action(cx.listener(CollabPanel::open_channel_notes)) + // .on_action(cx.listener(CollabPanel::join_channel_chat)) + // .on_action(cx.listener(CollabPanel::copy_channel_link)) .track_focus(&self.focus_handle) .size_full() - .overflow_scroll() - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::insert_space)) - .map(|el| { - if self.user_store.read(cx).current_user().is_none() { - el.child(self.render_signed_out(cx)) - } else { - el.child(self.render_signed_in(cx)) - } + .child(if self.user_store.read(cx).current_user().is_none() { + self.render_signed_out(cx) + } else { + self.render_signed_in(cx) }) .children(self.context_menu.as_ref().map(|(menu, position, _)| { overlay() @@ -3392,111 +3414,111 @@ impl Panel for CollabPanel { } impl FocusableView for CollabPanel { - fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.filter_editor.focus_handle(cx).clone() } } -// impl PartialEq for ListEntry { -// fn eq(&self, other: &Self) -> bool { -// match self { -// ListEntry::Header(section_1) => { -// if let ListEntry::Header(section_2) = other { -// return section_1 == section_2; -// } -// } -// ListEntry::CallParticipant { user: user_1, .. } => { -// if let ListEntry::CallParticipant { user: user_2, .. } = other { -// return user_1.id == user_2.id; -// } -// } -// ListEntry::ParticipantProject { -// project_id: project_id_1, -// .. -// } => { -// if let ListEntry::ParticipantProject { -// project_id: project_id_2, -// .. -// } = other -// { -// return project_id_1 == project_id_2; -// } -// } -// ListEntry::ParticipantScreen { -// peer_id: peer_id_1, .. -// } => { -// if let ListEntry::ParticipantScreen { -// peer_id: peer_id_2, .. -// } = other -// { -// return peer_id_1 == peer_id_2; -// } -// } -// ListEntry::Channel { -// channel: channel_1, .. -// } => { -// if let ListEntry::Channel { -// channel: channel_2, .. -// } = other -// { -// return channel_1.id == channel_2.id; -// } -// } -// ListEntry::ChannelNotes { channel_id } => { -// if let ListEntry::ChannelNotes { -// channel_id: other_id, -// } = other -// { -// return channel_id == other_id; -// } -// } -// ListEntry::ChannelChat { channel_id } => { -// if let ListEntry::ChannelChat { -// channel_id: other_id, -// } = other -// { -// return channel_id == other_id; -// } -// } -// ListEntry::ChannelInvite(channel_1) => { -// if let ListEntry::ChannelInvite(channel_2) = other { -// return channel_1.id == channel_2.id; -// } -// } -// ListEntry::IncomingRequest(user_1) => { -// if let ListEntry::IncomingRequest(user_2) = other { -// return user_1.id == user_2.id; -// } -// } -// ListEntry::OutgoingRequest(user_1) => { -// if let ListEntry::OutgoingRequest(user_2) = other { -// return user_1.id == user_2.id; -// } -// } -// ListEntry::Contact { -// contact: contact_1, .. -// } => { -// if let ListEntry::Contact { -// contact: contact_2, .. -// } = other -// { -// return contact_1.user.id == contact_2.user.id; -// } -// } -// ListEntry::ChannelEditor { depth } => { -// if let ListEntry::ChannelEditor { depth: other_depth } = other { -// return depth == other_depth; -// } -// } -// ListEntry::ContactPlaceholder => { -// if let ListEntry::ContactPlaceholder = other { -// return true; -// } -// } -// } -// false -// } -// } +impl PartialEq for ListEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ListEntry::Header(section_1) => { + if let ListEntry::Header(section_2) = other { + return section_1 == section_2; + } + } + // ListEntry::CallParticipant { user: user_1, .. } => { + // if let ListEntry::CallParticipant { user: user_2, .. } = other { + // return user_1.id == user_2.id; + // } + // } + // ListEntry::ParticipantProject { + // project_id: project_id_1, + // .. + // } => { + // if let ListEntry::ParticipantProject { + // project_id: project_id_2, + // .. + // } = other + // { + // return project_id_1 == project_id_2; + // } + // } + // ListEntry::ParticipantScreen { + // peer_id: peer_id_1, .. + // } => { + // if let ListEntry::ParticipantScreen { + // peer_id: peer_id_2, .. + // } = other + // { + // return peer_id_1 == peer_id_2; + // } + // } + ListEntry::Channel { + channel: channel_1, .. + } => { + if let ListEntry::Channel { + channel: channel_2, .. + } = other + { + return channel_1.id == channel_2.id; + } + } + // ListEntry::ChannelNotes { channel_id } => { + // if let ListEntry::ChannelNotes { + // channel_id: other_id, + // } = other + // { + // return channel_id == other_id; + // } + // } + // ListEntry::ChannelChat { channel_id } => { + // if let ListEntry::ChannelChat { + // channel_id: other_id, + // } = other + // { + // return channel_id == other_id; + // } + // } + // ListEntry::ChannelInvite(channel_1) => { + // if let ListEntry::ChannelInvite(channel_2) = other { + // return channel_1.id == channel_2.id; + // } + // } + ListEntry::IncomingRequest(user_1) => { + if let ListEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ListEntry::OutgoingRequest(user_1) => { + if let ListEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ListEntry::Contact { + contact: contact_1, .. + } => { + if let ListEntry::Contact { + contact: contact_2, .. + } = other + { + return contact_1.user.id == contact_2.user.id; + } + } + ListEntry::ChannelEditor { depth } => { + if let ListEntry::ChannelEditor { depth: other_depth } = other { + return depth == other_depth; + } + } + ListEntry::ContactPlaceholder => { + if let ListEntry::ContactPlaceholder = other { + return true; + } + } + } + false + } +} // fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { // Svg::new(svg_path) diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index fee04ec40c08dfbd5c72e44287695d690ebe472a..717ab2d8970bdba55d88dd51dc207b03bc146f3c 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -185,7 +185,7 @@ impl PickerDelegate for ContactFinderDelegate { div() .flex_1() .justify_between() - .children(user.avatar.clone().map(|avatar| img().data(avatar))) + .children(user.avatar.clone().map(|avatar| img(avatar))) .child(Label::new(user.github_login.clone())) .children(icon_path.map(|icon_path| svg().path(icon_path))), ) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index f9b58b1d56139326192edf7e5fa008d90351283d..04688b05492c8c298c33d32423cdb5e7ce1fe393 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -11,7 +11,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; -use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; +use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 4748f63e5d37e29174777ea326ce8158787acd09..9ff3c04de85d9afd12f2da0a2020a0f26d05fc2a 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -126,7 +126,7 @@ impl View for ProjectDiagnosticsEditor { json!({ "project": json!({ "language_servers": project.language_server_statuses().collect::>(), - "summary": project.diagnostic_summary(cx), + "summary": project.diagnostic_summary(false, cx), }), "summary": self.summary, "paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)| @@ -195,7 +195,7 @@ impl ProjectDiagnosticsEditor { }); let project = project_handle.read(cx); - let summary = project.diagnostic_summary(cx); + let summary = project.diagnostic_summary(false, cx); let mut this = Self { project: project_handle, summary, @@ -241,7 +241,7 @@ impl ProjectDiagnosticsEditor { let mut new_summaries: HashMap> = self .project .read(cx) - .diagnostic_summaries(cx) + .diagnostic_summaries(false, cx) .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { summaries.entry(server_id).or_default().insert(path); summaries @@ -320,7 +320,7 @@ impl ProjectDiagnosticsEditor { .context("rechecking diagnostics for paths")?; this.update(&mut cx, |this, cx| { - this.summary = this.project.read(cx).diagnostic_summary(cx); + this.summary = this.project.read(cx).diagnostic_summary(false, cx); cx.emit(Event::TitleChanged); })?; anyhow::Ok(()) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 8d3c2fedd6d9d65d7d28e0a88f05f1e85161d117..86d8d01db1e837ab56f8a221768b38cc3096ad6d 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -34,19 +34,19 @@ impl DiagnosticIndicator { } project::Event::DiskBasedDiagnosticsFinished { language_server_id } | project::Event::LanguageServerRemoved(language_server_id) => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); this.in_progress_checks.remove(language_server_id); cx.notify(); } project::Event::DiagnosticsUpdated { .. } => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); cx.notify(); } _ => {} }) .detach(); Self { - summary: project.read(cx).diagnostic_summary(cx), + summary: project.read(cx).diagnostic_summary(false, cx), in_progress_checks: project .read(cx) .language_servers_running_disk_based_diagnostics() diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index dc7f0a1f3f58dfe87784dcd3debbb3035471fb99..0a0f4da8932a50b1ddac155e27d3fb8ff22cd53a 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -165,7 +165,7 @@ impl ProjectDiagnosticsEditor { }); let project = project_handle.read(cx); - let summary = project.diagnostic_summary(cx); + let summary = project.diagnostic_summary(false, cx); let mut this = Self { project: project_handle, summary, @@ -252,7 +252,7 @@ impl ProjectDiagnosticsEditor { let mut new_summaries: HashMap> = self .project .read(cx) - .diagnostic_summaries(cx) + .diagnostic_summaries(false, cx) .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { summaries.entry(server_id).or_default().insert(path); summaries @@ -332,7 +332,7 @@ impl ProjectDiagnosticsEditor { .context("rechecking diagnostics for paths")?; this.update(&mut cx, |this, cx| { - this.summary = this.project.read(cx).diagnostic_summary(cx); + this.summary = this.project.read(cx).diagnostic_summary(false, cx); cx.emit(ItemEvent::UpdateTab); cx.emit(ItemEvent::UpdateBreadcrumbs); })?; diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs index ac24b7ad508613c6d41e02238d8c030e29abdb22..92b0641deae4d049fda3d968cf88b28568b50d41 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -77,13 +77,13 @@ impl DiagnosticIndicator { project::Event::DiskBasedDiagnosticsFinished { language_server_id } | project::Event::LanguageServerRemoved(language_server_id) => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); this.in_progress_checks.remove(language_server_id); cx.notify(); } project::Event::DiagnosticsUpdated { .. } => { - this.summary = project.read(cx).diagnostic_summary(cx); + this.summary = project.read(cx).diagnostic_summary(false, cx); cx.notify(); } @@ -92,7 +92,7 @@ impl DiagnosticIndicator { .detach(); Self { - summary: project.read(cx).diagnostic_summary(cx), + summary: project.read(cx).diagnostic_summary(false, cx), in_progress_checks: project .read(cx) .language_servers_running_disk_based_diagnostics() diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 408142ae07d453ba7de30604735f91295ee0217b..5aeecbae978cd27620cd0b560c8af14baf7762d4 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -162,7 +162,7 @@ impl WrapMap { { let tab_snapshot = new_snapshot.tab_snapshot.clone(); let range = TabPoint::zero()..tab_snapshot.max_point(); - let edits = new_snapshot + edits = new_snapshot .update( tab_snapshot, &[TabEdit { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 99d8a4bb93badfd5a2fe1eaa5df7561d605e444e..c7141fe5879505dd67782dabecb581113c0c52ed 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -63,6 +63,7 @@ use language::{ use lazy_static::lazy_static; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; use lsp::{DiagnosticSeverity, LanguageServerId}; +use mouse_context_menu::MouseContextMenu; use movement::TextLayoutDetails; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -407,133 +408,17 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) { init_settings(cx); - // cx.register_action_type(Editor::new_file); - // cx.register_action_type(Editor::new_file_in_direction); - // cx.register_action_type(Editor::cancel); - // cx.register_action_type(Editor::newline); - // cx.register_action_type(Editor::newline_above); - // cx.register_action_type(Editor::newline_below); - // cx.register_action_type(Editor::backspace); - // cx.register_action_type(Editor::delete); - // cx.register_action_type(Editor::tab); - // cx.register_action_type(Editor::tab_prev); - // cx.register_action_type(Editor::indent); - // cx.register_action_type(Editor::outdent); - // cx.register_action_type(Editor::delete_line); - // cx.register_action_type(Editor::join_lines); - // cx.register_action_type(Editor::sort_lines_case_sensitive); - // cx.register_action_type(Editor::sort_lines_case_insensitive); - // cx.register_action_type(Editor::reverse_lines); - // cx.register_action_type(Editor::shuffle_lines); - // cx.register_action_type(Editor::convert_to_upper_case); - // cx.register_action_type(Editor::convert_to_lower_case); - // cx.register_action_type(Editor::convert_to_title_case); - // cx.register_action_type(Editor::convert_to_snake_case); - // cx.register_action_type(Editor::convert_to_kebab_case); - // cx.register_action_type(Editor::convert_to_upper_camel_case); - // cx.register_action_type(Editor::convert_to_lower_camel_case); - // cx.register_action_type(Editor::delete_to_previous_word_start); - // cx.register_action_type(Editor::delete_to_previous_subword_start); - // cx.register_action_type(Editor::delete_to_next_word_end); - // cx.register_action_type(Editor::delete_to_next_subword_end); - // cx.register_action_type(Editor::delete_to_beginning_of_line); - // cx.register_action_type(Editor::delete_to_end_of_line); - // cx.register_action_type(Editor::cut_to_end_of_line); - // cx.register_action_type(Editor::duplicate_line); - // cx.register_action_type(Editor::move_line_up); - // cx.register_action_type(Editor::move_line_down); - // cx.register_action_type(Editor::transpose); - // cx.register_action_type(Editor::cut); - // cx.register_action_type(Editor::copy); - // cx.register_action_type(Editor::paste); - // cx.register_action_type(Editor::undo); - // cx.register_action_type(Editor::redo); - // cx.register_action_type(Editor::move_page_up); - // cx.register_action_type::(); - // cx.register_action_type(Editor::move_page_down); - // cx.register_action_type(Editor::next_screen); - // cx.register_action_type::(); - // cx.register_action_type::(); - // cx.register_action_type(Editor::move_to_previous_word_start); - // cx.register_action_type(Editor::move_to_previous_subword_start); - // cx.register_action_type(Editor::move_to_next_word_end); - // cx.register_action_type(Editor::move_to_next_subword_end); - // cx.register_action_type(Editor::move_to_beginning_of_line); - // cx.register_action_type(Editor::move_to_end_of_line); - // cx.register_action_type(Editor::move_to_start_of_paragraph); - // cx.register_action_type(Editor::move_to_end_of_paragraph); - // cx.register_action_type(Editor::move_to_beginning); - // cx.register_action_type(Editor::move_to_end); - // cx.register_action_type(Editor::select_up); - // cx.register_action_type(Editor::select_down); - // cx.register_action_type(Editor::select_left); - // cx.register_action_type(Editor::select_right); - // cx.register_action_type(Editor::select_to_previous_word_start); - // cx.register_action_type(Editor::select_to_previous_subword_start); - // cx.register_action_type(Editor::select_to_next_word_end); - // cx.register_action_type(Editor::select_to_next_subword_end); - // cx.register_action_type(Editor::select_to_beginning_of_line); - // cx.register_action_type(Editor::select_to_end_of_line); - // cx.register_action_type(Editor::select_to_start_of_paragraph); - // cx.register_action_type(Editor::select_to_end_of_paragraph); - // cx.register_action_type(Editor::select_to_beginning); - // cx.register_action_type(Editor::select_to_end); - // cx.register_action_type(Editor::select_all); - // cx.register_action_type(Editor::select_all_matches); - // cx.register_action_type(Editor::select_line); - // cx.register_action_type(Editor::split_selection_into_lines); - // cx.register_action_type(Editor::add_selection_above); - // cx.register_action_type(Editor::add_selection_below); - // cx.register_action_type(Editor::select_next); - // cx.register_action_type(Editor::select_previous); - // cx.register_action_type(Editor::toggle_comments); - // cx.register_action_type(Editor::select_larger_syntax_node); - // cx.register_action_type(Editor::select_smaller_syntax_node); - // cx.register_action_type(Editor::move_to_enclosing_bracket); - // cx.register_action_type(Editor::undo_selection); - // cx.register_action_type(Editor::redo_selection); - // cx.register_action_type(Editor::go_to_diagnostic); - // cx.register_action_type(Editor::go_to_prev_diagnostic); - // cx.register_action_type(Editor::go_to_hunk); - // cx.register_action_type(Editor::go_to_prev_hunk); - // cx.register_action_type(Editor::go_to_definition); - // cx.register_action_type(Editor::go_to_definition_split); - // cx.register_action_type(Editor::go_to_type_definition); - // cx.register_action_type(Editor::go_to_type_definition_split); - // cx.register_action_type(Editor::fold); - // cx.register_action_type(Editor::fold_at); - // cx.register_action_type(Editor::unfold_lines); - // cx.register_action_type(Editor::unfold_at); - // cx.register_action_type(Editor::gutter_hover); - // cx.register_action_type(Editor::fold_selected_ranges); - // cx.register_action_type(Editor::show_completions); - // cx.register_action_type(Editor::toggle_code_actions); - // cx.register_action_type(Editor::open_excerpts); - // cx.register_action_type(Editor::toggle_soft_wrap); - // cx.register_action_type(Editor::toggle_inlay_hints); - // cx.register_action_type(Editor::reveal_in_finder); - // cx.register_action_type(Editor::copy_path); - // cx.register_action_type(Editor::copy_relative_path); - // cx.register_action_type(Editor::copy_highlight_json); - // cx.add_async_action(Editor::format); - // cx.register_action_type(Editor::restart_language_server); - // cx.register_action_type(Editor::show_character_palette); - // cx.add_async_action(Editor::confirm_completion); - // cx.add_async_action(Editor::confirm_code_action); - // cx.add_async_action(Editor::rename); - // cx.add_async_action(Editor::confirm_rename); - // cx.add_async_action(Editor::find_all_references); - // cx.register_action_type(Editor::next_copilot_suggestion); - // cx.register_action_type(Editor::previous_copilot_suggestion); - // cx.register_action_type(Editor::copilot_suggest); - // cx.register_action_type(Editor::context_menu_first); - // cx.register_action_type(Editor::context_menu_prev); - // cx.register_action_type(Editor::context_menu_next); - // cx.register_action_type(Editor::context_menu_last); workspace::register_project_item::(cx); workspace::register_followable_item::(cx); workspace::register_deserializable_item::(cx); + cx.observe_new_views( + |workspace: &mut Workspace, cx: &mut ViewContext| { + workspace.register_action(Editor::new_file); + workspace.register_action(Editor::new_file_in_direction); + }, + ) + .detach(); } trait InvalidationRegion { @@ -621,8 +506,6 @@ pub struct Editor { ime_transaction: Option, active_diagnostics: Option, soft_wrap_mode_override: Option, - // get_field_editor_theme: Option>, - // override_text_style: Option>, project: Option>, collaboration_hub: Option>, blink_manager: Model, @@ -636,7 +519,7 @@ pub struct Editor { inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: RwLock>, - // mouse_context_menu: View, + mouse_context_menu: Option, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, available_code_actions: Option<(Model, Arc<[CodeAction]>)>, @@ -1734,21 +1617,11 @@ impl Editor { // Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) // } - // pub fn auto_height( - // max_lines: usize, - // field_editor_style: Option>, - // cx: &mut ViewContext, - // ) -> Self { - // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); - // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - // Self::new( - // EditorMode::AutoHeight { max_lines }, - // buffer, - // None, - // field_editor_style, - // cx, - // ) - // } + pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx) + } pub fn for_buffer( buffer: Model, @@ -1768,14 +1641,7 @@ impl Editor { } pub fn clone(&self, cx: &mut ViewContext) -> Self { - let mut clone = Self::new( - self.mode, - self.buffer.clone(), - self.project.clone(), - // todo! - // self.get_field_editor_theme.clone(), - cx, - ); + let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.clone(), cx); self.display_map.update(cx, |display_map, cx| { let snapshot = display_map.snapshot(cx); clone.display_map.update(cx, |display_map, cx| { @@ -1792,17 +1658,11 @@ impl Editor { mode: EditorMode, buffer: Model, project: Option>, - // todo!() - // get_field_editor_theme: Option>, cx: &mut ViewContext, ) -> Self { - // let editor_view_id = cx.view_id(); let style = cx.text_style(); let font_size = style.font_size.to_pixels(cx.rem_size()); let display_map = cx.build_model(|cx| { - // todo!() - // let settings = settings::get::(cx); - // let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx); DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx) }); @@ -1858,7 +1718,6 @@ impl Editor { ime_transaction: Default::default(), active_diagnostics: None, soft_wrap_mode_override, - // get_field_editor_theme, collaboration_hub: project.clone().map(|project| Box::new(project) as _), project, blink_manager: blink_manager.clone(), @@ -1872,8 +1731,7 @@ impl Editor { inlay_background_highlights: Default::default(), nav_history: None, context_menu: RwLock::new(None), - // mouse_context_menu: cx - // .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)), + mouse_context_menu: None, completion_tasks: Default::default(), next_completion_id: 0, next_inlay_id: 0, @@ -1882,7 +1740,6 @@ impl Editor { document_highlights_task: Default::default(), pending_rename: Default::default(), searchable: true, - // override_text_style: None, cursor_shape: Default::default(), autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, @@ -2000,25 +1857,25 @@ impl Editor { } } - // pub fn new_file_in_direction( - // workspace: &mut Workspace, - // action: &workspace::NewFileInDirection, - // cx: &mut ViewContext, - // ) { - // let project = workspace.project().clone(); - // if project.read(cx).is_remote() { - // cx.propagate(); - // } else if let Some(buffer) = project - // .update(cx, |project, cx| project.create_buffer("", None, cx)) - // .log_err() - // { - // workspace.split_item( - // action.0, - // Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), - // cx, - // ); - // } - // } + pub fn new_file_in_direction( + workspace: &mut Workspace, + action: &workspace::NewFileInDirection, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.split_item( + action.0, + Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { self.buffer.read(cx).replica_id() @@ -8374,6 +8231,18 @@ impl Editor { cx.notify(); } + pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext) { + let rem_size = cx.rem_size(); + self.display_map.update(cx, |map, cx| { + map.set_font( + style.text.font(), + style.text.font_size.to_pixels(rem_size), + cx, + ) + }); + self.style = Some(style); + } + pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { self.display_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) @@ -8796,62 +8665,56 @@ impl Editor { // self.searchable // } - // fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext) { - // let active_item = workspace.active_item(cx); - // let editor_handle = if let Some(editor) = active_item - // .as_ref() - // .and_then(|item| item.act_as::(cx)) - // { - // editor - // } else { - // cx.propagate(); - // return; - // }; - - // let editor = editor_handle.read(cx); - // let buffer = editor.buffer.read(cx); - // if buffer.is_singleton() { - // cx.propagate(); - // return; - // } + fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx); + if buffer.is_singleton() { + cx.propagate(); + return; + } - // let mut new_selections_by_buffer = HashMap::default(); - // for selection in editor.selections.all::(cx) { - // for (buffer, mut range, _) in - // buffer.range_to_buffer_ranges(selection.start..selection.end, cx) - // { - // if selection.reversed { - // mem::swap(&mut range.start, &mut range.end); - // } - // new_selections_by_buffer - // .entry(buffer) - // .or_insert(Vec::new()) - // .push(range) - // } - // } + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; - // editor_handle.update(cx, |editor, cx| { - // editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx); - // }); - // let pane = workspace.active_pane().clone(); - // pane.update(cx, |pane, _| pane.disable_history()); - - // // We defer the pane interaction because we ourselves are a workspace item - // // and activating a new item causes the pane to call a method on us reentrantly, - // // which panics if we're on the stack. - // cx.defer(move |workspace, cx| { - // for (buffer, ranges) in new_selections_by_buffer.into_iter() { - // let editor = workspace.open_project_item::(buffer, cx); - // editor.update(cx, |editor, cx| { - // editor.change_selections(Some(Autoscroll::newest()), cx, |s| { - // s.select_ranges(ranges); - // }); - // }); - // } - - // pane.update(cx, |pane, _| pane.enable_history()); - // }); - // } + let mut new_selections_by_buffer = HashMap::default(); + for selection in self.selections.all::(cx) { + for (buffer, mut range, _) in + buffer.range_to_buffer_ranges(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } + } + + self.push_to_nav_history(self.selections.newest_anchor().head(), None, cx); + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, _| pane.disable_history()); + + for (buffer, ranges) in new_selections_by_buffer.into_iter() { + let editor = workspace.open_project_item::(buffer, cx); + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select_ranges(ranges); + }); + }); + } + + pane.update(cx, |pane, _| pane.enable_history()); + }) + }); + } fn jump( &mut self, @@ -9397,7 +9260,7 @@ impl Render for Editor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let settings = ThemeSettings::get_global(cx); let text_style = match self.mode { - EditorMode::SingleLine => TextStyle { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().text, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features, @@ -9410,8 +9273,6 @@ impl Render for Editor { white_space: WhiteSpace::Normal, }, - EditorMode::AutoHeight { max_lines } => todo!(), - EditorMode::Full => TextStyle { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), @@ -9446,106 +9307,6 @@ impl Render for Editor { } } -// impl View for Editor { -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let style = self.style(cx); -// let font_changed = self.display_map.update(cx, |map, cx| { -// map.set_fold_ellipses_color(style.folds.ellipses.text_color); -// map.set_font_with_size(style.text.font_id, style.text.font_size, cx) -// }); - -// if font_changed { -// cx.defer(move |editor, cx: &mut ViewContext| { -// hide_hover(editor, cx); -// hide_link_definition(editor, cx); -// }); -// } - -// Stack::new() -// .with_child(EditorElement::new(style.clone())) -// .with_child(ChildView::new(&self.mouse_context_menu, cx)) -// .into_any() -// } - -// fn ui_name() -> &'static str { -// "Editor" -// } - -// fn focus_in(&mut self, focused: AnyView, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// let focused_event = EditorFocused(cx.handle()); -// cx.emit(Event::Focused); -// cx.emit_global(focused_event); -// } -// if let Some(rename) = self.pending_rename.as_ref() { -// cx.focus(&rename.editor); -// } else if cx.is_self_focused() || !focused.is::() { -// if !self.focused { -// self.blink_manager.update(cx, BlinkManager::enable); -// } -// self.focused = true; -// self.buffer.update(cx, |buffer, cx| { -// buffer.finalize_last_transaction(cx); -// if self.leader_peer_id.is_none() { -// buffer.set_active_selections( -// &self.selections.disjoint_anchors(), -// self.selections.line_mode, -// self.cursor_shape, -// cx, -// ); -// } -// }); -// } -// } - -// fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext) { -// let blurred_event = EditorBlurred(cx.handle()); -// cx.emit_global(blurred_event); -// self.focused = false; -// self.blink_manager.update(cx, BlinkManager::disable); -// self.buffer -// .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); -// self.hide_context_menu(cx); -// hide_hover(self, cx); -// cx.emit(Event::Blurred); -// cx.notify(); -// } - -// fn modifiers_changed( -// &mut self, -// event: &gpui::platform::ModifiersChangedEvent, -// cx: &mut ViewContext, -// ) -> bool { -// let pending_selection = self.has_pending_selection(); - -// if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { -// if event.cmd && !pending_selection { -// let point = point.clone(); -// let snapshot = self.snapshot(cx); -// let kind = point.definition_kind(event.shift); - -// show_link_definition(kind, self, point, snapshot, cx); -// return false; -// } -// } - -// { -// if self.link_go_to_definition_state.symbol_range.is_some() -// || !self.link_go_to_definition_state.definitions.is_empty() -// { -// self.link_go_to_definition_state.symbol_range.take(); -// self.link_go_to_definition_state.definitions.clear(); -// cx.notify(); -// } - -// self.link_go_to_definition_state.task = None; - -// self.clear_highlights::(cx); -// } - -// false -// } - impl InputHandler for Editor { fn text_for_range( &mut self, @@ -9792,72 +9553,6 @@ impl InputHandler for Editor { } } -// fn build_style( -// settings: &ThemeSettings, -// get_field_editor_theme: Option<&GetFieldEditorTheme>, -// override_text_style: Option<&OverrideTextStyle>, -// cx: &mut AppContext, -// ) -> EditorStyle { -// let font_cache = cx.font_cache(); -// let line_height_scalar = settings.line_height(); -// let theme_id = settings.theme.meta.id; -// let mut theme = settings.theme.editor.clone(); -// let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme { -// let field_editor_theme = get_field_editor_theme(&settings.theme); -// theme.text_color = field_editor_theme.text.color; -// theme.selection = field_editor_theme.selection; -// theme.background = field_editor_theme -// .container -// .background_color -// .unwrap_or_default(); -// EditorStyle { -// text: field_editor_theme.text, -// placeholder_text: field_editor_theme.placeholder_text, -// line_height_scalar, -// theme, -// theme_id, -// } -// } else { -// todo!(); -// // let font_family_id = settings.buffer_font_family; -// // let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); -// // let font_properties = Default::default(); -// // let font_id = font_cache -// // .select_font(font_family_id, &font_properties) -// // .unwrap(); -// // let font_size = settings.buffer_font_size(cx); -// // EditorStyle { -// // text: TextStyle { -// // color: settings.theme.editor.text_color, -// // font_family_name, -// // font_family_id, -// // font_id, -// // font_size, -// // font_properties, -// // underline: Default::default(), -// // soft_wrap: false, -// // }, -// // placeholder_text: None, -// // line_height_scalar, -// // theme, -// // theme_id, -// // } -// }; - -// if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) { -// if let Some(highlighted) = style -// .text -// .clone() -// .highlight(highlight_style, font_cache) -// .log_err() -// { -// style.text = highlighted; -// } -// } - -// style -// } - trait SelectionExt { fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 24402c7e379e112bf941bd85a51e249d95d257e9..3abe5a37f97b9ce2d23f0c0c1d215e05883588b9 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -9,9 +9,11 @@ use crate::{ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ - go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - update_inlay_link_and_hover_points, GoToDefinitionTrigger, + go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition, + update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, + LinkGoToDefinitionState, }, + mouse_context_menu, scroll::scroll_amount::ScrollAmount, CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, OpenExcerpts, PageDown, PageUp, Point, @@ -19,14 +21,15 @@ use crate::{ }; use anyhow::Result; use collections::{BTreeMap, HashMap}; +use git::diff::DiffHunkStatus; use gpui::{ - div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement, - IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size, - StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, - ViewContext, WeakView, WindowContext, WrappedLine, + div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement, + AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle, + DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla, + InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, + ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, + Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -139,8 +142,6 @@ impl EditorElement { register_action(view, cx, Editor::move_right); register_action(view, cx, Editor::move_down); register_action(view, cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() register_action(view, cx, Editor::cancel); register_action(view, cx, Editor::newline); register_action(view, cx, Editor::newline_above); @@ -263,7 +264,7 @@ impl EditorElement { register_action(view, cx, Editor::fold_selected_ranges); register_action(view, cx, Editor::show_completions); register_action(view, cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() + register_action(view, cx, Editor::open_excerpts); register_action(view, cx, Editor::toggle_soft_wrap); register_action(view, cx, Editor::toggle_inlay_hints); register_action(view, cx, hover_popover::hover); @@ -312,7 +313,57 @@ impl EditorElement { register_action(view, cx, Editor::context_menu_last); } - fn mouse_down( + fn register_key_listeners(&self, cx: &mut WindowContext) { + cx.on_key_event({ + let editor = self.editor.clone(); + move |event: &ModifiersChangedEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) { + cx.stop_propagation(); + } + } + }); + } + + fn modifiers_changed( + editor: &mut Editor, + event: &ModifiersChangedEvent, + cx: &mut ViewContext, + ) -> bool { + let pending_selection = editor.has_pending_selection(); + + if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point { + if event.command && !pending_selection { + let point = point.clone(); + let snapshot = editor.snapshot(cx); + let kind = point.definition_kind(event.shift); + + show_link_definition(kind, editor, point, snapshot, cx); + return false; + } + } + + { + if editor.link_go_to_definition_state.symbol_range.is_some() + || !editor.link_go_to_definition_state.definitions.is_empty() + { + editor.link_go_to_definition_state.symbol_range.take(); + editor.link_go_to_definition_state.definitions.clear(); + cx.notify(); + } + + editor.link_go_to_definition_state.task = None; + + editor.clear_highlights::(cx); + } + + false + } + + fn mouse_left_down( editor: &mut Editor, event: &MouseDownEvent, position_map: &PositionMap, @@ -365,25 +416,25 @@ impl EditorElement { true } - // fn mouse_right_down( - // editor: &mut Editor, - // position: gpui::Point, - // position_map: &PositionMap, - // text_bounds: Bounds, - // cx: &mut EventContext, - // ) -> bool { - // if !text_bounds.contains_point(position) { - // return false; - // } - // let point_for_position = position_map.point_for_position(text_bounds, position); - // mouse_context_menu::deploy_context_menu( - // editor, - // position, - // point_for_position.previous_valid, - // cx, - // ); - // true - // } + fn mouse_right_down( + editor: &mut Editor, + event: &MouseDownEvent, + position_map: &PositionMap, + text_bounds: Bounds, + cx: &mut ViewContext, + ) -> bool { + if !text_bounds.contains_point(&event.position) { + return false; + } + let point_for_position = position_map.point_for_position(text_bounds, event.position); + mouse_context_menu::deploy_context_menu( + editor, + event.position, + point_for_position.previous_valid, + cx, + ); + true + } fn mouse_up( editor: &mut Editor, @@ -725,87 +776,85 @@ impl EditorElement { } fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut WindowContext) { - // todo!() - // let diff_style = &theme::current(cx).editor.diff.clone(); - // let line_height = layout.position_map.line_height; - - // let scroll_position = layout.position_map.snapshot.scroll_position(); - // let scroll_top = scroll_position.y * line_height; - - // for hunk in &layout.display_hunks { - // let (display_row_range, status) = match hunk { - // //TODO: This rendering is entirely a horrible hack - // &DisplayDiffHunk::Folded { display_row: row } => { - // let start_y = row as f32 * line_height - scroll_top; - // let end_y = start_y + line_height; - - // let width = diff_style.removed_width_em * line_height; - // let highlight_origin = bounds.origin + point(-width, start_y); - // let highlight_size = point(width * 2., end_y - start_y); - // let highlight_bounds = Bounds::::new(highlight_origin, highlight_size); - - // cx.paint_quad(Quad { - // bounds: highlight_bounds, - // background: Some(diff_style.modified), - // border: Border::new(0., Color::transparent_black()).into(), - // corner_radii: (1. * line_height).into(), - // }); - - // continue; - // } - - // DisplayDiffHunk::Unfolded { - // display_row_range, - // status, - // } => (display_row_range, status), - // }; - - // let color = match status { - // DiffHunkStatus::Added => diff_style.inserted, - // DiffHunkStatus::Modified => diff_style.modified, - - // //TODO: This rendering is entirely a horrible hack - // DiffHunkStatus::Removed => { - // let row = display_row_range.start; - - // let offset = line_height / 2.; - // let start_y = row as f32 * line_height - offset - scroll_top; - // let end_y = start_y + line_height; - - // let width = diff_style.removed_width_em * line_height; - // let highlight_origin = bounds.origin + point(-width, start_y); - // let highlight_size = point(width * 2., end_y - start_y); - // let highlight_bounds = Bounds::::new(highlight_origin, highlight_size); - - // cx.paint_quad(Quad { - // bounds: highlight_bounds, - // background: Some(diff_style.deleted), - // border: Border::new(0., Color::transparent_black()).into(), - // corner_radii: (1. * line_height).into(), - // }); - - // continue; - // } - // }; - - // let start_row = display_row_range.start; - // let end_row = display_row_range.end; - - // let start_y = start_row as f32 * line_height - scroll_top; - // let end_y = end_row as f32 * line_height - scroll_top; - - // let width = diff_style.width_em * line_height; - // let highlight_origin = bounds.origin + point(-width, start_y); - // let highlight_size = point(width * 2., end_y - start_y); - // let highlight_bounds = Bounds::::new(highlight_origin, highlight_size); - - // cx.paint_quad(Quad { - // bounds: highlight_bounds, - // background: Some(color), - // border: Border::new(0., Color::transparent_black()).into(), - // corner_radii: (diff_style.corner_radius * line_height).into(), - // }); - // } + let line_height = layout.position_map.line_height; + + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_top = scroll_position.y * line_height; + + for hunk in &layout.display_hunks { + let (display_row_range, status) = match hunk { + //TODO: This rendering is entirely a horrible hack + &DisplayDiffHunk::Folded { display_row: row } => { + let start_y = row as f32 * line_height - scroll_top; + let end_y = start_y + line_height; + + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad( + highlight_bounds, + Corners::all(1. * line_height), + gpui::yellow(), // todo!("use the right color") + Edges::default(), + transparent_black(), + ); + + continue; + } + + DisplayDiffHunk::Unfolded { + display_row_range, + status, + } => (display_row_range, status), + }; + + let color = match status { + DiffHunkStatus::Added => gpui::green(), // todo!("use the appropriate color") + DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the appropriate color") + + //TODO: This rendering is entirely a horrible hack + DiffHunkStatus::Removed => { + let row = display_row_range.start; + + let offset = line_height / 2.; + let start_y = row as f32 * line_height - offset - scroll_top; + let end_y = start_y + line_height; + + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad( + highlight_bounds, + Corners::all(1. * line_height), + gpui::red(), // todo!("use the right color") + Edges::default(), + transparent_black(), + ); + + continue; + } + }; + + let start_row = display_row_range.start; + let end_row = display_row_range.end; + + let start_y = start_row as f32 * line_height - scroll_top; + let end_y = end_row as f32 * line_height - scroll_top; + + let width = 0.275 * line_height; + let highlight_origin = bounds.origin + point(-width, start_y); + let highlight_size = size(width * 2., end_y - start_y); + let highlight_bounds = Bounds::new(highlight_origin, highlight_size); + cx.paint_quad( + highlight_bounds, + Corners::all(0.05 * line_height), + color, // todo!("use the right color") + Edges::default(), + transparent_black(), + ); + } } fn paint_text( @@ -831,15 +880,19 @@ impl EditorElement { bounds: text_bounds, }), |cx| { - // todo!("cursor region") - // cx.scene().push_cursor_region(CursorRegion { - // bounds, - // style: if !editor.link_go_to_definition_state.definitions.is_empty { - // CursorStyle::PointingHand - // } else { - // CursorStyle::IBeam - // }, - // }); + if text_bounds.contains_point(&cx.mouse_position()) { + if self + .editor + .read(cx) + .link_go_to_definition_state + .definitions + .is_empty() + { + cx.set_cursor_style(CursorStyle::IBeam); + } else { + cx.set_cursor_style(CursorStyle::PointingHand); + } + } let fold_corner_radius = 0.15 * layout.position_map.line_height; cx.with_element_id(Some("folds"), |cx| { @@ -1138,6 +1191,22 @@ impl EditorElement { } } } + + if let Some(mouse_context_menu) = + self.editor.read(cx).mouse_context_menu.as_ref() + { + let element = overlay() + .position(mouse_context_menu.position) + .child(mouse_context_menu.context_menu.clone()) + .anchor(AnchorCorner::TopLeft) + .snap_to_window(); + element.draw( + gpui::Point::default(), + size(AvailableSpace::MinContent, AvailableSpace::MinContent), + cx, + |_, _| {}, + ); + } }) }, ) @@ -1662,11 +1731,6 @@ impl EditorElement { cx: &mut WindowContext, ) -> LayoutState { self.editor.update(cx, |editor, cx| { - // let mut size = constraint.max; - // if size.x.is_infinite() { - // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); - // } - let snapshot = editor.snapshot(cx); let style = self.style.clone(); @@ -1702,6 +1766,7 @@ impl EditorElement { }; editor.gutter_width = gutter_width; + let text_width = bounds.size.width - gutter_width; let overscroll = size(em_width, px(0.)); let snapshot = { @@ -1728,25 +1793,6 @@ impl EditorElement { .collect::>(); let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; - // todo!("this should happen during layout") - let editor_mode = snapshot.mode; - if let EditorMode::AutoHeight { max_lines } = editor_mode { - todo!() - // size.set_y( - // scroll_height - // .min(constraint.max_along(Axis::Vertical)) - // .max(constraint.min_along(Axis::Vertical)) - // .max(line_height) - // .min(line_height * max_lines as f32), - // ) - } else if let EditorMode::SingleLine = editor_mode { - bounds.size.height = line_height.min(bounds.size.height); - } - // todo!() - // else if size.y.is_infinite() { - // // size.set_y(scroll_height); - // } - // let gutter_size = size(gutter_width, bounds.size.height); let text_size = size(text_width, bounds.size.height); @@ -2064,7 +2110,7 @@ impl EditorElement { .unwrap(); LayoutState { - mode: editor_mode, + mode: snapshot.mode, position_map: Arc::new(PositionMap { size: bounds.size, scroll_position: point( @@ -2308,10 +2354,10 @@ impl EditorElement { return; } - let should_cancel = editor.update(cx, |editor, cx| { + let handled = editor.update(cx, |editor, cx| { Self::scroll(editor, event, &position_map, &interactive_bounds, cx) }); - if should_cancel { + if handled { cx.stop_propagation(); } } @@ -2327,19 +2373,25 @@ impl EditorElement { return; } - let should_cancel = editor.update(cx, |editor, cx| { - Self::mouse_down( - editor, - event, - &position_map, - text_bounds, - gutter_bounds, - &stacking_order, - cx, - ) - }); + let handled = match event.button { + MouseButton::Left => editor.update(cx, |editor, cx| { + Self::mouse_left_down( + editor, + event, + &position_map, + text_bounds, + gutter_bounds, + &stacking_order, + cx, + ) + }), + MouseButton::Right => editor.update(cx, |editor, cx| { + Self::mouse_right_down(editor, event, &position_map, text_bounds, cx) + }), + _ => false, + }; - if should_cancel { + if handled { cx.stop_propagation() } } @@ -2351,7 +2403,7 @@ impl EditorElement { let stacking_order = cx.stacking_order().clone(); move |event: &MouseUpEvent, phase, cx| { - let should_cancel = editor.update(cx, |editor, cx| { + let handled = editor.update(cx, |editor, cx| { Self::mouse_up( editor, event, @@ -2362,26 +2414,11 @@ impl EditorElement { ) }); - if should_cancel { + if handled { cx.stop_propagation() } } }); - //todo!() - // on_down(MouseButton::Right, { - // let position_map = layout.position_map.clone(); - // move |event, editor, cx| { - // if !Self::mouse_right_down( - // editor, - // event.position, - // position_map.as_ref(), - // text_bounds, - // cx, - // ) { - // cx.propagate_event(); - // } - // } - // }); cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); @@ -2617,19 +2654,44 @@ impl Element for EditorElement { cx: &mut gpui::WindowContext, ) -> (gpui::LayoutId, Self::State) { self.editor.update(cx, |editor, cx| { - editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. + editor.set_style(self.style.clone(), cx); - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = match editor.mode { + let layout_id = match editor.mode { EditorMode::SingleLine => { - self.style.text.line_height_in_pixels(cx.rem_size()).into() + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); + cx.request_layout(&style, None) + } + EditorMode::AutoHeight { max_lines } => { + let editor_handle = cx.view().clone(); + let max_line_number_width = + self.max_line_number_width(&editor.snapshot(cx), cx); + cx.request_measured_layout( + Style::default(), + move |known_dimensions, available_space, cx| { + editor_handle + .update(cx, |editor, cx| { + compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + ) + }) + .unwrap_or_default() + }, + ) + } + EditorMode::Full => { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + cx.request_layout(&style, None) } - EditorMode::AutoHeight { .. } => todo!(), - EditorMode::Full => relative(1.).into(), }; - let layout_id = cx.request_layout(&style, None); (layout_id, ()) }) @@ -2657,6 +2719,7 @@ impl Element for EditorElement { let dispatch_context = self.editor.read(cx).dispatch_context(cx); cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| { self.register_actions(cx); + self.register_key_listeners(cx); // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { @@ -2698,604 +2761,6 @@ impl IntoElement for EditorElement { } } -// impl EditorElement { -// type LayoutState = LayoutState; -// type PaintState = (); - -// fn layout( -// &mut self, -// constraint: SizeConstraint, -// editor: &mut Editor, -// cx: &mut ViewContext, -// ) -> (gpui::Point, Self::LayoutState) { -// let mut size = constraint.max; -// if size.x.is_infinite() { -// unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); -// } - -// let snapshot = editor.snapshot(cx); -// let style = self.style.clone(); - -// let line_height = (style.text.font_size * style.line_height_scalar).round(); - -// let gutter_padding; -// let gutter_width; -// let gutter_margin; -// if snapshot.show_gutter { -// let em_width = style.text.em_width(cx.font_cache()); -// gutter_padding = (em_width * style.gutter_padding_factor).round(); -// gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; -// gutter_margin = -style.text.descent(cx.font_cache()); -// } else { -// gutter_padding = 0.0; -// gutter_width = 0.0; -// gutter_margin = 0.0; -// }; - -// let text_width = size.x - gutter_width; -// let em_width = style.text.em_width(cx.font_cache()); -// let em_advance = style.text.em_advance(cx.font_cache()); -// let overscroll = point(em_width, 0.); -// let snapshot = { -// editor.set_visible_line_count(size.y / line_height, cx); - -// let editor_width = text_width - gutter_margin - overscroll.x - em_width; -// let wrap_width = match editor.soft_wrap_mode(cx) { -// SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, -// SoftWrap::EditorWidth => editor_width, -// SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), -// }; - -// if editor.set_wrap_width(Some(wrap_width), cx) { -// editor.snapshot(cx) -// } else { -// snapshot -// } -// }; - -// let wrap_guides = editor -// .wrap_guides(cx) -// .iter() -// .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) -// .collect(); - -// let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height; -// if let EditorMode::AutoHeight { max_lines } = snapshot.mode { -// size.set_y( -// scroll_height -// .min(constraint.max_along(Axis::Vertical)) -// .max(constraint.min_along(Axis::Vertical)) -// .max(line_height) -// .min(line_height * max_lines as f32), -// ) -// } else if let EditorMode::SingleLine = snapshot.mode { -// size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) -// } else if size.y.is_infinite() { -// size.set_y(scroll_height); -// } -// let gutter_size = point(gutter_width, size.y); -// let text_size = point(text_width, size.y); - -// let autoscroll_horizontally = editor.autoscroll_vertically(size.y, line_height, cx); -// let mut snapshot = editor.snapshot(cx); - -// let scroll_position = snapshot.scroll_position(); -// // The scroll position is a fractional point, the whole number of which represents -// // the top of the window in terms of display rows. -// let start_row = scroll_position.y as u32; -// let height_in_lines = size.y / line_height; -// let max_row = snapshot.max_point().row(); - -// // Add 1 to ensure selections bleed off screen -// let end_row = 1 + cmp::min( -// (scroll_position.y + height_in_lines).ceil() as u32, -// max_row, -// ); - -// let start_anchor = if start_row == 0 { -// Anchor::min() -// } else { -// snapshot -// .buffer_snapshot -// .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) -// }; -// let end_anchor = if end_row > max_row { -// Anchor::max -// } else { -// snapshot -// .buffer_snapshot -// .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) -// }; - -// let mut selections: Vec<(SelectionStyle, Vec)> = Vec::new(); -// let mut active_rows = BTreeMap::new(); -// let mut fold_ranges = Vec::new(); -// let is_singleton = editor.is_singleton(cx); - -// let highlighted_rows = editor.highlighted_rows(); -// let theme = theme::current(cx); -// let highlighted_ranges = editor.background_highlights_in_range( -// start_anchor..end_anchor, -// &snapshot.display_snapshot, -// theme.as_ref(), -// ); - -// fold_ranges.extend( -// snapshot -// .folds_in_range(start_anchor..end_anchor) -// .map(|anchor| { -// let start = anchor.start.to_point(&snapshot.buffer_snapshot); -// ( -// start.row, -// start.to_display_point(&snapshot.display_snapshot) -// ..anchor.end.to_display_point(&snapshot), -// ) -// }), -// ); - -// let mut newest_selection_head = None; - -// if editor.show_local_selections { -// let mut local_selections: Vec> = editor -// .selections -// .disjoint_in_range(start_anchor..end_anchor, cx); -// local_selections.extend(editor.selections.pending(cx)); -// let mut layouts = Vec::new(); -// let newest = editor.selections.newest(cx); -// for selection in local_selections.drain(..) { -// let is_empty = selection.start == selection.end; -// let is_newest = selection == newest; - -// let layout = SelectionLayout::new( -// selection, -// editor.selections.line_mode, -// editor.cursor_shape, -// &snapshot.display_snapshot, -// is_newest, -// true, -// ); -// if is_newest { -// newest_selection_head = Some(layout.head); -// } - -// for row in cmp::max(layout.active_rows.start, start_row) -// ..=cmp::min(layout.active_rows.end, end_row) -// { -// let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); -// *contains_non_empty_selection |= !is_empty; -// } -// layouts.push(layout); -// } - -// selections.push((style.selection, layouts)); -// } - -// if let Some(collaboration_hub) = &editor.collaboration_hub { -// // When following someone, render the local selections in their color. -// if let Some(leader_id) = editor.leader_peer_id { -// if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { -// if let Some(participant_index) = collaboration_hub -// .user_participant_indices(cx) -// .get(&collaborator.user_id) -// { -// if let Some((local_selection_style, _)) = selections.first_mut() { -// *local_selection_style = -// style.selection_style_for_room_participant(participant_index.0); -// } -// } -// } -// } - -// let mut remote_selections = HashMap::default(); -// for selection in snapshot.remote_selections_in_range( -// &(start_anchor..end_anchor), -// collaboration_hub.as_ref(), -// cx, -// ) { -// let selection_style = if let Some(participant_index) = selection.participant_index { -// style.selection_style_for_room_participant(participant_index.0) -// } else { -// style.absent_selection -// }; - -// // Don't re-render the leader's selections, since the local selections -// // match theirs. -// if Some(selection.peer_id) == editor.leader_peer_id { -// continue; -// } - -// remote_selections -// .entry(selection.replica_id) -// .or_insert((selection_style, Vec::new())) -// .1 -// .push(SelectionLayout::new( -// selection.selection, -// selection.line_mode, -// selection.cursor_shape, -// &snapshot.display_snapshot, -// false, -// false, -// )); -// } - -// selections.extend(remote_selections.into_values()); -// } - -// let scrollbar_settings = &settings::get::(cx).scrollbar; -// let show_scrollbars = match scrollbar_settings.show { -// ShowScrollbar::Auto => { -// // Git -// (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) -// || -// // Selections -// (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty) -// // Scrollmanager -// || editor.scroll_manager.scrollbars_visible() -// } -// ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), -// ShowScrollbar::Always => true, -// ShowScrollbar::Never => false, -// }; - -// let fold_ranges: Vec<(BufferRow, Range, Color)> = fold_ranges -// .into_iter() -// .map(|(id, fold)| { -// let color = self -// .style -// .folds -// .ellipses -// .background -// .style_for(&mut cx.mouse_state::(id as usize)) -// .color; - -// (id, fold, color) -// }) -// .collect(); - -// let head_for_relative = newest_selection_head.unwrap_or_else(|| { -// let newest = editor.selections.newest::(cx); -// SelectionLayout::new( -// newest, -// editor.selections.line_mode, -// editor.cursor_shape, -// &snapshot.display_snapshot, -// true, -// true, -// ) -// .head -// }); - -// let (line_number_layouts, fold_statuses) = self.layout_line_numbers( -// start_row..end_row, -// &active_rows, -// head_for_relative, -// is_singleton, -// &snapshot, -// cx, -// ); - -// let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); - -// let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); - -// let mut max_visible_line_width = 0.0; -// let line_layouts = -// self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx); -// for line_with_invisibles in &line_layouts { -// if line_with_invisibles.line.width() > max_visible_line_width { -// max_visible_line_width = line_with_invisibles.line.width(); -// } -// } - -// let style = self.style.clone(); -// let longest_line_width = layout_line( -// snapshot.longest_row(), -// &snapshot, -// &style, -// cx.text_layout_cache(), -// ) -// .width(); -// let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x; -// let em_width = style.text.em_width(cx.font_cache()); -// let (scroll_width, blocks) = self.layout_blocks( -// start_row..end_row, -// &snapshot, -// size.x, -// scroll_width, -// gutter_padding, -// gutter_width, -// em_width, -// gutter_width + gutter_margin, -// line_height, -// &style, -// &line_layouts, -// editor, -// cx, -// ); - -// let scroll_max = point( -// ((scroll_width - text_size.x) / em_width).max(0.0), -// max_row as f32, -// ); - -// let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); - -// let autoscrolled = if autoscroll_horizontally { -// editor.autoscroll_horizontally( -// start_row, -// text_size.x, -// scroll_width, -// em_width, -// &line_layouts, -// cx, -// ) -// } else { -// false -// }; - -// if clamped || autoscrolled { -// snapshot = editor.snapshot(cx); -// } - -// let style = editor.style(cx); - -// let mut context_menu = None; -// let mut code_actions_indicator = None; -// if let Some(newest_selection_head) = newest_selection_head { -// if (start_row..end_row).contains(&newest_selection_head.row()) { -// if editor.context_menu_visible() { -// context_menu = -// editor.render_context_menu(newest_selection_head, style.clone(), cx); -// } - -// let active = matches!( -// editor.context_menu.read().as_ref(), -// Some(crate::ContextMenu::CodeActions(_)) -// ); - -// code_actions_indicator = editor -// .render_code_actions_indicator(&style, active, cx) -// .map(|indicator| (newest_selection_head.row(), indicator)); -// } -// } - -// let visible_rows = start_row..start_row + line_layouts.len() as u32; -// let mut hover = editor.hover_state.render( -// &snapshot, -// &style, -// visible_rows, -// editor.workspace.as_ref().map(|(w, _)| w.clone()), -// cx, -// ); -// let mode = editor.mode; - -// let mut fold_indicators = editor.render_fold_indicators( -// fold_statuses, -// &style, -// editor.gutter_hovered, -// line_height, -// gutter_margin, -// cx, -// ); - -// if let Some((_, context_menu)) = context_menu.as_mut() { -// context_menu.layout( -// SizeConstraint { -// min: gpui::Point::::zero(), -// max: point( -// cx.window_size().x * 0.7, -// (12. * line_height).min((size.y - line_height) / 2.), -// ), -// }, -// editor, -// cx, -// ); -// } - -// if let Some((_, indicator)) = code_actions_indicator.as_mut() { -// indicator.layout( -// SizeConstraint::strict_along( -// Axis::Vertical, -// line_height * style.code_actions.vertical_scale, -// ), -// editor, -// cx, -// ); -// } - -// for fold_indicator in fold_indicators.iter_mut() { -// if let Some(indicator) = fold_indicator.as_mut() { -// indicator.layout( -// SizeConstraint::strict_along( -// Axis::Vertical, -// line_height * style.code_actions.vertical_scale, -// ), -// editor, -// cx, -// ); -// } -// } - -// if let Some((_, hover_popovers)) = hover.as_mut() { -// for hover_popover in hover_popovers.iter_mut() { -// hover_popover.layout( -// SizeConstraint { -// min: gpui::Point::::zero(), -// max: point( -// (120. * em_width) // Default size -// .min(size.x / 2.) // Shrink to half of the editor width -// .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters -// (16. * line_height) // Default size -// .min(size.y / 2.) // Shrink to half of the editor height -// .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines -// ), -// }, -// editor, -// cx, -// ); -// } -// } - -// let invisible_symbol_font_size = self.style.text.font_size / 2.0; -// let invisible_symbol_style = RunStyle { -// color: self.style.whitespace, -// font_id: self.style.text.font_id, -// underline: Default::default(), -// }; - -// ( -// size, -// LayoutState { -// mode, -// position_map: Arc::new(PositionMap { -// size, -// scroll_max, -// line_layouts, -// line_height, -// em_width, -// em_advance, -// snapshot, -// }), -// visible_display_row_range: start_row..end_row, -// wrap_guides, -// gutter_size, -// gutter_padding, -// text_size, -// scrollbar_row_range, -// show_scrollbars, -// is_singleton, -// max_row, -// gutter_margin, -// active_rows, -// highlighted_rows, -// highlighted_ranges, -// fold_ranges, -// line_number_layouts, -// display_hunks, -// blocks, -// selections, -// context_menu, -// code_actions_indicator, -// fold_indicators, -// tab_invisible: cx.text_layout_cache().layout_str( -// "→", -// invisible_symbol_font_size, -// &[("→".len(), invisible_symbol_style)], -// ), -// space_invisible: cx.text_layout_cache().layout_str( -// "•", -// invisible_symbol_font_size, -// &[("•".len(), invisible_symbol_style)], -// ), -// hover_popovers: hover, -// }, -// ) -// } - -// fn paint( -// &mut self, -// bounds: Bounds, -// visible_bounds: Bounds, -// layout: &mut Self::LayoutState, -// editor: &mut Editor, -// cx: &mut ViewContext, -// ) -> Self::PaintState { -// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); -// cx.scene().push_layer(Some(visible_bounds)); - -// let gutter_bounds = Bounds::::new(bounds.origin, layout.gutter_size); -// let text_bounds = Bounds::::new( -// bounds.origin + point(layout.gutter_size.x, 0.0), -// layout.text_size, -// ); - -// Self::attach_mouse_handlers( -// &layout.position_map, -// layout.hover_popovers.is_some(), -// visible_bounds, -// text_bounds, -// gutter_bounds, -// bounds, -// cx, -// ); - -// self.paint_background(gutter_bounds, text_bounds, layout, cx); -// if layout.gutter_size.x > 0. { -// self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx); -// } -// self.paint_text(text_bounds, visible_bounds, layout, editor, cx); - -// cx.scene().push_layer(Some(bounds)); -// if !layout.blocks.is_empty { -// self.paint_blocks(bounds, visible_bounds, layout, editor, cx); -// } -// self.paint_scrollbar(bounds, layout, &editor, cx); -// cx.scene().pop_layer(); -// cx.scene().pop_layer(); -// } - -// fn rect_for_text_range( -// &self, -// range_utf16: Range, -// bounds: Bounds, -// _: Bounds, -// layout: &Self::LayoutState, -// _: &Self::PaintState, -// _: &Editor, -// _: &ViewContext, -// ) -> Option> { -// let text_bounds = Bounds::::new( -// bounds.origin + point(layout.gutter_size.x, 0.0), -// layout.text_size, -// ); -// let content_origin = text_bounds.origin + point(layout.gutter_margin, 0.); -// let scroll_position = layout.position_map.snapshot.scroll_position(); -// let start_row = scroll_position.y as u32; -// let scroll_top = scroll_position.y * layout.position_map.line_height; -// let scroll_left = scroll_position.x * layout.position_map.em_width; - -// let range_start = OffsetUtf16(range_utf16.start) -// .to_display_point(&layout.position_map.snapshot.display_snapshot); -// if range_start.row() < start_row { -// return None; -// } - -// let line = &layout -// .position_map -// .line_layouts -// .get((range_start.row() - start_row) as usize)? -// .line; -// let range_start_x = line.x_for_index(range_start.column() as usize); -// let range_start_y = range_start.row() as f32 * layout.position_map.line_height; -// Some(Bounds::::new( -// content_origin -// + point( -// range_start_x, -// range_start_y + layout.position_map.line_height, -// ) -// - point(scroll_left, scroll_top), -// point( -// layout.position_map.em_width, -// layout.position_map.line_height, -// ), -// )) -// } - -// fn debug( -// &self, -// bounds: Bounds, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &Editor, -// _: &ViewContext, -// ) -> json::Value { -// json!({ -// "type": "BufferElement", -// "bounds": bounds.to_json() -// }) -// } -// } - type BufferRow = u32; pub struct LayoutState { @@ -4134,3 +3599,59 @@ pub fn register_action( } }) } + +fn compute_auto_height_layout( + editor: &mut Editor, + max_lines: usize, + max_line_number_width: Pixels, + known_dimensions: Size>, + cx: &mut ViewContext, +) -> Option> { + let mut width = known_dimensions.width?; + if let Some(height) = known_dimensions.height { + return Some(size(width, height)); + } + + let style = editor.style.as_ref().unwrap(); + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + let mut snapshot = editor.snapshot(cx); + let gutter_padding; + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let descent = cx.text_system().descent(font_id, font_size).unwrap(); + let gutter_padding_factor = 3.5; + gutter_padding = (em_width * gutter_padding_factor).round(); + gutter_width = max_line_number_width + gutter_padding * 2.0; + gutter_margin = -descent; + } else { + gutter_padding = Pixels::ZERO; + gutter_width = Pixels::ZERO; + gutter_margin = Pixels::ZERO; + }; + + editor.gutter_width = gutter_width; + let text_width = width - gutter_width; + let overscroll = size(em_width, px(0.)); + + let editor_width = text_width - gutter_margin - overscroll.width - em_width; + if editor.set_wrap_width(Some(editor_width), cx) { + snapshot = editor.snapshot(cx); + } + + let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; + let height = scroll_height + .max(line_height) + .min(line_height * max_lines as f32); + + Some(size(width, height)) +} diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index eca3b99d7807b455ba48ed99ef0287a9ee084abf..93bb37c6222932f395d738e4a8bac9ec20d7076c 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -4,13 +4,14 @@ use crate::{ EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, - FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, WindowContext, + div, point, AnyElement, AppContext, AsyncAppContext, AsyncWindowContext, Context, Div, Entity, + EntityId, EventEmitter, FocusHandle, IntoElement, Model, ParentElement, Pixels, Render, + SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, @@ -20,6 +21,7 @@ use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPat use rpc::proto::{self, update_view, PeerId}; use settings::Settings; use smallvec::SmallVec; +use std::fmt::Write; use std::{ borrow::Cow, cmp::{self, Ordering}, @@ -31,8 +33,11 @@ use std::{ use text::Selection; use theme::{ActiveTheme, Theme}; use ui::{Color, Label}; -use util::{paths::PathExt, ResultExt, TryFutureExt}; -use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; +use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; +use workspace::{ + item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, + StatusItemView, +}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, @@ -71,110 +76,108 @@ impl FollowableItem for Editor { workspace: View, remote_id: ViewId, state: &mut Option, - cx: &mut AppContext, + cx: &mut WindowContext, ) -> Option>>> { - todo!() + let project = workspace.read(cx).project().to_owned(); + let Some(proto::view::Variant::Editor(_)) = state else { + return None; + }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { + unreachable!() + }; + + let client = project.read(cx).client(); + let replica_id = project.read(cx).replica_id(); + let buffer_ids = state + .excerpts + .iter() + .map(|excerpt| excerpt.buffer_id) + .collect::>(); + let buffers = project.update(cx, |project, cx| { + buffer_ids + .iter() + .map(|id| project.open_buffer_by_id(*id, cx)) + .collect::>() + }); + + let pane = pane.downgrade(); + Some(cx.spawn(|mut cx| async move { + let mut buffers = futures::future::try_join_all(buffers).await?; + let editor = pane.update(&mut cx, |pane, cx| { + let mut editors = pane.items_of_type::(); + editors.find(|editor| { + let ids_match = editor.remote_id(&client, cx) == Some(remote_id); + let singleton_buffer_matches = state.singleton + && buffers.first() + == editor.read(cx).buffer.read(cx).as_singleton().as_ref(); + ids_match || singleton_buffer_matches + }) + })?; + + let editor = if let Some(editor) = editor { + editor + } else { + pane.update(&mut cx, |_, cx| { + let multibuffer = cx.build_model(|cx| { + let mut multibuffer; + if state.singleton && buffers.len() == 1 { + multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) + } else { + multibuffer = MultiBuffer::new(replica_id); + let mut excerpts = state.excerpts.into_iter().peekable(); + while let Some(excerpt) = excerpts.peek() { + let buffer_id = excerpt.buffer_id; + let buffer_excerpts = iter::from_fn(|| { + let excerpt = excerpts.peek()?; + (excerpt.buffer_id == buffer_id) + .then(|| excerpts.next().unwrap()) + }); + let buffer = + buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id); + if let Some(buffer) = buffer { + multibuffer.push_excerpts( + buffer.clone(), + buffer_excerpts.filter_map(deserialize_excerpt_range), + cx, + ); + } + } + }; + + if let Some(title) = &state.title { + multibuffer = multibuffer.with_title(title.clone()) + } + + multibuffer + }); + + cx.build_view(|cx| { + let mut editor = + Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); + editor.remote_id = Some(remote_id); + editor + }) + })? + }; + + update_editor_from_message( + editor.downgrade(), + project, + proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }, + &mut cx, + ) + .await?; + + Ok(editor) + })) } - // let project = workspace.read(cx).project().to_owned(); - // let Some(proto::view::Variant::Editor(_)) = state else { - // return None; - // }; - // let Some(proto::view::Variant::Editor(state)) = state.take() else { - // unreachable!() - // }; - - // let client = project.read(cx).client(); - // let replica_id = project.read(cx).replica_id(); - // let buffer_ids = state - // .excerpts - // .iter() - // .map(|excerpt| excerpt.buffer_id) - // .collect::>(); - // let buffers = project.update(cx, |project, cx| { - // buffer_ids - // .iter() - // .map(|id| project.open_buffer_by_id(*id, cx)) - // .collect::>() - // }); - - // let pane = pane.downgrade(); - // Some(cx.spawn(|mut cx| async move { - // let mut buffers = futures::future::try_join_all(buffers).await?; - // let editor = pane.read_with(&cx, |pane, cx| { - // let mut editors = pane.items_of_type::(); - // editors.find(|editor| { - // let ids_match = editor.remote_id(&client, cx) == Some(remote_id); - // let singleton_buffer_matches = state.singleton - // && buffers.first() - // == editor.read(cx).buffer.read(cx).as_singleton().as_ref(); - // ids_match || singleton_buffer_matches - // }) - // })?; - - // let editor = if let Some(editor) = editor { - // editor - // } else { - // pane.update(&mut cx, |_, cx| { - // let multibuffer = cx.add_model(|cx| { - // let mut multibuffer; - // if state.singleton && buffers.len() == 1 { - // multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) - // } else { - // multibuffer = MultiBuffer::new(replica_id); - // let mut excerpts = state.excerpts.into_iter().peekable(); - // while let Some(excerpt) = excerpts.peek() { - // let buffer_id = excerpt.buffer_id; - // let buffer_excerpts = iter::from_fn(|| { - // let excerpt = excerpts.peek()?; - // (excerpt.buffer_id == buffer_id) - // .then(|| excerpts.next().unwrap()) - // }); - // let buffer = - // buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id); - // if let Some(buffer) = buffer { - // multibuffer.push_excerpts( - // buffer.clone(), - // buffer_excerpts.filter_map(deserialize_excerpt_range), - // cx, - // ); - // } - // } - // }; - - // if let Some(title) = &state.title { - // multibuffer = multibuffer.with_title(title.clone()) - // } - - // multibuffer - // }); - - // cx.add_view(|cx| { - // let mut editor = - // Editor::for_multibuffer(multibuffer, Some(project.clone()), cx); - // editor.remote_id = Some(remote_id); - // editor - // }) - // })? - // }; - - // update_editor_from_message( - // editor.downgrade(), - // project, - // proto::update_view::Editor { - // selections: state.selections, - // pending_selection: state.pending_selection, - // scroll_top_anchor: state.scroll_top_anchor, - // scroll_x: state.scroll_x, - // scroll_y: state.scroll_y, - // ..Default::default() - // }, - // &mut cx, - // ) - // .await?; - - // Ok(editor) - // })) - // } fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { self.leader_peer_id = leader_peer_id; @@ -195,7 +198,7 @@ impl FollowableItem for Editor { cx.notify(); } - fn to_state_proto(&self, cx: &AppContext) -> Option { + fn to_state_proto(&self, cx: &WindowContext) -> Option { let buffer = self.buffer.read(cx); let scroll_anchor = self.scroll_manager.anchor(); let excerpts = buffer @@ -242,7 +245,7 @@ impl FollowableItem for Editor { &self, event: &Self::FollowableEvent, update: &mut Option, - cx: &AppContext, + cx: &WindowContext, ) -> bool { let update = update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); @@ -315,7 +318,7 @@ impl FollowableItem for Editor { }) } - fn is_project_item(&self, _cx: &AppContext) -> bool { + fn is_project_item(&self, _cx: &WindowContext) -> bool { true } } @@ -324,132 +327,129 @@ async fn update_editor_from_message( this: WeakView, project: Model, message: proto::update_view::Editor, - cx: &mut AsyncAppContext, + cx: &mut AsyncWindowContext, ) -> Result<()> { - todo!() + // Open all of the buffers of which excerpts were added to the editor. + let inserted_excerpt_buffer_ids = message + .inserted_excerpts + .iter() + .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) + .collect::>(); + let inserted_excerpt_buffers = project.update(cx, |project, cx| { + inserted_excerpt_buffer_ids + .into_iter() + .map(|id| project.open_buffer_by_id(id, cx)) + .collect::>() + })?; + let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?; + + // Update the editor's excerpts. + this.update(cx, |editor, cx| { + editor.buffer.update(cx, |multibuffer, cx| { + let mut removed_excerpt_ids = message + .deleted_excerpts + .into_iter() + .map(ExcerptId::from_proto) + .collect::>(); + removed_excerpt_ids.sort_by({ + let multibuffer = multibuffer.read(cx); + move |a, b| a.cmp(&b, &multibuffer) + }); + + let mut insertions = message.inserted_excerpts.into_iter().peekable(); + while let Some(insertion) = insertions.next() { + let Some(excerpt) = insertion.excerpt else { + continue; + }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { + continue; + }; + let buffer_id = excerpt.buffer_id; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else { + continue; + }; + + let adjacent_excerpts = iter::from_fn(|| { + let insertion = insertions.peek()?; + if insertion.previous_excerpt_id.is_none() + && insertion.excerpt.as_ref()?.buffer_id == buffer_id + { + insertions.next()?.excerpt + } else { + None + } + }); + + multibuffer.insert_excerpts_with_ids_after( + ExcerptId::from_proto(previous_excerpt_id), + buffer, + [excerpt] + .into_iter() + .chain(adjacent_excerpts) + .filter_map(|excerpt| { + Some(( + ExcerptId::from_proto(excerpt.id), + deserialize_excerpt_range(excerpt)?, + )) + }), + cx, + ); + } + + multibuffer.remove_excerpts(removed_excerpt_ids, cx); + }); + })?; + + // Deserialize the editor state. + let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| { + let buffer = editor.buffer.read(cx).read(cx); + let selections = message + .selections + .into_iter() + .filter_map(|selection| deserialize_selection(&buffer, selection)) + .collect::>(); + let pending_selection = message + .pending_selection + .and_then(|selection| deserialize_selection(&buffer, selection)); + let scroll_top_anchor = message + .scroll_top_anchor + .and_then(|anchor| deserialize_anchor(&buffer, anchor)); + anyhow::Ok((selections, pending_selection, scroll_top_anchor)) + })??; + + // Wait until the buffer has received all of the operations referenced by + // the editor's new state. + this.update(cx, |editor, cx| { + editor.buffer.update(cx, |buffer, cx| { + buffer.wait_for_anchors( + selections + .iter() + .chain(pending_selection.as_ref()) + .flat_map(|selection| [selection.start, selection.end]) + .chain(scroll_top_anchor), + cx, + ) + }) + })? + .await?; + + // Update the editor's state. + this.update(cx, |editor, cx| { + if !selections.is_empty() || pending_selection.is_some() { + editor.set_selections_from_remote(selections, pending_selection, cx); + editor.request_autoscroll_remotely(Autoscroll::newest(), cx); + } else if let Some(scroll_top_anchor) = scroll_top_anchor { + editor.set_scroll_anchor_remote( + ScrollAnchor { + anchor: scroll_top_anchor, + offset: point(message.scroll_x, message.scroll_y), + }, + cx, + ); + } + })?; + Ok(()) } -// Previous implementation of the above -// // Open all of the buffers of which excerpts were added to the editor. -// let inserted_excerpt_buffer_ids = message -// .inserted_excerpts -// .iter() -// .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) -// .collect::>(); -// let inserted_excerpt_buffers = project.update(cx, |project, cx| { -// inserted_excerpt_buffer_ids -// .into_iter() -// .map(|id| project.open_buffer_by_id(id, cx)) -// .collect::>() -// })?; -// let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?; - -// // Update the editor's excerpts. -// this.update(cx, |editor, cx| { -// editor.buffer.update(cx, |multibuffer, cx| { -// let mut removed_excerpt_ids = message -// .deleted_excerpts -// .into_iter() -// .map(ExcerptId::from_proto) -// .collect::>(); -// removed_excerpt_ids.sort_by({ -// let multibuffer = multibuffer.read(cx); -// move |a, b| a.cmp(&b, &multibuffer) -// }); - -// let mut insertions = message.inserted_excerpts.into_iter().peekable(); -// while let Some(insertion) = insertions.next() { -// let Some(excerpt) = insertion.excerpt else { -// continue; -// }; -// let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { -// continue; -// }; -// let buffer_id = excerpt.buffer_id; -// let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else { -// continue; -// }; - -// let adjacent_excerpts = iter::from_fn(|| { -// let insertion = insertions.peek()?; -// if insertion.previous_excerpt_id.is_none() -// && insertion.excerpt.as_ref()?.buffer_id == buffer_id -// { -// insertions.next()?.excerpt -// } else { -// None -// } -// }); - -// multibuffer.insert_excerpts_with_ids_after( -// ExcerptId::from_proto(previous_excerpt_id), -// buffer, -// [excerpt] -// .into_iter() -// .chain(adjacent_excerpts) -// .filter_map(|excerpt| { -// Some(( -// ExcerptId::from_proto(excerpt.id), -// deserialize_excerpt_range(excerpt)?, -// )) -// }), -// cx, -// ); -// } - -// multibuffer.remove_excerpts(removed_excerpt_ids, cx); -// }); -// })?; - -// // Deserialize the editor state. -// let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| { -// let buffer = editor.buffer.read(cx).read(cx); -// let selections = message -// .selections -// .into_iter() -// .filter_map(|selection| deserialize_selection(&buffer, selection)) -// .collect::>(); -// let pending_selection = message -// .pending_selection -// .and_then(|selection| deserialize_selection(&buffer, selection)); -// let scroll_top_anchor = message -// .scroll_top_anchor -// .and_then(|anchor| deserialize_anchor(&buffer, anchor)); -// anyhow::Ok((selections, pending_selection, scroll_top_anchor)) -// })??; - -// // Wait until the buffer has received all of the operations referenced by -// // the editor's new state. -// this.update(cx, |editor, cx| { -// editor.buffer.update(cx, |buffer, cx| { -// buffer.wait_for_anchors( -// selections -// .iter() -// .chain(pending_selection.as_ref()) -// .flat_map(|selection| [selection.start, selection.end]) -// .chain(scroll_top_anchor), -// cx, -// ) -// }) -// })? -// .await?; - -// // Update the editor's state. -// this.update(cx, |editor, cx| { -// if !selections.is_empty() || pending_selection.is_some() { -// editor.set_selections_from_remote(selections, pending_selection, cx); -// editor.request_autoscroll_remotely(Autoscroll::newest(), cx); -// } else if let Some(scroll_top_anchor) = scroll_top_anchor { -// editor.set_scroll_anchor_remote( -// ScrollAnchor { -// anchor: scroll_top_anchor, -// offset: point(message.scroll_x, message.scroll_y), -// }, -// cx, -// ); -// } -// })?; -// Ok(()) -// } fn serialize_excerpt( buffer_id: u64, @@ -529,39 +529,38 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { - todo!(); - // if let Ok(data) = data.downcast::() { - // let newest_selection = self.selections.newest::(cx); - // let buffer = self.buffer.read(cx).read(cx); - // let offset = if buffer.can_resolve(&data.cursor_anchor) { - // data.cursor_anchor.to_point(&buffer) - // } else { - // buffer.clip_point(data.cursor_position, Bias::Left) - // }; - - // let mut scroll_anchor = data.scroll_anchor; - // if !buffer.can_resolve(&scroll_anchor.anchor) { - // scroll_anchor.anchor = buffer.anchor_before( - // buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), - // ); - // } - - // drop(buffer); - - // if newest_selection.head() == offset { - // false - // } else { - // let nav_history = self.nav_history.take(); - // self.set_scroll_anchor(scroll_anchor, cx); - // self.change_selections(Some(Autoscroll::fit()), cx, |s| { - // s.select_ranges([offset..offset]) - // }); - // self.nav_history = nav_history; - // true - // } - // } else { - // false - // } + if let Ok(data) = data.downcast::() { + let newest_selection = self.selections.newest::(cx); + let buffer = self.buffer.read(cx).read(cx); + let offset = if buffer.can_resolve(&data.cursor_anchor) { + data.cursor_anchor.to_point(&buffer) + } else { + buffer.clip_point(data.cursor_position, Bias::Left) + }; + + let mut scroll_anchor = data.scroll_anchor; + if !buffer.can_resolve(&scroll_anchor.anchor) { + scroll_anchor.anchor = buffer.anchor_before( + buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), + ); + } + + drop(buffer); + + if newest_selection.head() == offset { + false + } else { + let nav_history = self.nav_history.take(); + self.set_scroll_anchor(scroll_anchor, cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([offset..offset]) + }); + self.nav_history = nav_history; + true + } + } else { + false + } } fn tab_tooltip_text(&self, cx: &AppContext) -> Option { @@ -765,35 +764,34 @@ impl Item for Editor { } fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option> { - todo!(); - // let cursor = self.selections.newest_anchor().head(); - // let multibuffer = &self.buffer().read(cx); - // let (buffer_id, symbols) = - // multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?; - // let buffer = multibuffer.buffer(buffer_id)?; - - // let buffer = buffer.read(cx); - // let filename = buffer - // .snapshot() - // .resolve_file_path( - // cx, - // self.project - // .as_ref() - // .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - // .unwrap_or_default(), - // ) - // .map(|path| path.to_string_lossy().to_string()) - // .unwrap_or_else(|| "untitled".to_string()); - - // let mut breadcrumbs = vec![BreadcrumbText { - // text: filename, - // highlights: None, - // }]; - // breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { - // text: symbol.text, - // highlights: Some(symbol.highlight_ranges), - // })); - // Some(breadcrumbs) + let cursor = self.selections.newest_anchor().head(); + let multibuffer = &self.buffer().read(cx); + let (buffer_id, symbols) = + multibuffer.symbols_containing(cursor, Some(&variant.syntax()), cx)?; + let buffer = multibuffer.buffer(buffer_id)?; + + let buffer = buffer.read(cx); + let filename = buffer + .snapshot() + .resolve_file_path( + cx, + self.project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(), + ) + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "untitled".to_string()); + + let mut breadcrumbs = vec![BreadcrumbText { + text: filename, + highlights: None, + }]; + breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { + text: symbol.text, + highlights: Some(symbol.highlight_ranges), + })); + Some(breadcrumbs) } fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { @@ -1120,86 +1118,78 @@ pub struct CursorPosition { _observe_active_editor: Option, } -// impl Default for CursorPosition { -// fn default() -> Self { -// Self::new() -// } -// } - -// impl CursorPosition { -// pub fn new() -> Self { -// Self { -// position: None, -// selected_count: 0, -// _observe_active_editor: None, -// } -// } - -// fn update_position(&mut self, editor: View, cx: &mut ViewContext) { -// let editor = editor.read(cx); -// let buffer = editor.buffer().read(cx).snapshot(cx); - -// self.selected_count = 0; -// let mut last_selection: Option> = None; -// for selection in editor.selections.all::(cx) { -// self.selected_count += selection.end - selection.start; -// if last_selection -// .as_ref() -// .map_or(true, |last_selection| selection.id > last_selection.id) -// { -// last_selection = Some(selection); -// } -// } -// self.position = last_selection.map(|s| s.head().to_point(&buffer)); - -// cx.notify(); -// } -// } - -// impl Entity for CursorPosition { -// type Event = (); -// } - -// impl View for CursorPosition { -// fn ui_name() -> &'static str { -// "CursorPosition" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// if let Some(position) = self.position { -// let theme = &theme::current(cx).workspace.status_bar; -// let mut text = format!( -// "{}{FILE_ROW_COLUMN_DELIMITER}{}", -// position.row + 1, -// position.column + 1 -// ); -// if self.selected_count > 0 { -// write!(text, " ({} selected)", self.selected_count).unwrap(); -// } -// Label::new(text, theme.cursor_position.clone()).into_any() -// } else { -// Empty::new().into_any() -// } -// } -// } - -// impl StatusItemView for CursorPosition { -// fn set_active_pane_item( -// &mut self, -// active_pane_item: Option<&dyn ItemHandle>, -// cx: &mut ViewContext, -// ) { -// if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { -// self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); -// self.update_position(editor, cx); -// } else { -// self.position = None; -// self._observe_active_editor = None; -// } - -// cx.notify(); -// } -// } +impl Default for CursorPosition { + fn default() -> Self { + Self::new() + } +} + +impl CursorPosition { + pub fn new() -> Self { + Self { + position: None, + selected_count: 0, + _observe_active_editor: None, + } + } + + fn update_position(&mut self, editor: View, cx: &mut ViewContext) { + let editor = editor.read(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + + self.selected_count = 0; + let mut last_selection: Option> = None; + for selection in editor.selections.all::(cx) { + self.selected_count += selection.end - selection.start; + if last_selection + .as_ref() + .map_or(true, |last_selection| selection.id > last_selection.id) + { + last_selection = Some(selection); + } + } + self.position = last_selection.map(|s| s.head().to_point(&buffer)); + + cx.notify(); + } +} + +impl Render for CursorPosition { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().when_some(self.position, |el, position| { + let mut text = format!( + "{}{FILE_ROW_COLUMN_DELIMITER}{}", + position.row + 1, + position.column + 1 + ); + if self.selected_count > 0 { + write!(text, " ({} selected)", self.selected_count).unwrap(); + } + + el.child(Label::new(text)) + }) + } +} + +impl StatusItemView for CursorPosition { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { + self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); + self.update_position(editor, cx); + } else { + self.position = None; + self._observe_active_editor = None; + } + + cx.notify(); + } +} fn path_for_buffer<'a>( buffer: &Model, diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index b70a826bf8cf3975bdd5c93df2ba67c31c0fb672..fdeec9110b97ff5c23b945bf31da590bbe8a30dc 100644 --- a/crates/editor2/src/mouse_context_menu.rs +++ b/crates/editor2/src/mouse_context_menu.rs @@ -1,5 +1,14 @@ -use crate::{DisplayPoint, Editor, EditorMode, SelectMode}; -use gpui::{Pixels, Point, ViewContext}; +use crate::{ + DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition, + Rename, RevealInFinder, SelectMode, ToggleCodeActions, +}; +use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext}; + +pub struct MouseContextMenu { + pub(crate) position: Point, + pub(crate) context_menu: View, + _subscription: Subscription, +} pub fn deploy_context_menu( editor: &mut Editor, @@ -7,50 +16,57 @@ pub fn deploy_context_menu( point: DisplayPoint, cx: &mut ViewContext, ) { - todo!(); + if !editor.is_focused(cx) { + editor.focus(cx); + } + + // Don't show context menu for inline editors + if editor.mode() != EditorMode::Full { + return; + } - // if !editor.focused { - // cx.focus_self(); - // } + // Don't show the context menu if there isn't a project associated with this editor + if editor.project.is_none() { + return; + } - // // Don't show context menu for inline editors - // if editor.mode() != EditorMode::Full { - // return; - // } + // Move the cursor to the clicked location so that dispatched actions make sense + editor.change_selections(None, cx, |s| { + s.clear_disjoint(); + s.set_pending_display_range(point..point, SelectMode::Character); + }); - // // Don't show the context menu if there isn't a project associated with this editor - // if editor.project.is_none() { - // return; - // } + 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) + .action( + "Code Actions", + Box::new(ToggleCodeActions { + deployed_from_indicator: false, + }), + cx, + ) + .separator() + .action("Reveal in Finder", Box::new(RevealInFinder), cx) + }); + let context_menu_focus = context_menu.focus_handle(cx); + cx.focus(&context_menu_focus); - // // Move the cursor to the clicked location so that dispatched actions make sense - // editor.change_selections(None, cx, |s| { - // s.clear_disjoint(); - // s.set_pending_display_range(point..point, SelectMode::Character); - // }); + let _subscription = cx.subscribe(&context_menu, move |this, _, event: &DismissEvent, cx| { + this.mouse_context_menu.take(); + if context_menu_focus.contains_focused(cx) { + this.focus(cx); + } + }); - // editor.mouse_context_menu.update(cx, |menu, cx| { - // menu.show( - // position, - // AnchorCorner::TopLeft, - // vec![ - // ContextMenuItem::action("Rename Symbol", Rename), - // ContextMenuItem::action("Go to Definition", GoToDefinition), - // ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition), - // ContextMenuItem::action("Find All References", FindAllReferences), - // ContextMenuItem::action( - // "Code Actions", - // ToggleCodeActions { - // deployed_from_indicator: false, - // }, - // ), - // ContextMenuItem::Separator, - // ContextMenuItem::action("Reveal in Finder", RevealInFinder), - // ], - // cx, - // ); - // }); - // cx.notify(); + editor.mouse_context_menu = Some(MouseContextMenu { + position, + context_menu, + _subscription, + }); + cx.notify(); } // #[cfg(test)] diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 6542ace5fbe057d8d8fb65cfa1f615ebeb3d89c1..8d71916210a6897b4fa649eef2ebbe05a041acb9 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -315,14 +315,11 @@ impl SelectionsCollection { let line = display_map.layout_row(row, &text_layout_details); - dbg!("****START COL****"); let start_col = line.closest_index_for_x(positions.start) as u32; if start_col < line_len || (is_empty && positions.start == line.width) { let start = DisplayPoint::new(row, start_col); - dbg!("****END COL****"); let end_col = line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); - dbg!(start_col, end_col); Some(Selection { id: post_inc(&mut self.next_selection_id), diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index ec37c57f2c9656a978e289cbb33f8580004f98f9..4f6e157e4e0cbf27831b578aeeb25860cbc8e632 100644 --- a/crates/editor2/src/test.rs +++ b/crates/editor2/src/test.rs @@ -27,7 +27,7 @@ pub fn marked_display_snapshot( let (unmarked_text, markers) = marked_text_offsets(text); let font = cx.text_style().font(); - let font_size: Pixels = 14.into(); + let font_size: Pixels = 14usize.into(); let buffer = MultiBuffer::build_simple(&unmarked_text, cx); let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx)); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index b7a4a387ab63a8592992c538e606c6e0d915290f..156b062df85f2c1fed4aa30fda7e41aa507297a4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -518,6 +518,7 @@ impl PickerDelegate for FileFinderDelegate { } fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + let raw_query = raw_query.trim(); if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); @@ -539,7 +540,6 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - let raw_query = &raw_query; let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { Ok::<_, std::convert::Infallible>(FileSearchQuery { raw_query: raw_query.to_owned(), @@ -735,6 +735,7 @@ mod tests { cx.dispatch_action(window.into(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + finder .update(cx, |finder, cx| { finder.delegate_mut().update_matches("bna".to_string(), cx) @@ -743,7 +744,6 @@ mod tests { finder.read_with(cx, |finder, _| { assert_eq!(finder.delegate().matches.len(), 2); }); - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); cx.dispatch_action(window.into(), SelectNext); cx.dispatch_action(window.into(), Confirm); @@ -762,6 +762,49 @@ mod tests { "bandana" ); }); + + for bandana_query in [ + "bandana", + " bandana", + "bandana ", + " bandana ", + " ndan ", + " band ", + ] { + finder + .update(cx, |finder, cx| { + finder + .delegate_mut() + .update_matches(bandana_query.to_string(), cx) + }) + .await; + finder.read_with(cx, |finder, _| { + assert_eq!( + finder.delegate().matches.len(), + 1, + "Wrong number of matches for bandana query '{bandana_query}'" + ); + }); + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + assert_eq!( + active_item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .title(cx), + "bandana", + "Wrong match for bandana query '{bandana_query}'" + ); + }); + } } #[gpui::test] diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index a24da580ebea9db1467f5936930558821fbd5220..9938b94edb2892d15f547e95df6ee3192f0f992b 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -15,7 +15,7 @@ use std::{ }, }; use text::Point; -use ui::{v_stack, HighlightedLabel, ListItem}; +use ui::{prelude::*, v_stack, HighlightedLabel, ListItem}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; @@ -552,6 +552,7 @@ impl PickerDelegate for FileFinderDelegate { raw_query: String, cx: &mut ViewContext>, ) -> Task<()> { + let raw_query = raw_query.trim(); if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); @@ -573,7 +574,6 @@ impl PickerDelegate for FileFinderDelegate { cx.notify(); Task::ready(()) } else { - let raw_query = &raw_query; let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { Ok::<_, std::convert::Infallible>(FileSearchQuery { raw_query: raw_query.to_owned(), @@ -766,18 +766,49 @@ mod tests { let (picker, workspace, cx) = build_find_picker(project, cx); cx.simulate_input("bna"); - picker.update(cx, |picker, _| { assert_eq!(picker.delegate.matches.len(), 2); }); - cx.dispatch_action(SelectNext); cx.dispatch_action(Confirm); - cx.read(|cx| { let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); assert_eq!(active_editor.read(cx).title(cx), "bandana"); }); + + for bandana_query in [ + "bandana", + " bandana", + "bandana ", + " bandana ", + " ndan ", + " band ", + ] { + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(bandana_query.to_string(), cx) + }) + .await; + picker.update(cx, |picker, _| { + assert_eq!( + picker.delegate.matches.len(), + 1, + "Wrong number of matches for bandana query '{bandana_query}'" + ); + }); + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!( + active_editor.read(cx).title(cx), + "bandana", + "Wrong match for bandana query '{bandana_query}'" + ); + }); + } } #[gpui::test] @@ -1225,7 +1256,7 @@ mod tests { // // TODO: without closing, the opened items do not propagate their history changes for some reason // it does work in real app though, only tests do not propagate. - workspace.update(cx, |_, cx| dbg!(cx.focused())); + workspace.update(cx, |_, cx| cx.focused()); let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; assert!( diff --git a/crates/gpui2/build.rs b/crates/gpui2/build.rs index 6e8a0868b969c7a85627fd974f0f1bde793eb587..24e493cb812d6e7478b266cc621eea7cbc77b051 100644 --- a/crates/gpui2/build.rs +++ b/crates/gpui2/build.rs @@ -65,6 +65,8 @@ fn generate_shader_bindings() -> PathBuf { "MonochromeSprite".into(), "PolychromeSprite".into(), "PathSprite".into(), + "SurfaceInputIndex".into(), + "SurfaceBounds".into(), ]); config.no_includes = true; config.enumeration.prefix_with_name = true; diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 9637720a67e78ba6f735534f924b4028d8d8c75f..c915753749f4f85bb75cf1440df0775b232d7fa4 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -2,8 +2,8 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, - TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle, - WindowOptions, + TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, VisualContext, WindowContext, + WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -502,6 +502,19 @@ impl<'a> VisualTestContext<'a> { self.cx.dispatch_action(self.window, action) } + pub fn window_title(&mut self) -> Option { + self.cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .window_title + .clone() + }) + .unwrap() + } + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { self.cx.simulate_keystrokes(self.window, keystrokes) } @@ -509,6 +522,39 @@ impl<'a> VisualTestContext<'a> { pub fn simulate_input(&mut self, input: &str) { self.cx.simulate_input(self.window, input) } + + pub fn simulate_activation(&mut self) { + self.simulate_window_events(&mut |handlers| { + handlers + .active_status_change + .iter_mut() + .for_each(|f| f(true)); + }) + } + + pub fn simulate_deactivation(&mut self) { + self.simulate_window_events(&mut |handlers| { + handlers + .active_status_change + .iter_mut() + .for_each(|f| f(false)); + }) + } + + fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) { + let handlers = self + .cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .handlers + .clone() + }) + .unwrap(); + f(&mut *handlers.lock()); + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 3922c0c4e4c794d9d58d73982bf50fbc35a3502d..ced0a4767cc988d58ea49dcc6b3f3c7a78b34b4f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -12,6 +12,7 @@ use smallvec::SmallVec; use std::{ any::{Any, TypeId}, cell::RefCell, + cmp::Ordering, fmt::Debug, mem, rc::Rc, @@ -357,6 +358,11 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.interactivity().scroll_handle = Some(scroll_handle.clone()); + self + } + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -626,6 +632,26 @@ impl Element for Div { let mut child_max = Point::default(); let content_size = if element_state.child_layout_ids.is_empty() { bounds.size + } else if let Some(scroll_handle) = self.interactivity.scroll_handle.as_ref() { + let mut state = scroll_handle.0.borrow_mut(); + state.child_bounds = Vec::with_capacity(element_state.child_layout_ids.len()); + state.bounds = bounds; + let requested = state.requested_scroll_top.take(); + + for (ix, child_layout_id) in element_state.child_layout_ids.iter().enumerate() { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + state.child_bounds.push(child_bounds); + + if let Some(requested) = requested.as_ref() { + if requested.0 == ix { + *state.offset.borrow_mut() = + bounds.origin - (child_bounds.origin - point(px(0.), requested.1)); + } + } + } + (child_max - child_min).into() } else { for child_layout_id in &element_state.child_layout_ids { let child_bounds = cx.layout_bounds(*child_layout_id); @@ -696,6 +722,7 @@ pub struct Interactivity { pub key_context: KeyContext, pub focusable: bool, pub tracked_focus_handle: Option, + pub scroll_handle: Option, pub focus_listeners: FocusListeners, pub group: Option, pub base_style: StyleRefinement, @@ -754,6 +781,10 @@ impl Interactivity { }); } + if let Some(scroll_handle) = self.scroll_handle.as_ref() { + element_state.scroll_offset = Some(scroll_handle.0.borrow().offset.clone()); + } + let style = self.compute_style(None, &mut element_state, cx); let layout_id = f(style, cx); (layout_id, element_state) @@ -1206,6 +1237,7 @@ impl Default for Interactivity { key_context: KeyContext::default(), focusable: false, tracked_focus_handle: None, + scroll_handle: None, focus_listeners: SmallVec::default(), // scroll_offset: Point::default(), group: None, @@ -1429,3 +1461,83 @@ where self.element.children_mut() } } + +#[derive(Default)] +struct ScrollHandleState { + // not great to have the nested rc's... + offset: Rc>>, + bounds: Bounds, + child_bounds: Vec>, + requested_scroll_top: Option<(usize, Pixels)>, +} + +#[derive(Clone)] +pub struct ScrollHandle(Rc>); + +impl ScrollHandle { + pub fn new() -> Self { + Self(Rc::default()) + } + + pub fn offset(&self) -> Point { + self.0.borrow().offset.borrow().clone() + } + + pub fn top_item(&self) -> usize { + let state = self.0.borrow(); + let top = state.bounds.top() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if top < bounds.top() { + Ordering::Greater + } else if top > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + pub fn bounds_for_item(&self, ix: usize) -> Option> { + self.0.borrow().child_bounds.get(ix).cloned() + } + + /// scroll_to_item scrolls the minimal amount to ensure that the item is + /// fully visible + pub fn scroll_to_item(&self, ix: usize) { + let state = self.0.borrow(); + + let Some(bounds) = state.child_bounds.get(ix) else { + return; + }; + + let scroll_offset = state.offset.borrow().y; + + if bounds.top() + scroll_offset < state.bounds.top() { + state.offset.borrow_mut().y = state.bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset > state.bounds.bottom() { + state.offset.borrow_mut().y = state.bounds.bottom() - bounds.bottom(); + } + } + + pub fn logical_scroll_top(&self) -> (usize, Pixels) { + let ix = self.top_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.top() + state.offset.borrow().y - state.bounds.top(), + ) + } else { + (ix, px(0.)) + } + } + + pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) { + self.0.borrow_mut().requested_scroll_top = Some((ix, px)); + } +} diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 2aece17b4707f4c9fa473e7d7388e9ffdc8093f8..f6aae2de66aebb7bc894bd63a8d9a85a6e74e089 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,10 +1,12 @@ use std::sync::Arc; use crate::{ - Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity, - IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, + point, size, Bounds, DevicePixels, Element, ImageData, InteractiveElement, + InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size, + StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; +use media::core_video::CVImageBuffer; use util::ResultExt; #[derive(Clone, Debug)] @@ -12,6 +14,7 @@ pub enum ImageSource { /// Image content will be loaded from provided URI at render time. Uri(SharedString), Data(Arc), + Surface(CVImageBuffer), } impl From for ImageSource { @@ -20,40 +23,45 @@ impl From for ImageSource { } } +impl From<&'static str> for ImageSource { + fn from(uri: &'static str) -> Self { + Self::Uri(uri.into()) + } +} + +impl From for ImageSource { + fn from(uri: String) -> Self { + Self::Uri(uri.into()) + } +} + impl From> for ImageSource { fn from(value: Arc) -> Self { Self::Data(value) } } +impl From for ImageSource { + fn from(value: CVImageBuffer) -> Self { + Self::Surface(value) + } +} + pub struct Img { interactivity: Interactivity, - source: Option, + source: ImageSource, grayscale: bool, } -pub fn img() -> Img { +pub fn img(source: impl Into) -> Img { Img { interactivity: Interactivity::default(), - source: None, + source: source.into(), grayscale: false, } } impl Img { - pub fn uri(mut self, uri: impl Into) -> Self { - self.source = Some(ImageSource::from(uri.into())); - self - } - pub fn data(mut self, data: Arc) -> Self { - self.source = Some(ImageSource::from(data)); - self - } - - pub fn source(mut self, source: impl Into) -> Self { - self.source = Some(source.into()); - self - } pub fn grayscale(mut self, grayscale: bool) -> Self { self.grayscale = grayscale; self @@ -68,9 +76,8 @@ impl Element for Img { element_state: Option, cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - self.interactivity.layout(element_state, cx, |style, cx| { - cx.request_layout(&style, None) - }) + self.interactivity + .layout(element_state, cx, |style, cx| cx.request_layout(&style, [])) } fn paint( @@ -85,10 +92,9 @@ impl Element for Img { element_state, cx, |style, _scroll_offset, cx| { - let corner_radii = style.corner_radii; - - if let Some(source) = self.source { - let image = match source { + let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size()); + cx.with_z_index(1, |cx| { + match self.source { ImageSource::Uri(uri) => { let image_future = cx.image_cache.get(uri.clone()); if let Some(data) = image_future @@ -96,7 +102,9 @@ impl Element for Img { .now_or_never() .and_then(|result| result.ok()) { - data + let new_bounds = preserve_aspect_ratio(bounds, data.size()); + cx.paint_image(new_bounds, corner_radii, data, self.grayscale) + .log_err(); } else { cx.spawn(|mut cx| async move { if image_future.await.ok().is_some() { @@ -104,17 +112,23 @@ impl Element for Img { } }) .detach(); - return; } } - ImageSource::Data(image) => image, + + ImageSource::Data(data) => { + let new_bounds = preserve_aspect_ratio(bounds, data.size()); + cx.paint_image(new_bounds, corner_radii, data, self.grayscale) + .log_err(); + } + + ImageSource::Surface(surface) => { + let size = size(surface.width().into(), surface.height().into()); + let new_bounds = preserve_aspect_ratio(bounds, size); + // TODO: Add support for corner_radii and grayscale. + cx.paint_surface(new_bounds, surface); + } }; - let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - cx.paint_image(bounds, corner_radii, image, self.grayscale) - .log_err() - }); - } + }); }, ) } @@ -143,3 +157,29 @@ impl InteractiveElement for Img { &mut self.interactivity } } + +fn preserve_aspect_ratio(bounds: Bounds, image_size: Size) -> Bounds { + let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension))); + let image_ratio = image_size.width / image_size.height; + let bounds_ratio = bounds.size.width / bounds.size.height; + + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } +} diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index d57daca0627081f516658cbc35ca161a4e0d1fc0..490d0b61a1a5f0e0fdddb374d2a3cb788a49a99d 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -144,7 +144,6 @@ impl TextState { runs: Option>, cx: &mut WindowContext, ) -> LayoutId { - let text_system = cx.text_system().clone(); let text_style = cx.text_style(); let font_size = text_style.font_size.to_pixels(cx.rem_size()); let line_height = text_style @@ -152,18 +151,16 @@ impl TextState { .to_pixels(font_size.into(), cx.rem_size()); let text = SharedString::from(text); - let rem_size = cx.rem_size(); - let runs = if let Some(runs) = runs { runs } else { vec![text_style.to_run(text.len())] }; - let layout_id = cx.request_measured_layout(Default::default(), rem_size, { + let layout_id = cx.request_measured_layout(Default::default(), { let element_state = self.clone(); - move |known_dimensions, available_space| { + move |known_dimensions, available_space, cx| { let wrap_width = if text_style.white_space == WhiteSpace::Normal { known_dimensions.width.or(match available_space.width { crate::AvailableSpace::Definite(x) => Some(x), @@ -181,7 +178,8 @@ impl TextState { } } - let Some(lines) = text_system + let Some(lines) = cx + .text_system() .shape_text( &text, font_size, &runs, wrap_width, // Wrap if we know the width. ) diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 2d5a46f3d99ef4887714b905f24cfbd29d5410c5..d8f4cc6804788d6f06e379321a79ef50c9fbd4d0 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -109,7 +109,6 @@ impl Element for UniformList { cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let max_items = self.item_count; - let rem_size = cx.rem_size(); let item_size = state .as_ref() .map(|s| s.item_size) @@ -120,9 +119,7 @@ impl Element for UniformList { .layout(state.map(|s| s.interactive), cx, |style, cx| { cx.request_measured_layout( style, - rem_size, - move |known_dimensions: Size>, - available_space: Size| { + move |known_dimensions, available_space, _cx| { let desired_height = item_size.height * max_items; let width = known_dimensions diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 119c9cb8a6c57c7e8faea1cec78e91b7b3aa838a..20afd2d288b29bc8953c41c79389b9331a80244b 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -655,6 +655,20 @@ pub struct Corners { pub bottom_left: T, } +impl Corners +where + T: Clone + Default + Debug, +{ + pub fn all(value: T) -> Self { + Self { + top_left: value.clone(), + top_right: value.clone(), + bottom_right: value.clone(), + bottom_left: value, + } + } +} + impl Corners { pub fn to_pixels(&self, size: Size, rem_size: Pixels) -> Corners { let max = size.width.max(size.height) / 2.; @@ -905,6 +919,12 @@ impl From for usize { } } +impl From for Pixels { + fn from(pixels: usize) -> Self { + Pixels(pixels as f32) + } +} + #[derive( Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign, )] @@ -959,6 +979,18 @@ impl From for DevicePixels { } } +impl From for usize { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as usize + } +} + +impl From for DevicePixels { + fn from(device_pixels: usize) -> Self { + DevicePixels(device_pixels as i32) + } +} + #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] #[repr(transparent)] pub struct ScaledPixels(pub(crate) f32); diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 3027c05fbd84c27c6f51fe02d5da84f38815d067..7375f47939899d8d9902e400b337da723f96dc34 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -158,6 +158,11 @@ pub(crate) trait PlatformWindow { fn draw(&self, scene: Scene); fn sprite_atlas(&self) -> Arc; + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestWindow> { + None + } } pub trait PlatformDispatcher: Send + Sync { diff --git a/crates/gpui2/src/platform/mac/metal_renderer.rs b/crates/gpui2/src/platform/mac/metal_renderer.rs index 0631c75de5e222e43f780f29fb9844f5e96c099c..19afb503324907a0b84bcac7722fc71ead05451d 100644 --- a/crates/gpui2/src/platform/mac/metal_renderer.rs +++ b/crates/gpui2/src/platform/mac/metal_renderer.rs @@ -1,7 +1,7 @@ use crate::{ point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels, Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, - Quad, ScaledPixels, Scene, Shadow, Size, Underline, + Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, }; use cocoa::{ base::{NO, YES}, @@ -9,6 +9,9 @@ use cocoa::{ quartzcore::AutoresizingMask, }; use collections::HashMap; +use core_foundation::base::TCFType; +use foreign_types::ForeignType; +use media::core_video::CVMetalTextureCache; use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; use smallvec::SmallVec; @@ -27,9 +30,11 @@ pub(crate) struct MetalRenderer { underlines_pipeline_state: metal::RenderPipelineState, monochrome_sprites_pipeline_state: metal::RenderPipelineState, polychrome_sprites_pipeline_state: metal::RenderPipelineState, + surfaces_pipeline_state: metal::RenderPipelineState, unit_vertices: metal::Buffer, instances: metal::Buffer, sprite_atlas: Arc, + core_video_texture_cache: CVMetalTextureCache, } impl MetalRenderer { @@ -143,6 +148,14 @@ impl MetalRenderer { "polychrome_sprite_fragment", MTLPixelFormat::BGRA8Unorm, ); + let surfaces_pipeline_state = build_pipeline_state( + &device, + &library, + "surfaces", + "surface_vertex", + "surface_fragment", + MTLPixelFormat::BGRA8Unorm, + ); let command_queue = device.new_command_queue(); let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); @@ -157,9 +170,11 @@ impl MetalRenderer { underlines_pipeline_state, monochrome_sprites_pipeline_state, polychrome_sprites_pipeline_state, + surfaces_pipeline_state, unit_vertices, instances, sprite_atlas, + core_video_texture_cache: CVMetalTextureCache::new(device.as_ptr()).unwrap(), } } @@ -268,6 +283,14 @@ impl MetalRenderer { command_encoder, ); } + PrimitiveBatch::Surfaces(surfaces) => { + self.draw_surfaces( + surfaces, + &mut instance_offset, + viewport_size, + command_encoder, + ); + } } } @@ -793,6 +816,102 @@ impl MetalRenderer { ); *offset = next_offset; } + + fn draw_surfaces( + &mut self, + surfaces: &[Surface], + offset: &mut usize, + viewport_size: Size, + command_encoder: &metal::RenderCommandEncoderRef, + ) { + command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state); + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size as *const _, + ); + + for surface in surfaces { + let texture_size = size( + DevicePixels::from(surface.image_buffer.width() as i32), + DevicePixels::from(surface.image_buffer.height() as i32), + ); + + assert_eq!( + surface.image_buffer.pixel_format_type(), + media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange + ); + + let y_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + MTLPixelFormat::R8Unorm, + surface.image_buffer.plane_width(0), + surface.image_buffer.plane_height(0), + 0, + ) + .unwrap(); + let cb_cr_texture = self + .core_video_texture_cache + .create_texture_from_image( + surface.image_buffer.as_concrete_TypeRef(), + ptr::null(), + MTLPixelFormat::RG8Unorm, + surface.image_buffer.plane_width(1), + surface.image_buffer.plane_height(1), + 1, + ) + .unwrap(); + + align_offset(offset); + let next_offset = *offset + mem::size_of::(); + assert!( + next_offset <= INSTANCE_BUFFER_SIZE, + "instance buffer exhausted" + ); + + command_encoder.set_vertex_buffer( + SurfaceInputIndex::Surfaces as u64, + Some(&self.instances), + *offset as u64, + ); + command_encoder.set_vertex_bytes( + SurfaceInputIndex::TextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size as *const _, + ); + command_encoder.set_fragment_texture( + SurfaceInputIndex::YTexture as u64, + Some(y_texture.as_texture_ref()), + ); + command_encoder.set_fragment_texture( + SurfaceInputIndex::CbCrTexture as u64, + Some(cb_cr_texture.as_texture_ref()), + ); + + unsafe { + let buffer_contents = + (self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds; + ptr::write( + buffer_contents, + SurfaceBounds { + bounds: surface.bounds, + content_mask: surface.content_mask.clone(), + }, + ); + } + + command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6); + *offset = next_offset; + } + } } fn build_pipeline_state( @@ -898,6 +1017,16 @@ enum SpriteInputIndex { AtlasTexture = 4, } +#[repr(C)] +enum SurfaceInputIndex { + Vertices = 0, + Surfaces = 1, + ViewportSize = 2, + TextureSize = 3, + YTexture = 4, + CbCrTexture = 5, +} + #[repr(C)] enum PathRasterizationInputIndex { Vertices = 0, @@ -911,3 +1040,10 @@ pub struct PathSprite { pub color: Hsla, pub tile: AtlasTile, } + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(C)] +pub struct SurfaceBounds { + pub bounds: Bounds, + pub content_mask: ContentMask, +} diff --git a/crates/gpui2/src/platform/mac/shaders.metal b/crates/gpui2/src/platform/mac/shaders.metal index 4def1c33b85a430d051376322dca47de93b9e70c..aba01b9d5b059da1c1df55c0a01120d8be10775b 100644 --- a/crates/gpui2/src/platform/mac/shaders.metal +++ b/crates/gpui2/src/platform/mac/shaders.metal @@ -469,6 +469,58 @@ fragment float4 path_sprite_fragment( return color; } +struct SurfaceVertexOutput { + float4 position [[position]]; + float2 texture_position; + float clip_distance [[clip_distance]][4]; +}; + +struct SurfaceFragmentInput { + float4 position [[position]]; + float2 texture_position; +}; + +vertex SurfaceVertexOutput surface_vertex( + uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]], + constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]], + constant Size_DevicePixels *viewport_size + [[buffer(SurfaceInputIndex_ViewportSize)]], + constant Size_DevicePixels *texture_size + [[buffer(SurfaceInputIndex_TextureSize)]]) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + SurfaceBounds surface = surfaces[surface_id]; + float4 device_position = + to_device_position(unit_vertex, surface.bounds, viewport_size); + float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds, + surface.content_mask.bounds); + // We are going to copy the whole texture, so the texture position corresponds + // to the current vertex of the unit triangle. + float2 texture_position = unit_vertex; + return SurfaceVertexOutput{ + device_position, + texture_position, + {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}}; +} + +fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]], + texture2d y_texture + [[texture(SurfaceInputIndex_YTexture)]], + texture2d cb_cr_texture + [[texture(SurfaceInputIndex_CbCrTexture)]]) { + constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear); + const float4x4 ycbcrToRGBTransform = + float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), + float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), + float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), + float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); + float4 ycbcr = float4( + y_texture.sample(texture_sampler, input.texture_position).r, + cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0); + + return ycbcrToRGBTransform * ycbcr; +} + float4 hsla_to_rgba(Hsla hsla) { float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range float s = hsla.s; diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index df1ed9b2a6dee715fb707b821e0cdba533ac90b2..4532b33f5037bcf5ab139a7acb3708ade901dd88 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -189,13 +189,9 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_become_active(&self, _callback: Box) { - unimplemented!() - } + fn on_become_active(&self, _callback: Box) {} - fn on_resign_active(&self, _callback: Box) { - unimplemented!() - } + fn on_resign_active(&self, _callback: Box) {} fn on_quit(&self, _callback: Box) {} diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index e355c3aa4b5ec0681faad153f24c1f3ca5a4fceb..2ad54eff0d0f8d33f97685f5032729291f720fb6 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -11,19 +11,20 @@ use std::{ }; #[derive(Default)] -struct Handlers { - active_status_change: Vec>, - input: Vec bool>>, - moved: Vec>, - resize: Vec, f32)>>, +pub(crate) struct TestWindowHandlers { + pub(crate) active_status_change: Vec>, + pub(crate) input: Vec bool>>, + pub(crate) moved: Vec>, + pub(crate) resize: Vec, f32)>>, } pub struct TestWindow { bounds: WindowBounds, current_scene: Mutex>, display: Rc, + pub(crate) window_title: Option, pub(crate) input_handler: Option>>>, - handlers: Mutex, + pub(crate) handlers: Arc>, platform: Weak, sprite_atlas: Arc, } @@ -42,6 +43,7 @@ impl TestWindow { input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), + window_title: Default::default(), } } } @@ -100,8 +102,8 @@ impl PlatformWindow for TestWindow { todo!() } - fn set_title(&mut self, _title: &str) { - todo!() + fn set_title(&mut self, title: &str) { + self.window_title = Some(title.to_owned()); } fn set_edited(&mut self, _edited: bool) { @@ -167,6 +169,10 @@ impl PlatformWindow for TestWindow { fn sprite_atlas(&self) -> sync::Arc { self.sprite_atlas.clone() } + + fn as_test(&self) -> Option<&TestWindow> { + Some(self) + } } pub struct TestAtlasState { diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index 87e89adfa0e15d18512d2a1a748ea5a212a48489..549260560236ffb43179caf6cb8fce5340c10b7a 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -25,6 +25,7 @@ pub(crate) struct SceneBuilder { underlines: Vec, monochrome_sprites: Vec, polychrome_sprites: Vec, + surfaces: Vec, } impl Default for SceneBuilder { @@ -38,6 +39,7 @@ impl Default for SceneBuilder { underlines: Vec::new(), monochrome_sprites: Vec::new(), polychrome_sprites: Vec::new(), + surfaces: Vec::new(), } } } @@ -120,6 +122,7 @@ impl SceneBuilder { (PrimitiveKind::PolychromeSprite, ix) => { self.polychrome_sprites[ix].order = draw_order as DrawOrder } + (PrimitiveKind::Surface, ix) => self.surfaces[ix].order = draw_order as DrawOrder, } } @@ -129,6 +132,7 @@ impl SceneBuilder { self.underlines.sort_unstable(); self.monochrome_sprites.sort_unstable(); self.polychrome_sprites.sort_unstable(); + self.surfaces.sort_unstable(); Scene { shadows: mem::take(&mut self.shadows), @@ -137,6 +141,7 @@ impl SceneBuilder { underlines: mem::take(&mut self.underlines), monochrome_sprites: mem::take(&mut self.monochrome_sprites), polychrome_sprites: mem::take(&mut self.polychrome_sprites), + surfaces: mem::take(&mut self.surfaces), } } @@ -185,6 +190,10 @@ impl SceneBuilder { sprite.order = layer_id; self.polychrome_sprites.push(sprite); } + Primitive::Surface(mut surface) => { + surface.order = layer_id; + self.surfaces.push(surface); + } } } } @@ -196,6 +205,7 @@ pub(crate) struct Scene { pub underlines: Vec, pub monochrome_sprites: Vec, pub polychrome_sprites: Vec, + pub surfaces: Vec, } impl Scene { @@ -224,6 +234,9 @@ impl Scene { polychrome_sprites: &self.polychrome_sprites, polychrome_sprites_start: 0, polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), + surfaces: &self.surfaces, + surfaces_start: 0, + surfaces_iter: self.surfaces.iter().peekable(), } } } @@ -247,6 +260,9 @@ struct BatchIterator<'a> { polychrome_sprites: &'a [PolychromeSprite], polychrome_sprites_start: usize, polychrome_sprites_iter: Peekable>, + surfaces: &'a [Surface], + surfaces_start: usize, + surfaces_iter: Peekable>, } impl<'a> Iterator for BatchIterator<'a> { @@ -272,6 +288,10 @@ impl<'a> Iterator for BatchIterator<'a> { self.polychrome_sprites_iter.peek().map(|s| s.order), PrimitiveKind::PolychromeSprite, ), + ( + self.surfaces_iter.peek().map(|s| s.order), + PrimitiveKind::Surface, + ), ]; orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); @@ -378,6 +398,21 @@ impl<'a> Iterator for BatchIterator<'a> { sprites: &self.polychrome_sprites[sprites_start..sprites_end], }) } + PrimitiveKind::Surface => { + let surfaces_start = self.surfaces_start; + let mut surfaces_end = surfaces_start; + while self + .surfaces_iter + .next_if(|surface| surface.order <= max_order) + .is_some() + { + surfaces_end += 1; + } + self.surfaces_start = surfaces_end; + Some(PrimitiveBatch::Surfaces( + &self.surfaces[surfaces_start..surfaces_end], + )) + } } } } @@ -391,6 +426,7 @@ pub enum PrimitiveKind { Underline, MonochromeSprite, PolychromeSprite, + Surface, } pub enum Primitive { @@ -400,6 +436,7 @@ pub enum Primitive { Underline(Underline), MonochromeSprite(MonochromeSprite), PolychromeSprite(PolychromeSprite), + Surface(Surface), } impl Primitive { @@ -411,6 +448,7 @@ impl Primitive { Primitive::Underline(underline) => &underline.bounds, Primitive::MonochromeSprite(sprite) => &sprite.bounds, Primitive::PolychromeSprite(sprite) => &sprite.bounds, + Primitive::Surface(surface) => &surface.bounds, } } @@ -422,6 +460,7 @@ impl Primitive { Primitive::Underline(underline) => &underline.content_mask, Primitive::MonochromeSprite(sprite) => &sprite.content_mask, Primitive::PolychromeSprite(sprite) => &sprite.content_mask, + Primitive::Surface(surface) => &surface.content_mask, } } } @@ -440,6 +479,7 @@ pub(crate) enum PrimitiveBatch<'a> { texture_id: AtlasTextureId, sprites: &'a [PolychromeSprite], }, + Surfaces(&'a [Surface]), } #[derive(Default, Debug, Clone, Eq, PartialEq)] @@ -593,6 +633,32 @@ impl From for Primitive { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Surface { + pub order: u32, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub image_buffer: media::core_video::CVImageBuffer, +} + +impl Ord for Surface { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.order.cmp(&other.order) + } +} + +impl PartialOrd for Surface { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for Primitive { + fn from(surface: Surface) -> Self { + Primitive::Surface(surface) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct PathId(pub(crate) usize); diff --git a/crates/gpui2/src/taffy.rs b/crates/gpui2/src/taffy.rs index 81a057055a1af794d0d19f0921d7807e09417f70..2bceb1bc139be7af1aedc67827da582f319bfc87 100644 --- a/crates/gpui2/src/taffy.rs +++ b/crates/gpui2/src/taffy.rs @@ -1,4 +1,7 @@ -use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style}; +use crate::{ + AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style, + WindowContext, +}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::fmt::Debug; @@ -9,13 +12,21 @@ use taffy::{ Taffy, }; -type Measureable = dyn Fn(Size>, Size) -> Size + Send + Sync; - pub struct TaffyLayoutEngine { - taffy: Taffy>, + taffy: Taffy, children_to_parents: HashMap, absolute_layout_bounds: HashMap>, computed_layouts: HashSet, + nodes_to_measure: HashMap< + LayoutId, + Box< + dyn FnMut( + Size>, + Size, + &mut WindowContext, + ) -> Size, + >, + >, } static EXPECT_MESSAGE: &'static str = @@ -28,6 +39,7 @@ impl TaffyLayoutEngine { children_to_parents: HashMap::default(), absolute_layout_bounds: HashMap::default(), computed_layouts: HashSet::default(), + nodes_to_measure: HashMap::default(), } } @@ -36,6 +48,7 @@ impl TaffyLayoutEngine { self.children_to_parents.clear(); self.absolute_layout_bounds.clear(); self.computed_layouts.clear(); + self.nodes_to_measure.clear(); } pub fn request_layout( @@ -65,18 +78,18 @@ impl TaffyLayoutEngine { &mut self, style: Style, rem_size: Pixels, - measure: impl Fn(Size>, Size) -> Size - + Send - + Sync + measure: impl FnMut(Size>, Size, &mut WindowContext) -> Size + 'static, ) -> LayoutId { let style = style.to_taffy(rem_size); - let measurable = Box::new(measure); - self.taffy - .new_leaf_with_context(style, measurable) + let layout_id = self + .taffy + .new_leaf_with_context(style, ()) .expect(EXPECT_MESSAGE) - .into() + .into(); + self.nodes_to_measure.insert(layout_id, Box::new(measure)); + layout_id } // Used to understand performance @@ -126,7 +139,12 @@ impl TaffyLayoutEngine { Ok(edges) } - pub fn compute_layout(&mut self, id: LayoutId, available_space: Size) { + pub fn compute_layout( + &mut self, + id: LayoutId, + available_space: Size, + cx: &mut WindowContext, + ) { // Leaving this here until we have a better instrumentation approach. // println!("Laying out {} children", self.count_all_children(id)?); // println!("Max layout depth: {}", self.max_depth(0, id)?); @@ -159,8 +177,8 @@ impl TaffyLayoutEngine { .compute_layout_with_measure( id.into(), available_space.into(), - |known_dimensions, available_space, _node_id, context| { - let Some(measure) = context else { + |known_dimensions, available_space, node_id, _context| { + let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else { return taffy::geometry::Size::default(); }; @@ -169,10 +187,11 @@ impl TaffyLayoutEngine { height: known_dimensions.height.map(Pixels), }; - measure(known_dimensions, available_space.into()).into() + measure(known_dimensions, available_space.into(), cx).into() }, ) .expect(EXPECT_MESSAGE); + // println!("compute_layout took {:?}", started_at.elapsed()); } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index f31b0ae753c2a759a776857658c925eb34787dee..280c52df2afad19af029a75e336222eae82aa74e 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -209,9 +209,7 @@ impl AnyView { ) { cx.with_absolute_element_offset(origin, |cx| { let (layout_id, rendered_element) = (self.layout)(self, cx); - cx.window - .layout_engine - .compute_layout(layout_id, available_space); + cx.compute_layout(layout_id, available_space); (self.paint)(self, rendered_element, cx); }) } @@ -240,6 +238,10 @@ impl Element for AnyView { } fn paint(self, _: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + debug_assert!( + state.is_some(), + "state is None. Did you include an AnyView twice in the tree?" + ); (self.paint)(&self, state.take().unwrap(), cx) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 1f2df0788d72760c0929d3213b3ec0a20e1410d1..5724f1e0701a2b960afb478fad0186649c29debd 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -8,8 +8,8 @@ use crate::{ MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, - Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, - VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, + UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; @@ -18,6 +18,7 @@ use futures::{ channel::{mpsc, oneshot}, StreamExt, }; +use media::core_video::CVImageBuffer; use parking_lot::RwLock; use slotmap::SlotMap; use smallvec::SmallVec; @@ -208,7 +209,7 @@ pub struct Window { sprite_atlas: Arc, rem_size: Pixels, viewport_size: Size, - pub(crate) layout_engine: TaffyLayoutEngine, + layout_engine: Option, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, pub(crate) previous_frame: Frame, @@ -326,7 +327,7 @@ impl Window { sprite_atlas, rem_size: px(16.), viewport_size: content_size, - layout_engine: TaffyLayoutEngine::new(), + layout_engine: Some(TaffyLayoutEngine::new()), root_view: None, element_id_stack: GlobalElementId::default(), previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), @@ -605,9 +606,11 @@ impl<'a> WindowContext<'a> { self.app.layout_id_buffer.extend(children.into_iter()); let rem_size = self.rem_size(); - self.window - .layout_engine - .request_layout(style, rem_size, &self.app.layout_id_buffer) + self.window.layout_engine.as_mut().unwrap().request_layout( + style, + rem_size, + &self.app.layout_id_buffer, + ) } /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, @@ -617,22 +620,25 @@ impl<'a> WindowContext<'a> { /// The given closure is invoked at layout time with the known dimensions and available space and /// returns a `Size`. pub fn request_measured_layout< - F: Fn(Size>, Size) -> Size + Send + Sync + 'static, + F: FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, >( &mut self, style: Style, - rem_size: Pixels, measure: F, ) -> LayoutId { + let rem_size = self.rem_size(); self.window .layout_engine + .as_mut() + .unwrap() .request_measured_layout(style, rem_size, measure) } pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { - self.window - .layout_engine - .compute_layout(layout_id, available_space) + let mut layout_engine = self.window.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self); + self.window.layout_engine = Some(layout_engine); } /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not @@ -642,6 +648,8 @@ impl<'a> WindowContext<'a> { let mut bounds = self .window .layout_engine + .as_mut() + .unwrap() .layout_bounds(layout_id) .map(Into::into); bounds.origin += self.element_offset(); @@ -677,6 +685,10 @@ impl<'a> WindowContext<'a> { self.window.platform_window.zoom(); } + pub fn set_window_title(&mut self, title: &str) { + self.window.platform_window.set_title(title); + } + pub fn display(&self) -> Option> { self.platform .displays() @@ -1116,6 +1128,23 @@ impl<'a> WindowContext<'a> { Ok(()) } + /// Paint a surface into the scene for the current frame at the current z-index. + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + let window = &mut *self.window; + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, + Surface { + order: 0, + bounds, + content_mask, + image_buffer, + }, + ); + } + /// Draw pixels to the display for this window based on the contents of its scene. pub(crate) fn draw(&mut self) { let root_view = self.window.root_view.take().unwrap(); @@ -1171,7 +1200,7 @@ impl<'a> WindowContext<'a> { self.text_system().start_frame(); let window = &mut *self.window; - window.layout_engine.clear(); + window.layout_engine.as_mut().unwrap().clear(); mem::swap(&mut window.previous_frame, &mut window.current_frame); let frame = &mut window.current_frame; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index af7504529cefbee215fb96e53798242da340a4d6..811e54940672ee075993d8cd981c945199409675 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -197,8 +197,12 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { - self.adapter.workspace_configuration(cx) + pub fn workspace_configuration( + &self, + workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { + self.adapter.workspace_configuration(workspace_root, cx) } pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -312,7 +316,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> { futures::future::ready(serde_json::json!({})).boxed() } diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 5c17592f0ca3ff2a1a352954b79e2ed0a937079e..8fdf524f69e16a219bb264504051bb96f7e565ad 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -200,8 +200,12 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { - self.adapter.workspace_configuration(cx) + pub fn workspace_configuration( + &self, + workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { + self.adapter.workspace_configuration(workspace_root, cx) } pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -315,7 +319,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> { futures::future::ready(serde_json::json!({})).boxed() } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 98fd81f012f163402af47a840ae765e8760b062b..dc5b63d222d0f1545dfce7eab0748f5fd9c23e2d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -429,8 +429,8 @@ impl LanguageServer { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { - process_id: Default::default(), - root_path: Default::default(), + process_id: None, + root_path: None, root_uri: Some(root_uri.clone()), initialization_options: options, capabilities: ClientCapabilities { @@ -451,12 +451,15 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), + diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + refresh_support: None, + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - ..Default::default() + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -501,7 +504,7 @@ impl LanguageServer { }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - ..Default::default() + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -515,6 +518,20 @@ impl LanguageServer { }), dynamic_registration: Some(false), }), + publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { + related_information: Some(true), + ..Default::default() + }), + formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + on_type_formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + diagnostic: Some(DiagnosticClientCapabilities { + related_document_support: Some(true), + dynamic_registration: None, + }), ..Default::default() }), experimental: Some(json!({ @@ -524,15 +541,15 @@ impl LanguageServer { work_done_progress: Some(true), ..Default::default() }), - ..Default::default() + general: None, }, - trace: Default::default(), + trace: None, workspace_folders: Some(vec![WorkspaceFolder { uri: root_uri, name: Default::default(), }]), - client_info: Default::default(), - locale: Default::default(), + client_info: None, + locale: None, }; let response = self.request::(params).await?; diff --git a/crates/lsp2/src/lsp2.rs b/crates/lsp2/src/lsp2.rs index 356d029c587b57aaaf182bb090d82d42f14123d9..788c424373deca7c1490dd954fa005e0943d8a99 100644 --- a/crates/lsp2/src/lsp2.rs +++ b/crates/lsp2/src/lsp2.rs @@ -434,8 +434,8 @@ impl LanguageServer { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { - process_id: Default::default(), - root_path: Default::default(), + process_id: None, + root_path: None, root_uri: Some(root_uri.clone()), initialization_options: options, capabilities: ClientCapabilities { @@ -456,12 +456,15 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), + diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + refresh_support: None, + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - ..Default::default() + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -503,7 +506,7 @@ impl LanguageServer { }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - ..Default::default() + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -517,6 +520,20 @@ impl LanguageServer { }), dynamic_registration: Some(false), }), + publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { + related_information: Some(true), + ..Default::default() + }), + formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + on_type_formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + diagnostic: Some(DiagnosticClientCapabilities { + related_document_support: Some(true), + dynamic_registration: None, + }), ..Default::default() }), experimental: Some(json!({ @@ -526,15 +543,15 @@ impl LanguageServer { work_done_progress: Some(true), ..Default::default() }), - ..Default::default() + general: None, }, - trace: Default::default(), + trace: None, workspace_folders: Some(vec![WorkspaceFolder { uri: root_uri, name: Default::default(), }]), - client_info: Default::default(), - locale: Default::default(), + client_info: None, + locale: None, }; let response = self.request::(params).await?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a2ad82585eadb9d4e010fe953ae17319ad23728f..21d64fe91f8509496c7641225863a86dcd2945ce 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2641,8 +2641,9 @@ impl Project { }); for (adapter, server) in servers { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(server.root_path(), cx)) + .await; server .notify::( lsp::DidChangeConfigurationParams { @@ -2753,7 +2754,7 @@ impl Project { stderr_capture.clone(), language.clone(), adapter.clone(), - worktree_path, + Arc::clone(&worktree_path), ProjectLspAdapterDelegate::new(self, cx), cx, ) { @@ -2776,6 +2777,7 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let result = Self::setup_and_insert_language_server( this, + &worktree_path, override_options, pending_server, adapter.clone(), @@ -2891,6 +2893,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModelHandle, + worktree_path: &Path, override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -2903,6 +2906,7 @@ impl Project { this, override_initialization_options, pending_server, + worktree_path, adapter.clone(), server_id, cx, @@ -2932,11 +2936,14 @@ impl Project { this: WeakModelHandle, override_options: Option, pending_server: PendingLanguageServer, + worktree_path: &Path, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(worktree_path, cx)) + .await; let language_server = pending_server.task.await?; language_server @@ -2964,11 +2971,14 @@ impl Project { language_server .on_request::({ let adapter = adapter.clone(); + let worktree_path = worktree_path.to_path_buf(); move |params, mut cx| { let adapter = adapter.clone(); + let worktree_path = worktree_path.clone(); async move { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(&worktree_path, cx)) + .await; Ok(params .items .into_iter() @@ -6523,9 +6533,15 @@ impl Project { }) } - pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { + pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary { let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in self.diagnostic_summaries(cx) { + for (_, _, path_summary) in + self.diagnostic_summaries(include_ignored, cx) + .filter(|(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) + { summary.error_count += path_summary.error_count; summary.warning_count += path_summary.warning_count; } @@ -6534,6 +6550,7 @@ impl Project { pub fn diagnostic_summaries<'a>( &'a self, + include_ignored: bool, cx: &'a AppContext, ) -> impl Iterator + 'a { self.visible_worktrees(cx).flat_map(move |worktree| { @@ -6544,6 +6561,10 @@ impl Project { .map(move |(path, server_id, summary)| { (ProjectPath { worktree_id, path }, server_id, summary) }) + .filter(move |(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 264c1ff7b54fa52dbffd87545603736704a5f932..5d061b868fb37dd730d09ea184fd3f7a91d447be 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -806,7 +806,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { +async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -814,7 +814,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { "/root", json!({ "dir": { + ".git": { + "HEAD": "ref: refs/heads/main", + }, + ".gitignore": "b.rs", "a.rs": "let a = 1;", + "b.rs": "let b = 2;", }, "other.rs": "let b = c;" }), @@ -822,6 +827,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root/dir", true, cx) + }) + .await + .unwrap(); + let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id()); let (worktree, _) = project .update(cx, |project, cx| { @@ -829,12 +841,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); + let other_worktree_id = worktree.read_with(cx, |tree, _| tree.id()); + let server_id = LanguageServerId(0); project.update(cx, |project, cx| { project .update_diagnostics( - LanguageServerId(0), + server_id, + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/root/dir/b.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "unused variable 'b'".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + project + .update_diagnostics( + server_id, lsp::PublishDiagnosticsParams { uri: Url::from_file_path("/root/other.rs").unwrap(), version: None, @@ -851,11 +881,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .unwrap(); }); - let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + let main_ignored_buffer = project + .update(cx, |project, cx| { + project.open_buffer((main_worktree_id, "b.rs"), cx) + }) .await .unwrap(); - buffer.read_with(cx, |buffer, _| { + main_ignored_buffer.read_with(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let ", None), + ("b", Some(DiagnosticSeverity::ERROR)), + (" = 2;", None), + ], + "Gigitnored buffers should still get in-buffer diagnostics", + ); + }); + let other_buffer = project + .update(cx, |project, cx| { + project.open_buffer((other_worktree_id, ""), cx) + }) + .await + .unwrap(); + other_buffer.read_with(cx, |buffer, _| { let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); assert_eq!( chunks @@ -866,13 +919,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ("let b = ", None), ("c", Some(DiagnosticSeverity::ERROR)), (";", None), - ] + ], + "Buffers from hidden projects should still get in-buffer diagnostics" ); }); project.read_with(cx, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).next(), None); - assert_eq!(project.diagnostic_summary(cx).error_count, 0); + assert_eq!(project.diagnostic_summaries(false, cx).next(), None); + assert_eq!( + project.diagnostic_summaries(true, cx).collect::>(), + vec![( + ProjectPath { + worktree_id: main_worktree_id, + path: Arc::from(Path::new("b.rs")), + }, + server_id, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + } + )] + ); + assert_eq!(project.diagnostic_summary(false, cx).error_count, 0); + assert_eq!(project.diagnostic_summary(true, cx).error_count, 1); }); } @@ -1145,7 +1214,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.read_with(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 1, warning_count: 0, @@ -1171,7 +1240,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.read_with(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 0, warning_count: 0, @@ -1763,7 +1832,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC .unwrap(); assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 2, warning_count: 0, diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index d2cc4fe406a0eb67de102e624ab9025338ad8689..12940dd2c427f8872fd95c9a1c66a5535738463e 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -2677,8 +2677,9 @@ impl Project { })?; for (adapter, server) in servers { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(server.root_path(), cx))? + .await; server .notify::( lsp::DidChangeConfigurationParams { @@ -2790,7 +2791,7 @@ impl Project { stderr_capture.clone(), language.clone(), adapter.clone(), - worktree_path, + Arc::clone(&worktree_path), ProjectLspAdapterDelegate::new(self, cx), cx, ) { @@ -2822,6 +2823,7 @@ impl Project { cx.spawn(move |this, mut cx| async move { let result = Self::setup_and_insert_language_server( this.clone(), + &worktree_path, initialization_options, pending_server, adapter.clone(), @@ -2942,6 +2944,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModel, + worktree_path: &Path, initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -2954,6 +2957,7 @@ impl Project { this.clone(), initialization_options, pending_server, + worktree_path, adapter.clone(), server_id, cx, @@ -2983,11 +2987,14 @@ impl Project { this: WeakModel, initialization_options: Option, pending_server: PendingLanguageServer, + worktree_path: &Path, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(worktree_path, cx))? + .await; let language_server = pending_server.task.await?; language_server @@ -3016,11 +3023,14 @@ impl Project { language_server .on_request::({ let adapter = adapter.clone(); + let worktree_path = worktree_path.to_path_buf(); move |params, cx| { let adapter = adapter.clone(); + let worktree_path = worktree_path.clone(); async move { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(&worktree_path, cx))? + .await; Ok(params .items .into_iter() @@ -6596,9 +6606,15 @@ impl Project { }) } - pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { + pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary { let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in self.diagnostic_summaries(cx) { + for (_, _, path_summary) in + self.diagnostic_summaries(include_ignored, cx) + .filter(|(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) + { summary.error_count += path_summary.error_count; summary.warning_count += path_summary.warning_count; } @@ -6607,17 +6623,23 @@ impl Project { pub fn diagnostic_summaries<'a>( &'a self, + include_ignored: bool, cx: &'a AppContext, ) -> impl Iterator + 'a { - self.visible_worktrees(cx).flat_map(move |worktree| { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - worktree - .diagnostic_summaries() - .map(move |(path, server_id, summary)| { - (ProjectPath { worktree_id, path }, server_id, summary) - }) - }) + self.visible_worktrees(cx) + .flat_map(move |worktree| { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + worktree + .diagnostic_summaries() + .map(move |(path, server_id, summary)| { + (ProjectPath { worktree_id, path }, server_id, summary) + }) + }) + .filter(move |(path, _, _)| { + let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored); + include_ignored || worktree == Some(false) + }) } pub fn disk_based_diagnostics_started( diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 53b2f6ba1fc7cc04489bce5256a0954b9e2bf7ff..4dfb8004e3e644309b89ee31d99f5ac07e05f4b3 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -823,7 +823,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { +async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); @@ -831,7 +831,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { "/root", json!({ "dir": { + ".git": { + "HEAD": "ref: refs/heads/main", + }, + ".gitignore": "b.rs", "a.rs": "let a = 1;", + "b.rs": "let b = 2;", }, "other.rs": "let b = c;" }), @@ -839,6 +844,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root/dir", true, cx) + }) + .await + .unwrap(); + let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id()); let (worktree, _) = project .update(cx, |project, cx| { @@ -846,12 +858,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { }) .await .unwrap(); - let worktree_id = worktree.update(cx, |tree, _| tree.id()); + let other_worktree_id = worktree.update(cx, |tree, _| tree.id()); + let server_id = LanguageServerId(0); project.update(cx, |project, cx| { project .update_diagnostics( - LanguageServerId(0), + server_id, + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/root/dir/b.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "unused variable 'b'".to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap(); + project + .update_diagnostics( + server_id, lsp::PublishDiagnosticsParams { uri: Url::from_file_path("/root/other.rs").unwrap(), version: None, @@ -868,11 +898,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { .unwrap(); }); - let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + let main_ignored_buffer = project + .update(cx, |project, cx| { + project.open_buffer((main_worktree_id, "b.rs"), cx) + }) .await .unwrap(); - buffer.update(cx, |buffer, _| { + main_ignored_buffer.update(cx, |buffer, _| { + let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); + assert_eq!( + chunks + .iter() + .map(|(s, d)| (s.as_str(), *d)) + .collect::>(), + &[ + ("let ", None), + ("b", Some(DiagnosticSeverity::ERROR)), + (" = 2;", None), + ], + "Gigitnored buffers should still get in-buffer diagnostics", + ); + }); + let other_buffer = project + .update(cx, |project, cx| { + project.open_buffer((other_worktree_id, ""), cx) + }) + .await + .unwrap(); + other_buffer.update(cx, |buffer, _| { let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); assert_eq!( chunks @@ -883,13 +936,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ("let b = ", None), ("c", Some(DiagnosticSeverity::ERROR)), (";", None), - ] + ], + "Buffers from hidden projects should still get in-buffer diagnostics" ); }); project.update(cx, |project, cx| { - assert_eq!(project.diagnostic_summaries(cx).next(), None); - assert_eq!(project.diagnostic_summary(cx).error_count, 0); + assert_eq!(project.diagnostic_summaries(false, cx).next(), None); + assert_eq!( + project.diagnostic_summaries(true, cx).collect::>(), + vec![( + ProjectPath { + worktree_id: main_worktree_id, + path: Arc::from(Path::new("b.rs")), + }, + server_id, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + } + )] + ); + assert_eq!(project.diagnostic_summary(false, cx).error_count, 0); + assert_eq!(project.diagnostic_summary(true, cx).error_count, 1); }); } @@ -1162,7 +1231,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 1, warning_count: 0, @@ -1188,7 +1257,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp }); project.update(cx, |project, cx| { assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 0, warning_count: 0, @@ -1777,7 +1846,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC .unwrap(); assert_eq!( - project.diagnostic_summary(cx), + project.diagnostic_summary(false, cx), DiagnosticSummary { error_count: 2, warning_count: 0, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index eb124bfca28840f4b99a3b022abbbee33611fc0e..875d4d4f83736aa6c768b6dda21e915e889afc06 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1627,9 +1627,21 @@ impl View for ProjectPanel { } } - fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { + fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { Self::reset_to_default_keymap_context(keymap); keymap.add_identifier("menu"); + + if let Some(window) = cx.active_window() { + window.read_with(cx, |cx| { + let identifier = if self.filename_editor.is_focused(cx) { + "editing" + } else { + "not_editing" + }; + + keymap.add_identifier(identifier); + }); + } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index dc584d52ff520a810b27ecc2a803738491643950..0a5a63f14a289f5f8aeb44a20a454a35f8bea20b 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -10,9 +10,9 @@ use anyhow::{anyhow, Result}; use gpui::{ actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, - PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + InteractiveElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, + Point, PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, + View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -29,8 +29,7 @@ use std::{ path::Path, sync::Arc, }; -use theme::ActiveTheme as _; -use ui::{v_stack, ContextMenu, IconElement, Label, ListItem}; +use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -1421,6 +1420,22 @@ impl ProjectPanel { // ); // }) } + + fn dispatch_context(&self, cx: &ViewContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("ProjectPanel"); + dispatch_context.add("menu"); + + let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) { + "editing" + } else { + "not_editing" + }; + + dispatch_context.add(identifier); + + dispatch_context + } } impl Render for ProjectPanel { @@ -1434,7 +1449,7 @@ impl Render for ProjectPanel { .id("project-panel") .size_full() .relative() - .key_context("ProjectPanel") + .key_context(self.dispatch_context(cx)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::expand_selected_entry)) @@ -2845,7 +2860,7 @@ mod tests { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.selection = Some(Selection { + panel.selection = Some(crate::Selection { worktree_id: worktree.id(), entry_id, }); diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 65a4ddfd422abc5700a11a90fd15391e82d8ea78..13def6b4a7ea4d6918353f117e37d5ee8fd3a96c 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -4,7 +4,7 @@ use gpui::{actions, Action, AppContext, IntoElement}; pub use mode::SearchMode; use project::search::SearchQuery; use ui::prelude::*; -use ui::{ButtonStyle2, Icon, IconButton}; +use ui::{ButtonStyle, Icon, IconButton}; //pub use project_search::{ProjectSearchBar, ProjectSearchView}; // use theme::components::{ // action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, @@ -91,8 +91,8 @@ impl SearchOptions { cx.dispatch_action(action.boxed_clone()); } }) - .style(ButtonStyle2::Subtle) - .when(active, |button| button.style(ButtonStyle2::Filled)) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) } } @@ -103,8 +103,8 @@ fn toggle_replace_button(active: bool) -> impl IntoElement { cx.dispatch_action(Box::new(ToggleReplace)); cx.notify(); }) - .style(ButtonStyle2::Subtle) - .when(active, |button| button.style(ButtonStyle2::Filled)) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) } fn render_replace_button( diff --git a/crates/storybook2/src/stories.rs b/crates/storybook2/src/stories.rs index 0eaf3d126c216650161896fa28cdb404aa1e30ac..2d63d1d491a497f8d45c3ce6736a17bae0022fcf 100644 --- a/crates/storybook2/src/stories.rs +++ b/crates/storybook2/src/stories.rs @@ -1,3 +1,4 @@ +mod auto_height_editor; mod focus; mod kitchen_sink; mod picker; @@ -5,6 +6,7 @@ mod scroll; mod text; mod z_index; +pub use auto_height_editor::*; pub use focus::*; pub use kitchen_sink::*; pub use picker::*; diff --git a/crates/storybook2/src/stories/auto_height_editor.rs b/crates/storybook2/src/stories/auto_height_editor.rs new file mode 100644 index 0000000000000000000000000000000000000000..2f3089a4e6e766057559376f284e570b19701c5c --- /dev/null +++ b/crates/storybook2/src/stories/auto_height_editor.rs @@ -0,0 +1,34 @@ +use editor::Editor; +use gpui::{ + div, white, Div, KeyBinding, ParentElement, Render, Styled, View, ViewContext, VisualContext, + WindowContext, +}; + +pub struct AutoHeightEditorStory { + editor: View, +} + +impl AutoHeightEditorStory { + pub fn new(cx: &mut WindowContext) -> View { + cx.bind_keys([KeyBinding::new("enter", editor::Newline, Some("Editor"))]); + cx.build_view(|cx| Self { + editor: cx.build_view(|cx| { + let mut editor = Editor::auto_height(3, cx); + editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + editor + }), + }) + } +} + +impl Render for AutoHeightEditorStory { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .size_full() + .bg(white()) + .text_sm() + .child(div().w_32().bg(gpui::black()).child(self.editor.clone())) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 0354097c0b19b525662efbd630a4dd33c15515da..4fe76ce878d7e9488013f7484581f0134f0e1c40 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -12,6 +12,7 @@ use ui::prelude::*; #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ComponentStory { + AutoHeightEditor, Avatar, Button, Checkbox, @@ -23,6 +24,7 @@ pub enum ComponentStory { Keybinding, Label, List, + ListHeader, ListItem, Scroll, Text, @@ -33,6 +35,7 @@ pub enum ComponentStory { impl ComponentStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { + Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(), Self::Avatar => cx.build_view(|_| ui::AvatarStory).into(), Self::Button => cx.build_view(|_| ui::ButtonStory).into(), Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(), @@ -44,6 +47,7 @@ impl ComponentStory { Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), Self::Label => cx.build_view(|_| ui::LabelStory).into(), Self::List => cx.build_view(|_| ui::ListStory).into(), + Self::ListHeader => cx.build_view(|_| ui::ListHeaderStory).into(), Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(), Self::Scroll => ScrollStory::view(cx).into(), Self::Text => TextStory::view(cx).into(), diff --git a/crates/theme2/src/styles/stories/players.rs b/crates/theme2/src/styles/stories/players.rs index d189d3bfb07bb0efc4691ca14abac4d94948d620..237f2f10817872ea07c0f7fafa5745ee8722cf13 100644 --- a/crates/theme2/src/styles/stories/players.rs +++ b/crates/theme2/src/styles/stories/players.rs @@ -55,9 +55,8 @@ impl Render for PlayerStory { .border_2() .border_color(player.cursor) .child( - img() + img("https://avatars.githubusercontent.com/u/1714999?v=4") .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") .size_6() .bg(gpui::red()), ) @@ -67,51 +66,62 @@ impl Render for PlayerStory { .child(div().flex().gap_1().children( cx.theme().players().0.clone().iter_mut().map(|player| { div() - .my_1() - .rounded_xl() - .flex() - .items_center() - .h_8() - .py_0p5() - .px_1p5() - .bg(player.background) - .child( - div().relative().neg_mx_1().rounded_full().z_index(3) + .my_1() + .rounded_xl() + .flex() + .items_center() + .h_8() + .py_0p5() + .px_1p5() + .bg(player.background) + .child( + div() + .relative() + .neg_mx_1() + .rounded_full() + .z_index(3) .border_2() .border_color(player.background) .size(px(28.)) .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ).child( - div().relative().neg_mx_1().rounded_full().z_index(2) - .border_2() - .border_color(player.background) - .size(px(28.)) - .child( - img() + img("https://avatars.githubusercontent.com/u/1714999?v=4") + .rounded_full() + .size(px(24.)) + .bg(gpui::red()), + ), + ) + .child( + div() + .relative() + .neg_mx_1() .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ).child( - div().relative().neg_mx_1().rounded_full().z_index(1) - .border_2() - .border_color(player.background) - .size(px(28.)) + .z_index(2) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img("https://avatars.githubusercontent.com/u/1714999?v=4") + .rounded_full() + .size(px(24.)) + .bg(gpui::red()), + ), + ) .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ) + div() + .relative() + .neg_mx_1() + .rounded_full() + .z_index(1) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img("https://avatars.githubusercontent.com/u/1714999?v=4") + .rounded_full() + .size(px(24.)) + .bg(gpui::red()), + ), + ) }), )) .child(Story::label("Player Selections")) diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index 7b0a0c3d3ae72c646bf5590a29a272528f312279..be55194e76ebdaedc0e72aa4a9f9d4d6314fb3eb 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, - SharedString, View, ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View, + ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; -use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings}; -use ui::ListItem; +use theme::{Theme, ThemeRegistry, ThemeSettings}; +use ui::{prelude::*, ListItem}; use util::ResultExt; use workspace::{ui::HighlightedLabel, Workspace}; diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 4d374d7bb1958aac826f1a64586c0d336aae1af7..3c8eefa6db2fad6551976949399204b0cbcc0e55 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -21,7 +21,7 @@ impl RenderOnce for Avatar { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let mut img = img(); + let mut img = img(self.src); if self.shape == Shape::Circle { img = img.rounded_full(); @@ -34,8 +34,7 @@ impl RenderOnce for Avatar { div() .size(size) .child( - img.source(self.src.clone()) - .size(size) + img.size(size) // todo!(Pull the avatar fallback background from the theme.) .bg(gpui::red()), ) diff --git a/crates/ui2/src/components/button/mod.rs b/crates/ui2/src/components/button.rs similarity index 100% rename from crates/ui2/src/components/button/mod.rs rename to crates/ui2/src/components/button.rs diff --git a/crates/ui2/src/components/button/button.rs b/crates/ui2/src/components/button/button.rs index b4e666e9adba796d2cdea6df55b591fd021f3519..ce26ee76a5df38c29e17c024bb839d0229b332d4 100644 --- a/crates/ui2/src/components/button/button.rs +++ b/crates/ui2/src/components/button/button.rs @@ -1,7 +1,7 @@ use gpui::AnyView; use crate::prelude::*; -use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle}; +use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Label, LineHeightStyle}; #[derive(IntoElement)] pub struct Button { @@ -54,12 +54,12 @@ impl ButtonCommon for Button { self.base.id() } - fn style(mut self, style: ButtonStyle2) -> Self { + fn style(mut self, style: ButtonStyle) -> Self { self.base = self.base.style(style); self } - fn size(mut self, size: ButtonSize2) -> Self { + fn size(mut self, size: ButtonSize) -> Self { self.base = self.base.size(size); self } @@ -79,7 +79,7 @@ impl RenderOnce for Button { } else if self.base.selected { Color::Selected } else { - Color::Default + self.label_color.unwrap_or_default() }; self.base.child( diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 6885ac86e1679ade99379601ac102813e2538027..29864a895d2acdfda6d745bd861314a8a8152f2c 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -1,4 +1,4 @@ -use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; +use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; use smallvec::SmallVec; use crate::h_stack; @@ -6,13 +6,13 @@ use crate::prelude::*; pub trait ButtonCommon: Clickable + Disableable { fn id(&self) -> &ElementId; - fn style(self, style: ButtonStyle2) -> Self; - fn size(self, size: ButtonSize2) -> Self; + fn style(self, style: ButtonStyle) -> Self; + fn size(self, size: ButtonSize) -> Self; fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self; } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum ButtonStyle2 { +pub enum ButtonStyle { #[default] Filled, // Tinted, @@ -21,54 +21,57 @@ pub enum ButtonStyle2 { } #[derive(Debug, Clone)] -pub struct ButtonStyle { +pub(crate) struct ButtonLikeStyles { pub background: Hsla, + #[allow(unused)] pub border_color: Hsla, + #[allow(unused)] pub label_color: Hsla, + #[allow(unused)] pub icon_color: Hsla, } -impl ButtonStyle2 { - pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle { +impl ButtonStyle { + pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_background, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, } } - pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle { + pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_hover, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_hover, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), // TODO: These are not great label_color: Color::Muted.color(cx), // TODO: These are not great @@ -77,23 +80,23 @@ impl ButtonStyle2 { } } - pub fn active(self, cx: &mut WindowContext) -> ButtonStyle { + pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_active, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_active, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), // TODO: These are not great label_color: Color::Muted.color(cx), // TODO: These are not great @@ -102,22 +105,23 @@ impl ButtonStyle2 { } } - pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle { + #[allow(unused)] + pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_background, border_color: cx.theme().colors().border_focused, label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: cx.theme().colors().border_focused, label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), border_color: cx.theme().colors().border_focused, label_color: Color::Accent.color(cx), icon_color: Color::Accent.color(cx), @@ -125,23 +129,23 @@ impl ButtonStyle2 { } } - pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle { + pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_disabled, border_color: cx.theme().colors().border_disabled, label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_disabled, border_color: cx.theme().colors().border_disabled, label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, @@ -150,19 +154,19 @@ impl ButtonStyle2 { } #[derive(Default, PartialEq, Clone, Copy)] -pub enum ButtonSize2 { +pub enum ButtonSize { #[default] Default, Compact, None, } -impl ButtonSize2 { +impl ButtonSize { fn height(self) -> Rems { match self { - ButtonSize2::Default => rems(22. / 16.), - ButtonSize2::Compact => rems(18. / 16.), - ButtonSize2::None => rems(16. / 16.), + ButtonSize::Default => rems(22. / 16.), + ButtonSize::Compact => rems(18. / 16.), + ButtonSize::None => rems(16. / 16.), } } } @@ -170,10 +174,10 @@ impl ButtonSize2 { #[derive(IntoElement)] pub struct ButtonLike { id: ElementId, - pub(super) style: ButtonStyle2, + pub(super) style: ButtonStyle, pub(super) disabled: bool, pub(super) selected: bool, - size: ButtonSize2, + size: ButtonSize, tooltip: Option AnyView>>, on_click: Option>, children: SmallVec<[AnyElement; 2]>, @@ -183,10 +187,10 @@ impl ButtonLike { pub fn new(id: impl Into) -> Self { Self { id: id.into(), - style: ButtonStyle2::default(), + style: ButtonStyle::default(), disabled: false, selected: false, - size: ButtonSize2::Default, + size: ButtonSize::Default, tooltip: None, children: SmallVec::new(), on_click: None, @@ -220,12 +224,12 @@ impl ButtonCommon for ButtonLike { &self.id } - fn style(mut self, style: ButtonStyle2) -> Self { + fn style(mut self, style: ButtonStyle) -> Self { self.style = style; self } - fn size(mut self, size: ButtonSize2) -> Self { + fn size(mut self, size: ButtonSize) -> Self { self.size = size; self } @@ -258,7 +262,12 @@ impl RenderOnce for ButtonLike { .active(|active| active.bg(self.style.active(cx).background)) .when_some( self.on_click.filter(|_| !self.disabled), - |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)), + |this, on_click| { + this.on_click(move |event, cx| { + cx.stop_propagation(); + (on_click)(event, cx) + }) + }, ) .when_some(self.tooltip, |this, tooltip| { if !self.selected { diff --git a/crates/ui2/src/components/button/icon_button.rs b/crates/ui2/src/components/button/icon_button.rs index 7746c3f8bee621e07ab73b870ecc14333483b798..a62832059d148c0d5ed9a1ec34aa6352695217f0 100644 --- a/crates/ui2/src/components/button/icon_button.rs +++ b/crates/ui2/src/components/button/icon_button.rs @@ -1,7 +1,7 @@ use gpui::{Action, AnyView}; use crate::prelude::*; -use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize}; +use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconElement, IconSize}; #[derive(IntoElement)] pub struct IconButton { @@ -65,12 +65,12 @@ impl ButtonCommon for IconButton { self.base.id() } - fn style(mut self, style: ButtonStyle2) -> Self { + fn style(mut self, style: ButtonStyle) -> Self { self.base = self.base.style(style); self } - fn size(mut self, size: ButtonSize2) -> Self { + fn size(mut self, size: ButtonSize) -> Self { self.base = self.base.size(size); self } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index aafd04539149a703ab48e35fe8401f3b7db81f18..88650b6ae8d2aa8d60cf0dbe682474f417919a9d 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,73 +1,11 @@ +mod list; mod list_header; mod list_item; mod list_separator; mod list_sub_header; -use gpui::{AnyElement, Div}; -use smallvec::SmallVec; - -use crate::prelude::*; -use crate::{v_stack, Label}; - +pub use list::*; pub use list_header::*; pub use list_item::*; pub use list_separator::*; pub use list_sub_header::*; - -#[derive(IntoElement)] -pub struct List { - /// Message to display when the list is empty - /// Defaults to "No items" - empty_message: SharedString, - header: Option, - toggle: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl List { - pub fn new() -> Self { - Self { - empty_message: "No items".into(), - header: None, - toggle: None, - children: SmallVec::new(), - } - } - - pub fn empty_message(mut self, empty_message: impl Into) -> Self { - self.empty_message = empty_message.into(); - self - } - - pub fn header(mut self, header: ListHeader) -> Self { - self.header = Some(header); - self - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } -} - -impl ParentElement for List { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } -} - -impl RenderOnce for List { - type Rendered = Div; - - fn render(self, _cx: &mut WindowContext) -> Self::Rendered { - v_stack() - .w_full() - .py_1() - .children(self.header.map(|header| header)) - .map(|this| match (self.children.is_empty(), self.toggle) { - (false, _) => this.children(self.children), - (true, Some(false)) => this, - (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)), - }) - } -} diff --git a/crates/ui2/src/components/list/list.rs b/crates/ui2/src/components/list/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..fdfe256bd6b40b3c3926e74d517c28dc1f2f9216 --- /dev/null +++ b/crates/ui2/src/components/list/list.rs @@ -0,0 +1,60 @@ +use gpui::{AnyElement, Div}; +use smallvec::SmallVec; + +use crate::{prelude::*, v_stack, Label, ListHeader}; + +#[derive(IntoElement)] +pub struct List { + /// Message to display when the list is empty + /// Defaults to "No items" + empty_message: SharedString, + header: Option, + toggle: Option, + children: SmallVec<[AnyElement; 2]>, +} + +impl List { + pub fn new() -> Self { + Self { + empty_message: "No items".into(), + header: None, + toggle: None, + children: SmallVec::new(), + } + } + + pub fn empty_message(mut self, empty_message: impl Into) -> Self { + self.empty_message = empty_message.into(); + self + } + + pub fn header(mut self, header: impl Into>) -> Self { + self.header = header.into(); + self + } + + pub fn toggle(mut self, toggle: impl Into>) -> Self { + self.toggle = toggle.into(); + self + } +} + +impl ParentElement for List { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl RenderOnce for List { + type Rendered = Div; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + v_stack().w_full().py_1().children(self.header).map(|this| { + match (self.children.is_empty(), self.toggle) { + (false, _) => this.children(self.children), + (true, Some(false)) => this, + (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)), + } + }) + } +} diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 8761acd60869e4ed91897644c8bcb98b898ae5e5..799b1c5dae886aba2e323def0cf9270b132ba2cb 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -1,22 +1,16 @@ use std::rc::Rc; -use gpui::{ClickEvent, Div}; +use gpui::{AnyElement, ClickEvent, Div}; +use smallvec::SmallVec; use crate::prelude::*; -use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label}; - -pub enum ListHeaderMeta { - Tools(Vec), - // TODO: This should be a button - Button(Label), - Text(Label), -} +use crate::{h_stack, Disclosure, Icon, IconElement, IconSize, Label}; #[derive(IntoElement)] pub struct ListHeader { label: SharedString, left_icon: Option, - meta: Option, + meta: SmallVec<[AnyElement; 2]>, toggle: Option, on_toggle: Option>, inset: bool, @@ -28,7 +22,7 @@ impl ListHeader { Self { label: label.into(), left_icon: None, - meta: None, + meta: SmallVec::new(), inset: false, toggle: None, on_toggle: None, @@ -49,21 +43,19 @@ impl ListHeader { self } - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; + pub fn left_icon(mut self, left_icon: impl Into>) -> Self { + self.left_icon = left_icon.into(); self } - pub fn right_button(self, button: IconButton) -> Self { - self.meta(Some(ListHeaderMeta::Tools(vec![button]))) - } - - pub fn meta(mut self, meta: Option) -> Self { - self.meta = meta; + pub fn meta(mut self, meta: impl IntoElement) -> Self { + self.meta.push(meta.into_any_element()); self } +} - pub fn selected(mut self, selected: bool) -> Self { +impl Selectable for ListHeader { + fn selected(mut self, selected: bool) -> Self { self.selected = selected; self } @@ -73,18 +65,6 @@ impl RenderOnce for ListHeader { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let meta = match self.meta { - Some(ListHeaderMeta::Tools(icons)) => div().child( - h_stack() - .gap_2() - .items_center() - .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))), - ), - Some(ListHeaderMeta::Button(label)) => div().child(label), - Some(ListHeaderMeta::Text(label)) => div().child(label), - None => div(), - }; - h_stack().w_full().relative().child( div() .h_5() @@ -118,7 +98,7 @@ impl RenderOnce for ListHeader { .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ), ) - .child(meta), + .child(h_stack().gap_2().items_center().children(self.meta)), ) } } diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 7ad1d5fb72d5c739f4514ed8a6e0a250d3d399ef..85198416cd94e7b498fa2c9283a175d390b77a82 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -83,11 +83,6 @@ impl ListItem { self } - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - pub fn left_child(mut self, left_content: impl IntoElement) -> Self { self.left_slot = Some(left_content.into_any_element()); self @@ -109,6 +104,13 @@ impl ListItem { } } +impl Selectable for ListItem { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + impl ParentElement for ListItem { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children diff --git a/crates/ui2/src/components/stories.rs b/crates/ui2/src/components/stories.rs index e870515caf170b3477c0c7a3ff75abf3d67c7780..113c2679b7f2b9cfec58d92d51060f7712b8a384 100644 --- a/crates/ui2/src/components/stories.rs +++ b/crates/ui2/src/components/stories.rs @@ -8,6 +8,7 @@ mod icon_button; mod keybinding; mod label; mod list; +mod list_header; mod list_item; pub use avatar::*; @@ -20,4 +21,5 @@ pub use icon_button::*; pub use keybinding::*; pub use label::*; pub use list::*; +pub use list_header::*; pub use list_item::*; diff --git a/crates/ui2/src/components/stories/button.rs b/crates/ui2/src/components/stories/button.rs index 918c321c25de39bcc90ceaeec2202abe54d7e2a8..17bcd8b26850333db4dabfbae2fc0e3570f1e7cc 100644 --- a/crates/ui2/src/components/stories/button.rs +++ b/crates/ui2/src/components/stories/button.rs @@ -2,7 +2,7 @@ use gpui::{Div, Render}; use story::Story; use crate::prelude::*; -use crate::{Button, ButtonStyle2}; +use crate::{Button, ButtonStyle}; pub struct ButtonStory; @@ -14,9 +14,13 @@ impl Render for ButtonStory { .child(Story::title_for::