diff --git a/Cargo.lock b/Cargo.lock index f02c748fbd..03945e4578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,25 @@ dependencies = [ "workspace", ] +[[package]] +name = "activity_indicator2" +version = "0.1.0" +dependencies = [ + "anyhow", + "auto_update2", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "project2", + "settings2", + "smallvec", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -1076,6 +1095,23 @@ dependencies = [ "workspace", ] +[[package]] +name = "breadcrumbs2" +version = "0.1.0" +dependencies = [ + "collections", + "editor2", + "gpui2", + "itertools 0.10.5", + "language2", + "project2", + "search2", + "settings2", + "theme2", + "ui2", + "workspace2", +] + [[package]] name = "bromberg_sl2" version = "0.6.0" @@ -1669,7 +1705,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.29.0" +version = "0.29.1" dependencies = [ "anyhow", "async-trait", @@ -9951,7 +9987,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", @@ -11697,6 +11733,7 @@ dependencies = [ name = "zed2" version = "0.109.0" dependencies = [ + "activity_indicator2", "ai2", "anyhow", "async-compression", @@ -11706,6 +11743,7 @@ dependencies = [ "audio2", "auto_update2", "backtrace", + "breadcrumbs2", "call2", "channel2", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 03a854b77f..cc8264f697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/activity_indicator", + "crates/activity_indicator2", "crates/ai", "crates/assistant", "crates/audio", @@ -8,6 +9,7 @@ members = [ "crates/auto_update", "crates/auto_update2", "crates/breadcrumbs", + "crates/breadcrumbs2", "crates/call", "crates/call2", "crates/channel", @@ -200,7 +202,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 ef6a655bdc..2a8d19f882 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/activity_indicator2/Cargo.toml b/crates/activity_indicator2/Cargo.toml new file mode 100644 index 0000000000..400869d2fd --- /dev/null +++ b/crates/activity_indicator2/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "activity_indicator2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/activity_indicator.rs" +doctest = false + +[dependencies] +auto_update = { path = "../auto_update2", package = "auto_update2" } +editor = { path = "../editor2", package = "editor2" } +language = { path = "../language2", package = "language2" } +gpui = { path = "../gpui2", package = "gpui2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +ui = { path = "../ui2", package = "ui2" } +util = { path = "../util" } +theme = { path = "../theme2", package = "theme2" } +workspace = { path = "../workspace2", package = "workspace2" } + +anyhow.workspace = true +futures.workspace = true +smallvec.workspace = true + +[dev-dependencies] +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } diff --git a/crates/activity_indicator2/src/activity_indicator.rs b/crates/activity_indicator2/src/activity_indicator.rs new file mode 100644 index 0000000000..1ee5a6689a --- /dev/null +++ b/crates/activity_indicator2/src/activity_indicator.rs @@ -0,0 +1,333 @@ +use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; +use editor::Editor; +use futures::StreamExt; +use gpui::{ + actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model, + ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext as _, +}; +use language::{LanguageRegistry, LanguageServerBinaryStatus}; +use project::{LanguageServerProgress, Project}; +use smallvec::SmallVec; +use std::{cmp::Reverse, fmt::Write, sync::Arc}; +use ui::h_stack; +use util::ResultExt; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +actions!(ShowErrorMessage); + +const DOWNLOAD_ICON: &str = "icons/download.svg"; +const WARNING_ICON: &str = "icons/warning.svg"; + +pub enum Event { + ShowError { lsp_name: Arc, error: String }, +} + +pub struct ActivityIndicator { + statuses: Vec, + project: Model, + auto_updater: Option>, +} + +struct LspStatus { + name: Arc, + status: LanguageServerBinaryStatus, +} + +struct PendingWork<'a> { + language_server_name: &'a str, + progress_token: &'a str, + progress: &'a LanguageServerProgress, +} + +#[derive(Default)] +struct Content { + icon: Option<&'static str>, + message: String, + on_click: Option)>>, +} + +impl ActivityIndicator { + pub fn new( + workspace: &mut Workspace, + languages: Arc, + cx: &mut ViewContext, + ) -> View { + let project = workspace.project().clone(); + let auto_updater = AutoUpdater::get(cx); + let this = cx.build_view(|cx: &mut ViewContext| { + let mut status_events = languages.language_server_binary_statuses(); + cx.spawn(|this, mut cx| async move { + while let Some((language, event)) = status_events.next().await { + this.update(&mut cx, |this, cx| { + this.statuses.retain(|s| s.name != language.name()); + this.statuses.push(LspStatus { + name: language.name(), + status: event, + }); + cx.notify(); + })?; + } + anyhow::Ok(()) + }) + .detach(); + cx.observe(&project, |_, _, cx| cx.notify()).detach(); + + if let Some(auto_updater) = auto_updater.as_ref() { + cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); + } + + // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) + // .detach(); + + Self { + statuses: Default::default(), + project: project.clone(), + auto_updater, + } + }); + + cx.subscribe(&this, move |workspace, _, event, cx| match event { + Event::ShowError { lsp_name, error } => { + if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer(error, None, cx)) + .log_err() + { + buffer.update(cx, |buffer, cx| { + buffer.edit( + [(0..0, format!("Language server error: {}\n\n", lsp_name))], + None, + cx, + ); + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_buffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + } + } + }) + .detach(); + this + } + + fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext) { + self.statuses.retain(|status| { + if let LanguageServerBinaryStatus::Failed { error } = &status.status { + cx.emit(Event::ShowError { + lsp_name: status.name.clone(), + error: error.clone(), + }); + false + } else { + true + } + }); + + cx.notify(); + } + + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + if let Some(updater) = &self.auto_updater { + updater.update(cx, |updater, cx| { + updater.dismiss_error(cx); + }); + } + cx.notify(); + } + + fn pending_language_server_work<'a>( + &self, + cx: &'a AppContext, + ) -> impl Iterator> { + self.project + .read(cx) + .language_server_statuses() + .rev() + .filter_map(|status| { + if status.pending_work.is_empty() { + None + } else { + let mut pending_work = status + .pending_work + .iter() + .map(|(token, progress)| PendingWork { + language_server_name: status.name.as_str(), + progress_token: token.as_str(), + progress, + }) + .collect::>(); + pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); + Some(pending_work) + } + }) + .flatten() + } + + fn content_to_render(&mut self, cx: &mut ViewContext) -> Content { + // Show any language server has pending activity. + let mut pending_work = self.pending_language_server_work(cx); + if let Some(PendingWork { + language_server_name, + progress_token, + progress, + }) = pending_work.next() + { + let mut message = language_server_name.to_string(); + + message.push_str(": "); + if let Some(progress_message) = progress.message.as_ref() { + message.push_str(progress_message); + } else { + message.push_str(progress_token); + } + + if let Some(percentage) = progress.percentage { + write!(&mut message, " ({}%)", percentage).unwrap(); + } + + let additional_work_count = pending_work.count(); + if additional_work_count > 0 { + write!(&mut message, " + {} more", additional_work_count).unwrap(); + } + + return Content { + icon: None, + message, + on_click: None, + }; + } + + // Show any language server installation info. + let mut downloading = SmallVec::<[_; 3]>::new(); + let mut checking_for_update = SmallVec::<[_; 3]>::new(); + let mut failed = SmallVec::<[_; 3]>::new(); + for status in &self.statuses { + let name = status.name.clone(); + match status.status { + LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name), + LanguageServerBinaryStatus::Downloading => downloading.push(name), + LanguageServerBinaryStatus::Failed { .. } => failed.push(name), + LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} + } + } + + if !downloading.is_empty() { + return Content { + icon: Some(DOWNLOAD_ICON), + message: format!( + "Downloading {} language server{}...", + downloading.join(", "), + if downloading.len() > 1 { "s" } else { "" } + ), + on_click: None, + }; + } else if !checking_for_update.is_empty() { + return Content { + icon: Some(DOWNLOAD_ICON), + message: format!( + "Checking for updates to {} language server{}...", + checking_for_update.join(", "), + if checking_for_update.len() > 1 { + "s" + } else { + "" + } + ), + on_click: None, + }; + } else if !failed.is_empty() { + return Content { + icon: Some(WARNING_ICON), + message: format!( + "Failed to download {} language server{}. Click to show error.", + failed.join(", "), + if failed.len() > 1 { "s" } else { "" } + ), + on_click: Some(Arc::new(|this, cx| { + this.show_error_message(&Default::default(), cx) + })), + }; + } + + // Show any application auto-update info. + if let Some(updater) = &self.auto_updater { + return match &updater.read(cx).status() { + AutoUpdateStatus::Checking => Content { + icon: Some(DOWNLOAD_ICON), + message: "Checking for Zed updates…".to_string(), + on_click: None, + }, + AutoUpdateStatus::Downloading => Content { + icon: Some(DOWNLOAD_ICON), + message: "Downloading Zed update…".to_string(), + on_click: None, + }, + AutoUpdateStatus::Installing => Content { + icon: Some(DOWNLOAD_ICON), + message: "Installing Zed update…".to_string(), + on_click: None, + }, + AutoUpdateStatus::Updated => Content { + icon: None, + message: "Click to restart and update Zed".to_string(), + on_click: Some(Arc::new(|_, cx| { + workspace::restart(&Default::default(), cx) + })), + }, + AutoUpdateStatus::Errored => Content { + icon: Some(WARNING_ICON), + message: "Auto update failed".to_string(), + on_click: Some(Arc::new(|this, cx| { + this.dismiss_error_message(&Default::default(), cx) + })), + }, + AutoUpdateStatus::Idle => Default::default(), + }; + } + + // todo!(show active tasks) + // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { + // return Content { + // icon: None, + // message: most_recent_active_task.to_string(), + // on_click: None, + // }; + // } + + Default::default() + } +} + +impl EventEmitter for ActivityIndicator {} + +impl Render for ActivityIndicator { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let content = self.content_to_render(cx); + + let mut result = h_stack() + .id("activity-indicator") + .on_action(cx.listener(Self::show_error_message)) + .on_action(cx.listener(Self::dismiss_error_message)); + + if let Some(on_click) = content.on_click { + result = result + .cursor(CursorStyle::PointingHand) + .on_click(cx.listener(move |this, _, cx| { + on_click(this, cx); + })) + } + + result + .children(content.icon.map(|icon| svg().path(icon))) + .child(SharedString::from(content.message)) + } +} + +impl StatusItemView for ActivityIndicator { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} +} diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index cac8bf6c54..e472e8c8df 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/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs index d2eab15d09..72dbe32b5a 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -102,7 +102,7 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo }) .detach(); - if let Some(version) = *ZED_APP_VERSION { + if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { let auto_updater = cx.build_model(|cx| { let updater = AutoUpdater::new(version, http_client, server_url); diff --git a/crates/breadcrumbs2/Cargo.toml b/crates/breadcrumbs2/Cargo.toml new file mode 100644 index 0000000000..8555afe980 --- /dev/null +++ b/crates/breadcrumbs2/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "breadcrumbs2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/breadcrumbs.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +language = { package = "language2", path = "../language2" } +project = { package = "project2", path = "../project2" } +search = { package = "search2", path = "../search2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +# outline = { path = "../outline" } +itertools = "0.10" + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/breadcrumbs2/src/breadcrumbs.rs b/crates/breadcrumbs2/src/breadcrumbs.rs new file mode 100644 index 0000000000..75195a3159 --- /dev/null +++ b/crates/breadcrumbs2/src/breadcrumbs.rs @@ -0,0 +1,204 @@ +use gpui::{ + Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, + ViewContext, WeakView, +}; +use itertools::Itertools; +use theme::ActiveTheme; +use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label}; +use workspace::{ + item::{ItemEvent, ItemHandle}, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +pub enum Event { + UpdateLocation, +} + +pub struct Breadcrumbs { + pane_focused: bool, + active_item: Option>, + subscription: Option, + _workspace: WeakView, +} + +impl Breadcrumbs { + pub fn new(workspace: &Workspace) -> Self { + Self { + pane_focused: false, + active_item: Default::default(), + subscription: Default::default(), + _workspace: workspace.weak_handle(), + } + } +} + +impl EventEmitter for Breadcrumbs {} +impl EventEmitter for Breadcrumbs {} + +impl Render for Breadcrumbs { + type Element = Component; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let button = ButtonLike::new("breadcrumbs") + .style(ButtonStyle::Transparent) + .disabled(true); + + let active_item = match &self.active_item { + Some(active_item) => active_item, + None => return button.into_element(), + }; + let not_editor = active_item.downcast::().is_none(); + + let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) { + Some(breadcrumbs) => breadcrumbs, + None => return button.into_element(), + } + .into_iter() + .map(|breadcrumb| { + StyledText::new(breadcrumb.text) + .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default()) + .into_any() + }); + + let button = button.children(Itertools::intersperse_with(breadcrumbs, || { + Label::new(" › ").into_any_element() + })); + + if not_editor || !self.pane_focused { + return button.into_element(); + } + + // let this = cx.view().downgrade(); + button + .style(ButtonStyle::Filled) + .disabled(false) + .on_click(move |_, _cx| { + todo!("outline::toggle"); + // this.update(cx, |this, cx| { + // if let Some(workspace) = this.workspace.upgrade() { + // workspace.update(cx, |_workspace, _cx| { + // outline::toggle(workspace, &Default::default(), cx) + // }) + // } + // }) + // .ok(); + }) + .into_element() + } +} + +// impl View for Breadcrumbs { +// fn ui_name() -> &'static str { +// "Breadcrumbs" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// let active_item = match &self.active_item { +// Some(active_item) => active_item, +// None => return Empty::new().into_any(), +// }; +// let not_editor = active_item.downcast::().is_none(); + +// let theme = theme::current(cx).clone(); +// let style = &theme.workspace.toolbar.breadcrumbs; + +// let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { +// Some(breadcrumbs) => breadcrumbs, +// None => return Empty::new().into_any(), +// } +// .into_iter() +// .map(|breadcrumb| { +// Text::new( +// breadcrumb.text, +// theme.workspace.toolbar.breadcrumbs.default.text.clone(), +// ) +// .with_highlights(breadcrumb.highlights.unwrap_or_default()) +// .into_any() +// }); + +// let crumbs = Flex::row() +// .with_children(Itertools::intersperse_with(breadcrumbs, || { +// Label::new(" › ", style.default.text.clone()).into_any() +// })) +// .constrained() +// .with_height(theme.workspace.toolbar.breadcrumb_height) +// .contained(); + +// if not_editor || !self.pane_focused { +// return crumbs +// .with_style(style.default.container) +// .aligned() +// .left() +// .into_any(); +// } + +// MouseEventHandler::new::(0, cx, |state, _| { +// let style = style.style_for(state); +// crumbs.with_style(style.container) +// }) +// .on_click(MouseButton::Left, |_, this, cx| { +// if let Some(workspace) = this.workspace.upgrade(cx) { +// workspace.update(cx, |workspace, cx| { +// outline::toggle(workspace, &Default::default(), cx) +// }) +// } +// }) +// .with_tooltip::( +// 0, +// "Show symbol outline".to_owned(), +// Some(Box::new(outline::Toggle)), +// theme.tooltip.clone(), +// cx, +// ) +// .aligned() +// .left() +// .into_any() +// } +// } + +impl ToolbarItemView for Breadcrumbs { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + cx.notify(); + self.active_item = None; + if let Some(item) = active_pane_item { + let this = cx.view().downgrade(); + self.subscription = Some(item.subscribe_to_item_events( + cx, + Box::new(move |event, cx| { + if let ItemEvent::UpdateBreadcrumbs = event { + this.update(cx, |_, cx| { + cx.emit(Event::UpdateLocation); + cx.notify(); + }) + .ok(); + } + }), + )); + self.active_item = Some(item.boxed_clone()); + item.breadcrumb_location(cx) + } else { + ToolbarItemLocation::Hidden + } + } + + // fn location_for_event( + // &self, + // _: &Event, + // current_location: ToolbarItemLocation, + // cx: &AppContext, + // ) -> ToolbarItemLocation { + // if let Some(active_item) = self.active_item.as_ref() { + // active_item.breadcrumb_location(cx) + // } else { + // current_location + // } + // } + + fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext) { + self.pane_focused = pane_focused; + } +} diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index df7dd847cf..a933057723 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 7b7cd7d9df..c38ebeac02 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/client/src/client.rs b/crates/client/src/client.rs index a14088cc50..c1666e9c1d 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -346,7 +346,7 @@ impl Drop for PendingEntitySubscription { } } -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub struct TelemetrySettings { pub diagnostics: bool, pub metrics: bool, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index a3e7449cf8..e2fc8ad3ad 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -350,6 +350,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; + dbg!(telemetry_settings); self.report_clickhouse_event(event, telemetry_settings, true) } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index bbaf521e15..33c3c14ddd 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.29.0" +version = "0.29.1" publish = false [[bin]] diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 68b06e435d..780fb783bc 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -1220,6 +1220,13 @@ impl Database { self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) .await?; + if new_parent + .ancestors_including_self() + .any(|id| id == channel.id) + { + Err(anyhow!("cannot move a channel into one of its descendants"))?; + } + new_parent_path = new_parent.path(); new_parent_channel = Some(new_parent); } else { diff --git a/crates/collab/src/db/tests/channel_tests.rs b/crates/collab/src/db/tests/channel_tests.rs index 43526c7f24..324917bbdd 100644 --- a/crates/collab/src/db/tests/channel_tests.rs +++ b/crates/collab/src/db/tests/channel_tests.rs @@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc) { (livestreaming_id, &[projects_id]), ], ); + + // Can't move a channel into its ancestor + db.move_channel(projects_id, Some(livestreaming_id), user_id) + .await + .unwrap_err(); + let result = db.get_channels_for_user(user_id).await.unwrap(); + assert_channel_tree( + result.channels, + &[ + (zed_id, &[]), + (projects_id, &[]), + (livestreaming_id, &[projects_id]), + ], + ); } test_both_dbs!( diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index fa7c4fe67d..c5820b5395 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/db/queries/channels.rs b/crates/collab2/src/db/queries/channels.rs index 68b06e435d..780fb783bc 100644 --- a/crates/collab2/src/db/queries/channels.rs +++ b/crates/collab2/src/db/queries/channels.rs @@ -1220,6 +1220,13 @@ impl Database { self.check_user_is_channel_admin(&new_parent, admin_id, &*tx) .await?; + if new_parent + .ancestors_including_self() + .any(|id| id == channel.id) + { + Err(anyhow!("cannot move a channel into one of its descendants"))?; + } + new_parent_path = new_parent.path(); new_parent_channel = Some(new_parent); } else { diff --git a/crates/collab2/src/db/tests/channel_tests.rs b/crates/collab2/src/db/tests/channel_tests.rs index 43526c7f24..8a7a19ed3a 100644 --- a/crates/collab2/src/db/tests/channel_tests.rs +++ b/crates/collab2/src/db/tests/channel_tests.rs @@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc) { .await .unwrap(); - // Dag is: zed - projects - livestreaming - // Move to same parent should be a no-op assert!(db .move_channel(projects_id, Some(zed_id), user_id) @@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc) { (livestreaming_id, &[projects_id]), ], ); + + // Can't move a channel into its ancestor + db.move_channel(projects_id, Some(livestreaming_id), user_id) + .await + .unwrap_err(); + let result = db.get_channels_for_user(user_id).await.unwrap(); + assert_channel_tree( + result.channels, + &[ + (zed_id, &[]), + (projects_id, &[]), + (livestreaming_id, &[projects_id]), + ], + ); } test_both_dbs!( diff --git a/crates/collab2/src/tests/integration_tests.rs b/crates/collab2/src/tests/integration_tests.rs index e579c384e3..2268a51f2b 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 969869599b..5f95f00d6f 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 64580f0efc..b90df68c2a 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -17,8 +17,9 @@ 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}; // use db::kvp::KEY_VALUE_STORE; // use drag_and_drop::{DragAndDrop, Draggable}; @@ -151,10 +152,10 @@ actions!( // ] // ); -// #[derive(Debug, Copy, Clone, PartialEq, Eq)] -// struct ChannelMoveClipboard { -// channel_id: ChannelId, -// } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct ChannelMoveClipboard { + channel_id: ChannelId, +} const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; @@ -168,17 +169,19 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext, Div, - EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, - ParentElement, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext, + AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, + Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, + ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString, + Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; -use project::Fs; +use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; +use ui::prelude::*; use ui::{ - h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, IconElement, Label, List, - ListHeader, ListItem, Toggle, Tooltip, + h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, + Label, List, ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -230,26 +233,6 @@ pub fn init(cx: &mut AppContext) { // }, // ); - // cx.add_action( - // |panel: &mut CollabPanel, - // action: &StartMoveChannelFor, - // _: &mut ViewContext| { - // panel.channel_clipboard = Some(ChannelMoveClipboard { - // channel_id: action.channel_id, - // }); - // }, - // ); - - // cx.add_action( - // |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { - // if let Some(channel) = panel.selected_channel() { - // panel.channel_clipboard = Some(ChannelMoveClipboard { - // channel_id: channel.id, - // }) - // } - // }, - // ); - // cx.add_action( // |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { // let Some(clipboard) = panel.channel_clipboard.take() else { @@ -303,12 +286,12 @@ impl ChannelEditingState { } pub struct CollabPanel { - width: Option, + width: Option, fs: Arc, focus_handle: FocusHandle, - // channel_clipboard: Option, + channel_clipboard: Option, pending_serialization: Task>, - // context_menu: ViewHandle, + context_menu: Option<(View, Point, Subscription)>, filter_editor: View, channel_name_editor: View, channel_editing_state: Option, @@ -317,9 +300,9 @@ pub struct CollabPanel { channel_store: Model, user_store: Model, client: Arc, - // project: ModelHandle, + project: Model, match_candidates: Vec, - // list_state: ListState, + scroll_handle: ScrollHandle, subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, @@ -337,7 +320,7 @@ enum ChannelDragTarget { #[derive(Serialize, Deserialize)] struct SerializedCollabPanel { - width: Option, + width: Option, collapsed_channels: Option>, } @@ -401,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| { @@ -416,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)); @@ -589,10 +568,10 @@ impl CollabPanel { let mut this = Self { width: None, focus_handle: cx.focus_handle(), - // channel_clipboard: None, + channel_clipboard: None, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), - // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + context_menu: None, channel_name_editor, filter_editor, entries: Vec::default(), @@ -600,16 +579,16 @@ impl CollabPanel { selection: None, channel_store: ChannelStore::global(cx), user_store: workspace.user_store().clone(), - // project: workspace.project().clone(), + 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); @@ -725,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)); @@ -1094,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)) - // } - // }); - // } + 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) + } + }); + } - // let old_scroll_top = self.list_state.logical_scroll_top(); + 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.reset(self.entries.len()); - - // 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., - // }) - // }); - - // 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,280 +1655,238 @@ impl CollabPanel { // .into_any() // } - // fn has_subchannels(&self, ix: usize) -> bool { - // self.entries.get(ix).map_or(false, |entry| { - // if let ListEntry::Channel { has_children, .. } = entry { - // *has_children - // } else { - // false - // } - // }) - // } + fn has_subchannels(&self, ix: usize) -> bool { + self.entries.get(ix).map_or(false, |entry| { + if let ListEntry::Channel { has_children, .. } = entry { + *has_children + } else { + false + } + }) + } - // fn deploy_channel_context_menu( - // &mut self, - // position: Option, - // channel: &Channel, - // ix: usize, - // cx: &mut ViewContext, - // ) { - // self.context_menu_on_selected = position.is_none(); + fn deploy_channel_context_menu( + &mut self, + position: Point, + channel_id: ChannelId, + ix: usize, + cx: &mut ViewContext, + ) { + let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { + self.channel_store + .read(cx) + .channel_for_id(clipboard.channel_id) + .map(|channel| channel.name.clone()) + }); + let this = cx.view().clone(); - // let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { - // self.channel_store - // .read(cx) - // .channel_for_id(clipboard.channel_id) - // .map(|channel| channel.name.clone()) - // }); + let context_menu = ContextMenu::build(cx, |mut context_menu, cx| { + if self.has_subchannels(ix) { + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + context_menu = context_menu.entry( + expand_action_name, + cx.handler_for(&this, move |this, cx| { + this.toggle_channel_collapsed(channel_id, cx) + }), + ); + } - // self.context_menu.update(cx, |context_menu, cx| { - // context_menu.set_position_mode(if self.context_menu_on_selected { - // OverlayPositionMode::Local - // } else { - // OverlayPositionMode::Window - // }); + context_menu = context_menu + .entry( + "Open Notes", + cx.handler_for(&this, move |this, cx| { + this.open_channel_notes(channel_id, cx) + }), + ) + .entry( + "Open Chat", + cx.handler_for(&this, move |this, cx| { + this.join_channel_chat(channel_id, cx) + }), + ) + .entry( + "Copy Channel Link", + cx.handler_for(&this, move |this, cx| { + this.copy_channel_link(channel_id, cx) + }), + ); - // let mut items = Vec::new(); + if self.channel_store.read(cx).is_channel_admin(channel_id) { + context_menu = context_menu + .separator() + .entry( + "New Subchannel", + cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)), + ) + .entry( + "Rename", + cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)), + ) + .entry( + "Move this channel", + cx.handler_for(&this, move |this, cx| { + this.start_move_channel(channel_id, cx) + }), + ); - // let select_action_name = if self.selection == Some(ix) { - // "Unselect" - // } else { - // "Select" - // }; + if let Some(channel_name) = clipboard_channel_name { + context_menu = context_menu.separator().entry( + format!("Move '#{}' here", channel_name), + cx.handler_for(&this, move |this, cx| { + this.move_channel_on_clipboard(channel_id, cx) + }), + ); + } - // items.push(ContextMenuItem::action( - // select_action_name, - // ToggleSelectedIx { ix }, - // )); + context_menu = context_menu + .separator() + .entry( + "Invite Members", + cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)), + ) + .entry( + "Manage Members", + cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)), + ) + .entry( + "Delete", + cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)), + ); + } - // if self.has_subchannels(ix) { - // let expand_action_name = if self.is_channel_collapsed(channel.id) { - // "Expand Subchannels" - // } else { - // "Collapse Subchannels" - // }; - // items.push(ContextMenuItem::action( - // expand_action_name, - // ToggleCollapse { - // location: channel.id, - // }, - // )); - // } + context_menu + }); - // items.push(ContextMenuItem::action( - // "Open Notes", - // OpenChannelNotes { - // channel_id: channel.id, - // }, - // )); + cx.focus_view(&context_menu); + 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)); - // items.push(ContextMenuItem::action( - // "Open Chat", - // JoinChannelChat { - // channel_id: channel.id, - // }, - // )); + cx.notify(); + } - // items.push(ContextMenuItem::action( - // "Copy Channel Link", - // CopyChannelLink { - // channel_id: channel.id, - // }, - // )); + 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); + } + }); + } - // if self.channel_store.read(cx).is_channel_admin(channel.id) { - // items.extend([ - // ContextMenuItem::Separator, - // ContextMenuItem::action( - // "New Subchannel", - // NewChannel { - // location: channel.id, - // }, - // ), - // ContextMenuItem::action( - // "Rename", - // RenameChannel { - // channel_id: channel.id, - // }, - // ), - // ContextMenuItem::action( - // "Move this channel", - // StartMoveChannelFor { - // channel_id: channel.id, - // }, - // ), - // ]); + self.update_entries(false, cx); + } - // if let Some(channel_name) = clipboard_channel_name { - // items.push(ContextMenuItem::Separator); - // items.push(ContextMenuItem::action( - // format!("Move '#{}' here", channel_name), - // MoveChannel { to: channel.id }, - // )); - // } + 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); + } - // items.extend([ - // ContextMenuItem::Separator, - // ContextMenuItem::action( - // "Invite Members", - // InviteMembers { - // channel_id: channel.id, - // }, - // ), - // ContextMenuItem::action( - // "Manage Members", - // ManageMembers { - // channel_id: channel.id, - // }, - // ), - // ContextMenuItem::Separator, - // ContextMenuItem::action( - // "Delete", - // RemoveChannel { - // channel_id: channel.id, - // }, - // ), - // ]); - // } + if let Some(ix) = self.selection { + self.scroll_handle.scroll_to_item(ix) + } + cx.notify(); + } - // context_menu.show( - // position.unwrap_or_default(), - // if self.context_menu_on_selected { - // gpui::elements::AnchorCorner::TopRight - // } else { - // gpui::elements::AnchorCorner::BottomLeft - // }, - // items, - // cx, - // ); - // }); + 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); + } - // 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); - // } - // }); - // } - - // 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); - // } - - // 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(); - // } - - // 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()?; + 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( - // &OpenChannelNotes { - // channel_id: channel.id, - // }, - // cx, - // ) - // } else { - // self.join_channel(channel.id, cx) - // } - // } - // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), - // _ => {} - // } - // } - // } + 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) { @@ -2025,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, @@ -2080,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() { @@ -2116,78 +2044,108 @@ impl CollabPanel { }); } - // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { - // self.collapsed_channels - // .retain(|channel| *channel != action.location); - // self.channel_editing_state = Some(ChannelEditingState::Create { - // location: Some(action.location.to_owned()), - // pending_name: None, - // }); - // self.update_entries(false, cx); - // self.select_channel_editor(); - // cx.focus(self.channel_name_editor.as_any()); - // cx.notify(); - // } + fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|channel| *channel != channel_id); + self.channel_editing_state = Some(ChannelEditingState::Create { + location: Some(channel_id), + pending_name: None, + }); + self.update_entries(false, cx); + self.select_channel_editor(); + cx.focus_view(&self.channel_name_editor); + cx.notify(); + } - // fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext) { - // self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); - // } + fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + todo!(); + // self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx); + } - // fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { - // self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); - // } + fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + todo!(); + // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); + } - // fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - // if let Some(channel) = self.selected_channel() { - // self.remove_channel(channel.id, cx) - // } - // } + fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.remove_channel(channel.id, cx) + } + } - // fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - // if let Some(channel) = self.selected_channel() { - // self.rename_channel( - // &RenameChannel { - // channel_id: channel.id, - // }, - // cx, - // ); - // } - // } + fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.rename_channel(channel.id, cx); + } + } - // fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { - // let channel_store = self.channel_store.read(cx); - // if !channel_store.is_channel_admin(action.channel_id) { - // return; - // } - // if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { - // self.channel_editing_state = Some(ChannelEditingState::Rename { - // location: action.channel_id.to_owned(), - // pending_name: None, - // }); - // self.channel_name_editor.update(cx, |editor, cx| { - // editor.set_text(channel.name.clone(), cx); - // editor.select_all(&Default::default(), cx); - // }); - // cx.focus(self.channel_name_editor.as_any()); - // self.update_entries(false, cx); - // self.select_channel_editor(); - // } - // } + fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + if !channel_store.is_channel_admin(channel_id) { + return; + } + if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() { + self.channel_editing_state = Some(ChannelEditingState::Rename { + location: channel_id, + pending_name: None, + }); + self.channel_name_editor.update(cx, |editor, cx| { + editor.set_text(channel.name.clone(), cx); + editor.select_all(&Default::default(), cx); + }); + cx.focus_view(&self.channel_name_editor); + self.update_entries(false, cx); + self.select_channel_editor(); + } + } - fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { + fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + self.channel_clipboard = Some(ChannelMoveClipboard { channel_id }); + } + + fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + if let Some(channel) = self.selected_channel() { + self.channel_clipboard = Some(ChannelMoveClipboard { + channel_id: channel.id, + }) + } + } + + fn move_channel_on_clipboard( + &mut self, + to_channel_id: ChannelId, + cx: &mut ViewContext, + ) { + if let Some(clipboard) = self.channel_clipboard.take() { + self.channel_store.update(cx, |channel_store, cx| { + channel_store + .move_channel(clipboard.channel_id, Some(to_channel_id), cx) + .detach_and_log_err(cx) + }) + } + } + + fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { todo!(); // ChannelView::open(action.channel_id, workspace, cx).detach(); } } - // 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 @@ -2235,35 +2193,29 @@ impl CollabPanel { // self.remove_channel(action.channel_id, cx) // } - // fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { - // let channel_store = self.channel_store.clone(); - // if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { - // let prompt_message = format!( - // "Are you sure you want to remove the channel \"{}\"?", - // channel.name - // ); - // let mut answer = - // cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); - // let window = cx.window(); - // cx.spawn(|this, mut cx| async move { - // if answer.next().await == Some(0) { - // if let Err(e) = channel_store - // .update(&mut cx, |channels, _| channels.remove_channel(channel_id)) - // .await - // { - // window.prompt( - // PromptLevel::Info, - // &format!("Failed to remove channel: {}", e), - // &["Ok"], - // &mut cx, - // ); - // } - // this.update(&mut cx, |_, cx| cx.focus_self()).ok(); - // } - // }) - // .detach(); - // } - // } + fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let channel_store = self.channel_store.clone(); + if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { + let prompt_message = format!( + "Are you sure you want to remove the channel \"{}\"?", + channel.name + ); + let mut answer = + cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + let window = cx.window(); + cx.spawn(|this, mut cx| async move { + if answer.await? == 0 { + channel_store + .update(&mut cx, |channels, _| channels.remove_channel(channel_id))? + .await + .notify_async_err(&mut cx); + this.update(&mut cx, |_, cx| cx.focus_self()).ok(); + } + anyhow::Ok(()) + }) + .detach(); + } + } // // Should move to the filter editor if clicking on it // // Should move selection to the channel editor if activating it @@ -2314,18 +2266,13 @@ impl CollabPanel { // .detach(); // } - // fn call( - // &mut self, - // recipient_user_id: u64, - // initial_project: Option>, - // cx: &mut ViewContext, - // ) { - // ActiveCall::global(cx) - // .update(cx, |call, cx| { - // call.invite(recipient_user_id, initial_project, cx) - // }) - // .detach_and_log_err(cx); - // } + fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| { + call.invite(recipient_user_id, Some(self.project.clone()), cx) + }) + .detach_and_log_err(cx); + } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { let Some(handle) = cx.window_handle().downcast::() else { @@ -2344,81 +2291,111 @@ impl CollabPanel { .detach() } - // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { - // let channel_id = action.channel_id; - // if let Some(workspace) = self.workspace.upgrade(cx) { - // cx.app_context().defer(move |cx| { - // workspace.update(cx, |workspace, cx| { - // if let Some(panel) = workspace.focus_panel::(cx) { - // panel.update(cx, |panel, cx| { - // panel - // .select_channel(channel_id, None, cx) - // .detach_and_log_err(cx); - // }); - // } - // }); - // }); - // } - // } - - // fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { - // let channel_store = self.channel_store.read(cx); - // let Some(channel) = channel_store.channel_for_id(action.channel_id) else { - // return; - // }; - // let item = ClipboardItem::new(channel.link()); - // cx.write_to_clipboard(item) - // } - - fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { - v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener( - |this, _, cx| { - let client = this.client.clone(); - cx.spawn(|_, mut cx| async move { - client - .authenticate_and_connect(true, &cx) - .await - .notify_async_err(&mut cx); - }) - .detach() - }, - ))) + fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + todo!(); + // if let Some(panel) = workspace.focus_panel::(cx) { + // panel.update(cx, |panel, cx| { + // panel + // .select_channel(channel_id, None, cx) + // .detach_and_log_err(cx); + // }); + // } + }); + }); } - fn render_signed_in(&mut self, cx: &mut ViewContext) -> List { - let is_selected = false; // todo!() this.selection == Some(ix); + fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + let Some(channel) = channel_store.channel_for_id(channel_id) else { + return; + }; + let item = ClipboardItem::new(channel.link()); + cx.write_to_clipboard(item) + } - List::new().children(self.entries.clone().into_iter().map(|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, cx) - .into_any_element(), - ListEntry::ChannelEditor { depth } => { - self.render_channel_editor(depth, cx).into_any_element() - } - } - })) + fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { + v_stack().child( + Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener( + |this, _, cx| { + let client = this.client.clone(); + cx.spawn(|_, mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach() + }, + )), + ) + } + + 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( @@ -2502,23 +2479,11 @@ impl CollabPanel { .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .tooltip(|cx| Tooltip::text("Search for new contact", cx)), ), - Section::Channels => { - // todo!() - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Root - // { - // is_dragged_over = true; - // } - - Some( - IconButton::new("add-channel", Icon::Plus) - .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) - .tooltip(|cx| Tooltip::text("Create a channel", cx)), - ) - } + Section::Channels => Some( + IconButton::new("add-channel", Icon::Plus) + .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) + .tooltip(|cx| Tooltip::text("Create a channel", cx)), + ), _ => None, }; @@ -2530,13 +2495,40 @@ impl CollabPanel { | Section::Offline => true, }; - ListHeader::new(text) - .when_some(button, |el, button| el.right_button(button)) - .selected(is_selected) - .when(can_collapse, |el| { - el.toggle(ui::Toggle::Toggled(is_collapsed)).on_toggle( - cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)), - ) + h_stack() + .w_full() + .map(|el| { + if can_collapse { + 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) + })), + ) + } else { + el.child( + ListHeader::new(text) + .when_some(button, |el, button| el.meta(button)) + .selected(is_selected), + ) + } + }) + .when(section == Section::Channels, |el| { + el.drag_over::(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .on_drop(cx.listener( + move |this, view: &View, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(view.read(cx).channel.id, None, cx) + }) + .detach_and_log_err(cx) + }, + )) }) } @@ -2568,113 +2560,57 @@ impl CollabPanel { .w_full() .justify_between() .child(Label::new(github_login.clone())) - .child( - div() - .id("remove_contact") - .invisible() - .group_hover("", |style| style.visible()) - .child( - IconButton::new("remove_contact", Icon::Close) - .color(Color::Muted) - .tooltip(|cx| Tooltip::text("Remove Contact", cx)) - .on_click(cx.listener(move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); - })), - ), - ), - ); + .when(calling, |el| { + el.child(Label::new("Calling").color(Color::Muted)) + }) + .when(!calling, |el| { + el.child( + div() + .id("remove_contact") + .invisible() + .group_hover("", |style| style.visible()) + .child( + IconButton::new("remove_contact", Icon::Close) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), + ), + ) + }), + ) + .left_child( + // todo!() handle contacts with no avatar + Avatar::data(contact.user.avatar.clone().unwrap()) + .availability_indicator(if online { Some(!busy) } else { None }), + ) + .when(online && !busy, |el| { + el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) + }); - if let Some(avatar) = contact.user.avatar.clone() { - item = item.left_avatar(avatar); - } - - div().group("").child(item) - // let event_handler = - // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { - // Flex::row() - // .with_children(contact.user.avatar.clone().map(|avatar| { - // let status_badge = if contact.online { - // Some( - // Empty::new() - // .collapsed() - // .contained() - // .with_style(if busy { - // collab_theme.contact_status_busy - // } else { - // collab_theme.contact_status_free - // }) - // .aligned(), - // ) - // } else { - // None - // }; - // Stack::new() - // .with_child( - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left(), - // ) - // .with_children(status_badge) - // })) - - // .with_children(if calling { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style( - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(state), - // ) - // }); - - // if online && !busy { - // let room = ActiveCall::global(cx).read(cx).room(); - // let label = if room.is_some() { - // format!("Invite {} to join call", contact.user.github_login) - // } else { - // format!("Call {}", contact.user.github_login) - // }; - - // event_handler - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.call(user_id, Some(initial_project.clone()), cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // contact.user.id as usize, - // label, - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else { - // event_handler - // .with_tooltip::( - // contact.user.id as usize, - // format!( - // "{} is {}", - // contact.user.github_login, - // if busy { "on a call" } else { "offline" } - // ), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // }; + div() + .id(github_login.clone()) + .group("") + .child(item) + .tooltip(move |cx| { + let text = if !online { + format!(" {} is offline", &github_login) + } else if busy { + format!(" {} is on a call", &github_login) + } else { + let room = ActiveCall::global(cx).read(cx).room(); + if room.is_some() { + format!("Invite {} to join call", &github_login) + } else { + format!("Call {}", &github_login) + } + }; + Tooltip::text(text, cx) + }) } fn render_contact_request( @@ -2699,13 +2635,13 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.respond_to_contact_request(user_id, false, cx); })) - .color(color) + .icon_color(color) .tooltip(|cx| Tooltip::text("Decline invite", cx)), IconButton::new("remove_contact", Icon::Check) .on_click(cx.listener(move |this, _, cx| { this.respond_to_contact_request(user_id, true, cx); })) - .color(color) + .icon_color(color) .tooltip(|cx| Tooltip::text("Accept invite", cx)), ] } else { @@ -2714,7 +2650,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.remove_contact(user_id, &github_login, cx); })) - .color(color) + .icon_color(color) .tooltip(|cx| Tooltip::text("Cancel invite", cx))] }; @@ -2747,6 +2683,7 @@ impl CollabPanel { depth: usize, has_children: bool, is_selected: bool, + ix: usize, cx: &mut ViewContext, ) -> impl IntoElement { let channel_id = channel.id; @@ -2767,9 +2704,8 @@ impl CollabPanel { .map(|channel| channel.visibility) == Some(proto::ChannelVisibility::Public); let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); - let disclosed = has_children - .then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()) - .unwrap_or(false); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); let has_messages_notification = channel.unseen_message_id.is_some(); let has_notes_notification = channel.unseen_note_version.is_some(); @@ -2800,79 +2736,111 @@ impl CollabPanel { None }; - div().group("").child( - ListItem::new(channel_id as usize) - .indent_level(depth) - .left_icon(if is_public { Icon::Public } else { Icon::Hash }) - .selected(is_selected || is_active) - .child( - h_stack() - .w_full() - .justify_between() - .child( - h_stack() - .id(channel_id as usize) - .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.render(cx))) - .tooltip(|cx| Tooltip::text("Join channel", cx)), - ) - .child( - h_stack() - .child( - div() - .id("channel_chat") - .when(!has_messages_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new("channel_chat", Icon::MessageBubbles) - .color(if has_messages_notification { + let width = self.width.unwrap_or(px(240.)); + + div() + .id(channel_id as usize) + .group("") + .on_drag({ + let channel = channel.clone(); + move |cx| { + let channel = channel.clone(); + cx.build_view({ |cx| DraggedChannelView { channel, width } }) + } + }) + .drag_over::(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .on_drop( + cx.listener(move |this, view: &View, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel( + view.read(cx).channel.id, + Some(channel_id), + cx, + ) + }) + .detach_and_log_err(cx) + }), + ) + .child( + ListItem::new(channel_id as usize) + .indent_level(depth) + .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle + .left_icon(if is_public { Icon::Public } else { Icon::Hash }) + .selected(is_selected || is_active) + .child( + h_stack() + .w_full() + .justify_between() + .child( + h_stack() + .id(channel_id as usize) + .child(Label::new(channel.name.clone())) + .children(face_pile.map(|face_pile| face_pile.render(cx))), + ) + .child( + h_stack() + .child( + div() + .id("channel_chat") + .when(!has_messages_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new( + "channel_chat", + Icon::MessageBubbles, + ) + .icon_color(if has_messages_notification { Color::Default } else { Color::Muted }), - ) - .tooltip(|cx| Tooltip::text("Open channel chat", cx)), - ) - .child( - div() - .id("channel_notes") - .when(!has_notes_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new("channel_notes", Icon::File) - .color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .tooltip(|cx| { - Tooltip::text("Open channel notes", cx) - }), - ), - ), - ), - ) - .toggle(if has_children { - Toggle::Toggled(disclosed) - } else { - Toggle::NotToggleable - }) - .on_toggle( - cx.listener(move |this, _, cx| this.toggle_channel_collapsed(channel_id, cx)), - ) - .on_click(cx.listener(move |this, _, cx| { - if this.drag_target_channel == ChannelDragTarget::None { - if is_active { - this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) - } else { - this.join_channel(channel_id, cx) + ) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)), + ) + .child( + div() + .id("channel_notes") + .when(!has_notes_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_notes", Icon::File) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .tooltip(|cx| { + Tooltip::text("Open channel notes", cx) + }), + ), + ), + ), + ) + .toggle(disclosed) + .on_toggle( + cx.listener(move |this, _, cx| { + this.toggle_channel_collapsed(channel_id, cx) + }), + ) + .on_click(cx.listener(move |this, _, cx| { + if this.drag_target_channel == ChannelDragTarget::None { + if is_active { + this.open_channel_notes(channel_id, cx) + } else { + this.join_channel(channel_id, cx) + } } - } - })) - .on_secondary_mouse_down(cx.listener(|this, _, cx| { - todo!() // open context menu - })), - ) + })) + .on_secondary_mouse_down(cx.listener( + move |this, event: &MouseDownEvent, cx| { + this.deploy_channel_context_menu(event.position, channel_id, ix, cx) + }, + )), + ) + .tooltip(|cx| Tooltip::text("Join channel", cx)) // let channel_id = channel.id; // let collab_theme = &theme.collab_panel; @@ -3100,7 +3068,8 @@ impl CollabPanel { // ) // }) // .on_click(MouseButton::Left, move |_, this, cx| { - // if this.drag_target_channel == ChannelDragTarget::None { + // if this. + // drag_target_channel == ChannelDragTarget::None { // if is_active { // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) // } else { @@ -3257,18 +3226,40 @@ impl Render for CollabPanel { type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() + 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) - .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)) - } + .size_full() + .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() + .position(*position) + .anchor(gpui::AnchorCorner::TopLeft) + .child(menu.clone()) + })) } } @@ -3391,14 +3382,15 @@ impl Panel for CollabPanel { } fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width) + self.width.map_or_else( + || CollaborationPanelSettings::get_global(cx).default_width, + |width| width.0, + ) } fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - // todo!() - // self.serialize(cx); + self.width = size.map(|s| px(s)); + self.serialize(cx); cx.notify(); } @@ -3422,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) @@ -3540,3 +3532,34 @@ impl FocusableView for CollabPanel { // .contained() // .with_style(style.container) // } + +struct DraggedChannelView { + channel: Channel, + width: Pixels, +} + +impl Render for DraggedChannelView { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + h_stack() + .font(ui_font) + .bg(cx.theme().colors().background) + .w(self.width) + .p_1() + .gap_1() + .child( + IconElement::new( + if self.channel.visibility == proto::ChannelVisibility::Public { + Icon::Public + } else { + Icon::Hash + }, + ) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(Label::new(self.channel.name.clone())) + } +} diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index fee04ec40c..717ab2d897 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/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index ac72176d67..2cdf32ca36 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -35,19 +35,19 @@ use gpui::{ ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; -use project::Project; +use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ - h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton, - IconElement, IconSize, KeyBinding, Tooltip, + h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, + IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; use crate::face_pile::FacePile; -// const MAX_PROJECT_NAME_LENGTH: usize = 40; -// const MAX_BRANCH_NAME_LENGTH: usize = 40; +const MAX_PROJECT_NAME_LENGTH: usize = 40; +const MAX_BRANCH_NAME_LENGTH: usize = 40; // actions!( // collab, @@ -103,17 +103,18 @@ impl Render for CollabTitlebarItem { .update(cx, |this, cx| this.call_state().remote_participants(cx)) .log_err() .flatten(); - let mic_icon = if self + let is_muted = self .workspace .update(cx, |this, cx| this.call_state().is_muted(cx)) .log_err() .flatten() - .unwrap_or_default() - { - ui::Icon::MicMute - } else { - ui::Icon::Mic - }; + .unwrap_or_default(); + let is_deafened = self + .workspace + .update(cx, |this, cx| this.call_state().is_deafened(cx)) + .log_err() + .flatten() + .unwrap_or_default(); let speakers_icon = if self .workspace .update(cx, |this, cx| this.call_state().is_deafened(cx)) @@ -149,53 +150,11 @@ impl Render for CollabTitlebarItem { .child( h_stack() .gap_1() - // TODO - Add player menu - .child( - div() - .border() - .border_color(gpui::red()) - .id("project_owner_indicator") - .child( - Button::new("player") - .variant(ButtonVariant::Ghost) - .color(Some(Color::Player(0))), - ) - .tooltip(move |cx| Tooltip::text("Toggle following", cx)), - ) - // TODO - Add project menu - .child( - div() - .border() - .border_color(gpui::red()) - .id("titlebar_project_menu_button") - .child(Button::new("project_name").variant(ButtonVariant::Ghost)) - .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), - ) - // TODO - Add git menu - .child( - div() - .border() - .border_color(gpui::red()) - .id("titlebar_git_menu_button") - .child( - Button::new("branch_name") - .variant(ButtonVariant::Ghost) - .color(Some(Color::Muted)), - ) - .tooltip(move |cx| { - cx.build_view(|_| { - Tooltip::new("Recent Branches") - .key_binding(KeyBinding::new(gpui::KeyBinding::new( - "cmd-b", - // todo!() Replace with real action. - gpui::NoAction, - None, - ))) - .meta("Only local branches shown") - }) - .into() - }), - ), + .when(is_in_room, |this| { + this.children(self.render_project_owner(cx)) + }) + .child(self.render_project_name(cx)) + .children(self.render_project_branch(cx)), ) .when_some( users.zip(current_user.clone()), @@ -236,62 +195,129 @@ impl Render for CollabTitlebarItem { .when(is_in_room, |this| { this.child( h_stack() + .gap_1() .child( h_stack() - .child(Button::new(if is_shared { "Unshare" } else { "Share" })) - .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().hang_up(cx).detach(); - }) - .log_err(); - } - })), + .gap_1() + .child( + Button::new( + "toggle_sharing", + if is_shared { "Unshare" } else { "Share" }, + ) + .style(ButtonStyle::Subtle), + ) + .child( + IconButton::new("leave-call", ui::Icon::Exit) + .style(ButtonStyle::Subtle) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().hang_up(cx).detach(); + }) + .log_err(); + } + }), + ), ) .child( h_stack() - .child(IconButton::new("mute-microphone", mic_icon).on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_mute(cx); - }) - .log_err(); - } - })) - .child(IconButton::new("mute-sound", speakers_icon).on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_deafen(cx); - }) - .log_err(); - } - })) - .child(IconButton::new("screen-share", ui::Icon::Screen).on_click( - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_screen_share(cx); - }) - .log_err(); - }, - )) + .gap_1() + .child( + IconButton::new( + "mute-microphone", + if is_muted { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }, + ) + .style(ButtonStyle::Subtle) + .selected(is_muted) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_mute(cx); + }) + .log_err(); + } + }), + ) + .child( + IconButton::new("mute-sound", speakers_icon) + .style(ButtonStyle::Subtle) + .selected(is_deafened.clone()) + .tooltip(move |cx| { + Tooltip::with_meta( + "Deafen Audio", + None, + "Mic will be muted", + cx, + ) + }) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_deafen(cx); + }) + .log_err(); + } + }), + ) + .child( + IconButton::new("screen-share", ui::Icon::Screen) + .style(ButtonStyle::Subtle) + .on_click(move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_screen_share(cx); + }) + .log_err(); + }), + ) .pl_2(), ), ) }) - .map(|this| { + .child(h_stack().px_1p5().map(|this| { if let Some(user) = current_user { this.when_some(user.avatar.clone(), |this, avatar| { - this.child(ui::Avatar::data(avatar)) + // TODO: Finish implementing user menu popover + // + this.child( + popover_menu("user-menu") + .menu(|cx| ContextMenu::build(cx, |menu, _| menu.header("ADADA"))) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack().gap_0p5().child(Avatar::data(avatar)).child( + IconElement::new(Icon::ChevronDown) + .color(Color::Muted), + ), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + .anchor(gpui::AnchorCorner::TopRight), + ) + // this.child( + // ButtonLike::new("user-menu") + // .child( + // h_stack().gap_0p5().child(Avatar::data(avatar)).child( + // IconElement::new(Icon::ChevronDown).color(Color::Muted), + // ), + // ) + // .style(ButtonStyle::Subtle) + // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + // ) }) } else { - this.child(Button::new("Sign in").on_click(move |_, cx| { + this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { let client = client.clone(); cx.spawn(move |mut cx| async move { client @@ -301,29 +327,8 @@ impl Render for CollabTitlebarItem { }) .detach(); })) - // Temporary, will be removed when the last part of button2 is merged - .child( - div().border().border_color(gpui::blue()).child( - ButtonLike::new("test-button") - .children([ - Avatar::uri( - "https://avatars.githubusercontent.com/u/1714999?v=4", - ) - .into_element() - .into_any(), - IconElement::new(ui::Icon::ChevronDown) - .size(IconSize::Small) - .into_element() - .into_any(), - ]) - .on_click(move |event, _cx| { - dbg!(format!("clicked: {:?}", event.down.position)); - }) - .tooltip(|cx| Tooltip::text("Test tooltip", cx)), - ), - ) } - }) + })) } } @@ -442,6 +447,110 @@ impl CollabTitlebarItem { } } + // resolve if you are in a room -> render_project_owner + // render_project_owner -> resolve if you are in a room -> Option + + pub fn render_project_owner(&self, cx: &mut ViewContext) -> Option { + // TODO: We can't finish implementing this until project sharing works + // - [ ] Show the project owner when the project is remote (maybe done) + // - [x] Show the project owner when the project is local + // - [ ] Show the project owner with a lock icon when the project is local and unshared + + let remote_id = self.project.read(cx).remote_id(); + let is_local = remote_id.is_none(); + let is_shared = self.project.read(cx).is_shared(); + let (user_name, participant_index) = { + if let Some(host) = self.project.read(cx).host() { + debug_assert!(!is_local); + let (Some(host_user), Some(participant_index)) = ( + self.user_store.read(cx).get_cached_user(host.user_id), + self.user_store + .read(cx) + .participant_indices() + .get(&host.user_id), + ) else { + return None; + }; + (host_user.github_login.clone(), participant_index.0) + } else { + debug_assert!(is_local); + let name = self + .user_store + .read(cx) + .current_user() + .map(|user| user.github_login.clone())?; + (name, 0) + } + }; + Some( + div().border().border_color(gpui::red()).child( + Button::new( + "project_owner_trigger", + format!("{user_name} ({})", !is_shared), + ) + .color(Color::Player(participant_index)) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + ), + ) + } + + pub fn render_project_name(&self, cx: &mut ViewContext) -> impl Element { + let name = { + let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + worktree.root_name() + }); + + names.next().unwrap_or("") + }; + + let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); + + div().border().border_color(gpui::red()).child( + Button::new("project_name_trigger", name) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), + ) + } + + pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { + let entry = { + let mut names_and_branches = + self.project.read(cx).visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + worktree.root_git_entry() + }); + + names_and_branches.next().flatten() + }; + + let branch_name = entry + .as_ref() + .and_then(RepositoryEntry::branch) + .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; + + Some( + div().border().border_color(gpui::red()).child( + Button::new("project_branch_trigger", branch_name) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| { + cx.build_view(|_| { + Tooltip::new("Recent Branches") + .key_binding(KeyBinding::new(gpui::KeyBinding::new( + "cmd-b", + // todo!() Replace with real action. + gpui::NoAction, + None, + ))) + .meta("Local branches only") + }) + .into() + }), + ), + ) + } + // fn collect_title_root_names( // &self, // theme: Arc, diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index 0519b6fc4a..ce6c8f0f57 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -2,10 +2,11 @@ use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; use futures::StreamExt; use gpui::{ - div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, - StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle, + div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext, + VisualContext as _, WindowHandle, }; use std::sync::{Arc, Weak}; +use ui::prelude::*; use ui::{h_stack, v_stack, Avatar, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -199,14 +200,24 @@ impl IncomingCallNotification { fn render_buttons(&self, cx: &mut ViewContext) -> impl Element { h_stack() - .child(Button::new("Accept").render(cx).bg(green()).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(true, cx) - })) - .child(Button::new("Decline").render(cx).bg(red()).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(false, cx) - })) + .child( + Button::new("accept", "Accept") + .render(cx) + // .bg(green()) + .on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + }), + ) + .child( + Button::new("decline", "Decline") + .render(cx) + // .bg(red()) + .on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) + }), + ) // enum Accept {} // enum Decline {} diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index f9b58b1d56..04688b0549 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 4748f63e5d..9ff3c04de8 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 8d3c2fedd6..86d8d01db1 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 dc7f0a1f3f..0a0f4da893 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 ac24b7ad50..92b0641dea 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/diagnostics2/src/toolbar_controls.rs b/crates/diagnostics2/src/toolbar_controls.rs index e513076ec8..1a604b76c8 100644 --- a/crates/diagnostics2/src/toolbar_controls.rs +++ b/crates/diagnostics2/src/toolbar_controls.rs @@ -1,5 +1,6 @@ use crate::ProjectDiagnosticsEditor; use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; +use ui::prelude::*; use ui::{Icon, IconButton, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 408142ae07..5aeecbae97 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 c8ce37d7e2..9b2681e563 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::{ @@ -73,7 +74,7 @@ use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; -use rpc::proto::*; +use rpc::proto::{self, *}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -99,7 +100,8 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip}; +use ui::prelude::*; +use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, @@ -152,9 +154,7 @@ pub fn render_parsed_markdown( } }), ); - let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights); - // todo!("add the ability to change cursor style for link ranges") let mut links = Vec::new(); let mut link_ranges = Vec::new(); for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { @@ -166,7 +166,7 @@ pub fn render_parsed_markdown( InteractiveText::new( element_id, - StyledText::new(parsed.text.clone()).with_runs(runs), + StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights), ) .on_click(link_ranges, move |clicked_range_ix, cx| { match &links[clicked_range_ix] { @@ -407,133 +407,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 +505,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 +518,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]>)>, @@ -971,95 +853,94 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - _editor: &Editor, - _cx: &mut ViewContext, + editor: &Editor, + cx: &mut ViewContext, ) -> Option> { - // todo!("implementation below "); - None + let settings = EditorSettings::get_global(cx); + if !settings.show_completion_documentation { + return None; + } + + let Some(project) = editor.project.clone() else { + return None; + }; + + let client = project.read(cx).client(); + let language_registry = project.read(cx).languages().clone(); + + let is_remote = project.read(cx).is_remote(); + let project_id = project.read(cx).remote_id(); + + let completions = self.completions.clone(); + let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); + + Some(cx.spawn(move |this, mut cx| async move { + if is_remote { + let Some(project_id) = project_id else { + log::error!("Remote project without remote_id"); + return; + }; + + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + Self::resolve_completion_documentation_remote( + project_id, + server_id, + completions.clone(), + completion_index, + completion, + client.clone(), + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } else { + for completion_index in completion_indices { + let completions_guard = completions.read(); + let completion = &completions_guard[completion_index]; + if completion.documentation.is_some() { + continue; + } + + let server_id = completion.server_id; + let completion = completion.lsp_completion.clone(); + drop(completions_guard); + + let server = project + .read_with(&mut cx, |project, _| { + project.language_server_for_id(server_id) + }) + .ok() + .flatten(); + let Some(server) = server else { + return; + }; + + Self::resolve_completion_documentation_local( + server, + completions.clone(), + completion_index, + completion, + language_registry.clone(), + ) + .await; + + _ = this.update(&mut cx, |_, cx| cx.notify()); + } + } + })) } - // { - // let settings = EditorSettings::get_global(cx); - // if !settings.show_completion_documentation { - // return None; - // } - - // let Some(project) = editor.project.clone() else { - // return None; - // }; - - // let client = project.read(cx).client(); - // let language_registry = project.read(cx).languages().clone(); - - // let is_remote = project.read(cx).is_remote(); - // let project_id = project.read(cx).remote_id(); - - // let completions = self.completions.clone(); - // let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - - // Some(cx.spawn(move |this, mut cx| async move { - // if is_remote { - // let Some(project_id) = project_id else { - // log::error!("Remote project without remote_id"); - // return; - // }; - - // for completion_index in completion_indices { - // let completions_guard = completions.read(); - // let completion = &completions_guard[completion_index]; - // if completion.documentation.is_some() { - // continue; - // } - - // let server_id = completion.server_id; - // let completion = completion.lsp_completion.clone(); - // drop(completions_guard); - - // Self::resolve_completion_documentation_remote( - // project_id, - // server_id, - // completions.clone(), - // completion_index, - // completion, - // client.clone(), - // language_registry.clone(), - // ) - // .await; - - // _ = this.update(&mut cx, |_, cx| cx.notify()); - // } - // } else { - // for completion_index in completion_indices { - // let completions_guard = completions.read(); - // let completion = &completions_guard[completion_index]; - // if completion.documentation.is_some() { - // continue; - // } - - // let server_id = completion.server_id; - // let completion = completion.lsp_completion.clone(); - // drop(completions_guard); - - // let server = project.read_with(&mut cx, |project, _| { - // project.language_server_for_id(server_id) - // }); - // let Some(server) = server else { - // return; - // }; - - // Self::resolve_completion_documentation_local( - // server, - // completions.clone(), - // completion_index, - // completion, - // language_registry.clone(), - // ) - // .await; - - // _ = this.update(&mut cx, |_, cx| cx.notify()); - // } - // } - // })) - // } fn attempt_resolve_selected_completion_documentation( &mut self, @@ -1080,10 +961,9 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completions_guard = completions.read(); let completion = &completions_guard[completion_index]; - // todo!() - // if completion.documentation.is_some() { - // return; - // } + if completion.documentation.is_some() { + return; + } let server_id = completion.server_id; let completion = completion.lsp_completion.clone(); @@ -1142,41 +1022,40 @@ impl CompletionsMenu { client: Arc, language_registry: Arc, ) { - // todo!() - // let request = proto::ResolveCompletionDocumentation { - // project_id, - // language_server_id: server_id.0 as u64, - // lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), - // }; + let request = proto::ResolveCompletionDocumentation { + project_id, + language_server_id: server_id.0 as u64, + lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(), + }; - // let Some(response) = client - // .request(request) - // .await - // .context("completion documentation resolve proto request") - // .log_err() - // else { - // return; - // }; + let Some(response) = client + .request(request) + .await + .context("completion documentation resolve proto request") + .log_err() + else { + return; + }; - // if response.text.is_empty() { - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(Documentation::Undocumented); - // } + if response.text.is_empty() { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } - // let documentation = if response.is_markdown { - // Documentation::MultiLineMarkdown( - // markdown::parse_markdown(&response.text, &language_registry, None).await, - // ) - // } else if response.text.lines().count() <= 1 { - // Documentation::SingleLine(response.text) - // } else { - // Documentation::MultiLinePlainText(response.text) - // }; + let documentation = if response.is_markdown { + Documentation::MultiLineMarkdown( + markdown::parse_markdown(&response.text, &language_registry, None).await, + ) + } else if response.text.lines().count() <= 1 { + Documentation::SingleLine(response.text) + } else { + Documentation::MultiLinePlainText(response.text) + }; - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(documentation); + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); } async fn resolve_completion_documentation_local( @@ -1186,38 +1065,37 @@ impl CompletionsMenu { completion: lsp::CompletionItem, language_registry: Arc, ) { - // todo!() - // let can_resolve = server - // .capabilities() - // .completion_provider - // .as_ref() - // .and_then(|options| options.resolve_provider) - // .unwrap_or(false); - // if !can_resolve { - // return; - // } + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return; + } - // let request = server.request::(completion); - // let Some(completion_item) = request.await.log_err() else { - // return; - // }; + let request = server.request::(completion); + let Some(completion_item) = request.await.log_err() else { + return; + }; - // if let Some(lsp_documentation) = completion_item.documentation { - // let documentation = language::prepare_completion_documentation( - // &lsp_documentation, - // &language_registry, - // None, // TODO: Try to reasonably work out which language the completion is for - // ) - // .await; + if let Some(lsp_documentation) = completion_item.documentation { + let documentation = language::prepare_completion_documentation( + &lsp_documentation, + &language_registry, + None, // TODO: Try to reasonably work out which language the completion is for + ) + .await; - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(documentation); - // } else { - // let mut completions = completions.write(); - // let completion = &mut completions[completion_index]; - // completion.documentation = Some(Documentation::Undocumented); - // } + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(documentation); + } else { + let mut completions = completions.write(); + let completion = &mut completions[completion_index]; + completion.documentation = Some(Documentation::Undocumented); + } } fn visible(&self) -> bool { @@ -1320,11 +1198,7 @@ impl CompletionsMenu { ), ); let completion_label = StyledText::new(completion.label.text.clone()) - .with_runs(text_runs_for_highlights( - &completion.label.text, - &style.text, - highlights, - )); + .with_highlights(&style.text, highlights); let documentation_label = if let Some(Documentation::SingleLine(text)) = documentation { Some(SharedString::from(text.clone())) @@ -1738,21 +1612,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, @@ -1772,14 +1636,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| { @@ -1796,17 +1653,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) }); @@ -1862,7 +1713,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(), @@ -1876,8 +1726,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, @@ -1886,7 +1735,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, @@ -2004,25 +1852,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() @@ -4391,7 +4239,7 @@ impl Editor { editor.fold_at(&FoldAt { buffer_row }, cx); } })) - .color(ui::Color::Muted) + .icon_color(ui::Color::Muted) }) }) .flatten() @@ -8378,6 +8226,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)) @@ -8800,62 +8660,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; - // }; + fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx); + if buffer.is_singleton() { + cx.propagate(); + return; + } - // let editor = editor_handle.read(cx); - // let buffer = editor.buffer.read(cx); - // if buffer.is_singleton() { - // cx.propagate(); - // return; - // } + let Some(workspace) = self.workspace() else { + 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 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) + } + } - // 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()); + 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.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); - // }); - // }); - // } + // 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()); - // pane.update(cx, |pane, _| pane.enable_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, @@ -9401,7 +9255,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, @@ -9414,8 +9268,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(), @@ -9450,106 +9302,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, @@ -9796,72 +9548,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; @@ -10064,31 +9750,6 @@ pub fn diagnostic_style( } } -pub fn text_runs_for_highlights( - text: &str, - default_style: &TextStyle, - highlights: impl IntoIterator, HighlightStyle)>, -) -> Vec { - let mut runs = Vec::new(); - let mut ix = 0; - for (range, highlight) in highlights { - if ix < range.start { - runs.push(default_style.clone().to_run(range.start - ix)); - } - runs.push( - default_style - .clone() - .highlight(highlight) - .to_run(range.len()), - ); - ix = range.end; - } - if ix < text.len() { - runs.push(default_style.to_run(text.len() - ix)); - } - runs -} - pub fn styled_runs_for_code_label<'a>( label: &'a CodeLabel, syntax_theme: &'a theme::SyntaxTheme, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 6865e81cfa..e640be8efe 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -5427,178 +5427,177 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) ); } -//todo!(completion) -// #[gpui::test] -// async fn test_completion(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_completion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string(), ":".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// cx.set_state(indoc! {" -// oneˇ -// two -// three -// "}); -// cx.simulate_keystroke("."); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.|<> -// two -// three -// "}, -// vec!["first_completion", "second_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor.context_menu_next(&Default::default(), cx); -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// "}); + cx.set_state(indoc! {" + oneˇ + two + three + "}); + cx.simulate_keystroke("."); + handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three + "}, + vec!["first_completion", "second_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor.context_menu_next(&Default::default(), cx); + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + "}); -// handle_resolve_completion_request( -// &mut cx, -// Some(vec![ -// ( -// //This overlaps with the primary completion edit which is -// //misbehavior from the LSP spec, test that we filter it out -// indoc! {" -// one.second_ˇcompletion -// two -// threeˇ -// "}, -// "overlapping additional edit", -// ), -// ( -// indoc! {" -// one.second_completion -// two -// threeˇ -// "}, -// "\nadditional edit", -// ), -// ]), -// ) -// .await; -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {" -// one.second_completionˇ -// two -// three -// additional edit -// "}); + handle_resolve_completion_request( + &mut cx, + Some(vec![ + ( + //This overlaps with the primary completion edit which is + //misbehavior from the LSP spec, test that we filter it out + indoc! {" + one.second_ˇcompletion + two + threeˇ + "}, + "overlapping additional edit", + ), + ( + indoc! {" + one.second_completion + two + threeˇ + "}, + "\nadditional edit", + ), + ]), + ) + .await; + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {" + one.second_completionˇ + two + three + additional edit + "}); -// cx.set_state(indoc! {" -// one.second_completion -// twoˇ -// threeˇ -// additional edit -// "}); -// cx.simulate_keystroke(" "); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("s"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.set_state(indoc! {" + one.second_completion + twoˇ + threeˇ + additional edit + "}); + cx.simulate_keystroke(" "); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("s"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sˇ -// three sˇ -// additional edit -// "}); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two s -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; + cx.assert_editor_state(indoc! {" + one.second_completion + two sˇ + three sˇ + additional edit + "}); + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two s + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; -// cx.simulate_keystroke("i"); + cx.simulate_keystroke("i"); -// handle_completion_request( -// &mut cx, -// indoc! {" -// one.second_completion -// two si -// three -// additional edit -// "}, -// vec!["fourth_completion", "fifth_completion", "sixth_completion"], -// ) -// .await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two si + three + additional edit + "}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {" -// one.second_completion -// two sixth_completionˇ -// three sixth_completionˇ -// additional edit -// "}); + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {" + one.second_completion + two sixth_completionˇ + three sixth_completionˇ + additional edit + "}); -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); -// cx.update(|cx| { -// cx.update_global::(|settings, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.show_completions_on_input = Some(false); -// }); -// }) -// }); -// cx.set_state("editorˇ"); -// cx.simulate_keystroke("."); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.simulate_keystroke("c"); -// cx.simulate_keystroke("l"); -// cx.simulate_keystroke("o"); -// cx.assert_editor_state("editor.cloˇ"); -// assert!(cx.editor(|e, _| e.context_menu.read().is_none())); -// cx.update_editor(|editor, cx| { -// editor.show_completions(&ShowCompletions, cx); -// }); -// handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state("editor.closeˇ"); -// handle_resolve_completion_request(&mut cx, None).await; -// apply_additional_edits.await.unwrap(); -// } + cx.update(|cx| { + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, |settings| { + settings.show_completions_on_input = Some(false); + }); + }) + }); + cx.set_state("editorˇ"); + cx.simulate_keystroke("."); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.simulate_keystroke("c"); + cx.simulate_keystroke("l"); + cx.simulate_keystroke("o"); + cx.assert_editor_state("editor.cloˇ"); + assert!(cx.editor(|e, _| e.context_menu.read().is_none())); + cx.update_editor(|editor, cx| { + editor.show_completions(&ShowCompletions, cx); + }); + handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state("editor.closeˇ"); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); +} #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { @@ -7803,197 +7802,196 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test ); } -//todo!(completions) -// #[gpui::test] -// async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![".".to_string()]), -// resolve_provider: Some(true), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); -// cx.simulate_keystroke("."); -// let completion_item = lsp::CompletionItem { -// label: "some".into(), -// kind: Some(lsp::CompletionItemKind::SNIPPET), -// detail: Some("Wrap the expression in an `Option::Some`".to_string()), -// documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "```rust\nSome(2)\n```".to_string(), -// })), -// deprecated: Some(false), -// sort_text: Some("fffffff2".to_string()), -// filter_text: Some("some".to_string()), -// insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), -// text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { -// range: lsp::Range { -// start: lsp::Position { -// line: 0, -// character: 22, -// }, -// end: lsp::Position { -// line: 0, -// character: 22, -// }, -// }, -// new_text: "Some(2)".to_string(), -// })), -// additional_text_edits: Some(vec![lsp::TextEdit { -// range: lsp::Range { -// start: lsp::Position { -// line: 0, -// character: 20, -// }, -// end: lsp::Position { -// line: 0, -// character: 22, -// }, -// }, -// new_text: "".to_string(), -// }]), -// ..Default::default() -// }; + cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); + cx.simulate_keystroke("."); + let completion_item = lsp::CompletionItem { + label: "some".into(), + kind: Some(lsp::CompletionItemKind::SNIPPET), + detail: Some("Wrap the expression in an `Option::Some`".to_string()), + documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "```rust\nSome(2)\n```".to_string(), + })), + deprecated: Some(false), + sort_text: Some("fffffff2".to_string()), + filter_text: Some("some".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 22, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "Some(2)".to_string(), + })), + additional_text_edits: Some(vec![lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 22, + }, + }, + new_text: "".to_string(), + }]), + ..Default::default() + }; -// let closure_completion_item = completion_item.clone(); -// let mut request = cx.handle_request::(move |_, _, _| { -// let task_completion_item = closure_completion_item.clone(); -// async move { -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// task_completion_item, -// ]))) -// } -// }); + let closure_completion_item = completion_item.clone(); + let mut request = cx.handle_request::(move |_, _, _| { + let task_completion_item = closure_completion_item.clone(); + async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + task_completion_item, + ]))) + } + }); -// request.next().await; + request.next().await; -// cx.condition(|editor, _| editor.context_menu_visible()) -// .await; -// let apply_additional_edits = cx.update_editor(|editor, cx| { -// editor -// .confirm_completion(&ConfirmCompletion::default(), cx) -// .unwrap() -// }); -// cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + let apply_additional_edits = cx.update_editor(|editor, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), cx) + .unwrap() + }); + cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); -// cx.handle_request::(move |_, _, _| { -// let task_completion_item = completion_item.clone(); -// async move { Ok(task_completion_item) } -// }) -// .next() -// .await -// .unwrap(); -// apply_additional_edits.await.unwrap(); -// cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); -// } + cx.handle_request::(move |_, _, _| { + let task_completion_item = completion_item.clone(); + async move { Ok(task_completion_item) } + }) + .next() + .await + .unwrap(); + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); +} -// #[gpui::test] -// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); +#[gpui::test] +async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); -// let mut cx = EditorLspTestContext::new( -// Language::new( -// LanguageConfig { -// path_suffixes: vec!["jsx".into()], -// overrides: [( -// "element".into(), -// LanguageConfigOverride { -// word_characters: Override::Set(['-'].into_iter().collect()), -// ..Default::default() -// }, -// )] -// .into_iter() -// .collect(), -// ..Default::default() -// }, -// Some(tree_sitter_typescript::language_tsx()), -// ) -// .with_override_query("(jsx_self_closing_element) @element") -// .unwrap(), -// lsp::ServerCapabilities { -// completion_provider: Some(lsp::CompletionOptions { -// trigger_characters: Some(vec![":".to_string()]), -// ..Default::default() -// }), -// ..Default::default() -// }, -// cx, -// ) -// .await; + let mut cx = EditorLspTestContext::new( + Language::new( + LanguageConfig { + path_suffixes: vec!["jsx".into()], + overrides: [( + "element".into(), + LanguageConfigOverride { + word_characters: Override::Set(['-'].into_iter().collect()), + ..Default::default() + }, + )] + .into_iter() + .collect(), + ..Default::default() + }, + Some(tree_sitter_typescript::language_tsx()), + ) + .with_override_query("(jsx_self_closing_element) @element") + .unwrap(), + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; -// cx.lsp -// .handle_request::(move |_, _| async move { -// Ok(Some(lsp::CompletionResponse::Array(vec![ -// lsp::CompletionItem { -// label: "bg-blue".into(), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "bg-red".into(), -// ..Default::default() -// }, -// lsp::CompletionItem { -// label: "bg-yellow".into(), -// ..Default::default() -// }, -// ]))) -// }); + cx.lsp + .handle_request::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "bg-blue".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-red".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-yellow".into(), + ..Default::default() + }, + ]))) + }); -// cx.set_state(r#"

"#); + cx.set_state(r#"

"#); -// // Trigger completion when typing a dash, because the dash is an extra -// // word character in the 'element' scope, which contains the cursor. -// cx.simulate_keystroke("-"); -// cx.executor().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-red", "bg-blue", "bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); + // Trigger completion when typing a dash, because the dash is an extra + // word character in the 'element' scope, which contains the cursor. + cx.simulate_keystroke("-"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-red", "bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); -// cx.simulate_keystroke("l"); -// cx.executor().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-blue", "bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); + cx.simulate_keystroke("l"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); -// // When filtering completions, consider the character after the '-' to -// // be the start of a subword. -// cx.set_state(r#"

"#); -// cx.simulate_keystroke("l"); -// cx.executor().run_until_parked(); -// cx.update_editor(|editor, _| { -// if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { -// assert_eq!( -// menu.matches.iter().map(|m| &m.string).collect::>(), -// &["bg-yellow"] -// ); -// } else { -// panic!("expected completion menu to be open"); -// } -// }); -// } + // When filtering completions, consider the character after the '-' to + // be the start of a subword. + cx.set_state(r#"

"#); + cx.simulate_keystroke("l"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} #[gpui::test] async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 74ca292ecf..3abe5a37f9 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; @@ -48,6 +51,7 @@ use std::{ }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; +use ui::prelude::*; use ui::{h_stack, IconButton, Tooltip}; use util::ResultExt; use workspace::item::Item; @@ -138,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); @@ -262,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); @@ -311,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, @@ -364,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, @@ -724,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 line_height = layout.position_map.line_height; - // let scroll_position = layout.position_map.snapshot.scroll_position(); - // let scroll_top = scroll_position.y * 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; + 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); + 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(), + ); - // 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; + } - // continue; - // } + DisplayDiffHunk::Unfolded { + display_row_range, + status, + } => (display_row_range, status), + }; - // 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") - // 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; - // //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 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(), + ); - // 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); + continue; + } + }; - // 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(), - // }); + let start_row = display_row_range.start; + let end_row = display_row_range.end; - // continue; - // } - // }; + let start_y = start_row as f32 * line_height - scroll_top; + let end_y = end_row as f32 * line_height - scroll_top; - // 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 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( @@ -830,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| { @@ -1137,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, + |_, _| {}, + ); + } }) }, ) @@ -1661,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(); @@ -1701,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 = { @@ -1727,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); @@ -2063,7 +2110,7 @@ impl EditorElement { .unwrap(); LayoutState { - mode: editor_mode, + mode: snapshot.mode, position_map: Arc::new(PositionMap { size: bounds.size, scroll_position: point( @@ -2307,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(); } } @@ -2326,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() } } @@ -2350,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, @@ -2361,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(); @@ -2616,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, ()) }) @@ -2656,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| { @@ -2697,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 { @@ -4133,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/hover_popover.rs b/crates/editor2/src/hover_popover.rs index f80168ed25..2f2e8ee937 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -15,7 +15,7 @@ use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; -use ui::Tooltip; +use ui::{StyledExt, Tooltip}; use util::TryFutureExt; use workspace::Workspace; @@ -476,8 +476,10 @@ impl InfoPopover { ) -> AnyElement { div() .id("info_popover") + .elevation_2(cx) + .text_ui() + .p_2() .overflow_y_scroll() - .bg(gpui::red()) .max_w(max_size.width) .max_h(max_size.height) // Prevent a mouse move on the popover from being propagated to the editor, diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 179aa8cd6f..c02f095257 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) - // }; + 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), - // ); - // } + 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); + 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 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 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 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 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 Default for CursorPosition { + fn default() -> Self { + Self::new() + } +} -// impl CursorPosition { -// pub fn new() -> Self { -// Self { -// position: None, -// selected_count: 0, -// _observe_active_editor: None, -// } -// } +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); + 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)); + 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(); -// } -// } + cx.notify(); + } +} -// impl Entity for CursorPosition { -// type Event = (); -// } +impl Render for CursorPosition { + type Element = Div; -// impl View for CursorPosition { -// fn ui_name() -> &'static str { -// "CursorPosition" -// } + 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(); + } -// 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() -// } -// } -// } + 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; -// } +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(); -// } -// } + cx.notify(); + } +} fn path_for_buffer<'a>( buffer: &Model, diff --git a/crates/editor2/src/link_go_to_definition.rs b/crates/editor2/src/link_go_to_definition.rs index d36762f395..092882573c 100644 --- a/crates/editor2/src/link_go_to_definition.rs +++ b/crates/editor2/src/link_go_to_definition.rs @@ -5,7 +5,7 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId, SelectPhase, }; -use gpui::{Task, ViewContext}; +use gpui::{px, Task, ViewContext}; use language::{Bias, ToOffset}; use lsp::LanguageServerId; use project::{ @@ -13,6 +13,7 @@ use project::{ ResolveState, }; use std::ops::Range; +use theme::ActiveTheme as _; use util::TryFutureExt; #[derive(Debug, Default)] @@ -485,40 +486,45 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // todo!() - // // Highlight symbol using theme link definition highlight style - // let style = theme::current(cx).editor.link_definition; - // let highlight_range = - // symbol_range.unwrap_or_else(|| match &trigger_point { - // TriggerPoint::Text(trigger_anchor) => { - // let snapshot = &snapshot.buffer_snapshot; - // // If no symbol range returned from language server, use the surrounding word. - // let (offset_range, _) = - // snapshot.surrounding_word(*trigger_anchor); - // RangeInEditor::Text( - // snapshot.anchor_before(offset_range.start) - // ..snapshot.anchor_after(offset_range.end), - // ) - // } - // TriggerPoint::InlayHint(highlight, _, _) => { - // RangeInEditor::Inlay(highlight.clone()) - // } - // }); + let style = gpui::HighlightStyle { + underline: Some(gpui::UnderlineStyle { + thickness: px(1.), + ..Default::default() + }), + color: Some(gpui::red()), + ..Default::default() + }; + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + RangeInEditor::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(highlight, _, _) => { + RangeInEditor::Inlay(highlight.clone()) + } + }); - // match highlight_range { - // RangeInEditor::Text(text_range) => this - // .highlight_text::( - // vec![text_range], - // style, - // cx, - // ), - // RangeInEditor::Inlay(highlight) => this - // .highlight_inlays::( - // vec![highlight], - // style, - // cx, - // ), - // } + match highlight_range { + RangeInEditor::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + RangeInEditor::Inlay(highlight) => this + .highlight_inlays::( + vec![highlight], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } diff --git a/crates/editor2/src/mouse_context_menu.rs b/crates/editor2/src/mouse_context_menu.rs index b70a826bf8..fdeec9110b 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); + } - // if !editor.focused { - // cx.focus_self(); - // } + // Don't show context menu for inline editors + if editor.mode() != EditorMode::Full { + return; + } - // // Don't show context menu for inline editors - // if editor.mode() != EditorMode::Full { - // return; - // } + // 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 the context menu if there isn't a project associated with this editor - // if editor.project.is_none() { - // 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); + }); - // // 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 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); - // 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(); + 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 = 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 d9f0ec3764..8d71916210 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -592,31 +592,32 @@ impl<'a> MutableSelectionsCollection<'a> { self.select(selections) } - pub fn select_anchor_ranges>>(&mut self, ranges: I) { - todo!() - // let buffer = self.buffer.read(self.cx).snapshot(self.cx); - // let selections = ranges - // .into_iter() - // .map(|range| { - // let mut start = range.start; - // let mut end = range.end; - // let reversed = if start.cmp(&end, &buffer).is_gt() { - // mem::swap(&mut start, &mut end); - // true - // } else { - // false - // }; - // Selection { - // id: post_inc(&mut self.collection.next_selection_id), - // start, - // end, - // reversed, - // goal: SelectionGoal::None, - // } - // }) - // .collect::>(); - - // self.select_anchors(selections) + pub fn select_anchor_ranges(&mut self, ranges: I) + where + I: IntoIterator>, + { + let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start.cmp(&end, &buffer).is_gt() { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.collection.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } + }) + .collect::>(); + self.select_anchors(selections) } pub fn new_selection_id(&mut self) -> usize { diff --git a/crates/editor2/src/test.rs b/crates/editor2/src/test.rs index ec37c57f2c..4f6e157e4e 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 b7a4a387ab..156b062df8 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 a24da580eb..9938b94edb 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 6e8a0868b9..24e493cb81 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.rs b/crates/gpui2/src/app.rs index e8f2a60a6a..94a7d3be0b 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -520,6 +520,10 @@ impl AppContext { self.platform.should_auto_hide_scrollbars() } + pub fn restart(&self) { + self.platform.restart() + } + pub(crate) fn push_effect(&mut self, effect: Effect) { match &effect { Effect::Notify { emitter } => { diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 9637720a67..c915753749 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 635e8b634f..ced0a4767c 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) @@ -824,7 +855,6 @@ impl Interactivity { .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); if let Some(group_bounds) = hover_group_bounds { - // todo!() needs cx.was_top_layer let hovered = group_bounds.contains_point(&cx.mouse_position()); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { @@ -836,13 +866,13 @@ impl Interactivity { } if self.hover_style.is_some() - || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) + || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() { - let interactive_bounds = interactive_bounds.clone(); - let hovered = interactive_bounds.visibly_contains(&cx.mouse_position(), cx); + let bounds = bounds.intersect(&cx.content_mask().bounds); + let hovered = bounds.contains_point(&cx.mouse_position()); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { - if interactive_bounds.visibly_contains(&event.position, cx) != hovered { + if bounds.contains_point(&event.position) != hovered { cx.notify(); } } @@ -1143,7 +1173,9 @@ impl Interactivity { let mouse_position = cx.mouse_position(); if let Some(group_hover) = self.group_hover_style.as_ref() { if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains_point(&mouse_position) { + if group_bounds.contains_point(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) + { style.refine(&group_hover.style); } } @@ -1162,7 +1194,6 @@ impl Interactivity { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { if *state_type == drag.view.entity_type() - // todo!() needs to handle cx.content_mask() and cx.is_top() && group_bounds.contains_point(&mouse_position) { style.refine(&group_drag_style.style); @@ -1175,7 +1206,6 @@ impl Interactivity { && bounds .intersect(&cx.content_mask().bounds) .contains_point(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) { style.refine(drag_over_style); } @@ -1207,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, @@ -1430,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 2aece17b47..f6aae2de66 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 a0715b81a9..d398b1f8fe 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,6 +1,7 @@ use crate::{ - Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent, - Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine, + Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId, + MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle, + WhiteSpace, WindowContext, WrappedLine, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; @@ -87,7 +88,28 @@ impl StyledText { } } - pub fn with_runs(mut self, runs: Vec) -> Self { + pub fn with_highlights( + mut self, + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Self { + let mut runs = Vec::new(); + let mut ix = 0; + for (range, highlight) in highlights { + if ix < range.start { + runs.push(default_style.clone().to_run(range.start - ix)); + } + runs.push( + default_style + .clone() + .highlight(highlight) + .to_run(range.len()), + ); + ix = range.end; + } + if ix < self.text.len() { + runs.push(default_style.to_run(self.text.len() - ix)); + } self.runs = Some(runs); self } @@ -144,7 +166,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 +173,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 +200,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. ) @@ -265,7 +285,9 @@ impl TextState { pub struct InteractiveText { element_id: ElementId, text: StyledText, - click_listener: Option)>>, + click_listener: + Option], InteractiveTextClickEvent, &mut WindowContext<'_>)>>, + clickable_ranges: Vec>, } struct InteractiveTextClickEvent { @@ -284,6 +306,7 @@ impl InteractiveText { element_id: id.into(), text, click_listener: None, + clickable_ranges: Vec::new(), } } @@ -292,7 +315,7 @@ impl InteractiveText { ranges: Vec>, listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, ) -> Self { - self.click_listener = Some(Box::new(move |event, cx| { + self.click_listener = Some(Box::new(move |ranges, event, cx| { for (range_ix, range) in ranges.iter().enumerate() { if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) { @@ -300,6 +323,7 @@ impl InteractiveText { } } })); + self.clickable_ranges = ranges; self } } @@ -334,6 +358,19 @@ impl Element for InteractiveText { fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { if let Some(click_listener) = self.click_listener { + if let Some(ix) = state + .text_state + .index_for_position(bounds, cx.mouse_position()) + { + if self + .clickable_ranges + .iter() + .any(|range| range.contains(&ix)) + { + cx.set_cursor_style(crate::CursorStyle::PointingHand) + } + } + let text_state = state.text_state.clone(); let mouse_down = state.mouse_down_index.clone(); if let Some(mouse_down_index) = mouse_down.get() { @@ -343,6 +380,7 @@ impl Element for InteractiveText { text_state.index_for_position(bounds, event.position) { click_listener( + &self.clickable_ranges, InteractiveTextClickEvent { mouse_down_index, mouse_up_index, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 2d5a46f3d9..d8f4cc6804 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 e1f039e309..20afd2d288 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.; @@ -740,7 +754,7 @@ impl Copy for Corners where T: Copy + Clone + Default + Debug {} Deserialize, )] #[repr(transparent)] -pub struct Pixels(pub(crate) f32); +pub struct Pixels(pub f32); impl std::ops::Div for Pixels { type Output = f32; @@ -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); @@ -1034,7 +1066,7 @@ impl sqlez::bindable::Bind for GlobalPixels { } #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] -pub struct Rems(f32); +pub struct Rems(pub f32); impl Mul for Rems { type Output = Pixels; diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 3027c05fbd..7375f47939 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 0631c75de5..19afb50332 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 4def1c33b8..aba01b9d5b 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 df1ed9b2a6..4532b33f50 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 e355c3aa4b..2ad54eff0d 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 87e89adfa0..5492605602 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 81a057055a..2bceb1bc13 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 f31b0ae753..280c52df2a 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 507c46d067..5724f1e070 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; @@ -2582,7 +2611,7 @@ impl WindowHandle { cx.read_window(self, |root_view, _cx| root_view.clone()) } - pub fn is_active(&self, cx: &WindowContext) -> Option { + pub fn is_active(&self, cx: &AppContext) -> Option { cx.windows .get(self.id) .and_then(|window| window.as_ref().map(|window| window.active)) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index af7504529c..811e549406 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 5c17592f0c..8fdf524f69 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 98fd81f012..dc5b63d222 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 356d029c58..788c424373 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 a2ad82585e..21d64fe91f 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 264c1ff7b5..5d061b868f 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/lsp_command.rs b/crates/project2/src/lsp_command.rs index 94c277db1e..a2de52b21a 100644 --- a/crates/project2/src/lsp_command.rs +++ b/crates/project2/src/lsp_command.rs @@ -717,8 +717,9 @@ async fn location_links_from_lsp( })? .await?; - buffer.update(&mut cx, |origin_buffer, cx| { + cx.update(|cx| { let origin_location = origin_range.map(|origin_range| { + let origin_buffer = buffer.read(cx); let origin_start = origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); let origin_end = diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index d2cc4fe406..12940dd2c4 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 53b2f6ba1f..4dfb8004e3 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 eb124bfca2..875d4d4f83 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 e4846af76c..0a5a63f14a 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)) @@ -1480,7 +1495,7 @@ impl Render for ProjectPanel { .children(self.context_menu.as_ref().map(|(menu, position, _)| { overlay() .position(*position) - .anchor(gpui::AnchorCorner::BottomLeft) + .anchor(gpui::AnchorCorner::TopLeft) .child(menu.clone()) })) } else { @@ -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/rpc2/src/rpc.rs b/crates/rpc2/src/rpc.rs index 4bf90669b2..6f35bf64bc 100644 --- a/crates/rpc2/src/rpc.rs +++ b/crates/rpc2/src/rpc.rs @@ -9,4 +9,4 @@ pub use notification::*; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 64; +pub const PROTOCOL_VERSION: u32 = 66; diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index d80d9f5d50..b3d6006113 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -18,7 +18,7 @@ use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; -use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; +use ui::{h_stack, Icon, IconButton, IconElement}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -214,10 +214,11 @@ impl Render for BufferSearchBar { .child( h_stack() .flex_none() - .child(ButtonGroup::new(vec![ - search_button_for_mode(SearchMode::Text), - search_button_for_mode(SearchMode::Regex), - ])) + .child( + h_stack() + .child(search_button_for_mode(SearchMode::Text)) + .child(search_button_for_mode(SearchMode::Regex)), + ) .when(supported_options.replacement, |this| { this.child(super::toggle_replace_button(self.replace_enabled)) }), @@ -586,8 +587,7 @@ impl BufferSearchBar { // let style = theme.search.action_button.clone(); - IconButton::new(0, ui::Icon::SelectAll) - .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches))) + IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches)) } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 118d9054e6..13def6b4a7 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -3,7 +3,8 @@ pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext, IntoElement}; pub use mode::SearchMode; use project::search::SearchQuery; -use ui::ButtonVariant; +use ui::prelude::*; +use ui::{ButtonStyle, Icon, IconButton}; //pub use project_search::{ProjectSearchBar, ProjectSearchView}; // use theme::components::{ // action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, @@ -83,35 +84,35 @@ impl SearchOptions { } pub fn as_button(&self, active: bool) -> impl IntoElement { - ui::IconButton::new(0, self.icon()) + IconButton::new(0, self.icon()) .on_click({ let action = self.to_toggle_action(); move |_, cx| { cx.dispatch_action(action.boxed_clone()); } }) - .variant(ui::ButtonVariant::Ghost) - .when(active, |button| button.variant(ButtonVariant::Filled)) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) } } fn toggle_replace_button(active: bool) -> impl IntoElement { // todo: add toggle_replace button - ui::IconButton::new(0, ui::Icon::Replace) + IconButton::new(0, Icon::Replace) .on_click(|_, cx| { cx.dispatch_action(Box::new(ToggleReplace)); cx.notify(); }) - .variant(ui::ButtonVariant::Ghost) - .when(active, |button| button.variant(ButtonVariant::Filled)) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) } fn render_replace_button( action: impl Action + 'static + Send + Sync, - icon: ui::Icon, + icon: Icon, ) -> impl IntoElement { // todo: add tooltip - ui::IconButton::new(0, icon).on_click(move |_, cx| { + IconButton::new(0, icon).on_click(move |_, cx| { cx.dispatch_action(action.boxed_clone()); }) } diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index f5a9a8c8f7..44ba287d78 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,5 +1,6 @@ use gpui::{ClickEvent, IntoElement, WindowContext}; -use ui::{Button, ButtonVariant, IconButton}; +use ui::prelude::*; +use ui::{Button, IconButton}; use crate::mode::SearchMode; @@ -23,13 +24,7 @@ pub(crate) fn render_search_mode_button( is_active: bool, on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Button { - let button_variant = if is_active { - ButtonVariant::Filled - } else { - ButtonVariant::Ghost - }; - - Button::new(mode.label()) + Button::new(mode.label(), mode.label()) + .selected(is_active) .on_click(on_click) - .variant(button_variant) } diff --git a/crates/storybook2/src/stories.rs b/crates/storybook2/src/stories.rs index 0eaf3d126c..2d63d1d491 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 0000000000..2f3089a4e6 --- /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/stories/text.rs b/crates/storybook2/src/stories/text.rs index 3cb39aa01a..ccd13cb4d8 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,6 +1,6 @@ use gpui::{ - blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, - TextRun, View, VisualContext, WindowContext, + blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render, + Styled, StyledText, View, VisualContext, WindowContext, }; use ui::v_stack; @@ -59,13 +59,11 @@ impl Render for TextStory { ))).child( InteractiveText::new( "interactive", - StyledText::new("Hello world, how is it going?").with_runs(vec![ - cx.text_style().to_run(6), - TextRun { + StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [ + (6..11, HighlightStyle { background_color: Some(green()), - ..cx.text_style().to_run(5) - }, - cx.text_style().to_run(18), + ..Default::default() + }), ]), ) .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 1e6ade644c..4fe76ce878 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -12,16 +12,19 @@ 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, ContextMenu, + Disclosure, Focus, Icon, IconButton, Keybinding, Label, List, + ListHeader, ListItem, Scroll, Text, @@ -32,16 +35,19 @@ 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(), Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(), + Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(), Self::Focus => FocusStory::view(cx).into(), Self::Icon => cx.build_view(|_| ui::IconStory).into(), Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(), 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 d189d3bfb0..237f2f1081 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() - .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.)) + 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(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( + 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 7b0a0c3d3a..be55194e76 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.rs b/crates/ui2/src/components.rs index 9dc061e31f..17271de48d 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -1,19 +1,17 @@ mod avatar; mod button; -mod button2; mod checkbox; mod context_menu; mod disclosure; mod divider; mod icon; -mod icon_button; mod keybinding; mod label; mod list; mod popover; -mod slot; +mod popover_menu; +mod right_click_menu; mod stack; -mod toggle; mod tooltip; #[cfg(feature = "stories")] @@ -21,20 +19,18 @@ mod stories; pub use avatar::*; pub use button::*; -pub use button2::*; pub use checkbox::*; pub use context_menu::*; pub use disclosure::*; pub use divider::*; pub use icon::*; -pub use icon_button::*; pub use keybinding::*; pub use label::*; pub use list::*; pub use popover::*; -pub use slot::*; +pub use popover_menu::*; +pub use right_click_menu::*; pub use stack::*; -pub use toggle::*; pub use tooltip::*; #[cfg(feature = "stories")] diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 57aa17ebba..3c8eefa6db 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::prelude::*; -use gpui::{img, ImageData, ImageSource, Img, IntoElement}; +use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Shape { @@ -13,14 +13,15 @@ pub enum Shape { #[derive(IntoElement)] pub struct Avatar { src: ImageSource, + is_available: Option, shape: Shape, } impl RenderOnce for Avatar { - type Rendered = Img; + type Rendered = Div; - fn render(self, _: &mut WindowContext) -> Self::Rendered { - let mut img = img(); + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + let mut img = img(self.src); if self.shape == Shape::Circle { img = img.rounded_full(); @@ -28,10 +29,28 @@ impl RenderOnce for Avatar { img = img.rounded_md(); } - img.source(self.src.clone()) - .size_4() - // todo!(Pull the avatar fallback background from the theme.) - .bg(gpui::red()) + let size = rems(1.0); + + div() + .size(size) + .child( + img.size(size) + // todo!(Pull the avatar fallback background from the theme.) + .bg(gpui::red()), + ) + .children(self.is_available.map(|is_free| { + // HACK: non-integer sizes result in oval indicators. + let indicator_size = (size.0 * cx.rem_size() * 0.4).round(); + + div() + .absolute() + .z_index(1) + .bg(if is_free { gpui::green() } else { gpui::red() }) + .size(indicator_size) + .rounded(indicator_size) + .bottom_0() + .right_0() + })) } } @@ -40,12 +59,14 @@ impl Avatar { Self { src: src.into().into(), shape: Shape::Circle, + is_available: None, } } pub fn data(src: Arc) -> Self { Self { src: src.into(), shape: Shape::Circle, + is_available: None, } } @@ -53,10 +74,15 @@ impl Avatar { Self { src, shape: Shape::Circle, + is_available: None, } } pub fn shape(mut self, shape: Shape) -> Self { self.shape = shape; self } + pub fn availability_indicator(mut self, is_available: impl Into>) -> Self { + self.is_available = is_available.into(); + self + } } diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index fbe5b951fa..25e88201f4 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,228 +1,8 @@ -use gpui::{ - ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext, -}; -use std::rc::Rc; +mod button; +pub(self) mod button_icon; +mod button_like; +mod icon_button; -use crate::prelude::*; -use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; - -/// Provides the flexibility to use either a standard -/// button or an icon button in a given context. -pub enum ButtonOrIconButton { - Button(Button), - IconButton(IconButton), -} - -impl From