From 0ac8aae17b683ff79b7e6644680ee28e08a5301f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2024 10:52:40 -0800 Subject: [PATCH] Remove 2 suffix for language_tools, search, terminal_view, auto_update Co-authored-by: Mikayla --- Cargo.lock | 102 +- Cargo.toml | 5 +- crates/activity_indicator/Cargo.toml | 2 +- crates/assistant2/Cargo.toml | 2 +- crates/auto_update/Cargo.toml | 16 +- crates/auto_update/src/auto_update.rs | 141 +- crates/auto_update/src/update_notification.rs | 120 +- crates/auto_update2/Cargo.toml | 29 - crates/auto_update2/src/auto_update.rs | 405 --- .../auto_update2/src/update_notification.rs | 56 - crates/breadcrumbs/Cargo.toml | 2 +- crates/collab_ui/Cargo.toml | 2 +- crates/feedback2/Cargo.toml | 2 +- crates/language_tools/Cargo.toml | 23 +- crates/language_tools/src/lsp_log.rs | 576 ++-- crates/language_tools/src/lsp_log_tests.rs | 25 +- crates/language_tools/src/syntax_tree_view.rs | 512 ++- crates/language_tools2/Cargo.toml | 34 - crates/language_tools2/src/language_tools.rs | 15 - crates/language_tools2/src/lsp_log.rs | 895 ------ crates/language_tools2/src/lsp_log_tests.rs | 107 - .../language_tools2/src/syntax_tree_view.rs | 533 --- crates/project_panel/Cargo.toml | 2 +- crates/quick_action_bar/Cargo.toml | 2 +- crates/search/Cargo.toml | 27 +- crates/search/src/buffer_search.rs | 1453 ++++----- crates/search/src/mode.rs | 35 +- crates/search/src/project_search.rs | 2749 ++++++++-------- crates/search/src/search.rs | 94 +- crates/search/src/search_bar.rs | 184 +- crates/search2/Cargo.toml | 40 - crates/search2/src/buffer_search.rs | 1858 ----------- crates/search2/src/history.rs | 184 -- crates/search2/src/mode.rs | 46 - crates/search2/src/project_search.rs | 2844 ----------------- crates/search2/src/search.rs | 108 - crates/search2/src/search_bar.rs | 18 - crates/terminal_view/Cargo.toml | 32 +- crates/terminal_view/src/terminal_element.rs | 1087 ++++--- crates/terminal_view/src/terminal_panel.rs | 383 ++- crates/terminal_view/src/terminal_view.rs | 593 ++-- crates/terminal_view2/Cargo.toml | 46 - crates/terminal_view2/README.md | 23 - .../terminal_view2/scripts/print256color.sh | 96 - crates/terminal_view2/scripts/truecolor.sh | 19 - crates/terminal_view2/src/persistence.rs | 71 - crates/terminal_view2/src/terminal_element.rs | 1054 ------ crates/terminal_view2/src/terminal_panel.rs | 437 --- crates/terminal_view2/src/terminal_view.rs | 1134 ------- crates/vim/Cargo.toml | 2 +- crates/zed/Cargo.toml | 8 +- 51 files changed, 3900 insertions(+), 14333 deletions(-) delete mode 100644 crates/auto_update2/Cargo.toml delete mode 100644 crates/auto_update2/src/auto_update.rs delete mode 100644 crates/auto_update2/src/update_notification.rs delete mode 100644 crates/language_tools2/Cargo.toml delete mode 100644 crates/language_tools2/src/language_tools.rs delete mode 100644 crates/language_tools2/src/lsp_log.rs delete mode 100644 crates/language_tools2/src/lsp_log_tests.rs delete mode 100644 crates/language_tools2/src/syntax_tree_view.rs delete mode 100644 crates/search2/Cargo.toml delete mode 100644 crates/search2/src/buffer_search.rs delete mode 100644 crates/search2/src/history.rs delete mode 100644 crates/search2/src/mode.rs delete mode 100644 crates/search2/src/project_search.rs delete mode 100644 crates/search2/src/search.rs delete mode 100644 crates/search2/src/search_bar.rs delete mode 100644 crates/terminal_view2/Cargo.toml delete mode 100644 crates/terminal_view2/README.md delete mode 100755 crates/terminal_view2/scripts/print256color.sh delete mode 100755 crates/terminal_view2/scripts/truecolor.sh delete mode 100644 crates/terminal_view2/src/persistence.rs delete mode 100644 crates/terminal_view2/src/terminal_element.rs delete mode 100644 crates/terminal_view2/src/terminal_panel.rs delete mode 100644 crates/terminal_view2/src/terminal_view.rs diff --git a/Cargo.lock b/Cargo.lock index e1887d273f..566aaf21e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,7 @@ name = "activity_indicator" version = "0.1.0" dependencies = [ "anyhow", - "auto_update2", + "auto_update", "editor2", "futures 0.3.28", "gpui2", @@ -392,7 +392,7 @@ dependencies = [ "rand 0.8.5", "regex", "schemars", - "search2", + "search", "semantic_index2", "serde", "serde_json", @@ -752,30 +752,6 @@ dependencies = [ [[package]] name = "auto_update" version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "db", - "gpui", - "isahc", - "lazy_static", - "log", - "menu", - "project", - "serde", - "serde_derive", - "serde_json", - "settings", - "smol", - "tempdir", - "theme", - "util", - "workspace", -] - -[[package]] -name = "auto_update2" -version = "0.1.0" dependencies = [ "anyhow", "client2", @@ -1119,7 +1095,7 @@ dependencies = [ "language2", "outline2", "project2", - "search2", + "search", "settings2", "theme2", "ui2", @@ -1799,7 +1775,7 @@ name = "collab_ui" version = "0.1.0" dependencies = [ "anyhow", - "auto_update2", + "auto_update", "call2", "channel2", "client2", @@ -3047,7 +3023,7 @@ dependencies = [ "postage", "project2", "regex", - "search2", + "search", "serde", "serde_derive", "settings2", @@ -4558,29 +4534,6 @@ dependencies = [ [[package]] name = "language_tools" version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "editor", - "env_logger", - "futures 0.3.28", - "gpui", - "language", - "lsp", - "project", - "serde", - "settings", - "theme", - "tree-sitter", - "unindent", - "util", - "workspace", -] - -[[package]] -name = "language_tools2" -version = "0.1.0" dependencies = [ "anyhow", "client2", @@ -6562,7 +6515,7 @@ dependencies = [ "pretty_assertions", "project2", "schemars", - "search2", + "search", "serde", "serde_derive", "serde_json", @@ -6761,7 +6714,7 @@ dependencies = [ "assistant2", "editor2", "gpui2", - "search2", + "search", "ui2", "workspace2", ] @@ -7768,35 +7721,6 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "search" version = "0.1.0" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "client", - "collections", - "editor", - "futures 0.3.28", - "gpui", - "language", - "log", - "menu", - "postage", - "project", - "semantic_index", - "serde", - "serde_derive", - "serde_json", - "settings", - "smallvec", - "smol", - "theme", - "unindent", - "util", - "workspace", -] - -[[package]] -name = "search2" -version = "0.1.0" dependencies = [ "anyhow", "bitflags 1.3.2", @@ -9084,7 +9008,7 @@ dependencies = [ ] [[package]] -name = "terminal_view2" +name = "terminal_view" version = "0.1.0" dependencies = [ "anyhow", @@ -10345,7 +10269,7 @@ dependencies = [ "nvim-rs", "parking_lot 0.11.2", "project2", - "search2", + "search", "serde", "serde_derive", "serde_json", @@ -11179,7 +11103,7 @@ dependencies = [ "async-tar", "async-trait", "audio2", - "auto_update2", + "auto_update", "backtrace", "breadcrumbs", "call2", @@ -11213,7 +11137,7 @@ dependencies = [ "journal2", "language2", "language_selector", - "language_tools2", + "language_tools", "lazy_static", "libc", "log", @@ -11237,7 +11161,7 @@ dependencies = [ "rsa 0.4.0", "rust-embed", "schemars", - "search2", + "search", "semantic_index2", "serde", "serde_derive", @@ -11249,7 +11173,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", - "terminal_view2", + "terminal_view", "text2", "theme2", "theme_selector", diff --git a/Cargo.toml b/Cargo.toml index 5f35f8b4fc..9e37b5556b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "crates/audio", "crates/audio2", "crates/auto_update", - "crates/auto_update2", "crates/breadcrumbs", "crates/call", "crates/call2", @@ -57,7 +56,6 @@ members = [ "crates/language2", "crates/language_selector", "crates/language_tools", - "crates/language_tools2", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", @@ -88,7 +86,6 @@ members = [ "crates/rpc", "crates/rpc2", "crates/search", - "crates/search2", "crates/semantic_index", "crates/semantic_index2", "crates/settings", @@ -101,7 +98,7 @@ members = [ "crates/sum_tree", "crates/terminal", "crates/terminal2", - "crates/terminal_view2", + "crates/terminal_view", "crates/text", "crates/theme", "crates/theme2", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index c9921ffcce..a9c03d540e 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -9,7 +9,7 @@ path = "src/activity_indicator.rs" doctest = false [dependencies] -auto_update = { path = "../auto_update2", package = "auto_update2" } +auto_update = { path = "../auto_update" } editor = { path = "../editor2", package = "editor2" } language = { path = "../language2", package = "language2" } gpui = { path = "../gpui2", package = "gpui2" } diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 2fbc8850be..aa5eb48a13 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -19,7 +19,7 @@ language = { package = "language2", path = "../language2" } menu = { package = "menu2", path = "../menu2" } multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } project = { package = "project2", path = "../project2" } -search = { package = "search2", path = "../search2" } +search = { path = "../search" } semantic_index = { package = "semantic_index2", path = "../semantic_index2" } settings = { package = "settings2", path = "../settings2" } theme = { package = "theme2", path = "../theme2" } diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 884ed2b7a0..cbc1b5581e 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -9,14 +9,14 @@ path = "src/auto_update.rs" doctest = false [dependencies] -db = { path = "../db" } -client = { path = "../client" } -gpui = { path = "../gpui" } -menu = { path = "../menu" } -project = { path = "../project" } -settings = { path = "../settings" } -theme = { path = "../theme" } -workspace = { path = "../workspace" } +db = { package = "db2", path = "../db2" } +client = { package = "client2", path = "../client2" } +gpui = { package = "gpui2", path = "../gpui2" } +menu = { package = "menu2", path = "../menu2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } util = { path = "../util" } anyhow.workspace = true isahc.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index cf285ac7cf..691b83479f 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -3,18 +3,23 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; +use db::RELEASE_CHANNEL; use gpui::{ - actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, - Task, WeakViewHandle, + actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task, + ViewContext, VisualContext, WindowContext, }; use isahc::AsyncBody; + use serde::Deserialize; use serde_derive::Serialize; -use settings::{Setting, SettingsStore}; -use smol::{fs::File, io::AsyncReadExt, process::Command}; +use smol::io::AsyncReadExt; + +use settings::{Settings, SettingsStore}; +use smol::{fs::File, process::Command}; + use std::{ffi::OsString, sync::Arc, time::Duration}; use update_notification::UpdateNotification; -use util::channel::ReleaseChannel; +use util::channel::{AppCommitSha, ReleaseChannel}; use util::http::HttpClient; use workspace::Workspace; @@ -42,9 +47,9 @@ pub enum AutoUpdateStatus { pub struct AutoUpdater { status: AutoUpdateStatus, - current_version: AppVersion, + current_version: SemanticVersion, http_client: Arc, - pending_poll: Option>, + pending_poll: Option>>, server_url: String, } @@ -54,13 +59,9 @@ struct JsonRelease { url: String, } -impl Entity for AutoUpdater { - type Event = (); -} - struct AutoUpdateSetting(bool); -impl Setting for AutoUpdateSetting { +impl Settings for AutoUpdateSetting { const KEY: Option<&'static str> = Some("auto_update"); type FileContent = Option; @@ -68,7 +69,7 @@ impl Setting for AutoUpdateSetting { fn load( default_value: &Option, user_values: &[&Option], - _: &AppContext, + _: &mut AppContext, ) -> Result { Ok(Self( Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, @@ -77,18 +78,31 @@ impl Setting for AutoUpdateSetting { } pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { - settings::register::(cx); + AutoUpdateSetting::register(cx); - if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) { - let auto_updater = cx.add_model(|cx| { + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(|_, action: &Check, cx| check(action, cx)); + + workspace.register_action(|_, action, cx| view_release_notes(action, cx)); + + // @nate - code to trigger update notification on launch + // todo!("remove this when Nate is done") + // workspace.show_notification(0, _cx, |cx| { + // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) + // }); + }) + .detach(); + + if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { + let auto_updater = cx.new_model(|cx| { let updater = AutoUpdater::new(version, http_client, server_url); - let mut update_subscription = settings::get::(cx) + let mut update_subscription = AutoUpdateSetting::get_global(cx) .0 .then(|| updater.start_polling(cx)); - cx.observe_global::(move |updater, cx| { - if settings::get::(cx).0 { + cx.observe_global::(move |updater, cx| { + if AutoUpdateSetting::get_global(cx).0 { if update_subscription.is_none() { update_subscription = Some(updater.start_polling(cx)) } @@ -101,19 +115,22 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo updater }); cx.set_global(Some(auto_updater)); - cx.add_global_action(check); - cx.add_global_action(view_release_notes); - cx.add_action(UpdateNotification::dismiss); } } -pub fn check(_: &Check, cx: &mut AppContext) { +pub fn check(_: &Check, cx: &mut WindowContext) { if let Some(updater) = AutoUpdater::get(cx) { updater.update(cx, |updater, cx| updater.poll(cx)); + } else { + drop(cx.prompt( + gpui::PromptLevel::Info, + "Auto-updates disabled for non-bundled app.", + &["Ok"], + )); } } -fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; @@ -122,31 +139,28 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { match cx.global::() { ReleaseChannel::Dev => {} ReleaseChannel::Nightly => {} - ReleaseChannel::Preview => cx - .platform() - .open_url(&format!("{server_url}/releases/preview/{current_version}")), - ReleaseChannel::Stable => cx - .platform() - .open_url(&format!("{server_url}/releases/stable/{current_version}")), + ReleaseChannel::Preview => { + cx.open_url(&format!("{server_url}/releases/preview/{current_version}")) + } + ReleaseChannel::Stable => { + cx.open_url(&format!("{server_url}/releases/stable/{current_version}")) + } } } } } -pub fn notify_of_any_new_update( - workspace: WeakViewHandle, - cx: &mut AppContext, -) -> Option<()> { +pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { let updater = AutoUpdater::get(cx)?; let version = updater.read(cx).current_version; let should_show_notification = updater.read(cx).should_show_update_notification(cx); - cx.spawn(|mut cx| async move { + cx.spawn(|workspace, mut cx| async move { let should_show_notification = should_show_notification.await?; if should_show_notification { workspace.update(&mut cx, |workspace, cx| { workspace.show_notification(0, cx, |cx| { - cx.add_view(|_| UpdateNotification::new(version)) + cx.new_view(|_| UpdateNotification::new(version)) }); updater .read(cx) @@ -162,12 +176,12 @@ pub fn notify_of_any_new_update( } impl AutoUpdater { - pub fn get(cx: &mut AppContext) -> Option> { - cx.default_global::>>().clone() + pub fn get(cx: &mut AppContext) -> Option> { + cx.default_global::>>().clone() } fn new( - current_version: AppVersion, + current_version: SemanticVersion, http_client: Arc, server_url: String, ) -> Self { @@ -180,11 +194,11 @@ impl AutoUpdater { } } - pub fn start_polling(&self, cx: &mut ModelContext) -> Task<()> { + pub fn start_polling(&self, cx: &mut ModelContext) -> Task> { cx.spawn(|this, mut cx| async move { loop { - this.update(&mut cx, |this, cx| this.poll(cx)); - cx.background().timer(POLL_INTERVAL).await; + this.update(&mut cx, |this, cx| this.poll(cx))?; + cx.background_executor().timer(POLL_INTERVAL).await; } }) } @@ -198,7 +212,7 @@ impl AutoUpdater { cx.notify(); self.pending_poll = Some(cx.spawn(|this, mut cx| async move { - let result = Self::update(this.clone(), cx.clone()).await; + let result = Self::update(this.upgrade()?, cx.clone()).await; this.update(&mut cx, |this, cx| { this.pending_poll = None; if let Err(error) = result { @@ -206,7 +220,8 @@ impl AutoUpdater { this.status = AutoUpdateStatus::Errored; cx.notify(); } - }); + }) + .ok() })); } @@ -219,26 +234,26 @@ impl AutoUpdater { cx.notify(); } - async fn update(this: ModelHandle, mut cx: AsyncAppContext) -> Result<()> { + async fn update(this: Model, mut cx: AsyncAppContext) -> Result<()> { let (client, server_url, current_version) = this.read_with(&cx, |this, _| { ( this.http_client.clone(), this.server_url.clone(), this.current_version, ) - }); + })?; let mut url_string = format!( "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg" ); - cx.read(|cx| { + cx.update(|cx| { if cx.has_global::() { if let Some(param) = cx.global::().release_query_param() { url_string += "&"; url_string += param; } } - }); + })?; let mut response = client.get(&url_string, Default::default(), true).await?; @@ -251,26 +266,32 @@ impl AutoUpdater { let release: JsonRelease = serde_json::from_slice(body.as_slice()).context("error deserializing release")?; - let latest_version = release.version.parse::()?; - if latest_version <= current_version { + let should_download = match *RELEASE_CHANNEL { + ReleaseChannel::Nightly => cx + .try_read_global::(|sha, _| release.version != sha.0) + .unwrap_or(true), + _ => release.version.parse::()? <= current_version, + }; + + if !should_download { this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Idle; cx.notify(); - }); + })?; return Ok(()); } this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Downloading; cx.notify(); - }); + })?; let temp_dir = tempdir::TempDir::new("zed-auto-update")?; let dmg_path = temp_dir.path().join("Zed.dmg"); let mount_path = temp_dir.path().join("Zed"); let running_app_path = ZED_APP_PATH .clone() - .map_or_else(|| cx.platform().app_path(), Ok)?; + .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?; let running_app_filename = running_app_path .file_name() .ok_or_else(|| anyhow!("invalid running app path"))?; @@ -279,15 +300,15 @@ impl AutoUpdater { let mut dmg_file = File::create(&dmg_path).await?; - let (installation_id, release_channel, telemetry) = cx.read(|cx| { + let (installation_id, release_channel, telemetry) = cx.update(|cx| { let installation_id = cx.global::>().telemetry().installation_id(); let release_channel = cx .has_global::() .then(|| cx.global::().display_name()); - let telemetry = settings::get::(cx).metrics; + let telemetry = TelemetrySettings::get_global(cx).metrics; (installation_id, release_channel, telemetry) - }); + })?; let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { installation_id, @@ -302,7 +323,7 @@ impl AutoUpdater { this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Installing; cx.notify(); - }); + })?; let output = Command::new("hdiutil") .args(&["attach", "-nobrowse"]) @@ -348,7 +369,7 @@ impl AutoUpdater { .detach_and_log_err(cx); this.status = AutoUpdateStatus::Updated; cx.notify(); - }); + })?; Ok(()) } @@ -357,7 +378,7 @@ impl AutoUpdater { should_show: bool, cx: &AppContext, ) -> Task> { - cx.background().spawn(async move { + cx.background_executor().spawn(async move { if should_show { KEY_VALUE_STORE .write_kvp( @@ -375,7 +396,7 @@ impl AutoUpdater { } fn should_show_update_notification(&self, cx: &AppContext) -> Task> { - cx.background().spawn(async move { + cx.background_executor().spawn(async move { Ok(KEY_VALUE_STORE .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? .is_some()) diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index e4a5c23534..f00172591e 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -1,106 +1,56 @@ -use crate::ViewReleaseNotes; use gpui::{ - elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, - platform::{AppVersion, CursorStyle, MouseButton}, - Element, Entity, View, ViewContext, + div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render, + SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, }; use menu::Cancel; use util::channel::ReleaseChannel; -use workspace::notifications::Notification; +use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; pub struct UpdateNotification { - version: AppVersion, + version: SemanticVersion, } -pub enum Event { - Dismiss, -} - -impl Entity for UpdateNotification { - type Event = Event; -} - -impl View for UpdateNotification { - fn ui_name() -> &'static str { - "UpdateNotification" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - let theme = theme::current(cx).clone(); - let theme = &theme.update_notification; +impl EventEmitter for UpdateNotification {} +impl Render for UpdateNotification { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let app_name = cx.global::().display_name(); - MouseEventHandler::new::(0, cx, |state, cx| { - Flex::column() - .with_child( - Flex::row() - .with_child( - Text::new( - format!("Updated to {app_name} {}", self.version), - theme.message.text.clone(), - ) - .contained() - .with_style(theme.message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx) - }) - .aligned() - .constrained() - .with_height(cx.font_cache().line_height(theme.message.text.font_size)) - .aligned() - .top() - .flex_float(), - ), - ) - .with_child({ - let style = theme.action_message.style_for(state); - Text::new("View the release notes", style.text.clone()) - .contained() - .with_style(style.container) - }) - .contained() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - crate::view_release_notes(&Default::default(), cx) - }) - .into_any_named("update notification") - } -} - -impl Notification for UpdateNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) + v_stack() + .on_action(cx.listener(UpdateNotification::dismiss)) + .elevation_3(cx) + .p_4() + .child( + h_stack() + .justify_between() + .child(Label::new(format!( + "Updated to {app_name} {}", + self.version + ))) + .child( + div() + .id("cancel") + .child(IconElement::new(Icon::Close)) + .cursor_pointer() + .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))), + ), + ) + .child( + div() + .id("notes") + .child(Label::new("View the release notes")) + .cursor_pointer() + .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), + ) } } impl UpdateNotification { - pub fn new(version: AppVersion) -> Self { + pub fn new(version: SemanticVersion) -> Self { Self { version } } pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismiss); + cx.emit(DismissEvent); } } diff --git a/crates/auto_update2/Cargo.toml b/crates/auto_update2/Cargo.toml deleted file mode 100644 index 20eb129746..0000000000 --- a/crates/auto_update2/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "auto_update2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/auto_update.rs" -doctest = false - -[dependencies] -db = { package = "db2", path = "../db2" } -client = { package = "client2", path = "../client2" } -gpui = { package = "gpui2", path = "../gpui2" } -menu = { package = "menu2", path = "../menu2" } -project = { package = "project2", path = "../project2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } -util = { path = "../util" } -anyhow.workspace = true -isahc.workspace = true -lazy_static.workspace = true -log.workspace = true -serde.workspace = true -serde_derive.workspace = true -serde_json.workspace = true -smol.workspace = true -tempdir.workspace = true diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs deleted file mode 100644 index 691b83479f..0000000000 --- a/crates/auto_update2/src/auto_update.rs +++ /dev/null @@ -1,405 +0,0 @@ -mod update_notification; - -use anyhow::{anyhow, Context, Result}; -use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; -use db::kvp::KEY_VALUE_STORE; -use db::RELEASE_CHANNEL; -use gpui::{ - actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task, - ViewContext, VisualContext, WindowContext, -}; -use isahc::AsyncBody; - -use serde::Deserialize; -use serde_derive::Serialize; -use smol::io::AsyncReadExt; - -use settings::{Settings, SettingsStore}; -use smol::{fs::File, process::Command}; - -use std::{ffi::OsString, sync::Arc, time::Duration}; -use update_notification::UpdateNotification; -use util::channel::{AppCommitSha, ReleaseChannel}; -use util::http::HttpClient; -use workspace::Workspace; - -const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; -const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); - -actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); - -#[derive(Serialize)] -struct UpdateRequestBody { - installation_id: Option>, - release_channel: Option<&'static str>, - telemetry: bool, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum AutoUpdateStatus { - Idle, - Checking, - Downloading, - Installing, - Updated, - Errored, -} - -pub struct AutoUpdater { - status: AutoUpdateStatus, - current_version: SemanticVersion, - http_client: Arc, - pending_poll: Option>>, - server_url: String, -} - -#[derive(Deserialize)] -struct JsonRelease { - version: String, - url: String, -} - -struct AutoUpdateSetting(bool); - -impl Settings for AutoUpdateSetting { - const KEY: Option<&'static str> = Some("auto_update"); - - type FileContent = Option; - - fn load( - default_value: &Option, - user_values: &[&Option], - _: &mut AppContext, - ) -> Result { - Ok(Self( - Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, - )) - } -} - -pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { - AutoUpdateSetting::register(cx); - - cx.observe_new_views(|workspace: &mut Workspace, _cx| { - workspace.register_action(|_, action: &Check, cx| check(action, cx)); - - workspace.register_action(|_, action, cx| view_release_notes(action, cx)); - - // @nate - code to trigger update notification on launch - // todo!("remove this when Nate is done") - // workspace.show_notification(0, _cx, |cx| { - // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) - // }); - }) - .detach(); - - if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) { - let auto_updater = cx.new_model(|cx| { - let updater = AutoUpdater::new(version, http_client, server_url); - - let mut update_subscription = AutoUpdateSetting::get_global(cx) - .0 - .then(|| updater.start_polling(cx)); - - cx.observe_global::(move |updater, cx| { - if AutoUpdateSetting::get_global(cx).0 { - if update_subscription.is_none() { - update_subscription = Some(updater.start_polling(cx)) - } - } else { - update_subscription.take(); - } - }) - .detach(); - - updater - }); - cx.set_global(Some(auto_updater)); - } -} - -pub fn check(_: &Check, cx: &mut WindowContext) { - if let Some(updater) = AutoUpdater::get(cx) { - updater.update(cx, |updater, cx| updater.poll(cx)); - } else { - drop(cx.prompt( - gpui::PromptLevel::Info, - "Auto-updates disabled for non-bundled app.", - &["Ok"], - )); - } -} - -pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { - if let Some(auto_updater) = AutoUpdater::get(cx) { - let auto_updater = auto_updater.read(cx); - let server_url = &auto_updater.server_url; - let current_version = auto_updater.current_version; - if cx.has_global::() { - match cx.global::() { - ReleaseChannel::Dev => {} - ReleaseChannel::Nightly => {} - ReleaseChannel::Preview => { - cx.open_url(&format!("{server_url}/releases/preview/{current_version}")) - } - ReleaseChannel::Stable => { - cx.open_url(&format!("{server_url}/releases/stable/{current_version}")) - } - } - } - } -} - -pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { - let updater = AutoUpdater::get(cx)?; - let version = updater.read(cx).current_version; - let should_show_notification = updater.read(cx).should_show_update_notification(cx); - - cx.spawn(|workspace, mut cx| async move { - let should_show_notification = should_show_notification.await?; - if should_show_notification { - workspace.update(&mut cx, |workspace, cx| { - workspace.show_notification(0, cx, |cx| { - cx.new_view(|_| UpdateNotification::new(version)) - }); - updater - .read(cx) - .set_should_show_update_notification(false, cx) - .detach_and_log_err(cx); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - None -} - -impl AutoUpdater { - pub fn get(cx: &mut AppContext) -> Option> { - cx.default_global::>>().clone() - } - - fn new( - current_version: SemanticVersion, - http_client: Arc, - server_url: String, - ) -> Self { - Self { - status: AutoUpdateStatus::Idle, - current_version, - http_client, - server_url, - pending_poll: None, - } - } - - pub fn start_polling(&self, cx: &mut ModelContext) -> Task> { - cx.spawn(|this, mut cx| async move { - loop { - this.update(&mut cx, |this, cx| this.poll(cx))?; - cx.background_executor().timer(POLL_INTERVAL).await; - } - }) - } - - pub fn poll(&mut self, cx: &mut ModelContext) { - if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated { - return; - } - - self.status = AutoUpdateStatus::Checking; - cx.notify(); - - self.pending_poll = Some(cx.spawn(|this, mut cx| async move { - let result = Self::update(this.upgrade()?, cx.clone()).await; - this.update(&mut cx, |this, cx| { - this.pending_poll = None; - if let Err(error) = result { - log::error!("auto-update failed: error:{:?}", error); - this.status = AutoUpdateStatus::Errored; - cx.notify(); - } - }) - .ok() - })); - } - - pub fn status(&self) -> AutoUpdateStatus { - self.status - } - - pub fn dismiss_error(&mut self, cx: &mut ModelContext) { - self.status = AutoUpdateStatus::Idle; - cx.notify(); - } - - async fn update(this: Model, mut cx: AsyncAppContext) -> Result<()> { - let (client, server_url, current_version) = this.read_with(&cx, |this, _| { - ( - this.http_client.clone(), - this.server_url.clone(), - this.current_version, - ) - })?; - - let mut url_string = format!( - "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg" - ); - cx.update(|cx| { - if cx.has_global::() { - if let Some(param) = cx.global::().release_query_param() { - url_string += "&"; - url_string += param; - } - } - })?; - - let mut response = client.get(&url_string, Default::default(), true).await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading release")?; - let release: JsonRelease = - serde_json::from_slice(body.as_slice()).context("error deserializing release")?; - - let should_download = match *RELEASE_CHANNEL { - ReleaseChannel::Nightly => cx - .try_read_global::(|sha, _| release.version != sha.0) - .unwrap_or(true), - _ => release.version.parse::()? <= current_version, - }; - - if !should_download { - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Idle; - cx.notify(); - })?; - return Ok(()); - } - - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Downloading; - cx.notify(); - })?; - - let temp_dir = tempdir::TempDir::new("zed-auto-update")?; - let dmg_path = temp_dir.path().join("Zed.dmg"); - let mount_path = temp_dir.path().join("Zed"); - let running_app_path = ZED_APP_PATH - .clone() - .map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?; - let running_app_filename = running_app_path - .file_name() - .ok_or_else(|| anyhow!("invalid running app path"))?; - let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into(); - mounted_app_path.push("/"); - - let mut dmg_file = File::create(&dmg_path).await?; - - let (installation_id, release_channel, telemetry) = cx.update(|cx| { - let installation_id = cx.global::>().telemetry().installation_id(); - let release_channel = cx - .has_global::() - .then(|| cx.global::().display_name()); - let telemetry = TelemetrySettings::get_global(cx).metrics; - - (installation_id, release_channel, telemetry) - })?; - - let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { - installation_id, - release_channel, - telemetry, - })?); - - let mut response = client.get(&release.url, request_body, true).await?; - smol::io::copy(response.body_mut(), &mut dmg_file).await?; - log::info!("downloaded update. path:{:?}", dmg_path); - - this.update(&mut cx, |this, cx| { - this.status = AutoUpdateStatus::Installing; - cx.notify(); - })?; - - let output = Command::new("hdiutil") - .args(&["attach", "-nobrowse"]) - .arg(&dmg_path) - .arg("-mountroot") - .arg(&temp_dir.path()) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to mount: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - let output = Command::new("rsync") - .args(&["-av", "--delete"]) - .arg(&mounted_app_path) - .arg(&running_app_path) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to copy app: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - let output = Command::new("hdiutil") - .args(&["detach"]) - .arg(&mount_path) - .output() - .await?; - if !output.status.success() { - Err(anyhow!( - "failed to unmount: {:?}", - String::from_utf8_lossy(&output.stderr) - ))?; - } - - this.update(&mut cx, |this, cx| { - this.set_should_show_update_notification(true, cx) - .detach_and_log_err(cx); - this.status = AutoUpdateStatus::Updated; - cx.notify(); - })?; - Ok(()) - } - - fn set_should_show_update_notification( - &self, - should_show: bool, - cx: &AppContext, - ) -> Task> { - cx.background_executor().spawn(async move { - if should_show { - KEY_VALUE_STORE - .write_kvp( - SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(), - "".to_string(), - ) - .await?; - } else { - KEY_VALUE_STORE - .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string()) - .await?; - } - Ok(()) - }) - } - - fn should_show_update_notification(&self, cx: &AppContext) -> Task> { - cx.background_executor().spawn(async move { - Ok(KEY_VALUE_STORE - .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? - .is_some()) - }) - } -} diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs deleted file mode 100644 index f00172591e..0000000000 --- a/crates/auto_update2/src/update_notification.rs +++ /dev/null @@ -1,56 +0,0 @@ -use gpui::{ - div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render, - SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, -}; -use menu::Cancel; -use util::channel::ReleaseChannel; -use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; - -pub struct UpdateNotification { - version: SemanticVersion, -} - -impl EventEmitter for UpdateNotification {} - -impl Render for UpdateNotification { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - let app_name = cx.global::().display_name(); - - v_stack() - .on_action(cx.listener(UpdateNotification::dismiss)) - .elevation_3(cx) - .p_4() - .child( - h_stack() - .justify_between() - .child(Label::new(format!( - "Updated to {app_name} {}", - self.version - ))) - .child( - div() - .id("cancel") - .child(IconElement::new(Icon::Close)) - .cursor_pointer() - .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))), - ), - ) - .child( - div() - .id("notes") - .child(Label::new("View the release notes")) - .cursor_pointer() - .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), - ) - } -} - -impl UpdateNotification { - pub fn new(version: SemanticVersion) -> Self { - Self { version } - } - - pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent); - } -} diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index bca907ab15..712d2ecc39 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -15,7 +15,7 @@ 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" } +search = { path = "../search" } settings = { package = "settings2", path = "../settings2" } theme = { package = "theme2", path = "../theme2" } workspace = { package = "workspace2", path = "../workspace2" } diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index a71cecfeea..77d423db31 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -22,7 +22,7 @@ test-support = [ ] [dependencies] -auto_update = { package = "auto_update2", path = "../auto_update2" } +auto_update = { path = "../auto_update" } db = { package = "db2", path = "../db2" } call = { package = "call2", path = "../call2" } client = { package = "client2", path = "../client2" } diff --git a/crates/feedback2/Cargo.toml b/crates/feedback2/Cargo.toml index 9fe125ec57..b4746929e6 100644 --- a/crates/feedback2/Cargo.toml +++ b/crates/feedback2/Cargo.toml @@ -18,7 +18,7 @@ gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } menu = { package = "menu2", path = "../menu2" } project = { package = "project2", path = "../project2" } -search = { package = "search2", path = "../search2" } +search = { path = "../search" } settings = { package = "settings2", path = "../settings2" } theme = { package = "theme2", path = "../theme2" } ui = { package = "ui2", path = "../ui2" } diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index e67a4b36df..95b15067fa 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -10,24 +10,25 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { path = "../editor" } -settings = { path = "../settings" } -theme = { path = "../theme" } -language = { path = "../language" } -project = { path = "../project" } -workspace = { path = "../workspace" } -gpui = { path = "../gpui" } +editor = { package = "editor2", path = "../editor2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +language = { package = "language2", path = "../language2" } +project = { package = "project2", path = "../project2" } +workspace = { package = "workspace2", path = "../workspace2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } -lsp = { path = "../lsp" } +lsp = { package = "lsp2", path = "../lsp2" } futures.workspace = true serde.workspace = true anyhow.workspace = true tree-sitter.workspace = true [dev-dependencies] -client = { path = "../client", features = ["test-support"] } -editor = { path = "../editor", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } env_logger.workspace = true unindent.workspace = true diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index c75fea256d..e38de7d373 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1,25 +1,20 @@ use collections::{HashMap, VecDeque}; -use editor::{Editor, MoveToEnd}; +use editor::{Editor, EditorEvent, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, - elements::{ - AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, - ParentElement, Stack, - }, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View, - ViewContext, ViewHandle, WeakModelHandle, + actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle, + FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, + View, ViewContext, VisualContext, WeakModel, WindowContext, }; use language::{LanguageServerId, LanguageServerName}; use lsp::IoKind; use project::{search::SearchQuery, Project}; use std::{borrow::Cow, sync::Arc}; -use theme::{ui, Theme}; +use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection}; use workspace::{ item::{Item, ItemHandle}, - searchable::{SearchableItem, SearchableItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated, + searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; const SEND_LINE: &str = "// Send:"; @@ -27,8 +22,8 @@ const RECEIVE_LINE: &str = "// Receive:"; const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { - projects: HashMap, ProjectState>, - io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, IoKind, String)>, + projects: HashMap, ProjectState>, + io_tx: mpsc::UnboundedSender<(WeakModel, LanguageServerId, IoKind, String)>, } struct ProjectState { @@ -49,19 +44,19 @@ struct LanguageServerRpcState { } pub struct LspLogView { - pub(crate) editor: ViewHandle, + pub(crate) editor: View, editor_subscription: Subscription, - log_store: ModelHandle, + log_store: Model, current_server_id: Option, is_showing_rpc_trace: bool, - project: ModelHandle, + project: Model, + focus_handle: FocusHandle, _log_store_subscriptions: Vec, } pub struct LspLogToolbarItemView { - log_view: Option>, + log_view: Option>, _log_view_subscription: Option, - menu_open: bool, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -83,37 +78,30 @@ pub(crate) struct LogMenuItem { actions!(debug, [OpenLanguageServerLogs]); pub fn init(cx: &mut AppContext) { - let log_store = cx.add_model(|cx| LogStore::new(cx)); + let log_store = cx.new_model(|cx| LogStore::new(cx)); - cx.subscribe_global::({ - let log_store = log_store.clone(); - move |event, cx| { - let workspace = &event.0; - if let Some(workspace) = workspace.upgrade(cx) { - let project = workspace.read(cx).project().clone(); - if project.read(cx).is_local() { - log_store.update(cx, |store, cx| { - store.add_project(&project, cx); - }); - } - } + cx.observe_new_views(move |workspace: &mut Workspace, cx| { + let project = workspace.project(); + if project.read(cx).is_local() { + log_store.update(cx, |store, cx| { + store.add_project(&project, cx); + }); } - }) - .detach(); - cx.add_action( - move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| { + let log_store = log_store.clone(); + workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| { let project = workspace.project().read(cx); if project.is_local() { workspace.add_item( - Box::new(cx.add_view(|cx| { + Box::new(cx.new_view(|cx| { LspLogView::new(workspace.project().clone(), log_store.clone(), cx) })), cx, ); } - }, - ); + }); + }) + .detach(); } impl LogStore { @@ -123,28 +111,28 @@ impl LogStore { projects: HashMap::default(), io_tx, }; - cx.spawn_weak(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { while let Some((project, server_id, io_kind, message)) = io_rx.next().await { - if let Some(this) = this.upgrade(&cx) { + if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { this.on_io(project, server_id, io_kind, &message, cx); - }); + })?; } } anyhow::Ok(()) }) - .detach(); + .detach_and_log_err(cx); this } - pub fn add_project(&mut self, project: &ModelHandle, cx: &mut ModelContext) { + pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { let weak_project = project.downgrade(); self.projects.insert( - weak_project, + project.downgrade(), ProjectState { servers: HashMap::default(), _subscriptions: [ - cx.observe_release(&project, move |this, _, _| { + cx.observe_release(project, move |this, _, _| { this.projects.remove(&weak_project); }), cx.subscribe(project, |this, project, event, cx| match event { @@ -166,7 +154,7 @@ impl LogStore { fn add_language_server( &mut self, - project: &ModelHandle, + project: &Model, id: LanguageServerId, cx: &mut ModelContext, ) -> Option<&mut LanguageServerState> { @@ -194,22 +182,21 @@ impl LogStore { server_state._io_logs_subscription = server.as_ref().map(|server| { server.on_io(move |io_kind, message| { io_tx - .unbounded_send((weak_project, id, io_kind, message.to_string())) + .unbounded_send((weak_project.clone(), id, io_kind, message.to_string())) .ok(); }) }); - let this = cx.weak_handle(); + let this = cx.handle().downgrade(); let weak_project = project.downgrade(); server_state._lsp_logs_subscription = server.map(|server| { let server_id = server.server_id(); server.on_notification::({ move |params, mut cx| { - if let Some((project, this)) = - weak_project.upgrade(&mut cx).zip(this.upgrade(&mut cx)) - { + if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade()) { this.update(&mut cx, |this, cx| { this.add_language_server_log(&project, server_id, ¶ms.message, cx); - }); + }) + .ok(); } } }) @@ -219,7 +206,7 @@ impl LogStore { fn add_language_server_log( &mut self, - project: &ModelHandle, + project: &Model, id: LanguageServerId, message: &str, cx: &mut ModelContext, @@ -251,7 +238,7 @@ impl LogStore { fn remove_language_server( &mut self, - project: &ModelHandle, + project: &Model, id: LanguageServerId, cx: &mut ModelContext, ) -> Option<()> { @@ -263,7 +250,7 @@ impl LogStore { fn server_logs( &self, - project: &ModelHandle, + project: &Model, server_id: LanguageServerId, ) -> Option<&VecDeque> { let weak_project = project.downgrade(); @@ -274,7 +261,7 @@ impl LogStore { fn enable_rpc_trace_for_language_server( &mut self, - project: &ModelHandle, + project: &Model, server_id: LanguageServerId, ) -> Option<&mut LanguageServerRpcState> { let weak_project = project.downgrade(); @@ -291,7 +278,7 @@ impl LogStore { pub fn disable_rpc_trace_for_language_server( &mut self, - project: &ModelHandle, + project: &Model, server_id: LanguageServerId, _: &mut ModelContext, ) -> Option<()> { @@ -304,7 +291,7 @@ impl LogStore { fn on_io( &mut self, - project: WeakModelHandle, + project: WeakModel, language_server_id: LanguageServerId, io_kind: IoKind, message: &str, @@ -314,7 +301,7 @@ impl LogStore { IoKind::StdOut => true, IoKind::StdIn => false, IoKind::StdErr => { - let project = project.upgrade(cx)?; + let project = project.upgrade()?; let message = format!("stderr: {}", message.trim()); self.add_language_server_log(&project, language_server_id, &message, cx); return Some(()); @@ -365,8 +352,8 @@ impl LogStore { impl LspLogView { pub fn new( - project: ModelHandle, - log_store: ModelHandle, + project: Model, + log_store: Model, cx: &mut ViewContext, ) -> Self { let server_id = log_store @@ -427,14 +414,25 @@ impl LspLogView { } }); let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx); + + let focus_handle = cx.focus_handle(); + let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| { + cx.focus_view(&log_view.editor); + }); + let mut this = Self { + focus_handle, editor, editor_subscription, project, log_store, current_server_id: None, is_showing_rpc_trace: false, - _log_store_subscriptions: vec![model_changes_subscription, events_subscriptions], + _log_store_subscriptions: vec![ + model_changes_subscription, + events_subscriptions, + focus_subscription, + ], }; if let Some(server_id) = server_id { this.show_logs_for_server(server_id, cx); @@ -445,15 +443,20 @@ impl LspLogView { fn editor_for_logs( log_contents: String, cx: &mut ViewContext, - ) -> (ViewHandle, Subscription) { - let editor = cx.add_view(|cx| { - let mut editor = Editor::multi_line(None, cx); + ) -> (View, Subscription) { + let editor = cx.new_view(|cx| { + let mut editor = Editor::multi_line(cx); editor.set_text(log_contents, cx); editor.move_to_end(&MoveToEnd, cx); editor.set_read_only(true); editor }); - let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone())); + let editor_subscription = cx.subscribe( + &editor, + |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| { + cx.emit(event.clone()) + }, + ); (editor, editor_subscription) } @@ -516,6 +519,7 @@ impl LspLogView { self.editor_subscription = editor_subscription; cx.notify(); } + cx.focus(&self.focus_handle); } fn show_rpc_trace_for_server( @@ -540,22 +544,24 @@ impl LspLogView { .as_singleton() .expect("log buffer should be a singleton") .update(cx, |_, cx| { - cx.spawn_weak({ + cx.spawn({ let buffer = cx.handle(); |_, mut cx| async move { let language = language.await.ok(); buffer.update(&mut cx, |buffer, cx| { buffer.set_language(language, cx); - }); + }) } }) - .detach(); + .detach_and_log_err(cx); }); self.editor = editor; self.editor_subscription = editor_subscription; cx.notify(); } + + cx.focus(&self.focus_handle); } fn toggle_rpc_trace_for_server( @@ -588,33 +594,31 @@ fn log_contents(lines: &VecDeque) -> String { } } -impl View for LspLogView { - fn ui_name() -> &'static str { - "LspLogView" +impl Render for LspLogView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + self.editor + .update(cx, |editor, cx| editor.render(cx).into_any_element()) } +} - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.editor, cx).into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.editor); - } +impl FocusableView for LspLogView { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } impl Item for LspLogView { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> AnyElement { - Label::new("LSP Logs", style.label.clone()).into_any() + type Event = EditorEvent; + + fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) { + Editor::to_item_events(event, f) } - fn as_searchable(&self, handle: &ViewHandle) -> Option> { + fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { + Label::new("LSP Logs").into_any_element() + } + + fn as_searchable(&self, handle: &View) -> Option> { Some(Box::new(handle.clone())) } } @@ -622,15 +626,6 @@ impl Item for LspLogView { impl SearchableItem for LspLogView { type Match = ::Match; - fn to_search_event( - &mut self, - event: &Self::Event, - cx: &mut ViewContext, - ) -> Option { - self.editor - .update(cx, |editor, cx| editor.to_search_event(event, cx)) - } - fn clear_matches(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |e, cx| e.clear_matches(cx)) } @@ -689,22 +684,21 @@ impl SearchableItem for LspLogView { } } +impl EventEmitter for LspLogToolbarItemView {} + impl ToolbarItemView for LspLogToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) -> workspace::ToolbarItemLocation { - self.menu_open = false; if let Some(item) = active_pane_item { if let Some(log_view) = item.downcast::() { self.log_view = Some(log_view.clone()); self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { cx.notify(); })); - return ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), - }; + return ToolbarItemLocation::PrimaryLeft; } } self.log_view = None; @@ -713,15 +707,10 @@ impl ToolbarItemView for LspLogToolbarItemView { } } -impl View for LspLogToolbarItemView { - fn ui_name() -> &'static str { - "LspLogView" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx).clone(); - let Some(log_view) = self.log_view.as_ref() else { - return Empty::new().into_any(); +impl Render for LspLogToolbarItemView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(log_view) = self.log_view.clone() else { + return div(); }; let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| { let menu_rows = log_view.menu_items(cx).unwrap_or_default(); @@ -736,99 +725,128 @@ impl View for LspLogToolbarItemView { None } }); - let server_selected = current_server.is_some(); - enum LspLogScroll {} - enum Menu {} - let lsp_menu = Stack::new() - .with_child(Self::render_language_server_menu_header( - current_server, - &theme, - cx, - )) - .with_children(if self.menu_open { - Some( - Overlay::new( - MouseEventHandler::new::(0, cx, move |_, cx| { - Flex::column() - .scrollable::(0, None, cx) - .with_children(menu_rows.into_iter().map(|row| { - Self::render_language_server_menu_item( - row.server_id, - row.server_name, - &row.worktree_root_name, - row.rpc_trace_enabled, - row.logs_selected, - row.rpc_trace_selected, - &theme, - cx, - ) - })) - .contained() - .with_style(theme.toolbar_dropdown_menu.container) - .constrained() - .with_width(400.) - .with_height(400.) - }) - .on_down_out(MouseButton::Left, |_, this, cx| { - this.menu_open = false; - cx.notify() - }), - ) - .with_hoverable(true) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopLeft) - .with_z_index(999) - .aligned() - .bottom() - .left(), - ) - } else { - None - }) - .aligned() - .left() - .clipped(); - - enum LspCleanupButton {} - let log_cleanup_button = - MouseEventHandler::new::(1, cx, |state, cx| { - let theme = theme::current(cx).clone(); - let style = theme - .workspace - .toolbar - .toggleable_text_tool - .in_state(server_selected) - .style_for(state); - Label::new("Clear", style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(log_view) = this.log_view.as_ref() { - log_view.update(cx, |log_view, cx| { - log_view.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.clear(cx); - editor.set_read_only(true); - }); + let log_toolbar_view = cx.view().clone(); + let lsp_menu = popover_menu("LspLogView") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "language_server_menu_header", + current_server + .and_then(|row| { + Some(Cow::Owned(format!( + "{} ({}) - {}", + row.server_name.0, + row.worktree_root_name, + if row.rpc_trace_selected { + RPC_MESSAGES + } else { + SERVER_LOGS + }, + ))) }) - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .right(); + .unwrap_or_else(|| "No server selected".into()), + )) + .menu(move |cx| { + let menu_rows = menu_rows.clone(); + let log_view = log_view.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for (ix, row) in menu_rows.into_iter().enumerate() { + let server_selected = Some(row.server_id) == current_server_id; + menu = menu + .header(format!( + "{} ({})", + row.server_name.0, row.worktree_root_name + )) + .entry( + SERVER_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_logs_for_server(row.server_id, cx); + }), + ); + if server_selected && row.logs_selected { + debug_assert_eq!( + Some(ix * 3 + 1), + menu.select_last(), + "Could not scroll to a just added LSP menu item" + ); + } - Flex::row() - .with_child(lsp_menu) - .with_child(log_cleanup_button) - .contained() - .aligned() - .left() - .into_any_named("lsp log controls") + menu = menu.custom_entry( + { + let log_toolbar_view = log_toolbar_view.clone(); + move |cx| { + h_stack() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().z_index(120).child( + Checkbox::new( + ix, + if row.rpc_trace_enabled { + Selection::Selected + } else { + Selection::Unselected + }, + ) + .on_click(cx.listener_for( + &log_toolbar_view, + move |view, selection, cx| { + let enabled = matches!( + selection, + Selection::Selected + ); + view.toggle_logging_for_server( + row.server_id, + enabled, + cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .into_any_element() + } + }, + cx.handler_for(&log_view, move |view, cx| { + view.show_rpc_trace_for_server(row.server_id, cx); + }), + ); + if server_selected && row.rpc_trace_selected { + debug_assert_eq!( + Some(ix * 3 + 2), + menu.select_last(), + "Could not scroll to a just added LSP menu item" + ); + } + } + menu + }) + .into() + }); + + h_stack().size_full().child(lsp_menu).child( + div() + .child( + Button::new("clear_log_button", "Clear").on_click(cx.listener( + |this, _, cx| { + if let Some(log_view) = this.log_view.as_ref() { + log_view.update(cx, |log_view, cx| { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + }) + } + }, + )), + ) + .ml_2(), + ) } } @@ -838,17 +856,11 @@ const SERVER_LOGS: &str = "Server Logs"; impl LspLogToolbarItemView { pub fn new() -> Self { Self { - menu_open: false, log_view: None, _log_view_subscription: None, } } - fn toggle_menu(&mut self, cx: &mut ViewContext) { - self.menu_open = !self.menu_open; - cx.notify(); - } - fn toggle_logging_for_server( &mut self, id: LanguageServerId, @@ -862,144 +874,11 @@ impl LspLogToolbarItemView { log_view.show_logs_for_server(id, cx); cx.notify(); } + cx.focus(&log_view.focus_handle); }); } cx.notify(); } - - fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { - if let Some(log_view) = &self.log_view { - log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx)); - self.menu_open = false; - cx.notify(); - } - } - - fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { - if let Some(log_view) = &self.log_view { - log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx)); - self.menu_open = false; - cx.notify(); - } - } - - fn render_language_server_menu_header( - current_server: Option, - theme: &Arc, - cx: &mut ViewContext, - ) -> impl Element { - enum ToggleMenu {} - MouseEventHandler::new::(0, cx, move |state, _| { - let label: Cow = current_server - .and_then(|row| { - Some( - format!( - "{} ({}) - {}", - row.server_name.0, - row.worktree_root_name, - if row.rpc_trace_selected { - RPC_MESSAGES - } else { - SERVER_LOGS - }, - ) - .into(), - ) - }) - .unwrap_or_else(|| "No server selected".into()); - let style = theme.toolbar_dropdown_menu.header.style_for(state); - Label::new(label, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.toggle_menu(cx); - }) - } - - fn render_language_server_menu_item( - id: LanguageServerId, - name: LanguageServerName, - worktree_root_name: &str, - rpc_trace_enabled: bool, - logs_selected: bool, - rpc_trace_selected: bool, - theme: &Arc, - cx: &mut ViewContext, - ) -> impl Element { - enum ActivateLog {} - enum ActivateRpcTrace {} - enum LanguageServerCheckbox {} - - Flex::column() - .with_child({ - let style = &theme.toolbar_dropdown_menu.section_header; - Label::new( - format!("{} ({})", name.0, worktree_root_name), - style.text.clone(), - ) - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height) - }) - .with_child( - MouseEventHandler::new::(id.0, cx, move |state, _| { - let style = theme - .toolbar_dropdown_menu - .item - .in_state(logs_selected) - .style_for(state); - Label::new(SERVER_LOGS, style.text.clone()) - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.show_logs_for_server(id, cx); - }), - ) - .with_child( - MouseEventHandler::new::(id.0, cx, move |state, cx| { - let style = theme - .toolbar_dropdown_menu - .item - .in_state(rpc_trace_selected) - .style_for(state); - Flex::row() - .with_child( - Label::new(RPC_MESSAGES, style.text.clone()) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height), - ) - .with_child( - ui::checkbox_with_label::( - Empty::new(), - &theme.welcome.checkbox, - rpc_trace_enabled, - id.0, - cx, - move |this, enabled, cx| { - this.toggle_logging_for_server(id, enabled, cx); - }, - ) - .flex_float(), - ) - .align_children_center() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.show_rpc_trace_for_server(id, cx); - }), - ) - } } pub enum Event { @@ -1010,14 +889,7 @@ pub enum Event { }, } -impl Entity for LogStore { - type Event = Event; -} - -impl Entity for LspLogView { - type Event = editor::Event; -} - -impl Entity for LspLogToolbarItemView { - type Event = (); -} +impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} +impl EventEmitter for LspLogView {} +impl EventEmitter for LspLogView {} diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index 0830c3dac9..194b6d1ae8 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -4,7 +4,7 @@ use crate::lsp_log::LogMenuItem; use super::*; use futures::StreamExt; -use gpui::{serde_json::json, TestAppContext}; +use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext}; use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; use project::{FakeFs, Project}; use settings::SettingsStore; @@ -32,7 +32,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { })) .await; - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/the-root", json!({ @@ -46,7 +46,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { project.languages().add(Arc::new(rust_language)); }); - let log_store = cx.add_model(|cx| LogStore::new(cx)); + let log_store = cx.new_model(|cx| LogStore::new(cx)); log_store.update(cx, |store, cx| store.add_project(&project, cx)); let _rust_buffer = project @@ -61,17 +61,17 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { .receive_notification::() .await; - let log_view = cx - .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)) - .root(cx); + let window = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)); + let log_view = window.root(cx).unwrap(); + let mut cx = VisualTestContext::from_window(*window, cx); language_server.notify::(lsp::LogMessageParams { message: "hello from the server".into(), typ: lsp::MessageType::INFO, }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); - log_view.read_with(cx, |view, cx| { + log_view.update(&mut cx, |view, cx| { assert_eq!( view.menu_items(cx).unwrap(), &[LogMenuItem { @@ -79,7 +79,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { server_name: LanguageServerName("the-rust-language-server".into()), worktree_root_name: project .read(cx) - .worktrees(cx) + .worktrees() .next() .unwrap() .read(cx) @@ -95,11 +95,10 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { } fn init_test(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); client::init_settings(cx); Project::init_settings(cx); diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 15a3f53c13..a36264261e 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -1,85 +1,85 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ - actions, - elements::{ - AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, - ParentElement, ScrollTarget, Stack, UniformList, UniformListState, - }, - fonts::TextStyle, - platform::{CursorStyle, MouseButton}, - AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle, + actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, + EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, + MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; -use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; -use std::{mem, ops::Range, sync::Arc}; -use theme::{Theme, ThemeSettings}; +use language::{Buffer, OwnedSyntaxLayerInfo}; +use settings::Settings; +use std::{mem, ops::Range}; +use theme::{ActiveTheme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; +use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; use workspace::{ item::{Item, ItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(debug, [OpenSyntaxTreeView]); pub fn init(cx: &mut AppContext) { - cx.add_action( - move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| { let active_item = workspace.active_item(cx); let workspace_handle = workspace.weak_handle(); let syntax_tree_view = - cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); - workspace.add_item(Box::new(syntax_tree_view), cx); - }, - ); + cx.new_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); + workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx) + }); + }) + .detach(); } pub struct SyntaxTreeView { - workspace_handle: WeakViewHandle, + workspace_handle: WeakView, editor: Option, - mouse_y: Option, - line_height: Option, - list_state: UniformListState, + mouse_y: Option, + line_height: Option, + list_scroll_handle: UniformListScrollHandle, selected_descendant_ix: Option, hovered_descendant_ix: Option, + focus_handle: FocusHandle, } pub struct SyntaxTreeToolbarItemView { - tree_view: Option>, + tree_view: Option>, subscription: Option, - menu_open: bool, } struct EditorState { - editor: ViewHandle, + editor: View, active_buffer: Option, _subscription: gpui::Subscription, } #[derive(Clone)] struct BufferState { - buffer: ModelHandle, + buffer: Model, excerpt_id: ExcerptId, active_layer: Option, } impl SyntaxTreeView { pub fn new( - workspace_handle: WeakViewHandle, + workspace_handle: WeakView, active_item: Option>, cx: &mut ViewContext, ) -> Self { let mut this = Self { workspace_handle: workspace_handle.clone(), - list_state: UniformListState::default(), + list_scroll_handle: UniformListScrollHandle::new(), editor: None, mouse_y: None, line_height: None, hovered_descendant_ix: None, selected_descendant_ix: None, + focus_handle: cx.focus_handle(), }; this.workspace_updated(active_item, cx); cx.observe( - &workspace_handle.upgrade(cx).unwrap(), + &workspace_handle.upgrade().unwrap(), |this, workspace, cx| { this.workspace_updated(workspace.read(cx).active_item(cx), cx); }, @@ -95,7 +95,7 @@ impl SyntaxTreeView { cx: &mut ViewContext, ) { if let Some(item) = active_item { - if item.id() != cx.view_id() { + if item.item_id() != cx.entity_id() { if let Some(editor) = item.act_as::(cx) { self.set_editor(editor, cx); } @@ -103,7 +103,7 @@ impl SyntaxTreeView { } } - fn set_editor(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + fn set_editor(&mut self, editor: View, cx: &mut ViewContext) { if let Some(state) = &self.editor { if state.editor == editor { return; @@ -115,8 +115,8 @@ impl SyntaxTreeView { let subscription = cx.subscribe(&editor, |this, _, event, cx| { let did_reparse = match event { - editor::Event::Reparsed => true, - editor::Event::SelectionsChanged { .. } => false, + editor::EditorEvent::Reparsed => true, + editor::EditorEvent::SelectionsChanged { .. } => false, _ => return, }; this.editor_updated(did_reparse, cx); @@ -202,15 +202,15 @@ impl SyntaxTreeView { let descendant_ix = cursor.descendant_index(); self.selected_descendant_ix = Some(descendant_ix); - self.list_state.scroll_to(ScrollTarget::Show(descendant_ix)); + self.list_scroll_handle.scroll_to_item(descendant_ix); cx.notify(); Some(()) } - fn handle_click(&mut self, y: f32, cx: &mut ViewContext) -> Option<()> { + fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext) -> Option<()> { let line_height = self.line_height?; - let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; + let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| { // Put the cursor at the beginning of the node. @@ -225,14 +225,14 @@ impl SyntaxTreeView { fn hover_state_changed(&mut self, cx: &mut ViewContext) { if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) { - let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; + let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; if self.hovered_descendant_ix != Some(ix) { self.hovered_descendant_ix = Some(ix); self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| { editor.clear_background_highlights::(cx); editor.highlight_background::( vec![range], - |theme| theme.editor.document_highlight_write_background, + |theme| theme.editor_document_highlight_write_background, cx, ); }); @@ -275,113 +275,48 @@ impl SyntaxTreeView { Some(()) } - fn render_node( - cursor: &TreeCursor, - depth: u32, - selected: bool, - hovered: bool, - list_hovered: bool, - style: &TextStyle, - editor_theme: &theme::Editor, - cx: &AppContext, - ) -> gpui::AnyElement { - let node = cursor.node(); - let mut range_style = style.clone(); - let em_width = style.em_width(cx.font_cache()); - let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); - - range_style.color = editor_theme.line_number; - - let mut anonymous_node_style = style.clone(); - let string_color = editor_theme - .syntax - .highlights - .iter() - .find_map(|(name, style)| (name == "string").then(|| style.color)?); - let property_color = editor_theme - .syntax - .highlights - .iter() - .find_map(|(name, style)| (name == "property").then(|| style.color)?); - if let Some(color) = string_color { - anonymous_node_style.color = color; - } - - let mut row = Flex::row(); + fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div { + let colors = cx.theme().colors(); + let mut row = h_stack(); if let Some(field_name) = cursor.field_name() { - let mut field_style = style.clone(); - if let Some(color) = property_color { - field_style.color = color; - } - - row.add_children([ - Label::new(field_name, field_style), - Label::new(": ", style.clone()), - ]); + row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]); } + let node = cursor.node(); return row - .with_child( - if node.is_named() { - Label::new(node.kind(), style.clone()) - } else { - Label::new(format!("\"{}\"", node.kind()), anonymous_node_style) - } - .contained() - .with_margin_right(em_width), - ) - .with_child(Label::new(format_node_range(node), range_style)) - .contained() - .with_background_color(if selected { - editor_theme.selection.selection - } else if hovered && list_hovered { - editor_theme.active_line_background + .child(if node.is_named() { + Label::new(node.kind()).color(Color::Default) } else { - Default::default() + Label::new(format!("\"{}\"", node.kind())).color(Color::Created) }) - .with_padding_left(gutter_padding + depth as f32 * 18.0) - .into_any(); + .child( + div() + .child(Label::new(format_node_range(node)).color(Color::Muted)) + .pl_1(), + ) + .text_bg(if selected { + colors.element_selected + } else { + Hsla::default() + }) + .pl(rems(depth as f32)) + .hover(|style| style.bg(colors.element_hover)); } } -impl Entity for SyntaxTreeView { - type Event = (); -} - -impl View for SyntaxTreeView { - fn ui_name() -> &'static str { - "SyntaxTreeView" - } - - fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let settings = settings::get::(cx); - 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 = cx - .font_cache() - .select_font(font_family_id, &font_properties) - .unwrap(); - let font_size = settings.buffer_font_size(cx); - - let editor_theme = settings.theme.editor.clone(); - let style = TextStyle { - color: editor_theme.text_color, - font_family_name, - font_family_id, - font_id, - font_size, - font_properties: Default::default(), - underline: Default::default(), - soft_wrap: false, - }; - - let line_height = cx.font_cache().line_height(font_size); +impl Render for SyntaxTreeView { + fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let line_height = cx + .text_style() + .line_height_in_pixels(settings.buffer_font_size(cx)); if Some(line_height) != self.line_height { self.line_height = Some(line_height); self.hover_state_changed(cx); } + let mut rendered = div().flex_1(); + if let Some(layer) = self .editor .as_ref() @@ -389,108 +324,118 @@ impl View for SyntaxTreeView { .and_then(|buffer| buffer.active_layer.as_ref()) { let layer = layer.clone(); - let theme = editor_theme.clone(); - return MouseEventHandler::new::(0, cx, move |state, cx| { - let list_hovered = state.hovered(); - UniformList::new( - self.list_state.clone(), - layer.node().descendant_count(), - cx, - move |this, range, items, cx| { - let mut cursor = layer.node().walk(); - let mut descendant_ix = range.start as usize; - cursor.goto_descendant(descendant_ix); - let mut depth = cursor.depth(); - let mut visited_children = false; - while descendant_ix < range.end { - if visited_children { - if cursor.goto_next_sibling() { - visited_children = false; - } else if cursor.goto_parent() { - depth -= 1; - } else { - break; - } + let list = uniform_list( + cx.view().clone(), + "SyntaxTreeView", + layer.node().descendant_count(), + move |this, range, cx| { + let mut items = Vec::new(); + let mut cursor = layer.node().walk(); + let mut descendant_ix = range.start as usize; + cursor.goto_descendant(descendant_ix); + let mut depth = cursor.depth(); + let mut visited_children = false; + while descendant_ix < range.end { + if visited_children { + if cursor.goto_next_sibling() { + visited_children = false; + } else if cursor.goto_parent() { + depth -= 1; } else { - items.push(Self::render_node( - &cursor, - depth, - Some(descendant_ix) == this.selected_descendant_ix, - Some(descendant_ix) == this.hovered_descendant_ix, - list_hovered, - &style, - &theme, - cx, - )); - descendant_ix += 1; - if cursor.goto_first_child() { - depth += 1; - } else { - visited_children = true; - } + break; + } + } else { + items.push(Self::render_node( + &cursor, + depth, + Some(descendant_ix) == this.selected_descendant_ix, + cx, + )); + descendant_ix += 1; + if cursor.goto_first_child() { + depth += 1; + } else { + visited_children = true; } } - }, - ) - }) - .on_move(move |event, this, cx| { - let y = event.position.y() - event.region.origin_y(); - this.mouse_y = Some(y); - this.hover_state_changed(cx); - }) - .on_click(MouseButton::Left, move |event, this, cx| { - let y = event.position.y() - event.region.origin_y(); - this.handle_click(y, cx); - }) - .contained() - .with_background_color(editor_theme.background) - .into_any(); + } + items + }, + ) + .size_full() + .track_scroll(self.list_scroll_handle.clone()) + .on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| { + tree_view.mouse_y = Some(event.position.y); + tree_view.hover_state_changed(cx); + })) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |tree_view, event: &MouseDownEvent, cx| { + tree_view.handle_click(event.position.y, cx); + }), + ) + .text_bg(cx.theme().colors().background); + + rendered = rendered.child( + canvas(move |bounds, cx| { + list.into_any_element().draw( + bounds.origin, + bounds.size.map(AvailableSpace::Definite), + cx, + ) + }) + .size_full(), + ); } - Empty::new().into_any() + rendered + } +} + +impl EventEmitter<()> for SyntaxTreeView {} + +impl FocusableView for SyntaxTreeView { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() } } impl Item for SyntaxTreeView { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> gpui::AnyElement { - Label::new("Syntax Tree", style.label.clone()).into_any() + type Event = (); + + fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} + + fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { + Label::new("Syntax Tree").into_any_element() } fn clone_on_split( &self, - _workspace_id: workspace::WorkspaceId, + _: workspace::WorkspaceId, cx: &mut ViewContext, - ) -> Option + ) -> Option> where Self: Sized, { - let mut clone = Self::new(self.workspace_handle.clone(), None, cx); - if let Some(editor) = &self.editor { - clone.set_editor(editor.editor.clone(), cx) - } - Some(clone) + Some(cx.new_view(|cx| { + let mut clone = Self::new(self.workspace_handle.clone(), None, cx); + if let Some(editor) = &self.editor { + clone.set_editor(editor.editor.clone(), cx) + } + clone + })) } } impl SyntaxTreeToolbarItemView { pub fn new() -> Self { Self { - menu_open: false, tree_view: None, subscription: None, } } - fn render_menu( - &mut self, - cx: &mut ViewContext<'_, '_, Self>, - ) -> Option> { - let theme = theme::current(cx).clone(); + fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option> { let tree_view = self.tree_view.as_ref()?; let tree_view = tree_view.read(cx); @@ -499,51 +444,32 @@ impl SyntaxTreeToolbarItemView { let active_layer = buffer_state.active_layer.clone()?; let active_buffer = buffer_state.buffer.read(cx).snapshot(); - enum Menu {} - + let view = cx.view().clone(); Some( - Stack::new() - .with_child(Self::render_header(&theme, &active_layer, cx)) - .with_children(self.menu_open.then(|| { - Overlay::new( - MouseEventHandler::new::(0, cx, move |_, cx| { - Flex::column() - .with_children(active_buffer.syntax_layers().enumerate().map( - |(ix, layer)| { - Self::render_menu_item(&theme, &active_layer, layer, ix, cx) - }, - )) - .contained() - .with_style(theme.toolbar_dropdown_menu.container) - .constrained() - .with_width(400.) - .with_height(400.) - }) - .on_down_out(MouseButton::Left, |_, this, cx| { - this.menu_open = false; - cx.notify() - }), - ) - .with_hoverable(true) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopLeft) - .with_z_index(999) - .aligned() - .bottom() - .left() - })) - .aligned() - .left() - .clipped() - .into_any(), + popover_menu("Syntax Tree") + .trigger(Self::render_header(&active_layer)) + .menu(move |cx| { + ContextMenu::build(cx, |mut menu, cx| { + for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() { + menu = menu.entry( + format!( + "{} {}", + layer.language.name(), + format_node_range(layer.node()) + ), + None, + cx.handler_for(&view, move |view, cx| { + view.select_layer(layer_ix, cx); + }), + ); + } + menu + }) + .into() + }), ) } - fn toggle_menu(&mut self, cx: &mut ViewContext) { - self.menu_open = !self.menu_open; - cx.notify(); - } - fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext) -> Option<()> { let tree_view = self.tree_view.as_ref()?; tree_view.update(cx, |view, cx| { @@ -553,77 +479,16 @@ impl SyntaxTreeToolbarItemView { let layer = snapshot.syntax_layers().nth(layer_ix)?; buffer_state.active_layer = Some(layer.to_owned()); view.selected_descendant_ix = None; - self.menu_open = false; cx.notify(); + view.focus_handle.focus(cx); Some(()) }) } - fn render_header( - theme: &Arc, - active_layer: &OwnedSyntaxLayerInfo, - cx: &mut ViewContext, - ) -> impl Element { - enum ToggleMenu {} - MouseEventHandler::new::(0, cx, move |state, _| { - let style = theme.toolbar_dropdown_menu.header.style_for(state); - Flex::row() - .with_child( - Label::new(active_layer.language.name().to_string(), style.text.clone()) - .contained() - .with_margin_right(style.secondary_text_spacing), - ) - .with_child(Label::new( - format_node_range(active_layer.node()), - style - .secondary_text - .clone() - .unwrap_or_else(|| style.text.clone()), - )) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.toggle_menu(cx); - }) - } - - fn render_menu_item( - theme: &Arc, - active_layer: &OwnedSyntaxLayerInfo, - layer: SyntaxLayerInfo, - layer_ix: usize, - cx: &mut ViewContext, - ) -> impl Element { - enum ActivateLayer {} - MouseEventHandler::new::(layer_ix, cx, move |state, _| { - let is_selected = layer.node() == active_layer.node(); - let style = theme - .toolbar_dropdown_menu - .item - .in_state(is_selected) - .style_for(state); - Flex::row() - .with_child( - Label::new(layer.language.name().to_string(), style.text.clone()) - .contained() - .with_margin_right(style.secondary_text_spacing), - ) - .with_child(Label::new( - format_node_range(layer.node()), - style - .secondary_text - .clone() - .unwrap_or_else(|| style.text.clone()), - )) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.select_layer(layer_ix, cx); - }) + fn render_header(active_layer: &OwnedSyntaxLayerInfo) -> ButtonLike { + ButtonLike::new("syntax tree header") + .child(Label::new(active_layer.language.name())) + .child(Label::new(format_node_range(active_layer.node()))) } } @@ -639,35 +504,26 @@ fn format_node_range(node: Node) -> String { ) } -impl Entity for SyntaxTreeToolbarItemView { - type Event = (); -} - -impl View for SyntaxTreeToolbarItemView { - fn ui_name() -> &'static str { - "SyntaxTreeToolbarItemView" - } - - fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement { +impl Render for SyntaxTreeToolbarItemView { + fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> impl IntoElement { self.render_menu(cx) - .unwrap_or_else(|| Empty::new().into_any()) + .unwrap_or_else(|| popover_menu("Empty Syntax Tree")) } } +impl EventEmitter for SyntaxTreeToolbarItemView {} + impl ToolbarItemView for SyntaxTreeToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, - ) -> workspace::ToolbarItemLocation { - self.menu_open = false; + ) -> ToolbarItemLocation { if let Some(item) = active_pane_item { if let Some(view) = item.downcast::() { self.tree_view = Some(view.clone()); self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify())); - return ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), - }; + return ToolbarItemLocation::PrimaryLeft; } } self.tree_view = None; diff --git a/crates/language_tools2/Cargo.toml b/crates/language_tools2/Cargo.toml deleted file mode 100644 index bf5202cda9..0000000000 --- a/crates/language_tools2/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "language_tools2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/language_tools.rs" -doctest = false - -[dependencies] -collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -language = { package = "language2", path = "../language2" } -project = { package = "project2", path = "../project2" } -workspace = { package = "workspace2", path = "../workspace2" } -gpui = { package = "gpui2", path = "../gpui2" } -ui = { package = "ui2", path = "../ui2" } -util = { path = "../util" } -lsp = { package = "lsp2", path = "../lsp2" } -futures.workspace = true -serde.workspace = true -anyhow.workspace = true -tree-sitter.workspace = true - -[dev-dependencies] -client = { package = "client2", path = "../client2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } -env_logger.workspace = true -unindent.workspace = true diff --git a/crates/language_tools2/src/language_tools.rs b/crates/language_tools2/src/language_tools.rs deleted file mode 100644 index 0a1f31f03f..0000000000 --- a/crates/language_tools2/src/language_tools.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod lsp_log; -mod syntax_tree_view; - -#[cfg(test)] -mod lsp_log_tests; - -use gpui::AppContext; - -pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView}; -pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView}; - -pub fn init(cx: &mut AppContext) { - lsp_log::init(cx); - syntax_tree_view::init(cx); -} diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs deleted file mode 100644 index e38de7d373..0000000000 --- a/crates/language_tools2/src/lsp_log.rs +++ /dev/null @@ -1,895 +0,0 @@ -use collections::{HashMap, VecDeque}; -use editor::{Editor, EditorEvent, MoveToEnd}; -use futures::{channel::mpsc, StreamExt}; -use gpui::{ - actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle, - FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, - View, ViewContext, VisualContext, WeakModel, WindowContext, -}; -use language::{LanguageServerId, LanguageServerName}; -use lsp::IoKind; -use project::{search::SearchQuery, Project}; -use std::{borrow::Cow, sync::Arc}; -use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection}; -use workspace::{ - item::{Item, ItemHandle}, - searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, -}; - -const SEND_LINE: &str = "// Send:"; -const RECEIVE_LINE: &str = "// Receive:"; -const MAX_STORED_LOG_ENTRIES: usize = 2000; - -pub struct LogStore { - projects: HashMap, ProjectState>, - io_tx: mpsc::UnboundedSender<(WeakModel, LanguageServerId, IoKind, String)>, -} - -struct ProjectState { - servers: HashMap, - _subscriptions: [gpui::Subscription; 2], -} - -struct LanguageServerState { - log_messages: VecDeque, - rpc_state: Option, - _io_logs_subscription: Option, - _lsp_logs_subscription: Option, -} - -struct LanguageServerRpcState { - rpc_messages: VecDeque, - last_message_kind: Option, -} - -pub struct LspLogView { - pub(crate) editor: View, - editor_subscription: Subscription, - log_store: Model, - current_server_id: Option, - is_showing_rpc_trace: bool, - project: Model, - focus_handle: FocusHandle, - _log_store_subscriptions: Vec, -} - -pub struct LspLogToolbarItemView { - log_view: Option>, - _log_view_subscription: Option, -} - -#[derive(Copy, Clone, PartialEq, Eq)] -enum MessageKind { - Send, - Receive, -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct LogMenuItem { - pub server_id: LanguageServerId, - pub server_name: LanguageServerName, - pub worktree_root_name: String, - pub rpc_trace_enabled: bool, - pub rpc_trace_selected: bool, - pub logs_selected: bool, -} - -actions!(debug, [OpenLanguageServerLogs]); - -pub fn init(cx: &mut AppContext) { - let log_store = cx.new_model(|cx| LogStore::new(cx)); - - cx.observe_new_views(move |workspace: &mut Workspace, cx| { - let project = workspace.project(); - if project.read(cx).is_local() { - log_store.update(cx, |store, cx| { - store.add_project(&project, cx); - }); - } - - let log_store = log_store.clone(); - workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| { - let project = workspace.project().read(cx); - if project.is_local() { - workspace.add_item( - Box::new(cx.new_view(|cx| { - LspLogView::new(workspace.project().clone(), log_store.clone(), cx) - })), - cx, - ); - } - }); - }) - .detach(); -} - -impl LogStore { - pub fn new(cx: &mut ModelContext) -> Self { - let (io_tx, mut io_rx) = mpsc::unbounded(); - let this = Self { - projects: HashMap::default(), - io_tx, - }; - cx.spawn(|this, mut cx| async move { - while let Some((project, server_id, io_kind, message)) = io_rx.next().await { - if let Some(this) = this.upgrade() { - this.update(&mut cx, |this, cx| { - this.on_io(project, server_id, io_kind, &message, cx); - })?; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - this - } - - pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { - let weak_project = project.downgrade(); - self.projects.insert( - project.downgrade(), - ProjectState { - servers: HashMap::default(), - _subscriptions: [ - cx.observe_release(project, move |this, _, _| { - this.projects.remove(&weak_project); - }), - cx.subscribe(project, |this, project, event, cx| match event { - project::Event::LanguageServerAdded(id) => { - this.add_language_server(&project, *id, cx); - } - project::Event::LanguageServerRemoved(id) => { - this.remove_language_server(&project, *id, cx); - } - project::Event::LanguageServerLog(id, message) => { - this.add_language_server_log(&project, *id, message, cx); - } - _ => {} - }), - ], - }, - ); - } - - fn add_language_server( - &mut self, - project: &Model, - id: LanguageServerId, - cx: &mut ModelContext, - ) -> Option<&mut LanguageServerState> { - let project_state = self.projects.get_mut(&project.downgrade())?; - let server_state = project_state.servers.entry(id).or_insert_with(|| { - cx.notify(); - LanguageServerState { - rpc_state: None, - log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - _io_logs_subscription: None, - _lsp_logs_subscription: None, - } - }); - - let server = project.read(cx).language_server_for_id(id); - if let Some(server) = server.as_deref() { - if server.has_notification_handler::() { - // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. - return Some(server_state); - } - } - - let weak_project = project.downgrade(); - let io_tx = self.io_tx.clone(); - server_state._io_logs_subscription = server.as_ref().map(|server| { - server.on_io(move |io_kind, message| { - io_tx - .unbounded_send((weak_project.clone(), id, io_kind, message.to_string())) - .ok(); - }) - }); - let this = cx.handle().downgrade(); - let weak_project = project.downgrade(); - server_state._lsp_logs_subscription = server.map(|server| { - let server_id = server.server_id(); - server.on_notification::({ - move |params, mut cx| { - if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade()) { - this.update(&mut cx, |this, cx| { - this.add_language_server_log(&project, server_id, ¶ms.message, cx); - }) - .ok(); - } - } - }) - }); - Some(server_state) - } - - fn add_language_server_log( - &mut self, - project: &Model, - id: LanguageServerId, - message: &str, - cx: &mut ModelContext, - ) -> Option<()> { - let language_server_state = match self - .projects - .get_mut(&project.downgrade())? - .servers - .get_mut(&id) - { - Some(existing_state) => existing_state, - None => self.add_language_server(&project, id, cx)?, - }; - - let log_lines = &mut language_server_state.log_messages; - while log_lines.len() >= MAX_STORED_LOG_ENTRIES { - log_lines.pop_front(); - } - let message = message.trim(); - log_lines.push_back(message.to_string()); - cx.emit(Event::NewServerLogEntry { - id, - entry: message.to_string(), - is_rpc: false, - }); - cx.notify(); - Some(()) - } - - fn remove_language_server( - &mut self, - project: &Model, - id: LanguageServerId, - cx: &mut ModelContext, - ) -> Option<()> { - let project_state = self.projects.get_mut(&project.downgrade())?; - project_state.servers.remove(&id); - cx.notify(); - Some(()) - } - - fn server_logs( - &self, - project: &Model, - server_id: LanguageServerId, - ) -> Option<&VecDeque> { - let weak_project = project.downgrade(); - let project_state = self.projects.get(&weak_project)?; - let server_state = project_state.servers.get(&server_id)?; - Some(&server_state.log_messages) - } - - fn enable_rpc_trace_for_language_server( - &mut self, - project: &Model, - server_id: LanguageServerId, - ) -> Option<&mut LanguageServerRpcState> { - let weak_project = project.downgrade(); - let project_state = self.projects.get_mut(&weak_project)?; - let server_state = project_state.servers.get_mut(&server_id)?; - let rpc_state = server_state - .rpc_state - .get_or_insert_with(|| LanguageServerRpcState { - rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - last_message_kind: None, - }); - Some(rpc_state) - } - - pub fn disable_rpc_trace_for_language_server( - &mut self, - project: &Model, - server_id: LanguageServerId, - _: &mut ModelContext, - ) -> Option<()> { - let project = project.downgrade(); - let project_state = self.projects.get_mut(&project)?; - let server_state = project_state.servers.get_mut(&server_id)?; - server_state.rpc_state.take(); - Some(()) - } - - fn on_io( - &mut self, - project: WeakModel, - language_server_id: LanguageServerId, - io_kind: IoKind, - message: &str, - cx: &mut ModelContext, - ) -> Option<()> { - let is_received = match io_kind { - IoKind::StdOut => true, - IoKind::StdIn => false, - IoKind::StdErr => { - let project = project.upgrade()?; - let message = format!("stderr: {}", message.trim()); - self.add_language_server_log(&project, language_server_id, &message, cx); - return Some(()); - } - }; - - let state = self - .projects - .get_mut(&project)? - .servers - .get_mut(&language_server_id)? - .rpc_state - .as_mut()?; - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send - }; - - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - rpc_log_lines.push_back(line_before_message.to_string()); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - entry: line_before_message.to_string(), - is_rpc: true, - }); - } - - while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let message = message.trim(); - rpc_log_lines.push_back(message.to_string()); - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - entry: message.to_string(), - is_rpc: true, - }); - cx.notify(); - Some(()) - } -} - -impl LspLogView { - pub fn new( - project: Model, - log_store: Model, - cx: &mut ViewContext, - ) -> Self { - let server_id = log_store - .read(cx) - .projects - .get(&project.downgrade()) - .and_then(|project| project.servers.keys().copied().next()); - let model_changes_subscription = cx.observe(&log_store, |this, store, cx| { - (|| -> Option<()> { - let project_state = store.read(cx).projects.get(&this.project.downgrade())?; - if let Some(current_lsp) = this.current_server_id { - if !project_state.servers.contains_key(¤t_lsp) { - if let Some(server) = project_state.servers.iter().next() { - if this.is_showing_rpc_trace { - this.show_rpc_trace_for_server(*server.0, cx) - } else { - this.show_logs_for_server(*server.0, cx) - } - } else { - this.current_server_id = None; - this.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.clear(cx); - editor.set_read_only(true); - }); - cx.notify(); - } - } - } else { - if let Some(server) = project_state.servers.iter().next() { - if this.is_showing_rpc_trace { - this.show_rpc_trace_for_server(*server.0, cx) - } else { - this.show_logs_for_server(*server.0, cx) - } - } - } - - Some(()) - })(); - - cx.notify(); - }); - let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { - Event::NewServerLogEntry { id, entry, is_rpc } => { - if log_view.current_server_id == Some(*id) { - if (*is_rpc && log_view.is_showing_rpc_trace) - || (!*is_rpc && !log_view.is_showing_rpc_trace) - { - log_view.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.handle_input(entry.trim(), cx); - editor.handle_input("\n", cx); - editor.set_read_only(true); - }); - } - } - } - }); - let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx); - - let focus_handle = cx.focus_handle(); - let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| { - cx.focus_view(&log_view.editor); - }); - - let mut this = Self { - focus_handle, - editor, - editor_subscription, - project, - log_store, - current_server_id: None, - is_showing_rpc_trace: false, - _log_store_subscriptions: vec![ - model_changes_subscription, - events_subscriptions, - focus_subscription, - ], - }; - if let Some(server_id) = server_id { - this.show_logs_for_server(server_id, cx); - } - this - } - - fn editor_for_logs( - log_contents: String, - cx: &mut ViewContext, - ) -> (View, Subscription) { - let editor = cx.new_view(|cx| { - let mut editor = Editor::multi_line(cx); - editor.set_text(log_contents, cx); - editor.move_to_end(&MoveToEnd, cx); - editor.set_read_only(true); - editor - }); - let editor_subscription = cx.subscribe( - &editor, - |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| { - cx.emit(event.clone()) - }, - ); - (editor, editor_subscription) - } - - pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { - let log_store = self.log_store.read(cx); - let state = log_store.projects.get(&self.project.downgrade())?; - let mut rows = self - .project - .read(cx) - .language_servers() - .filter_map(|(server_id, language_server_name, worktree_id)| { - let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; - let state = state.servers.get(&server_id)?; - Some(LogMenuItem { - server_id, - server_name: language_server_name, - worktree_root_name: worktree.read(cx).root_name().to_string(), - rpc_trace_enabled: state.rpc_state.is_some(), - rpc_trace_selected: self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), - logs_selected: !self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), - }) - }) - .chain( - self.project - .read(cx) - .supplementary_language_servers() - .filter_map(|(&server_id, (name, _))| { - let state = state.servers.get(&server_id)?; - Some(LogMenuItem { - server_id, - server_name: name.clone(), - worktree_root_name: "supplementary".to_string(), - rpc_trace_enabled: state.rpc_state.is_some(), - rpc_trace_selected: self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), - logs_selected: !self.is_showing_rpc_trace - && self.current_server_id == Some(server_id), - }) - }), - ) - .collect::>(); - rows.sort_by_key(|row| row.server_id); - rows.dedup_by_key(|row| row.server_id); - Some(rows) - } - - fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { - let log_contents = self - .log_store - .read(cx) - .server_logs(&self.project, server_id) - .map(log_contents); - if let Some(log_contents) = log_contents { - self.current_server_id = Some(server_id); - self.is_showing_rpc_trace = false; - let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx); - self.editor = editor; - self.editor_subscription = editor_subscription; - cx.notify(); - } - cx.focus(&self.focus_handle); - } - - fn show_rpc_trace_for_server( - &mut self, - server_id: LanguageServerId, - cx: &mut ViewContext, - ) { - let rpc_log = self.log_store.update(cx, |log_store, _| { - log_store - .enable_rpc_trace_for_language_server(&self.project, server_id) - .map(|state| log_contents(&state.rpc_messages)) - }); - if let Some(rpc_log) = rpc_log { - self.current_server_id = Some(server_id); - self.is_showing_rpc_trace = true; - let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx); - let language = self.project.read(cx).languages().language_for_name("JSON"); - editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("log buffer should be a singleton") - .update(cx, |_, cx| { - cx.spawn({ - let buffer = cx.handle(); - |_, mut cx| async move { - let language = language.await.ok(); - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language(language, cx); - }) - } - }) - .detach_and_log_err(cx); - }); - - self.editor = editor; - self.editor_subscription = editor_subscription; - cx.notify(); - } - - cx.focus(&self.focus_handle); - } - - fn toggle_rpc_trace_for_server( - &mut self, - server_id: LanguageServerId, - enabled: bool, - cx: &mut ViewContext, - ) { - self.log_store.update(cx, |log_store, cx| { - if enabled { - log_store.enable_rpc_trace_for_language_server(&self.project, server_id); - } else { - log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx); - } - }); - if !enabled && Some(server_id) == self.current_server_id { - self.show_logs_for_server(server_id, cx); - cx.notify(); - } - } -} - -fn log_contents(lines: &VecDeque) -> String { - let (a, b) = lines.as_slices(); - let log_contents = a.join("\n"); - if b.is_empty() { - log_contents - } else { - log_contents + "\n" + &b.join("\n") - } -} - -impl Render for LspLogView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - self.editor - .update(cx, |editor, cx| editor.render(cx).into_any_element()) - } -} - -impl FocusableView for LspLogView { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for LspLogView { - type Event = EditorEvent; - - fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) { - Editor::to_item_events(event, f) - } - - fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { - Label::new("LSP Logs").into_any_element() - } - - fn as_searchable(&self, handle: &View) -> Option> { - Some(Box::new(handle.clone())) - } -} - -impl SearchableItem for LspLogView { - type Match = ::Match; - - fn clear_matches(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |e, cx| e.clear_matches(cx)) - } - - fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - self.editor - .update(cx, |e, cx| e.update_matches(matches, cx)) - } - - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - self.editor.update(cx, |e, cx| e.query_suggestion(cx)) - } - - fn activate_match( - &mut self, - index: usize, - matches: Vec, - cx: &mut ViewContext, - ) { - self.editor - .update(cx, |e, cx| e.activate_match(index, matches, cx)) - } - - fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - self.editor - .update(cx, |e, cx| e.select_matches(matches, cx)) - } - - fn find_matches( - &mut self, - query: Arc, - cx: &mut ViewContext, - ) -> gpui::Task> { - self.editor.update(cx, |e, cx| e.find_matches(query, cx)) - } - - fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { - // Since LSP Log is read-only, it doesn't make sense to support replace operation. - } - fn supported_options() -> workspace::searchable::SearchOptions { - workspace::searchable::SearchOptions { - case: true, - word: true, - regex: true, - // LSP log is read-only. - replacement: false, - } - } - fn active_match_index( - &mut self, - matches: Vec, - cx: &mut ViewContext, - ) -> Option { - self.editor - .update(cx, |e, cx| e.active_match_index(matches, cx)) - } -} - -impl EventEmitter for LspLogToolbarItemView {} - -impl ToolbarItemView for LspLogToolbarItemView { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> workspace::ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(log_view) = item.downcast::() { - self.log_view = Some(log_view.clone()); - self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { - cx.notify(); - })); - return ToolbarItemLocation::PrimaryLeft; - } - } - self.log_view = None; - self._log_view_subscription = None; - ToolbarItemLocation::Hidden - } -} - -impl Render for LspLogToolbarItemView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let Some(log_view) = self.log_view.clone() else { - return div(); - }; - let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| { - let menu_rows = log_view.menu_items(cx).unwrap_or_default(); - let current_server_id = log_view.current_server_id; - (menu_rows, current_server_id) - }); - - let current_server = current_server_id.and_then(|current_server_id| { - if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) { - Some(menu_rows[ix].clone()) - } else { - None - } - }); - - let log_toolbar_view = cx.view().clone(); - let lsp_menu = popover_menu("LspLogView") - .anchor(AnchorCorner::TopLeft) - .trigger(Button::new( - "language_server_menu_header", - current_server - .and_then(|row| { - Some(Cow::Owned(format!( - "{} ({}) - {}", - row.server_name.0, - row.worktree_root_name, - if row.rpc_trace_selected { - RPC_MESSAGES - } else { - SERVER_LOGS - }, - ))) - }) - .unwrap_or_else(|| "No server selected".into()), - )) - .menu(move |cx| { - let menu_rows = menu_rows.clone(); - let log_view = log_view.clone(); - let log_toolbar_view = log_toolbar_view.clone(); - ContextMenu::build(cx, move |mut menu, cx| { - for (ix, row) in menu_rows.into_iter().enumerate() { - let server_selected = Some(row.server_id) == current_server_id; - menu = menu - .header(format!( - "{} ({})", - row.server_name.0, row.worktree_root_name - )) - .entry( - SERVER_LOGS, - None, - cx.handler_for(&log_view, move |view, cx| { - view.show_logs_for_server(row.server_id, cx); - }), - ); - if server_selected && row.logs_selected { - debug_assert_eq!( - Some(ix * 3 + 1), - menu.select_last(), - "Could not scroll to a just added LSP menu item" - ); - } - - menu = menu.custom_entry( - { - let log_toolbar_view = log_toolbar_view.clone(); - move |cx| { - h_stack() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - div().z_index(120).child( - Checkbox::new( - ix, - if row.rpc_trace_enabled { - Selection::Selected - } else { - Selection::Unselected - }, - ) - .on_click(cx.listener_for( - &log_toolbar_view, - move |view, selection, cx| { - let enabled = matches!( - selection, - Selection::Selected - ); - view.toggle_logging_for_server( - row.server_id, - enabled, - cx, - ); - cx.stop_propagation(); - }, - )), - ), - ) - .into_any_element() - } - }, - cx.handler_for(&log_view, move |view, cx| { - view.show_rpc_trace_for_server(row.server_id, cx); - }), - ); - if server_selected && row.rpc_trace_selected { - debug_assert_eq!( - Some(ix * 3 + 2), - menu.select_last(), - "Could not scroll to a just added LSP menu item" - ); - } - } - menu - }) - .into() - }); - - h_stack().size_full().child(lsp_menu).child( - div() - .child( - Button::new("clear_log_button", "Clear").on_click(cx.listener( - |this, _, cx| { - if let Some(log_view) = this.log_view.as_ref() { - log_view.update(cx, |log_view, cx| { - log_view.editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.clear(cx); - editor.set_read_only(true); - }); - }) - } - }, - )), - ) - .ml_2(), - ) - } -} - -const RPC_MESSAGES: &str = "RPC Messages"; -const SERVER_LOGS: &str = "Server Logs"; - -impl LspLogToolbarItemView { - pub fn new() -> Self { - Self { - log_view: None, - _log_view_subscription: None, - } - } - - fn toggle_logging_for_server( - &mut self, - id: LanguageServerId, - enabled: bool, - cx: &mut ViewContext, - ) { - if let Some(log_view) = &self.log_view { - log_view.update(cx, |log_view, cx| { - log_view.toggle_rpc_trace_for_server(id, enabled, cx); - if !enabled && Some(id) == log_view.current_server_id { - log_view.show_logs_for_server(id, cx); - cx.notify(); - } - cx.focus(&log_view.focus_handle); - }); - } - cx.notify(); - } -} - -pub enum Event { - NewServerLogEntry { - id: LanguageServerId, - entry: String, - is_rpc: bool, - }, -} - -impl EventEmitter for LogStore {} -impl EventEmitter for LspLogView {} -impl EventEmitter for LspLogView {} -impl EventEmitter for LspLogView {} diff --git a/crates/language_tools2/src/lsp_log_tests.rs b/crates/language_tools2/src/lsp_log_tests.rs deleted file mode 100644 index 194b6d1ae8..0000000000 --- a/crates/language_tools2/src/lsp_log_tests.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::sync::Arc; - -use crate::lsp_log::LogMenuItem; - -use super::*; -use futures::StreamExt; -use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext}; -use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; -use project::{FakeFs, Project}; -use settings::SettingsStore; - -#[gpui::test] -async fn test_lsp_logs(cx: &mut TestAppContext) { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } - - init_test(cx); - - let mut rust_language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_rust_servers = rust_language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-rust-language-server", - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/the-root", - json!({ - "test.rs": "", - "package.json": "", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(Arc::new(rust_language)); - }); - - let log_store = cx.new_model(|cx| LogStore::new(cx)); - log_store.update(cx, |store, cx| store.add_project(&project, cx)); - - let _rust_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/the-root/test.rs", cx) - }) - .await - .unwrap(); - - let mut language_server = fake_rust_servers.next().await.unwrap(); - language_server - .receive_notification::() - .await; - - let window = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)); - let log_view = window.root(cx).unwrap(); - let mut cx = VisualTestContext::from_window(*window, cx); - - language_server.notify::(lsp::LogMessageParams { - message: "hello from the server".into(), - typ: lsp::MessageType::INFO, - }); - cx.executor().run_until_parked(); - - log_view.update(&mut cx, |view, cx| { - assert_eq!( - view.menu_items(cx).unwrap(), - &[LogMenuItem { - server_id: language_server.server.server_id(), - server_name: LanguageServerName("the-rust-language-server".into()), - worktree_root_name: project - .read(cx) - .worktrees() - .next() - .unwrap() - .read(cx) - .root_name() - .to_string(), - rpc_trace_enabled: false, - rpc_trace_selected: false, - logs_selected: true, - }] - ); - assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n"); - }); -} - -fn init_test(cx: &mut gpui::TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - language::init(cx); - client::init_settings(cx); - Project::init_settings(cx); - editor::init_settings(cx); - }); -} diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs deleted file mode 100644 index a36264261e..0000000000 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ /dev/null @@ -1,533 +0,0 @@ -use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; -use gpui::{ - actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, - EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, -}; -use language::{Buffer, OwnedSyntaxLayerInfo}; -use settings::Settings; -use std::{mem, ops::Range}; -use theme::{ActiveTheme, ThemeSettings}; -use tree_sitter::{Node, TreeCursor}; -use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; -use workspace::{ - item::{Item, ItemHandle}, - SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, -}; - -actions!(debug, [OpenSyntaxTreeView]); - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| { - let active_item = workspace.active_item(cx); - let workspace_handle = workspace.weak_handle(); - let syntax_tree_view = - cx.new_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); - workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx) - }); - }) - .detach(); -} - -pub struct SyntaxTreeView { - workspace_handle: WeakView, - editor: Option, - mouse_y: Option, - line_height: Option, - list_scroll_handle: UniformListScrollHandle, - selected_descendant_ix: Option, - hovered_descendant_ix: Option, - focus_handle: FocusHandle, -} - -pub struct SyntaxTreeToolbarItemView { - tree_view: Option>, - subscription: Option, -} - -struct EditorState { - editor: View, - active_buffer: Option, - _subscription: gpui::Subscription, -} - -#[derive(Clone)] -struct BufferState { - buffer: Model, - excerpt_id: ExcerptId, - active_layer: Option, -} - -impl SyntaxTreeView { - pub fn new( - workspace_handle: WeakView, - active_item: Option>, - cx: &mut ViewContext, - ) -> Self { - let mut this = Self { - workspace_handle: workspace_handle.clone(), - list_scroll_handle: UniformListScrollHandle::new(), - editor: None, - mouse_y: None, - line_height: None, - hovered_descendant_ix: None, - selected_descendant_ix: None, - focus_handle: cx.focus_handle(), - }; - - this.workspace_updated(active_item, cx); - cx.observe( - &workspace_handle.upgrade().unwrap(), - |this, workspace, cx| { - this.workspace_updated(workspace.read(cx).active_item(cx), cx); - }, - ) - .detach(); - - this - } - - fn workspace_updated( - &mut self, - active_item: Option>, - cx: &mut ViewContext, - ) { - if let Some(item) = active_item { - if item.item_id() != cx.entity_id() { - if let Some(editor) = item.act_as::(cx) { - self.set_editor(editor, cx); - } - } - } - } - - fn set_editor(&mut self, editor: View, cx: &mut ViewContext) { - if let Some(state) = &self.editor { - if state.editor == editor { - return; - } - editor.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx) - }); - } - - let subscription = cx.subscribe(&editor, |this, _, event, cx| { - let did_reparse = match event { - editor::EditorEvent::Reparsed => true, - editor::EditorEvent::SelectionsChanged { .. } => false, - _ => return, - }; - this.editor_updated(did_reparse, cx); - }); - - self.editor = Some(EditorState { - editor, - _subscription: subscription, - active_buffer: None, - }); - self.editor_updated(true, cx); - } - - fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext) -> Option<()> { - // Find which excerpt the cursor is in, and the position within that excerpted buffer. - let editor_state = self.editor.as_mut()?; - let editor = &editor_state.editor.read(cx); - let selection_range = editor.selections.last::(cx).range(); - let multibuffer = editor.buffer().read(cx); - let (buffer, range, excerpt_id) = multibuffer - .range_to_buffer_ranges(selection_range, cx) - .pop()?; - - // If the cursor has moved into a different excerpt, retrieve a new syntax layer - // from that buffer. - let buffer_state = editor_state - .active_buffer - .get_or_insert_with(|| BufferState { - buffer: buffer.clone(), - excerpt_id, - active_layer: None, - }); - let mut prev_layer = None; - if did_reparse { - prev_layer = buffer_state.active_layer.take(); - } - if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id { - buffer_state.buffer = buffer.clone(); - buffer_state.excerpt_id = excerpt_id; - buffer_state.active_layer = None; - } - - let layer = match &mut buffer_state.active_layer { - Some(layer) => layer, - None => { - let snapshot = buffer.read(cx).snapshot(); - let layer = if let Some(prev_layer) = prev_layer { - let prev_range = prev_layer.node().byte_range(); - snapshot - .syntax_layers() - .filter(|layer| layer.language == &prev_layer.language) - .min_by_key(|layer| { - let range = layer.node().byte_range(); - ((range.start as i64) - (prev_range.start as i64)).abs() - + ((range.end as i64) - (prev_range.end as i64)).abs() - })? - } else { - snapshot.syntax_layers().next()? - }; - buffer_state.active_layer.insert(layer.to_owned()) - } - }; - - // Within the active layer, find the syntax node under the cursor, - // and scroll to it. - let mut cursor = layer.node().walk(); - while cursor.goto_first_child_for_byte(range.start).is_some() { - if !range.is_empty() && cursor.node().end_byte() == range.start { - cursor.goto_next_sibling(); - } - } - - // Ascend to the smallest ancestor that contains the range. - loop { - let node_range = cursor.node().byte_range(); - if node_range.start <= range.start && node_range.end >= range.end { - break; - } - if !cursor.goto_parent() { - break; - } - } - - let descendant_ix = cursor.descendant_index(); - self.selected_descendant_ix = Some(descendant_ix); - self.list_scroll_handle.scroll_to_item(descendant_ix); - - cx.notify(); - Some(()) - } - - fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext) -> Option<()> { - let line_height = self.line_height?; - let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; - - self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| { - // Put the cursor at the beginning of the node. - mem::swap(&mut range.start, &mut range.end); - - editor.change_selections(Some(Autoscroll::newest()), cx, |selections| { - selections.select_ranges(vec![range]); - }); - }); - Some(()) - } - - fn hover_state_changed(&mut self, cx: &mut ViewContext) { - if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) { - let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; - if self.hovered_descendant_ix != Some(ix) { - self.hovered_descendant_ix = Some(ix); - self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| { - editor.clear_background_highlights::(cx); - editor.highlight_background::( - vec![range], - |theme| theme.editor_document_highlight_write_background, - cx, - ); - }); - cx.notify(); - } - } - } - - fn update_editor_with_range_for_descendant_ix( - &self, - descendant_ix: usize, - cx: &mut ViewContext, - mut f: impl FnMut(&mut Editor, Range, &mut ViewContext), - ) -> Option<()> { - let editor_state = self.editor.as_ref()?; - let buffer_state = editor_state.active_buffer.as_ref()?; - let layer = buffer_state.active_layer.as_ref()?; - - // Find the node. - let mut cursor = layer.node().walk(); - cursor.goto_descendant(descendant_ix); - let node = cursor.node(); - let range = node.byte_range(); - - // Build a text anchor range. - let buffer = buffer_state.buffer.read(cx); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - - // Build a multibuffer anchor range. - let multibuffer = editor_state.editor.read(cx).buffer(); - let multibuffer = multibuffer.read(cx).snapshot(cx); - let excerpt_id = buffer_state.excerpt_id; - let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start) - ..multibuffer.anchor_in_excerpt(excerpt_id, range.end); - - // Update the editor with the anchor range. - editor_state.editor.update(cx, |editor, cx| { - f(editor, range, cx); - }); - Some(()) - } - - fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div { - let colors = cx.theme().colors(); - let mut row = h_stack(); - if let Some(field_name) = cursor.field_name() { - row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]); - } - - let node = cursor.node(); - return row - .child(if node.is_named() { - Label::new(node.kind()).color(Color::Default) - } else { - Label::new(format!("\"{}\"", node.kind())).color(Color::Created) - }) - .child( - div() - .child(Label::new(format_node_range(node)).color(Color::Muted)) - .pl_1(), - ) - .text_bg(if selected { - colors.element_selected - } else { - Hsla::default() - }) - .pl(rems(depth as f32)) - .hover(|style| style.bg(colors.element_hover)); - } -} - -impl Render for SyntaxTreeView { - fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let line_height = cx - .text_style() - .line_height_in_pixels(settings.buffer_font_size(cx)); - if Some(line_height) != self.line_height { - self.line_height = Some(line_height); - self.hover_state_changed(cx); - } - - let mut rendered = div().flex_1(); - - if let Some(layer) = self - .editor - .as_ref() - .and_then(|editor| editor.active_buffer.as_ref()) - .and_then(|buffer| buffer.active_layer.as_ref()) - { - let layer = layer.clone(); - let list = uniform_list( - cx.view().clone(), - "SyntaxTreeView", - layer.node().descendant_count(), - move |this, range, cx| { - let mut items = Vec::new(); - let mut cursor = layer.node().walk(); - let mut descendant_ix = range.start as usize; - cursor.goto_descendant(descendant_ix); - let mut depth = cursor.depth(); - let mut visited_children = false; - while descendant_ix < range.end { - if visited_children { - if cursor.goto_next_sibling() { - visited_children = false; - } else if cursor.goto_parent() { - depth -= 1; - } else { - break; - } - } else { - items.push(Self::render_node( - &cursor, - depth, - Some(descendant_ix) == this.selected_descendant_ix, - cx, - )); - descendant_ix += 1; - if cursor.goto_first_child() { - depth += 1; - } else { - visited_children = true; - } - } - } - items - }, - ) - .size_full() - .track_scroll(self.list_scroll_handle.clone()) - .on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| { - tree_view.mouse_y = Some(event.position.y); - tree_view.hover_state_changed(cx); - })) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |tree_view, event: &MouseDownEvent, cx| { - tree_view.handle_click(event.position.y, cx); - }), - ) - .text_bg(cx.theme().colors().background); - - rendered = rendered.child( - canvas(move |bounds, cx| { - list.into_any_element().draw( - bounds.origin, - bounds.size.map(AvailableSpace::Definite), - cx, - ) - }) - .size_full(), - ); - } - - rendered - } -} - -impl EventEmitter<()> for SyntaxTreeView {} - -impl FocusableView for SyntaxTreeView { - fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for SyntaxTreeView { - type Event = (); - - fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} - - fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { - Label::new("Syntax Tree").into_any_element() - } - - fn clone_on_split( - &self, - _: workspace::WorkspaceId, - cx: &mut ViewContext, - ) -> Option> - where - Self: Sized, - { - Some(cx.new_view(|cx| { - let mut clone = Self::new(self.workspace_handle.clone(), None, cx); - if let Some(editor) = &self.editor { - clone.set_editor(editor.editor.clone(), cx) - } - clone - })) - } -} - -impl SyntaxTreeToolbarItemView { - pub fn new() -> Self { - Self { - tree_view: None, - subscription: None, - } - } - - fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option> { - let tree_view = self.tree_view.as_ref()?; - let tree_view = tree_view.read(cx); - - let editor_state = tree_view.editor.as_ref()?; - let buffer_state = editor_state.active_buffer.as_ref()?; - let active_layer = buffer_state.active_layer.clone()?; - let active_buffer = buffer_state.buffer.read(cx).snapshot(); - - let view = cx.view().clone(); - Some( - popover_menu("Syntax Tree") - .trigger(Self::render_header(&active_layer)) - .menu(move |cx| { - ContextMenu::build(cx, |mut menu, cx| { - for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() { - menu = menu.entry( - format!( - "{} {}", - layer.language.name(), - format_node_range(layer.node()) - ), - None, - cx.handler_for(&view, move |view, cx| { - view.select_layer(layer_ix, cx); - }), - ); - } - menu - }) - .into() - }), - ) - } - - fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext) -> Option<()> { - let tree_view = self.tree_view.as_ref()?; - tree_view.update(cx, |view, cx| { - let editor_state = view.editor.as_mut()?; - let buffer_state = editor_state.active_buffer.as_mut()?; - let snapshot = buffer_state.buffer.read(cx).snapshot(); - let layer = snapshot.syntax_layers().nth(layer_ix)?; - buffer_state.active_layer = Some(layer.to_owned()); - view.selected_descendant_ix = None; - cx.notify(); - view.focus_handle.focus(cx); - Some(()) - }) - } - - fn render_header(active_layer: &OwnedSyntaxLayerInfo) -> ButtonLike { - ButtonLike::new("syntax tree header") - .child(Label::new(active_layer.language.name())) - .child(Label::new(format_node_range(active_layer.node()))) - } -} - -fn format_node_range(node: Node) -> String { - let start = node.start_position(); - let end = node.end_position(); - format!( - "[{}:{} - {}:{}]", - start.row + 1, - start.column + 1, - end.row + 1, - end.column + 1, - ) -} - -impl Render for SyntaxTreeToolbarItemView { - fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> impl IntoElement { - self.render_menu(cx) - .unwrap_or_else(|| popover_menu("Empty Syntax Tree")) - } -} - -impl EventEmitter for SyntaxTreeToolbarItemView {} - -impl ToolbarItemView for SyntaxTreeToolbarItemView { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation { - if let Some(item) = active_pane_item { - if let Some(view) = item.downcast::() { - self.tree_view = Some(view.clone()); - self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify())); - return ToolbarItemLocation::PrimaryLeft; - } - } - self.tree_view = None; - self.subscription = None; - ToolbarItemLocation::Hidden - } -} diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 56352f254f..457e6b25b0 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -15,7 +15,7 @@ editor = { path = "../editor2", package = "editor2" } gpui = { path = "../gpui2", package = "gpui2" } menu = { path = "../menu2", package = "menu2" } project = { path = "../project2", package = "project2" } -search = { package = "search2", path = "../search2" } +search = { path = "../search" } settings = { path = "../settings2", package = "settings2" } theme = { path = "../theme2", package = "theme2" } ui = { path = "../ui2", package = "ui2" } diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index c263c29299..5a421a805a 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -12,7 +12,7 @@ doctest = false assistant = { package = "assistant2", path = "../assistant2" } editor = { package = "editor2", path = "../editor2" } gpui = { package = "gpui2", path = "../gpui2" } -search = { package = "search2", path = "../search2" } +search = { path = "../search" } workspace = { package = "workspace2", path = "../workspace2" } ui = { package = "ui2", path = "../ui2" } diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 4ebd31a2bc..5bb318992e 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -11,16 +11,17 @@ doctest = false [dependencies] bitflags = "1" collections = { path = "../collections" } -editor = { path = "../editor" } -gpui = { path = "../gpui" } -language = { path = "../language" } -menu = { path = "../menu" } -project = { path = "../project" } -settings = { path = "../settings" } -theme = { path = "../theme" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } -workspace = { path = "../workspace" } -semantic_index = { path = "../semantic_index" } +ui = {package = "ui2", path = "../ui2"} +workspace = { package = "workspace2", path = "../workspace2" } +semantic_index = { package = "semantic_index2", path = "../semantic_index2" } anyhow.workspace = true futures.workspace = true log.workspace = true @@ -31,9 +32,9 @@ smallvec.workspace = true smol.workspace = true serde_json.workspace = true [dev-dependencies] -client = { path = "../client", features = ["test-support"] } -editor = { path = "../editor", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } unindent.workspace = true diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 29ffe7c021..67aa4955bc 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,82 +1,55 @@ use crate::{ history::SearchHistory, - mode::{next_mode, SearchMode, Side}, - search_bar::{render_nav_button, render_search_mode_button}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, - SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, - ToggleWholeWord, + mode::{next_mode, SearchMode}, + search_bar::render_nav_button, + ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, + ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, + ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, }; use collections::HashMap; -use editor::Editor; +use editor::{Editor, EditorElement, EditorStyle, Tab}; use futures::channel::oneshot; use gpui::{ - actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription, - Task, View, ViewContext, ViewHandle, WindowContext, + actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, + FontStyle, FontWeight, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, + Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, + WhiteSpace, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; +use settings::Settings; use std::{any::Any, sync::Arc}; +use theme::ThemeSettings; +use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, ToggleButton, Tooltip}; use util::ResultExt; use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, - Pane, ToolbarItemLocation, ToolbarItemView, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; -#[derive(Clone, Deserialize, PartialEq)] +#[derive(PartialEq, Clone, Deserialize)] pub struct Deploy { pub focus: bool, } -actions!(buffer_search, [Dismiss, FocusEditor]); impl_actions!(buffer_search, [Deploy]); +actions!(buffer_search, [Dismiss, FocusEditor]); + pub enum Event { UpdateLocation, } pub fn init(cx: &mut AppContext) { - cx.add_action(BufferSearchBar::deploy_bar); - cx.add_action(BufferSearchBar::dismiss); - cx.add_action(BufferSearchBar::focus_editor); - cx.add_action(BufferSearchBar::select_next_match); - cx.add_action(BufferSearchBar::select_prev_match); - cx.add_action(BufferSearchBar::select_all_matches); - cx.add_action(BufferSearchBar::select_next_match_on_pane); - cx.add_action(BufferSearchBar::select_prev_match_on_pane); - cx.add_action(BufferSearchBar::select_all_matches_on_pane); - cx.add_action(BufferSearchBar::handle_editor_cancel); - cx.add_action(BufferSearchBar::next_history_query); - cx.add_action(BufferSearchBar::previous_history_query); - cx.add_action(BufferSearchBar::cycle_mode); - cx.add_action(BufferSearchBar::cycle_mode_on_pane); - cx.add_action(BufferSearchBar::replace_all); - cx.add_action(BufferSearchBar::replace_next); - cx.add_action(BufferSearchBar::replace_all_on_pane); - cx.add_action(BufferSearchBar::replace_next_on_pane); - cx.add_action(BufferSearchBar::toggle_replace); - cx.add_action(BufferSearchBar::toggle_replace_on_a_pane); - add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); - add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); -} - -fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { - cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if search_bar.show(cx) { - search_bar.toggle_search_option(option, cx); - } - }); - } - cx.propagate_action(); - }); + cx.observe_new_views(|editor: &mut Workspace, _| BufferSearchBar::register(editor)) + .detach(); } pub struct BufferSearchBar { - query_editor: ViewHandle, - replacement_editor: ViewHandle, + query_editor: View, + replacement_editor: View, active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, @@ -93,51 +66,54 @@ pub struct BufferSearchBar { replace_enabled: bool, } -impl Entity for BufferSearchBar { - type Event = Event; +impl BufferSearchBar { + fn render_text_input(&self, editor: &View, cx: &ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if editor.read(cx).read_only() { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + + EditorElement::new( + &editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } } -impl View for BufferSearchBar { - fn ui_name() -> &'static str { - "BufferSearchBar" - } - - fn update_keymap_context( - &self, - keymap: &mut gpui::keymap_matcher::KeymapContext, - cx: &AppContext, - ) { - Self::reset_to_default_keymap_context(keymap); - let in_replace = self - .replacement_editor - .read_with(cx, |_, cx| cx.is_self_focused()) - .unwrap_or(false); - if in_replace { - keymap.add_identifier("in_replace"); +impl EventEmitter for BufferSearchBar {} +impl EventEmitter for BufferSearchBar {} +impl Render for BufferSearchBar { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + if self.dismissed { + return div(); } - } - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.query_editor); - } - } + let supported_options = self.supported_options(); - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx).clone(); - let query_container_style = if self.query_contains_error { - theme.search.invalid_editor - } else { - theme.search.editor.input.container - }; - let supported_options = self - .active_searchable_item - .as_ref() - .map(|active_searchable_item| active_searchable_item.supported_options()) - .unwrap_or_default(); - - let previous_query_keystrokes = - cx.binding_for_action(&PreviousHistoryQuery {}) + if self.query_editor.read(cx).placeholder_text().is_none() { + let query_focus_handle = self.query_editor.focus_handle(cx); + let up_keystrokes = cx + .bindings_for_action_in(&PreviousHistoryQuery {}, &query_focus_handle) + .into_iter() + .next() .map(|binding| { binding .keystrokes() @@ -145,62 +121,40 @@ impl View for BufferSearchBar { .map(|k| k.to_string()) .collect::>() }); - let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { - (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { - format!( - "Search ({}/{} for previous/next query)", - previous_query_keystrokes.join(" "), - next_query_keystrokes.join(" ") - ) + let down_keystrokes = cx + .bindings_for_action_in(&NextHistoryQuery {}, &query_focus_handle) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + + let placeholder_text = + up_keystrokes + .zip(down_keystrokes) + .map(|(up_keystrokes, down_keystrokes)| { + Arc::from(format!( + "Search ({}/{} for previous/next query)", + up_keystrokes.join(" "), + down_keystrokes.join(" ") + )) + }); + + if let Some(placeholder_text) = placeholder_text { + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(placeholder_text, cx); + }); } - (None, Some(next_query_keystrokes)) => { - format!( - "Search ({} for next query)", - next_query_keystrokes.join(" ") - ) - } - (Some(previous_query_keystrokes), None) => { - format!( - "Search ({} for previous query)", - previous_query_keystrokes.join(" ") - ) - } - (None, None) => String::new(), - }; - self.query_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(new_placeholder_text, cx); - }); + } + self.replacement_editor.update(cx, |editor, cx| { editor.set_placeholder_text("Replace with...", cx); }); - let search_button_for_mode = |mode, side, cx: &mut ViewContext| { - let is_active = self.current_mode == mode; - render_search_mode_button( - mode, - side, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, - ) - }; - let search_option_button = |option| { - let is_active = self.search_options.contains(option); - option.as_button( - is_active, - theme.tooltip.clone(), - theme.search.option_button_component.clone(), - ) - }; let match_count = self .active_searchable_item .as_ref() @@ -217,143 +171,210 @@ impl View for BufferSearchBar { "No matches".to_string() }; - Some( - Label::new(message, theme.search.match_index.text.clone()) - .contained() - .with_style(theme.search.match_index.container) - .aligned(), - ) + Some(ui::Label::new(message)) }); - let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { - render_nav_button( - label, - direction, - self.active_match_index.is_some(), - move |_, this, cx| match direction { - Direction::Prev => this.select_prev_match(&Default::default(), cx), - Direction::Next => this.select_next_match(&Default::default(), cx), - }, - cx, - ) - }; - let query_column = Flex::row() - .with_child( - Svg::for_style(theme.search.editor_icon.clone().icon) - .contained() - .with_style(theme.search.editor_icon.clone().container), - ) - .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_children( - supported_options - .case - .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), - ) - .with_children( - supported_options - .word - .then(|| search_option_button(SearchOptions::WHOLE_WORD)), - ) - .flex_float() - .contained(), - ) - .align_children_center() - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false); let should_show_replace_input = self.replace_enabled && supported_options.replacement; + let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); - let replacement = should_show_replace_input.then(|| { - Flex::row() - .with_child( - Svg::for_style(theme.search.replace_icon.clone().icon) - .contained() - .with_style(theme.search.replace_icon.clone().container), - ) - .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true)) - .align_children_center() - .flex(1., true) - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false) - }); - let replace_all = should_show_replace_input.then(|| { - super::replace_action( - ReplaceAll, - "Replace all", - "icons/replace_all.svg", - theme.tooltip.clone(), - theme.search.action_button.clone(), + let mut key_context = KeyContext::default(); + key_context.add("BufferSearchBar"); + if in_replace { + key_context.add("in_replace"); + } + let editor_border = if self.query_contains_error { + Color::Error.color(cx) + } else { + cx.theme().colors().border + }; + h_stack() + .w_full() + .gap_2() + .key_context(key_context) + .capture_action(cx.listener(Self::tab)) + .on_action(cx.listener(Self::previous_history_query)) + .on_action(cx.listener(Self::next_history_query)) + .on_action(cx.listener(Self::dismiss)) + .on_action(cx.listener(Self::select_next_match)) + .on_action(cx.listener(Self::select_prev_match)) + .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| { + this.activate_search_mode(SearchMode::Regex, cx); + })) + .on_action(cx.listener(|this, _: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx); + })) + .when(self.supported_options().replacement, |this| { + this.on_action(cx.listener(Self::toggle_replace)) + .when(in_replace, |this| { + this.on_action(cx.listener(Self::replace_next)) + .on_action(cx.listener(Self::replace_all)) + }) + }) + .when(self.supported_options().case, |this| { + this.on_action(cx.listener(Self::toggle_case_sensitive)) + }) + .when(self.supported_options().word, |this| { + this.on_action(cx.listener(Self::toggle_whole_word)) + }) + .child( + h_stack() + .flex_1() + .px_2() + .py_1() + .gap_2() + .border_1() + .border_color(editor_border) + .rounded_lg() + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(self.render_text_input(&self.query_editor, cx)) + .children(supported_options.case.then(|| { + self.render_search_option_button( + SearchOptions::CASE_SENSITIVE, + cx.listener(|this, _, cx| { + this.toggle_case_sensitive(&ToggleCaseSensitive, cx) + }), + ) + })) + .children(supported_options.word.then(|| { + self.render_search_option_button( + SearchOptions::WHOLE_WORD, + cx.listener(|this, _, cx| this.toggle_whole_word(&ToggleWholeWord, cx)), + ) + })), ) - }); - let replace_next = should_show_replace_input.then(|| { - super::replace_action( - ReplaceNext, - "Replace next", - "icons/replace_next.svg", - theme.tooltip.clone(), - theme.search.action_button.clone(), + .child( + h_stack() + .gap_2() + .flex_none() + .child( + h_stack() + .child( + ToggleButton::new("search-mode-text", SearchMode::Text.label()) + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.current_mode == SearchMode::Text) + .on_click(cx.listener(move |_, _event, cx| { + cx.dispatch_action(SearchMode::Text.action()) + })) + .tooltip(|cx| { + Tooltip::for_action( + SearchMode::Text.tooltip(), + &*SearchMode::Text.action(), + cx, + ) + }) + .first(), + ) + .child( + ToggleButton::new("search-mode-regex", SearchMode::Regex.label()) + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .selected(self.current_mode == SearchMode::Regex) + .on_click(cx.listener(move |_, _event, cx| { + cx.dispatch_action(SearchMode::Regex.action()) + })) + .tooltip(|cx| { + Tooltip::for_action( + SearchMode::Regex.tooltip(), + &*SearchMode::Regex.action(), + cx, + ) + }) + .last(), + ), + ) + .when(supported_options.replacement, |this| { + this.child( + IconButton::new( + "buffer-search-bar-toggle-replace-button", + Icon::Replace, + ) + .style(ButtonStyle::Subtle) + .when(self.replace_enabled, |button| { + button.style(ButtonStyle::Filled) + }) + .on_click(cx.listener(|this, _: &ClickEvent, cx| { + this.toggle_replace(&ToggleReplace, cx); + })) + .tooltip(|cx| { + Tooltip::for_action("Toggle replace", &ToggleReplace, cx) + }), + ) + }), ) - }); - let switches_column = supported_options.replacement.then(|| { - Flex::row() - .align_children_center() - .with_child(super::toggle_replace_button( - self.replace_enabled, - theme.tooltip.clone(), - theme.search.option_button_component.clone(), - )) - .constrained() - .with_height(theme.search.search_bar_row_height) - .contained() - .with_style(theme.search.option_button_group) - }); - let mode_column = Flex::row() - .with_child(search_button_for_mode( - SearchMode::Text, - Some(Side::Left), - cx, - )) - .with_child(search_button_for_mode( - SearchMode::Regex, - Some(Side::Right), - cx, - )) - .contained() - .with_style(theme.search.modes_container) - .constrained() - .with_height(theme.search.search_bar_row_height); + .child( + h_stack() + .gap_0p5() + .flex_1() + .when(self.replace_enabled, |this| { + this.child( + h_stack() + .flex_1() + // We're giving this a fixed height to match the height of the search input, + // which has an icon inside that is increasing its height. + .h_8() + .px_2() + .py_1() + .gap_2() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_lg() + .child(self.render_text_input(&self.replacement_editor, cx)), + ) + .when(should_show_replace_input, |this| { + this.child( + IconButton::new("search-replace-next", ui::Icon::ReplaceNext) + .tooltip(move |cx| { + Tooltip::for_action("Replace next", &ReplaceNext, cx) + }) + .on_click(cx.listener(|this, _, cx| { + this.replace_next(&ReplaceNext, cx) + })), + ) + .child( + IconButton::new("search-replace-all", ui::Icon::ReplaceAll) + .tooltip(move |cx| { + Tooltip::for_action("Replace all", &ReplaceAll, cx) + }) + .on_click( + cx.listener(|this, _, cx| { + this.replace_all(&ReplaceAll, cx) + }), + ), + ) + }) + }), + ) + .child( + h_stack() + .gap_0p5() + .flex_none() + .child( + IconButton::new("select-all", ui::Icon::SelectAll) + .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone())) + .tooltip(|cx| { + Tooltip::for_action("Select all matches", &SelectAllMatches, cx) + }), + ) + .children(match_count) + .child(render_nav_button( + ui::Icon::ChevronLeft, + self.active_match_index.is_some(), + "Select previous match", + &SelectPrevMatch, + )) + .child(render_nav_button( + ui::Icon::ChevronRight, + self.active_match_index.is_some(), + "Select next match", + &SelectNextMatch, + )), + ) + } +} - let nav_column = Flex::row() - .align_children_center() - .with_children(replace_next) - .with_children(replace_all) - .with_child(self.render_action_button("icons/select-all.svg", cx)) - .with_child(Flex::row().with_children(match_count)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex_float(); - - Flex::row() - .with_child(query_column) - .with_child(mode_column) - .with_children(switches_column) - .with_children(replacement) - .with_child(nav_column) - .contained() - .with_style(theme.search.container) - .into_any_named("search bar") +impl FocusableView for BufferSearchBar { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.query_editor.focus_handle(cx) } } @@ -366,23 +387,26 @@ impl ToolbarItemView for BufferSearchBar { cx.notify(); self.active_searchable_item_subscription.take(); self.active_searchable_item.take(); + self.pending_search.take(); if let Some(searchable_item_handle) = item.and_then(|item| item.to_searchable_item_handle(cx)) { - let this = cx.weak_handle(); - self.active_searchable_item_subscription = - Some(searchable_item_handle.subscribe_to_search_events( + let this = cx.view().downgrade(); + + searchable_item_handle + .subscribe_to_search_events( cx, Box::new(move |search_event, cx| { - if let Some(this) = this.upgrade(cx) { + if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.on_active_searchable_item_event(search_event, cx) }); } }), - )); + ) + .detach(); self.active_searchable_item = Some(searchable_item_handle); let _ = self.update_matches(cx); @@ -390,48 +414,105 @@ impl ToolbarItemView for BufferSearchBar { return ToolbarItemLocation::Secondary; } } - ToolbarItemLocation::Hidden } - fn location_for_event( - &self, - _: &Self::Event, - _: ToolbarItemLocation, - _: &AppContext, - ) -> ToolbarItemLocation { - if self.active_searchable_item.is_some() && !self.dismissed { - ToolbarItemLocation::Secondary - } else { - ToolbarItemLocation::Hidden - } - } - - fn row_count(&self, _: &ViewContext) -> usize { + fn row_count(&self, _: &WindowContext<'_>) -> usize { 1 } } impl BufferSearchBar { - pub fn new(cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::auto_height( - 2, - Some(Arc::new(|theme| theme.search.editor.input.clone())), - cx, - ) + fn register(workspace: &mut Workspace) { + workspace.register_action(move |workspace, deploy: &Deploy, cx| { + let pane = workspace.active_pane(); + + pane.update(cx, |this, cx| { + this.toolbar().update(cx, |this, cx| { + if let Some(search_bar) = this.item_of_type::() { + search_bar.update(cx, |this, cx| { + this.deploy(deploy, cx); + }); + return; + } + let view = cx.new_view(|cx| BufferSearchBar::new(cx)); + this.add_item(view.clone(), cx); + view.update(cx, |this, cx| this.deploy(deploy, cx)); + cx.notify(); + }) + }); }); + fn register_action( + workspace: &mut Workspace, + update: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + workspace.register_action(move |workspace, action: &A, cx| { + let pane = workspace.active_pane(); + pane.update(cx, move |this, cx| { + this.toolbar().update(cx, move |this, cx| { + if let Some(search_bar) = this.item_of_type::() { + search_bar.update(cx, move |this, cx| update(this, action, cx)); + cx.notify(); + } + }) + }); + }); + } + + register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { + if this.supported_options().case { + this.toggle_case_sensitive(action, cx); + } + }); + register_action(workspace, |this, action: &ToggleWholeWord, cx| { + if this.supported_options().word { + this.toggle_whole_word(action, cx); + } + }); + register_action(workspace, |this, action: &ToggleReplace, cx| { + if this.supported_options().replacement { + this.toggle_replace(action, cx); + } + }); + register_action(workspace, |this, _: &ActivateRegexMode, cx| { + if this.supported_options().regex { + this.activate_search_mode(SearchMode::Regex, cx); + } + }); + register_action(workspace, |this, _: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx); + }); + register_action(workspace, |this, action: &CycleMode, cx| { + if this.supported_options().regex { + // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting + // cycling. + this.cycle_mode(action, cx) + } + }); + register_action(workspace, |this, action: &SelectNextMatch, cx| { + this.select_next_match(action, cx); + }); + register_action(workspace, |this, action: &SelectPrevMatch, cx| { + this.select_prev_match(action, cx); + }); + register_action(workspace, |this, action: &SelectAllMatches, cx| { + this.select_all_matches(action, cx); + }); + register_action(workspace, |this, _: &editor::Cancel, cx| { + if !this.dismissed { + this.dismiss(&Dismiss, cx); + return; + } + cx.propagate(); + }); + } + pub fn new(cx: &mut ViewContext) -> Self { + let query_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - let replacement_editor = cx.add_view(|cx| { - Editor::auto_height( - 2, - Some(Arc::new(|theme| theme.search.editor.input.clone())), - cx, - ) - }); - // cx.subscribe(&replacement_editor, Self::on_query_editor_event) - // .detach(); + let replacement_editor = cx.new_view(|cx| Editor::single_line(cx)); + cx.subscribe(&replacement_editor, Self::on_query_editor_event) + .detach(); Self { query_editor, replacement_editor, @@ -465,9 +546,13 @@ impl BufferSearchBar { } } if let Some(active_editor) = self.active_searchable_item.as_ref() { - cx.focus(active_editor.as_any()); + let handle = active_editor.focus_handle(cx); + cx.focus(&handle); } cx.emit(Event::UpdateLocation); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Hidden, + )); cx.notify(); } @@ -476,7 +561,8 @@ impl BufferSearchBar { self.search_suggested(cx); if deploy.focus { self.select_query(cx); - cx.focus_self(); + let handle = self.query_editor.focus_handle(cx); + cx.focus(&handle); } return true; } @@ -484,6 +570,14 @@ impl BufferSearchBar { false } + pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext) { + if self.is_dismissed() { + self.deploy(action, cx); + } else { + self.dismiss(&Dismiss, cx); + } + } + pub fn show(&mut self, cx: &mut ViewContext) -> bool { if self.active_searchable_item.is_none() { return false; @@ -491,9 +585,18 @@ impl BufferSearchBar { self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); + cx.emit(ToolbarItemEvent::ChangeLocation( + ToolbarItemLocation::Secondary, + )); true } + fn supported_options(&self) -> workspace::searchable::SearchOptions { + self.active_searchable_item + .as_deref() + .map(SearchableItemHandle::supported_options) + .unwrap_or_default() + } pub fn search_suggested(&mut self, cx: &mut ViewContext) { let search = self .query_suggestion(cx) @@ -579,26 +682,14 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_action_button( + fn render_search_option_button( &self, - icon: &'static str, - cx: &mut ViewContext, - ) -> AnyElement { - let tooltip = "Select All Matches"; - let tooltip_style = theme::current(cx).tooltip.clone(); - - let theme = theme::current(cx); - let style = theme.search.action_button.clone(); - - gpui::elements::Component::element(SafeStylable::with_style( - theme::components::action_button::Button::action(SelectAllMatches) - .with_tooltip(tooltip, tooltip_style) - .with_contents(theme::components::svg::Svg::new(icon)), - style, - )) - .into_any() + option: SearchOptions, + action: impl Fn(&ClickEvent, &mut WindowContext) + 'static, + ) -> impl IntoElement { + let is_active = self.search_options.contains(option); + option.as_button(is_active, action) } - pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { assert_ne!( mode, @@ -613,33 +704,10 @@ impl BufferSearchBar { cx.notify(); } - fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { - let mut propagate_action = true; - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if search_bar.deploy(action, cx) { - propagate_action = false; - } - }); - } - if propagate_action { - cx.propagate_action(); - } - } - - fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if !search_bar.read(cx).dismissed { - search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx)); - return; - } - } - cx.propagate_action(); - } - pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { if let Some(active_editor) = self.active_searchable_item.as_ref() { - cx.focus(active_editor.as_any()); + let handle = active_editor.focus_handle(cx); + cx.focus(&handle); } } @@ -690,6 +758,7 @@ impl BufferSearchBar { { let new_match_index = searchable_item .match_index_for_direction(matches, index, direction, count, cx); + searchable_item.update_matches(matches, cx); searchable_item.activate_match(new_match_index, matches, cx); } @@ -713,43 +782,13 @@ impl BufferSearchBar { } } - fn select_next_match_on_pane( - pane: &mut Pane, - action: &SelectNextMatch, - cx: &mut ViewContext, - ) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx)); - } - } - - fn select_prev_match_on_pane( - pane: &mut Pane, - action: &SelectPrevMatch, - cx: &mut ViewContext, - ) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx)); - } - } - - fn select_all_matches_on_pane( - pane: &mut Pane, - action: &SelectAllMatches, - cx: &mut ViewContext, - ) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx)); - } - } - fn on_query_editor_event( &mut self, - _: ViewHandle, - event: &editor::Event, + _: View, + event: &editor::EditorEvent, cx: &mut ViewContext, ) { - if let editor::Event::Edited { .. } = event { + if let editor::EditorEvent::Edited { .. } = event { self.query_contains_error = false; self.clear_matches(cx); let search = self.update_matches(cx); @@ -761,7 +800,7 @@ impl BufferSearchBar { } } - fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext) { + fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext) { match event { SearchEvent::MatchesInvalidated => { let _ = self.update_matches(cx); @@ -770,6 +809,12 @@ impl BufferSearchBar { } } + fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext) { + self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) + } + fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext) { + self.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + } fn clear_matches(&mut self, cx: &mut ViewContext) { let mut active_item_matches = None; for (searchable_item, matches) in self.searchable_items_with_matches.drain() { @@ -812,6 +857,7 @@ impl BufferSearchBar { Ok(query) => query.with_replacement(self.replacement(cx)), Err(_) => { self.query_contains_error = true; + self.active_match_index = None; cx.notify(); return done_rx; } @@ -828,6 +874,7 @@ impl BufferSearchBar { Ok(query) => query.with_replacement(self.replacement(cx)), Err(_) => { self.query_contains_error = true; + self.active_match_index = None; cx.notify(); return done_rx; } @@ -836,11 +883,13 @@ impl BufferSearchBar { .into(); self.active_search = Some(query.clone()); let query_text = query.as_str().to_string(); + let matches = active_searchable_item.find_matches(query, cx); let active_searchable_item = active_searchable_item.downgrade(); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let matches = matches.await; + this.update(&mut cx, |this, cx| { if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) @@ -884,6 +933,14 @@ impl BufferSearchBar { } } + fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if let Some(item) = self.active_searchable_item.as_ref() { + let focus_handle = item.focus_handle(cx); + cx.focus(&focus_handle); + cx.stop_propagation(); + } + } + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { if let Some(new_query) = self.search_history.next().map(str::to_string) { let _ = self.search(&new_query, Some(self.search_options), cx); @@ -908,53 +965,16 @@ impl BufferSearchBar { fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { self.activate_search_mode(next_mode(&self.current_mode, false), cx); } - fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { - let mut should_propagate = true; - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| { - if bar.show(cx) { - should_propagate = false; - bar.cycle_mode(action, cx); - false - } else { - true - } - }); - } - if should_propagate { - cx.propagate_action(); - } - } fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { if let Some(_) = &self.active_searchable_item { self.replace_enabled = !self.replace_enabled; if !self.replace_enabled { - cx.focus(&self.query_editor); + let handle = self.query_editor.focus_handle(cx); + cx.focus(&handle); } cx.notify(); } } - fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { - let mut should_propagate = true; - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| { - if let Some(_) = &bar.active_searchable_item { - should_propagate = false; - bar.replace_enabled = !bar.replace_enabled; - if bar.dismissed { - bar.show(cx); - } - if !bar.replace_enabled { - cx.focus(&bar.query_editor); - } - cx.notify(); - } - }); - } - if should_propagate { - cx.propagate_action(); - } - } fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { let mut should_propagate = true; if !self.dismissed && self.active_search.is_some() { @@ -978,8 +998,8 @@ impl BufferSearchBar { } } } - if should_propagate { - cx.propagate_action(); + if !should_propagate { + cx.stop_propagation(); } } pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { @@ -1002,37 +1022,41 @@ impl BufferSearchBar { } } } - fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.replace_next(action, cx)); - return; - } - cx.propagate_action(); - } - fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.replace_all(action, cx)); - return; - } - cx.propagate_action(); - } } #[cfg(test)] mod tests { + use std::ops::Range; + use super::*; use editor::{DisplayPoint, Editor}; - use gpui::{color::Color, test::EmptyView, TestAppContext}; + use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext}; use language::Buffer; + use smol::stream::StreamExt as _; use unindent::Unindent as _; - fn init_test(cx: &mut TestAppContext) -> (ViewHandle, ViewHandle) { - crate::project_search::tests::init_test(cx); + fn init_globals(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = settings::SettingsStore::test(cx); + cx.set_global(store); + editor::init(cx); - let buffer = cx.add_model(|cx| { + language::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + }); + } + fn init_test( + cx: &mut TestAppContext, + ) -> ( + View, + View, + &mut VisualTestContext<'_>, + ) { + init_globals(cx); + let buffer = cx.new_model(|cx| { Buffer::new( 0, - cx.model_id() as u64, + cx.entity_id().as_u64(), r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -1042,23 +1066,29 @@ mod tests { .unindent(), ) }); - let window = cx.add_window(|_| EmptyView); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let (_, cx) = cx.add_window_view(|_| EmptyView {}); + let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); - let search_bar = window.add_view(cx, |cx| { + let search_bar = cx.new_view(|cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); search_bar }); - (editor, search_bar) + (editor, search_bar, cx) } #[gpui::test] async fn test_search_simple(cx: &mut TestAppContext) { - let (editor, search_bar) = init_test(cx); - + let (editor, search_bar, cx) = init_test(cx); + // todo! osiewicz: these tests asserted on background color as well, that should be brought back. + let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { + background_highlights + .into_iter() + .map(|(range, _)| range) + .collect::>() + }; // Search for a string that appears with different casing. // By default, search is case-insensitive. search_bar @@ -1067,16 +1097,10 @@ mod tests { .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( - editor.all_text_background_highlights(cx), + display_points_of(editor.all_text_background_highlights(cx)), &[ - ( - DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - Color::red(), - ), - ( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - ), + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), ] ); }); @@ -1085,14 +1109,12 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); }); - editor.next_notification(cx).await; + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_text_background_highlights(cx), - &[( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - )] + display_points_of(editor.all_text_background_highlights(cx)), + &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] ); }); @@ -1104,36 +1126,15 @@ mod tests { .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( - editor.all_text_background_highlights(cx), + display_points_of(editor.all_text_background_highlights(cx)), &[ - ( - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), - Color::red(), - ), - ( - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), - ), - ( - DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), - Color::red(), - ), - ( - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), - Color::red(), - ), - ( - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), - ), - ( - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), - ), - ( - DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), - Color::red(), - ), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), ] ); }); @@ -1142,23 +1143,15 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); }); - editor.next_notification(cx).await; + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_text_background_highlights(cx), + display_points_of(editor.all_text_background_highlights(cx)), &[ - ( - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), - ), - ( - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), - ), - ( - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), - ), + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), ] ); }); @@ -1176,7 +1169,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1187,7 +1180,7 @@ mod tests { [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(1)); }); @@ -1198,7 +1191,7 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); @@ -1209,7 +1202,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1220,7 +1213,7 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); @@ -1231,7 +1224,7 @@ mod tests { [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(1)); }); @@ -1242,7 +1235,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1261,7 +1254,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1280,7 +1273,7 @@ mod tests { [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(1)); }); @@ -1299,7 +1292,7 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); @@ -1318,7 +1311,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1337,14 +1330,14 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); } #[gpui::test] async fn test_search_option_handling(cx: &mut TestAppContext) { - let (editor, search_bar) = init_test(cx); + let (editor, search_bar, cx) = init_test(cx); // show with options should make current search case sensitive search_bar @@ -1354,13 +1347,17 @@ mod tests { }) .await .unwrap(); + // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back. + let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { + background_highlights + .into_iter() + .map(|(range, _)| range) + .collect::>() + }; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_text_background_highlights(cx), - &[( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - )] + display_points_of(editor.all_text_background_highlights(cx)), + &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] ); }); @@ -1380,14 +1377,12 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) }); - editor.next_notification(cx).await; + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_text_background_highlights(cx), - &[( - DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), - Color::red(), - ),] + display_points_of(editor.all_text_background_highlights(cx)), + &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),] ); }); @@ -1403,8 +1398,7 @@ mod tests { #[gpui::test] async fn test_search_select_all_matches(cx: &mut TestAppContext) { - crate::project_search::tests::init_test(cx); - + init_globals(cx); let buffer_text = r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -1420,186 +1414,196 @@ mod tests { expected_query_matches_count > 1, "Should pick a query with multiple results" ); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); - let window = cx.add_window(|_| EmptyView); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); + let window = cx.add_window(|_| EmptyView {}); - let search_bar = window.add_view(cx, |cx| { + let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = window.build_view(cx, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); search_bar }); - search_bar - .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.activate_current_match(cx); - }); - - window.read_with(cx, |cx| { - assert!( - !editor.is_focused(cx), - "Initially, the editor should not be focused" - ); - }); - - let initial_selections = editor.update(cx, |editor, cx| { - let initial_selections = editor.selections.display_ranges(cx); - assert_eq!( - initial_selections.len(), 1, - "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", - ); - initial_selections - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should focus editor after successful SelectAllMatches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_next_match(&SelectNextMatch, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should still have editor focused after SelectNextMatch" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - 1, - "On next match, should deselect items and select the next match" - ); - assert_ne!( - all_selections, initial_selections, - "Next match should be different from the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should be updated to the next one" - ); - }); - - search_bar.update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should focus editor after successful SelectAllMatches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPrevMatch, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should still have editor focused after SelectPrevMatch" - ); - }); - let last_match_selections = search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - 1, - "On previous match, should deselect items and select the previous item" - ); - assert_eq!( - all_selections, initial_selections, - "Previous match should be the same as the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should be updated to the previous one" - ); - all_selections - }); - - search_bar - .update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.search("abas_nonexistent_match", None, cx) + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx)) }) + .unwrap() .await .unwrap(); - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - window.read_with(cx, |cx| { - assert!( - !editor.is_focused(cx), - "Should not switch focus to editor if SelectAllMatches does not find any matches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections, last_match_selections, - "Should not select anything new if there are no matches" - ); - assert!( - search_bar.active_match_index.is_none(), - "For no matches, there should be no active match index" - ); - }); + let initial_selections = window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.activate_current_match(cx); + }); + assert!( + !editor.read(cx).is_focused(cx), + "Initially, the editor should not be focused" + ); + let initial_selections = editor.update(cx, |editor, cx| { + let initial_selections = editor.selections.display_ranges(cx); + assert_eq!( + initial_selections.len(), 1, + "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", + ); + initial_selections + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + assert!( + editor.read(cx).is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should not change after selecting all matches" + ); + }); + + search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx)); + initial_selections + }).unwrap(); + + window + .update(cx, |_, cx| { + assert!( + editor.read(cx).is_focused(cx), + "Should still have editor focused after SelectNextMatch" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On next match, should deselect items and select the next match" + ); + assert_ne!( + all_selections, initial_selections, + "Next match should be different from the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should be updated to the next one" + ); + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + assert!( + editor.read(cx).is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should not change after selecting all matches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + }); + }) + .unwrap(); + let last_match_selections = window + .update(cx, |_, cx| { + assert!( + editor.read(cx).is_focused(&cx), + "Should still have editor focused after SelectPrevMatch" + ); + + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On previous match, should deselect items and select the previous item" + ); + assert_eq!( + all_selections, initial_selections, + "Previous match should be the same as the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should be updated to the previous one" + ); + all_selections + }) + }) + .unwrap(); + + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.search("abas_nonexistent_match", None, cx) + }) + }) + .unwrap() + .await + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + assert!( + editor.update(cx, |this, cx| !this.is_focused(cx.window_context())), + "Should not switch focus to editor if SelectAllMatches does not find any matches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections, last_match_selections, + "Should not select anything new if there are no matches" + ); + assert!( + search_bar.active_match_index.is_none(), + "For no matches, there should be no active match index" + ); + }); + }) + .unwrap(); } #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { - crate::project_search::tests::init_test(cx); - + //crate::project_search::tests::init_test(cx); + init_globals(cx); let buffer_text = r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -1607,12 +1611,12 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); - let window = cx.add_window(|_| EmptyView); + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); + let (_, cx) = cx.add_window_view(|_| EmptyView {}); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); - let search_bar = window.add_view(cx, |cx| { + let search_bar = cx.new_view(|cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1635,7 +1639,7 @@ mod tests { .await .unwrap(); // Ensure that the latest search is active. - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1644,14 +1648,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), ""); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), ""); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1660,7 +1664,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1669,7 +1673,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "b"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1678,14 +1682,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "a"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "a"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1694,7 +1698,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "b"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1703,7 +1707,7 @@ mod tests { .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) .await .unwrap(); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "ba"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); @@ -1712,42 +1716,43 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "b"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "ba"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), ""); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); } + #[gpui::test] async fn test_replace_simple(cx: &mut TestAppContext) { - let (editor, search_bar) = init_test(cx); + let (editor, search_bar, cx) = init_test(cx); search_bar .update(cx, |search_bar, cx| { @@ -1764,7 +1769,7 @@ mod tests { search_bar.replace_all(&ReplaceAll, cx) }); assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex or regexp;[1] also referred to as rational expr$1[2][3]) is a sequence of characters that specifies a search @@ -1790,7 +1795,7 @@ mod tests { }); // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex banana regexp;[1] also referred to as rational expr$1[2][3]) is a sequence of characters that specifies a search @@ -1814,7 +1819,7 @@ mod tests { search_bar.replace_all(&ReplaceAll, cx) }); assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex banana regexp;1number also referred to as rational expr$12number3number) is a sequence of characters that specifies a search @@ -1840,7 +1845,7 @@ mod tests { // The only word affected by this edit should be `algorithms`, even though there's a bunch // of words in this text that would match this regex if not for WHOLE_WORD. assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex banana regexp;1number also referred to as rational expr$12number3number) is a sequence of characters that specifies a search diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 8afc2bd3f4..3fd53cee49 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -1,6 +1,7 @@ -use gpui::Action; +use gpui::{Action, SharedString}; use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; + // TODO: Update the default search mode to get from config #[derive(Copy, Clone, Debug, Default, PartialEq)] pub enum SearchMode { @@ -10,12 +11,6 @@ pub enum SearchMode { Regex, } -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum Side { - Left, - Right, -} - impl SearchMode { pub(crate) fn label(&self) -> &'static str { match self { @@ -24,28 +19,14 @@ impl SearchMode { SearchMode::Regex => "Regex", } } - - pub(crate) fn region_id(&self) -> usize { - match self { - SearchMode::Text => 3, - SearchMode::Semantic => 4, - SearchMode::Regex => 5, - } + pub(crate) fn tooltip(&self) -> SharedString { + format!("Activate {} Mode", self.label()).into() } - - pub(crate) fn tooltip_text(&self) -> &'static str { + pub(crate) fn action(&self) -> Box { match self { - SearchMode::Text => "Activate Text Search", - SearchMode::Semantic => "Activate Semantic Search", - SearchMode::Regex => "Activate Regex Search", - } - } - - pub(crate) fn activate_action(&self) -> Box { - match self { - SearchMode::Text => Box::new(ActivateTextMode), - SearchMode::Semantic => Box::new(ActivateSemanticMode), - SearchMode::Regex => Box::new(ActivateRegexMode), + SearchMode::Text => ActivateTextMode.boxed_clone(), + SearchMode::Semantic => ActivateSemanticMode.boxed_clone(), + SearchMode::Regex => ActivateRegexMode.boxed_clone(), } } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index af38346c58..9a91d619a4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,24 +1,22 @@ use crate::{ - history::SearchHistory, - mode::{SearchMode, Side}, - search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, - ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery, - PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, - ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord, + history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode, + ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, + ToggleReplace, ToggleWholeWord, }; -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use collections::HashMap; use editor::{ - items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, - SelectAll, MAX_TAB_TITLE_LEN, + items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent, + MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, }; -use futures::StreamExt; +use editor::{EditorElement, EditorStyle}; use gpui::{ - actions, - elements::*, - platform::{MouseButton, PromptLevel}, - Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, - Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + actions, div, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, EventEmitter, + FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement, IntoElement, + KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled, + Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView, + WhiteSpace, WindowContext, }; use menu::Confirm; use project::{ @@ -26,96 +24,57 @@ use project::{ Entry, Project, }; use semantic_index::{SemanticIndex, SemanticIndexStatus}; -use smallvec::SmallVec; + +use settings::Settings; +use smol::stream::StreamExt; use std::{ any::{Any, TypeId}, - borrow::Cow, collections::HashSet, mem, ops::{Not, Range}, path::PathBuf, - sync::Arc, time::{Duration, Instant}, }; +use theme::ThemeSettings; + +use ui::{ + h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon, + LabelSize, Selectable, Tooltip, +}; use util::{paths::PathMatcher, ResultExt as _}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, searchable::{Direction, SearchableItem, SearchableItemHandle}, - ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, + ItemNavHistory, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, + WorkspaceId, }; actions!( project_search, - [SearchInNew, ToggleFocus, NextField, ToggleFilters,] + [SearchInNew, ToggleFocus, NextField, ToggleFilters] ); #[derive(Default)] -struct ActiveSearches(HashMap, WeakViewHandle>); +struct ActiveSearches(HashMap, WeakView>); #[derive(Default)] -struct ActiveSettings(HashMap, ProjectSearchSettings>); +struct ActiveSettings(HashMap, ProjectSearchSettings>); pub fn init(cx: &mut AppContext) { + // todo!() po cx.set_global(ActiveSearches::default()); cx.set_global(ActiveSettings::default()); - cx.add_action(ProjectSearchView::deploy); - cx.add_action(ProjectSearchView::move_focus_to_results); - cx.add_action(ProjectSearchBar::confirm); - cx.add_action(ProjectSearchBar::search_in_new); - cx.add_action(ProjectSearchBar::select_next_match); - cx.add_action(ProjectSearchBar::select_prev_match); - cx.add_action(ProjectSearchBar::replace_next); - cx.add_action(ProjectSearchBar::replace_all); - cx.add_action(ProjectSearchBar::cycle_mode); - cx.add_action(ProjectSearchBar::next_history_query); - cx.add_action(ProjectSearchBar::previous_history_query); - cx.add_action(ProjectSearchBar::activate_regex_mode); - cx.add_action(ProjectSearchBar::toggle_replace); - cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane); - cx.add_action(ProjectSearchBar::activate_text_mode); - - // This action should only be registered if the semantic index is enabled - // We are registering it all the time, as I dont want to introduce a dependency - // for Semantic Index Settings globally whenever search is tested. - cx.add_action(ProjectSearchBar::activate_semantic_mode); - - cx.capture_action(ProjectSearchBar::tab); - cx.capture_action(ProjectSearchBar::tab_previous); - cx.capture_action(ProjectSearchView::replace_all); - cx.capture_action(ProjectSearchView::replace_next); - add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); - add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); - add_toggle_option_action::(SearchOptions::INCLUDE_IGNORED, cx); - add_toggle_filters_action::(cx); -} - -fn add_toggle_filters_action(cx: &mut AppContext) { - cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) { - return; - } - } - cx.propagate_action(); - }); -} - -fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { - cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(option, cx) - }) { - return; - } - } - cx.propagate_action(); - }); + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace + .register_action(ProjectSearchView::deploy) + .register_action(ProjectSearchBar::search_in_new); + }) + .detach(); } struct ProjectSearch { - project: ModelHandle, - excerpts: ModelHandle, + project: Model, + excerpts: Model, pending_search: Option>>, match_ranges: Vec>, active_query: Option, @@ -132,10 +91,11 @@ enum InputPanel { } pub struct ProjectSearchView { - model: ModelHandle, - query_editor: ViewHandle, - replacement_editor: ViewHandle, - results_editor: ViewHandle, + focus_handle: FocusHandle, + model: Model, + query_editor: View, + replacement_editor: View, + results_editor: View, semantic_state: Option, semantic_permissioned: Option, search_options: SearchOptions, @@ -143,11 +103,12 @@ pub struct ProjectSearchView { active_match_index: Option, search_id: usize, query_editor_was_focused: bool, - included_files_editor: ViewHandle, - excluded_files_editor: ViewHandle, + included_files_editor: View, + excluded_files_editor: View, filters_enabled: bool, replace_enabled: bool, current_mode: SearchMode, + _subscriptions: Vec, } struct SemanticState { @@ -164,20 +125,16 @@ struct ProjectSearchSettings { } pub struct ProjectSearchBar { - active_project_search: Option>, + active_project_search: Option>, subscription: Option, } -impl Entity for ProjectSearch { - type Event = (); -} - impl ProjectSearch { - fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + fn new(project: Model, cx: &mut ModelContext) -> Self { let replica_id = project.read(cx).replica_id(); Self { project, - excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + excerpts: cx.new_model(|_| MultiBuffer::new(replica_id)), pending_search: Default::default(), match_ranges: Default::default(), active_query: None, @@ -187,12 +144,12 @@ impl ProjectSearch { } } - fn clone(&self, cx: &mut ModelContext) -> ModelHandle { - cx.add_model(|cx| Self { + fn clone(&self, cx: &mut ModelContext) -> Model { + cx.new_model(|cx| Self { project: self.project.clone(), excerpts: self .excerpts - .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + .update(cx, |excerpts, cx| cx.new_model(|cx| excerpts.clone(cx))), pending_search: Default::default(), match_ranges: self.match_ranges.clone(), active_query: self.active_query.clone(), @@ -210,33 +167,38 @@ impl ProjectSearch { self.search_history.add(query.as_str().to_string()); self.active_query = Some(query); self.match_ranges.clear(); - self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + self.pending_search = Some(cx.spawn(|this, mut cx| async move { let mut matches = search; - let this = this.upgrade(&cx)?; + let this = this.upgrade()?; this.update(&mut cx, |this, cx| { this.match_ranges.clear(); this.excerpts.update(cx, |this, cx| this.clear(cx)); this.no_results = Some(true); - }); + }) + .ok()?; while let Some((buffer, anchors)) = matches.next().await { - let mut ranges = this.update(&mut cx, |this, cx| { - this.no_results = Some(false); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + let mut ranges = this + .update(&mut cx, |this, cx| { + this.no_results = Some(false); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) }) - }); + .ok()?; while let Some(range) = ranges.next().await { - this.update(&mut cx, |this, _| this.match_ranges.push(range)); + this.update(&mut cx, |this, _| this.match_ranges.push(range)) + .ok()?; } - this.update(&mut cx, |_, cx| cx.notify()); + this.update(&mut cx, |_, cx| cx.notify()).ok()?; } this.update(&mut cx, |this, cx| { this.pending_search.take(); cx.notify(); - }); + }) + .ok()?; None })); @@ -271,14 +233,17 @@ impl ProjectSearch { this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); }); - }); + }) + .ok()?; for (buffer, ranges) in matches { - let mut match_ranges = this.update(&mut cx, |this, cx| { - this.no_results = Some(false); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + let mut match_ranges = this + .update(&mut cx, |this, cx| { + this.no_results = Some(false); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) }) - }); + .ok()?; while let Some(match_range) = match_ranges.next().await { this.update(&mut cx, |this, cx| { this.match_ranges.push(match_range); @@ -286,14 +251,16 @@ impl ProjectSearch { this.match_ranges.push(match_range); } cx.notify(); - }); + }) + .ok()?; } } this.update(&mut cx, |this, cx| { this.pending_search.take(); cx.notify(); - }); + }) + .ok()?; None })); @@ -305,221 +272,120 @@ impl ProjectSearch { pub enum ViewEvent { UpdateTab, Activate, - EditorEvent(editor::Event), + EditorEvent(editor::EditorEvent), Dismiss, } -impl Entity for ProjectSearchView { - type Event = ViewEvent; -} +impl EventEmitter for ProjectSearchView {} -impl View for ProjectSearchView { - fn ui_name() -> &'static str { - "ProjectSearchView" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let model = &self.model.read(cx); - if model.match_ranges.is_empty() { - enum Status {} - - let theme = theme::current(cx).clone(); - - // If Search is Active -> Major: Searching..., Minor: None - // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...} - // If Regex -> Major: "Search using Regex", Minor: {ex...} - // If Text -> Major: "Text search all files and folders", Minor: {...} - - let current_mode = self.current_mode; - let mut major_text = if model.pending_search.is_some() { - Cow::Borrowed("Searching...") - } else if model.no_results.is_some_and(|v| v) { - Cow::Borrowed("No Results") +impl Render for ProjectSearchView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + if self.has_matches() { + div() + .flex_1() + .size_full() + .track_focus(&self.focus_handle) + .child(self.results_editor.clone()) + .into_any() + } else { + let model = self.model.read(cx); + let has_no_results = model.no_results.unwrap_or(false); + let is_search_underway = model.pending_search.is_some(); + let mut major_text = if is_search_underway { + Label::new("Searching...") + } else if has_no_results { + Label::new("No results") } else { - match current_mode { - SearchMode::Text => Cow::Borrowed("Text search all files and folders"), - SearchMode::Semantic => { - Cow::Borrowed("Search all code objects using Natural Language") - } - SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), - } + Label::new(format!("{} search all files", self.current_mode.label())) }; let mut show_minor_text = true; let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { let status = semantic.index_status; - match status { - SemanticIndexStatus::NotAuthenticated => { - major_text = Cow::Borrowed("Not Authenticated"); - show_minor_text = false; - Some(vec![ - "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables." - .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()]) - } - SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]), - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { - if remaining_files == 0 { - Some(vec![format!("Indexing...")]) - } else { - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = - rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) { - Some(vec![format!( - "Remaining files to index (rate limit resets in {}s): {}", - remaining_seconds.as_secs(), - remaining_files - )]) - } else { - Some(vec![format!("Remaining files to index: {}", remaining_files)]) + match status { + SemanticIndexStatus::NotAuthenticated => { + major_text = Label::new("Not Authenticated"); + show_minor_text = false; + Some( + "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()) + } + SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + if remaining_files == 0 { + Some("Indexing...".to_string()) + } else { + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = + rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) { + Some(format!( + "Remaining files to index (rate limit resets in {}s): {}", + remaining_seconds.as_secs(), + remaining_files + )) + } else { + Some(format!("Remaining files to index: {}", remaining_files)) + } + } else { + Some(format!("Remaining files to index: {}", remaining_files)) + } + } + } + SemanticIndexStatus::NotIndexed => None, } - } else { - Some(vec![format!("Remaining files to index: {}", remaining_files)]) - } - } - } - SemanticIndexStatus::NotIndexed => None, - } }); + let major_text = div().justify_center().max_w_96().child(major_text); - let minor_text = if let Some(no_results) = model.no_results { + let minor_text: Option = if let Some(no_results) = model.no_results { if model.pending_search.is_none() && no_results { - vec!["No results found in this project for the provided query".to_owned()] + Some("No results found in this project for the provided query".into()) } else { - vec![] + None } } else { - match current_mode { - SearchMode::Semantic => { - let mut minor_text: Vec = Vec::new(); - minor_text.push("".into()); - if let Some(semantic_status) = semantic_status { - minor_text.extend(semantic_status); - } - if show_minor_text { - minor_text - .push("Simply explain the code you are looking to find.".into()); - minor_text.push( - "ex. 'prompt user for permissions to index their project'".into(), - ); - } - minor_text - } - _ => vec![ - "".to_owned(), - "Include/exclude specific paths with the filter option.".to_owned(), - "Matching exact word and/or casing is available too.".to_owned(), - ], + if let Some(mut semantic_status) = semantic_status { + semantic_status.extend(self.landing_text_minor().chars()); + Some(semantic_status.into()) + } else { + Some(self.landing_text_minor()) } }; - - let previous_query_keystrokes = - cx.binding_for_action(&PreviousHistoryQuery {}) - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let next_query_keystrokes = - cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { - (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { - format!( - "Search ({}/{} for previous/next query)", - previous_query_keystrokes.join(" "), - next_query_keystrokes.join(" ") - ) - } - (None, Some(next_query_keystrokes)) => { - format!( - "Search ({} for next query)", - next_query_keystrokes.join(" ") - ) - } - (Some(previous_query_keystrokes), None) => { - format!( - "Search ({} for previous query)", - previous_query_keystrokes.join(" ") - ) - } - (None, None) => String::new(), - }; - self.query_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(new_placeholder_text, cx); + let minor_text = minor_text.map(|text| { + div() + .items_center() + .max_w_96() + .child(Label::new(text).size(LabelSize::Small)) }); - - MouseEventHandler::new::(0, cx, |_, _| { - Flex::column() - .with_child(Flex::column().contained().flex(1., true)) - .with_child( - Flex::column() - .align_children_center() - .with_child(Label::new( - major_text, - theme.search.major_results_status.clone(), - )) - .with_children( - minor_text.into_iter().map(|x| { - Label::new(x, theme.search.minor_results_status.clone()) - }), - ) - .aligned() - .top() - .contained() - .flex(7., true), - ) - .contained() - .with_background_color(theme.editor.background) - }) - .on_down(MouseButton::Left, |_, _, cx| { - cx.focus_parent(); - }) - .into_any_named("project search view") - } else { - ChildView::new(&self.results_editor, cx) - .flex(1., true) - .into_any_named("project search view") - } - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - let handle = cx.weak_handle(); - cx.update_global(|state: &mut ActiveSearches, cx| { - state - .0 - .insert(self.model.read(cx).project.downgrade(), handle) - }); - - cx.update_global(|state: &mut ActiveSettings, cx| { - state.0.insert( - self.model.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - - if cx.is_self_focused() { - if self.query_editor_was_focused { - cx.focus(&self.query_editor); - } else { - cx.focus(&self.results_editor); - } + v_stack() + .flex_1() + .size_full() + .justify_center() + .track_focus(&self.focus_handle) + .child( + h_stack() + .size_full() + .justify_center() + .child(h_stack().flex_1()) + .child(v_stack().child(major_text).children(minor_text)) + .child(h_stack().flex_1()), + ) + .into_any() } } } +impl FocusableView for ProjectSearchView { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + impl Item for ProjectSearchView { - fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + type Event = ViewEvent; + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { let query_text = self.query_editor.read(cx).text(cx); query_text @@ -528,20 +394,17 @@ impl Item for ProjectSearchView { .then(|| query_text.into()) .or_else(|| Some("Project Search".into())) } - fn should_close_item_on_event(event: &Self::Event) -> bool { - event == &Self::Event::Dismiss - } fn act_as_type<'a>( &'a self, type_id: TypeId, - self_handle: &'a ViewHandle, + self_handle: &'a View, _: &'a AppContext, - ) -> Option<&'a AnyViewHandle> { + ) -> Option { if type_id == TypeId::of::() { - Some(self_handle) + Some(self_handle.clone().into()) } else if type_id == TypeId::of::() { - Some(&self.results_editor) + Some(self.results_editor.clone().into()) } else { None } @@ -552,45 +415,40 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.deactivated(cx)); } - fn tab_content( - &self, - _detail: Option, - tab_theme: &theme::Tab, - cx: &AppContext, - ) -> AnyElement { - Flex::row() - .with_child( - Svg::new("icons/magnifying_glass.svg") - .with_color(tab_theme.label.text.color) - .constrained() - .with_width(tab_theme.type_icon_width) - .aligned() - .contained() - .with_margin_right(tab_theme.spacing), - ) - .with_child({ - let tab_name: Option> = self - .model - .read(cx) - .search_history - .current() - .as_ref() - .map(|query| { - let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); - query_text.into() - }); - Label::new( - tab_name - .filter(|name| !name.is_empty()) - .unwrap_or("Project search".into()), - tab_theme.label.clone(), - ) - .aligned() - }) + fn tab_content(&self, _: Option, selected: bool, cx: &WindowContext<'_>) -> AnyElement { + let last_query: Option = self + .model + .read(cx) + .search_history + .current() + .as_ref() + .map(|query| { + let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); + query_text.into() + }); + let tab_name = last_query + .filter(|query| !query.is_empty()) + .unwrap_or_else(|| "Project search".into()); + h_stack() + .gap_2() + .child(IconElement::new(Icon::MagnifyingGlass).color(if selected { + Color::Default + } else { + Color::Muted + })) + .child(Label::new(tab_name).color(if selected { + Color::Default + } else { + Color::Muted + })) .into_any() } - fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) { + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(EntityId, &dyn project::Item), + ) { self.results_editor.for_each_project_item(cx, f) } @@ -612,7 +470,7 @@ impl Item for ProjectSearchView { fn save( &mut self, - project: ModelHandle, + project: Model, cx: &mut ViewContext, ) -> Task> { self.results_editor @@ -621,7 +479,7 @@ impl Item for ProjectSearchView { fn save_as( &mut self, - _: ModelHandle, + _: Model, _: PathBuf, _: &mut ViewContext, ) -> Task> { @@ -630,19 +488,23 @@ impl Item for ProjectSearchView { fn reload( &mut self, - project: ModelHandle, + project: Model, cx: &mut ViewContext, ) -> Task> { self.results_editor .update(cx, |editor, cx| editor.reload(project, cx)) } - fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> where Self: Sized, { let model = self.model.update(cx, |model, cx| model.clone(cx)); - Some(Self::new(model, cx, None)) + Some(cx.new_view(|cx| Self::new(model, cx, None))) } fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { @@ -661,14 +523,17 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.navigate(data, cx)) } - fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { match event { ViewEvent::UpdateTab => { - smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab] + f(ItemEvent::UpdateBreadcrumbs); + f(ItemEvent::UpdateTab); } - ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event), - ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem], - _ => SmallVec::new(), + ViewEvent::EditorEvent(editor_event) => { + Editor::to_item_events(editor_event, f); + } + ViewEvent::Dismiss => f(ItemEvent::CloseItem), + _ => {} } } @@ -689,12 +554,12 @@ impl Item for ProjectSearchView { } fn deserialize( - _project: ModelHandle, - _workspace: WeakViewHandle, + _project: Model, + _workspace: WeakView, _workspace_id: workspace::WorkspaceId, _item_id: workspace::ItemId, _cx: &mut ViewContext, - ) -> Task>> { + ) -> Task>> { unimplemented!() } } @@ -751,7 +616,7 @@ impl ProjectSearchView { fn semantic_index_changed( &mut self, - semantic_index: ModelHandle, + semantic_index: Model, cx: &mut ViewContext, ) { let project = self.model.read(cx).project.clone(); @@ -767,7 +632,7 @@ impl ProjectSearchView { semantic_state.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { loop { - cx.background().timer(Duration::from_secs(1)).await; + cx.background_executor().timer(Duration::from_secs(1)).await; this.update(&mut cx, |_, cx| cx.notify()).log_err(); } })); @@ -809,7 +674,7 @@ impl ProjectSearchView { let has_permission = has_permission.await?; if !has_permission { - let mut answer = this.update(&mut cx, |this, cx| { + let answer = this.update(&mut cx, |this, cx| { let project = this.model.read(cx).project.clone(); let project_name = project .read(cx) @@ -829,7 +694,7 @@ impl ProjectSearchView { ) })?; - if answer.next().await == Some(0) { + if answer.await? == 0 { this.update(&mut cx, |this, _| { this.semantic_permissioned = Some(true); })?; @@ -907,7 +772,7 @@ impl ProjectSearchView { } fn new( - model: ModelHandle, + model: Model, cx: &mut ViewContext, settings: Option, ) -> Self { @@ -915,6 +780,7 @@ impl ProjectSearchView { let excerpts; let mut replacement_text = None; let mut query_text = String::new(); + let mut subscriptions = Vec::new(); // Read in settings if available let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings { @@ -937,87 +803,85 @@ impl ProjectSearchView { options = SearchOptions::from_query(active_query); } } - cx.observe(&model, |this, _, cx| this.model_changed(cx)) - .detach(); + subscriptions.push(cx.observe(&model, |this, _, cx| this.model_changed(cx))); - let query_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.search.editor.input.clone())), - cx, - ); + let query_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Text search all files", cx); editor.set_text(query_text, cx); editor }); // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes - cx.subscribe(&query_editor, |_, _, event, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }) - .detach(); - let replacement_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.search.editor.input.clone())), - cx, - ); + subscriptions.push( + cx.subscribe(&query_editor, |_, _, event: &EditorEvent, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }), + ); + let replacement_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Replace in project..", cx); if let Some(text) = replacement_text { editor.set_text(text, cx); } editor }); - let results_editor = cx.add_view(|cx| { + let results_editor = cx.new_view(|cx| { let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); editor.set_searchable(false); editor }); - cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) - .detach(); + subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))); - cx.subscribe(&results_editor, |this, _, event, cx| { - if matches!(event, editor::Event::SelectionsChanged { .. }) { - this.update_match_index(cx); - } - // Reraise editor events for workspace item activation purposes - cx.emit(ViewEvent::EditorEvent(event.clone())); - }) - .detach(); + subscriptions.push( + cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| { + if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) { + this.update_match_index(cx); + } + // Reraise editor events for workspace item activation purposes + cx.emit(ViewEvent::EditorEvent(event.clone())); + }), + ); - let included_files_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| { - theme.search.include_exclude_editor.input.clone() - })), - cx, - ); + let included_files_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Include: crates/**/*.toml", cx); editor }); // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes - cx.subscribe(&included_files_editor, |_, _, event, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }) - .detach(); + subscriptions.push( + cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }), + ); - let excluded_files_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| { - theme.search.include_exclude_editor.input.clone() - })), - cx, - ); + let excluded_files_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx); editor }); // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes - cx.subscribe(&excluded_files_editor, |_, _, event, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }) - .detach(); + subscriptions.push( + cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }), + ); + + let focus_handle = cx.focus_handle(); + subscriptions.push(cx.on_focus_in(&focus_handle, |this, cx| { + if this.focus_handle.is_focused(cx) { + if this.has_matches() { + this.results_editor.focus_handle(cx).focus(cx); + } else { + this.query_editor.focus_handle(cx).focus(cx); + } + } + })); // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { + focus_handle, replacement_editor, search_id: model.read(cx).search_id, model, @@ -1034,6 +898,7 @@ impl ProjectSearchView { filters_enabled, current_mode, replace_enabled: false, + _subscriptions: subscriptions, }; this.model_changed(cx); this @@ -1051,6 +916,7 @@ impl ProjectSearchView { }) .unwrap_or(Task::ready(Ok(false))) } + pub fn new_search_in_directory( workspace: &mut Workspace, dir_entry: &Entry, @@ -1063,8 +929,8 @@ impl ProjectSearchView { return; }; - let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None)); + let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, None)); workspace.add_item(Box::new(search.clone()), cx); search.update(cx, |search, cx| { search @@ -1082,8 +948,8 @@ impl ProjectSearchView { cx: &mut ViewContext, ) { // Clean up entries for dropped projects - cx.update_global(|state: &mut ActiveSearches, cx| { - state.0.retain(|project, _| project.is_upgradable(cx)) + cx.update_global(|state: &mut ActiveSearches, _cx| { + state.0.retain(|project, _| project.is_upgradable()) }); let query = workspace.active_item(cx).and_then(|item| { @@ -1107,8 +973,8 @@ impl ProjectSearchView { None }; - let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings)); + let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings)); workspace.add_item(Box::new(search.clone()), cx); @@ -1141,33 +1007,46 @@ impl ProjectSearchView { } fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { + // Do not bail early in this function, as we want to fill out `self.panels_with_errors`. let text = self.query_editor.read(cx).text(cx); let included_files = match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) { Ok(included_files) => { - self.panels_with_errors.remove(&InputPanel::Include); + let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Include); + if should_unmark_error { + cx.notify(); + } included_files } Err(_e) => { - self.panels_with_errors.insert(InputPanel::Include); - cx.notify(); - return None; + let should_mark_error = self.panels_with_errors.insert(InputPanel::Include); + if should_mark_error { + cx.notify(); + } + vec![] } }; let excluded_files = match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) { Ok(excluded_files) => { - self.panels_with_errors.remove(&InputPanel::Exclude); + let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Exclude); + if should_unmark_error { + cx.notify(); + } + excluded_files } Err(_e) => { - self.panels_with_errors.insert(InputPanel::Exclude); - cx.notify(); - return None; + let should_mark_error = self.panels_with_errors.insert(InputPanel::Exclude); + if should_mark_error { + cx.notify(); + } + vec![] } }; + let current_mode = self.current_mode; - match current_mode { + let query = match current_mode { SearchMode::Regex => { match SearchQuery::regex( text, @@ -1178,12 +1057,20 @@ impl ProjectSearchView { excluded_files, ) { Ok(query) => { - self.panels_with_errors.remove(&InputPanel::Query); + let should_unmark_error = + self.panels_with_errors.remove(&InputPanel::Query); + if should_unmark_error { + cx.notify(); + } + Some(query) } Err(_e) => { - self.panels_with_errors.insert(InputPanel::Query); - cx.notify(); + let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); + if should_mark_error { + cx.notify(); + } + None } } @@ -1197,16 +1084,27 @@ impl ProjectSearchView { excluded_files, ) { Ok(query) => { - self.panels_with_errors.remove(&InputPanel::Query); + let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); + if should_unmark_error { + cx.notify(); + } + Some(query) } Err(_e) => { - self.panels_with_errors.insert(InputPanel::Query); - cx.notify(); + let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); + if should_mark_error { + cx.notify(); + } + None } }, + }; + if !self.panels_with_errors.is_empty() { + return None; } + query } fn parse_path_matches(text: &str) -> anyhow::Result> { @@ -1243,7 +1141,8 @@ impl ProjectSearchView { query_editor.select_all(&SelectAll, cx); }); self.query_editor_was_focused = true; - cx.focus(&self.query_editor); + let editor_handle = self.query_editor.focus_handle(cx); + cx.focus(&editor_handle); } fn set_query(&mut self, query: &str, cx: &mut ViewContext) { @@ -1257,7 +1156,8 @@ impl ProjectSearchView { query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor])); }); self.query_editor_was_focused = false; - cx.focus(&self.results_editor); + let results_handle = self.results_editor.focus_handle(cx); + cx.focus(&results_handle); } fn model_changed(&mut self, cx: &mut ViewContext) { @@ -1281,11 +1181,11 @@ impl ProjectSearchView { } editor.highlight_background::( match_ranges, - |theme| theme.search.match_background, + |theme| theme.search_match_background, cx, ); }); - if is_new_search && self.query_editor.is_focused(cx) { + if is_new_search && self.query_editor.focus_handle(cx).is_focused(cx) { self.focus_results_editor(cx); } } @@ -1311,21 +1211,26 @@ impl ProjectSearchView { self.active_match_index.is_some() } - fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |search_view, cx| { - if !search_view.results_editor.is_focused(cx) - && !search_view.model.read(cx).match_ranges.is_empty() - { - return search_view.focus_results_editor(cx); - } - }); + fn landing_text_minor(&self) -> SharedString { + match self.current_mode { + SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(), + SearchMode::Semantic => "\nSimply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into() + } + } + fn border_color_for(&self, panel: InputPanel, cx: &WindowContext) -> Hsla { + if self.panels_with_errors.contains(&panel) { + Color::Error.color(cx) + } else { + cx.theme().colors().border + } + } + fn move_focus_to_results(&mut self, cx: &mut ViewContext) { + if !self.results_editor.focus_handle(cx).is_focused(cx) + && !self.model.read(cx).match_ranges.is_empty() + { + cx.stop_propagation(); + return self.focus_results_editor(cx); } - - cx.propagate_action(); } } @@ -1342,32 +1247,32 @@ impl ProjectSearchBar { subscription: Default::default(), } } - fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |this, cx| { + + fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext) { + if let Some(view) = self.active_project_search.as_ref() { + view.update(cx, |this, cx| { let new_mode = crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); this.activate_search_mode(new_mode, cx); - cx.focus(&this.query_editor); - }) + let editor_handle = this.query_editor.focus_handle(cx); + cx.focus(&editor_handle); + }); } } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - let mut should_propagate = true; if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - if !search_view.replacement_editor.is_focused(cx) { - should_propagate = false; + if !search_view + .replacement_editor + .focus_handle(cx) + .is_focused(cx) + { + cx.stop_propagation(); search_view.search(cx); } }); } - if should_propagate { - cx.propagate_action(); - } } fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { @@ -1388,61 +1293,19 @@ impl ProjectSearchBar { new_query }); if let Some(new_query) = new_query { - let model = cx.add_model(|cx| { + let model = cx.new_model(|cx| { let mut model = ProjectSearch::new(workspace.project().clone(), cx); model.search(new_query, cx); model }); workspace.add_item( - Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))), + Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))), cx, ); } } } - fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx)); - } else { - cx.propagate_action(); - } - } - - fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx)); - } else { - cx.propagate_action(); - } - } - fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx)); - } else { - cx.propagate_action(); - } - } - fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx)); - } else { - cx.propagate_action(); - } - } - fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { self.cycle_field(Direction::Next, cx); } @@ -1456,7 +1319,6 @@ impl ProjectSearchBar { Some(active_project_search) => active_project_search, None => { - cx.propagate_action(); return; } }; @@ -1475,12 +1337,11 @@ impl ProjectSearchBar { let current_index = match views .iter() .enumerate() - .find(|(_, view)| view.is_focused(cx)) + .find(|(_, view)| view.focus_handle(cx).is_focused(cx)) { Some((index, _)) => index, None => { - cx.propagate_action(); return; } }; @@ -1490,7 +1351,9 @@ impl ProjectSearchBar { Direction::Prev if current_index == 0 => views.len() - 1, Direction::Prev => (current_index - 1) % views.len(), }; - cx.focus(views[new_index]); + let next_focus_handle = views[new_index].focus_handle(cx); + cx.focus(&next_focus_handle); + cx.stop_propagation(); }); } @@ -1507,80 +1370,21 @@ impl ProjectSearchBar { false } } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { if let Some(search) = &self.active_project_search { search.update(cx, |this, cx| { this.replace_enabled = !this.replace_enabled; - if !this.replace_enabled { - cx.focus(&this.query_editor); - } + let editor_to_focus = if !this.replace_enabled { + this.query_editor.focus_handle(cx) + } else { + this.replacement_editor.focus_handle(cx) + }; + cx.focus(&editor_to_focus); cx.notify(); }); } } - fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { - let mut should_propagate = true; - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |this, cx| { - should_propagate = false; - this.replace_enabled = !this.replace_enabled; - if !this.replace_enabled { - cx.focus(&this.query_editor); - } - cx.notify(); - }); - } - if should_propagate { - cx.propagate_action(); - } - } - fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Text, cx) - }); - } else { - cx.propagate_action(); - } - } - - fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Regex, cx) - }); - } else { - cx.propagate_action(); - } - } - - fn activate_semantic_mode( - pane: &mut Pane, - _: &ActivateSemanticMode, - cx: &mut ViewContext, - ) { - if SemanticIndex::enabled(cx) { - if let Some(search_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |view, cx| { - view.activate_search_mode(SearchMode::Semantic, cx) - }); - } else { - cx.propagate_action(); - } - } - } fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { @@ -1592,7 +1396,7 @@ impl ProjectSearchBar { search_view .excluded_files_editor .update(cx, |_, cx| cx.notify()); - cx.refresh_windows(); + cx.refresh(); cx.notify(); }); cx.notify(); @@ -1602,6 +1406,15 @@ impl ProjectSearchBar { } } + fn move_focus_to_results(&self, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.move_focus_to_results(cx); + }); + cx.notify(); + } + } + fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { // Update Current Mode if let Some(search_view) = self.active_project_search.as_ref() { @@ -1660,317 +1473,417 @@ impl ProjectSearchBar { }); } } -} -impl Entity for ProjectSearchBar { - type Event = (); -} - -impl View for ProjectSearchBar { - fn ui_name() -> &'static str { - "ProjectSearchBar" + fn new_placeholder_text(&self, cx: &mut ViewContext) -> Option { + let previous_query_keystrokes = cx + .bindings_for_action(&PreviousHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = cx + .bindings_for_action(&NextHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => Some(format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + )), + (None, Some(next_query_keystrokes)) => Some(format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + )), + (Some(previous_query_keystrokes), None) => Some(format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + )), + (None, None) => None, + }; + new_placeholder_text } - fn update_keymap_context( - &self, - keymap: &mut gpui::keymap_matcher::KeymapContext, - cx: &AppContext, - ) { - Self::reset_to_default_keymap_context(keymap); - let in_replace = self - .active_project_search - .as_ref() - .map(|search| { - search - .read(cx) - .replacement_editor - .read_with(cx, |_, cx| cx.is_self_focused()) - }) - .flatten() - .unwrap_or(false); - if in_replace { - keymap.add_identifier("in_replace"); - } - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(_search) = self.active_project_search.as_ref() { - let search = _search.read(cx); - let theme = theme::current(cx).clone(); - let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { - theme.search.invalid_editor + fn render_text_input(&self, editor: &View, cx: &ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if editor.read(cx).read_only() { + cx.theme().colors().text_disabled } else { - theme.search.editor.input.container - }; + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; - let search = _search.read(cx); - let filter_button = render_option_button_icon( - search.filters_enabled, - "icons/filter.svg", - 0, - "Toggle filters", - Box::new(ToggleFilters), - move |_, this, cx| { - this.toggle_filters(cx); - }, - cx, + EditorElement::new( + &editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } +} + +impl Render for ProjectSearchBar { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Some(search) = self.active_project_search.clone() else { + return div(); + }; + let mut key_context = KeyContext::default(); + key_context.add("ProjectSearchBar"); + if let Some(placeholder_text) = self.new_placeholder_text(cx) { + search.update(cx, |search, cx| { + search.query_editor.update(cx, |this, cx| { + this.set_placeholder_text(placeholder_text, cx) + }) + }); + } + let search = search.read(cx); + let semantic_is_available = SemanticIndex::enabled(cx); + + let query_column = v_stack().child( + h_stack() + .min_w(rems(512. / 16.)) + .px_2() + .py_1() + .gap_2() + .bg(cx.theme().colors().editor_background) + .border_1() + .border_color(search.border_color_for(InputPanel::Query, cx)) + .rounded_lg() + .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) + .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx))) + .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(self.render_text_input(&search.query_editor, cx)) + .child( + h_stack() + .child( + IconButton::new("project-search-filter-button", Icon::Filter) + .tooltip(|cx| { + Tooltip::for_action("Toggle filters", &ToggleFilters, cx) + }) + .on_click(cx.listener(|this, _, cx| { + this.toggle_filters(cx); + })) + .selected( + self.active_project_search + .as_ref() + .map(|search| search.read(cx).filters_enabled) + .unwrap_or_default(), + ), + ) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.child( + IconButton::new( + "project-search-case-sensitive", + Icon::CaseSensitive, + ) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle case sensitive", + &ToggleCaseSensitive, + cx, + ) + }) + .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx)) + .on_click(cx.listener( + |this, _, cx| { + this.toggle_search_option( + SearchOptions::CASE_SENSITIVE, + cx, + ); + }, + )), + ) + .child( + IconButton::new("project-search-whole-word", Icon::WholeWord) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle whole word", + &ToggleWholeWord, + cx, + ) + }) + .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })), + ) + }), + ), + ); + + let mode_column = v_stack().items_start().justify_start().child( + h_stack() + .child( + h_stack() + .child( + Button::new("project-search-text-button", "Text") + .selected(search.current_mode == SearchMode::Text) + .on_click(cx.listener(|this, _, cx| { + this.activate_search_mode(SearchMode::Text, cx) + })) + .tooltip(|cx| { + Tooltip::for_action("Toggle text search", &ActivateTextMode, cx) + }), + ) + .child( + Button::new("project-search-regex-button", "Regex") + .selected(search.current_mode == SearchMode::Regex) + .on_click(cx.listener(|this, _, cx| { + this.activate_search_mode(SearchMode::Regex, cx) + })) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle regular expression search", + &ActivateRegexMode, + cx, + ) + }), + ) + .when(semantic_is_available, |this| { + this.child( + Button::new("project-search-semantic-button", "Semantic") + .selected(search.current_mode == SearchMode::Semantic) + .on_click(cx.listener(|this, _, cx| { + this.activate_search_mode(SearchMode::Semantic, cx) + })) + .tooltip(|cx| { + Tooltip::for_action( + "Toggle semantic search", + &ActivateSemanticMode, + cx, + ) + }), + ) + }), + ) + .child( + IconButton::new("project-search-toggle-replace", Icon::Replace) + .on_click(cx.listener(|this, _, cx| { + this.toggle_replace(&ToggleReplace, cx); + })) + .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)), + ), + ); + let replace_column = if search.replace_enabled { + h_stack() + .flex_1() + .h_full() + .gap_2() + .px_2() + .py_1() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_lg() + .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small)) + .child(self.render_text_input(&search.replacement_editor, cx)) + } else { + // Fill out the space if we don't have a replacement editor. + h_stack().flex_1() + }; + let actions_column = h_stack() + .when(search.replace_enabled, |this| { + this.child( + IconButton::new("project-search-replace-next", Icon::ReplaceNext) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_next(&ReplaceNext, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), + ) + .child( + IconButton::new("project-search-replace-all", Icon::ReplaceAll) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_all(&ReplaceAll, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)), + ) + }) + .when_some(search.active_match_index, |mut this, index| { + let index = index + 1; + let match_quantity = search.model.read(cx).match_ranges.len(); + if match_quantity > 0 { + debug_assert!(match_quantity >= index); + this = this.child(Label::new(format!("{index}/{match_quantity}"))) + } + this + }) + .child( + IconButton::new("project-search-prev-match", Icon::ChevronLeft) + .disabled(search.active_match_index.is_none()) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.select_match(Direction::Prev, cx); + }) + } + })) + .tooltip(|cx| { + Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx) + }), + ) + .child( + IconButton::new("project-search-next-match", Icon::ChevronRight) + .disabled(search.active_match_index.is_none()) + .on_click(cx.listener(|this, _, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.select_match(Direction::Next, cx); + }) + } + })) + .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)), ); - let search = _search.read(cx); - let is_semantic_available = SemanticIndex::enabled(cx); - let is_semantic_disabled = search.semantic_state.is_none(); - let icon_style = theme.search.editor_icon.clone(); - let is_active = search.active_match_index.is_some(); - - let render_option_button_icon = |path, option, cx: &mut ViewContext| { - crate::search_bar::render_option_button_icon( - self.is_option_enabled(option, cx), - path, - option.bits as usize, - format!("Toggle {}", option.label()), - option.to_toggle_action(), - move |_, this, cx| { - this.toggle_search_option(option, cx); - }, - cx, - ) - }; - let case_sensitive = is_semantic_disabled.then(|| { - render_option_button_icon( - "icons/case_insensitive.svg", - SearchOptions::CASE_SENSITIVE, - cx, - ) - }); - - let whole_word = is_semantic_disabled.then(|| { - render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) - }); - - let include_ignored = is_semantic_disabled.then(|| { - render_option_button_icon( - "icons/file_icons/git.svg", - SearchOptions::INCLUDE_IGNORED, - cx, - ) - }); - - let search_button_for_mode = |mode, side, cx: &mut ViewContext| { - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - render_search_mode_button( - mode, - side, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, - ) - }; - - let search = _search.read(cx); - - let include_container_style = - if search.panels_with_errors.contains(&InputPanel::Include) { - theme.search.invalid_include_exclude_editor - } else { - theme.search.include_exclude_editor.input.container - }; - - let exclude_container_style = - if search.panels_with_errors.contains(&InputPanel::Exclude) { - theme.search.invalid_include_exclude_editor - } else { - theme.search.include_exclude_editor.input.container - }; - - let matches = search.active_match_index.map(|match_ix| { - Label::new( - format!( - "{}/{}", - match_ix + 1, - search.model.read(cx).match_ranges.len() - ), - theme.search.match_index.text.clone(), - ) - .contained() - .with_style(theme.search.match_index.container) - .aligned() - }); - let should_show_replace_input = search.replace_enabled; - let replacement = should_show_replace_input.then(|| { - Flex::row() - .with_child( - Svg::for_style(theme.search.replace_icon.clone().icon) - .contained() - .with_style(theme.search.replace_icon.clone().container), - ) - .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true)) - .align_children_center() - .flex(1., true) - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false) - }); - let replace_all = should_show_replace_input.then(|| { - super::replace_action( - ReplaceAll, - "Replace all", - "icons/replace_all.svg", - theme.tooltip.clone(), - theme.search.action_button.clone(), - ) - }); - let replace_next = should_show_replace_input.then(|| { - super::replace_action( - ReplaceNext, - "Replace next", - "icons/replace_next.svg", - theme.tooltip.clone(), - theme.search.action_button.clone(), - ) - }); - let query_column = Flex::column() - .with_spacing(theme.search.search_row_spacing) - .with_child( - Flex::row() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container), - ) - .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) - .flex(1., false) - .constrained() - .contained(), - ) - .align_children_center() - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .with_children(search.filters_enabled.then(|| { - Flex::row() - .with_child( - Flex::row() - .with_child( - ChildView::new(&search.included_files_editor, cx) - .contained() - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex(1., true), - ) - .with_children(include_ignored) - .contained() - .with_style(include_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex(1., true), - ) - .with_child( - ChildView::new(&search.excluded_files_editor, cx) - .contained() - .with_style(exclude_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex(1., true), - ) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .flex(1., false) + v_stack() + .key_context(key_context) + .flex_grow() + .gap_2() + .on_action(cx.listener(|this, _: &ToggleFocus, cx| this.move_focus_to_results(cx))) + .on_action(cx.listener(|this, _: &ToggleFilters, cx| { + this.toggle_filters(cx); + })) + .on_action(cx.listener(|this, _: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx) + })) + .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| { + this.activate_search_mode(SearchMode::Regex, cx) + })) + .on_action(cx.listener(|this, _: &ActivateSemanticMode, cx| { + this.activate_search_mode(SearchMode::Semantic, cx) + })) + .capture_action(cx.listener(|this, action, cx| { + this.tab(action, cx); + cx.stop_propagation(); + })) + .capture_action(cx.listener(|this, action, cx| { + this.tab_previous(action, cx); + cx.stop_propagation(); + })) + .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) + .on_action(cx.listener(|this, action, cx| { + this.cycle_mode(action, cx); + })) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.on_action(cx.listener(|this, action, cx| { + this.toggle_replace(action, cx); })) - .flex(1., false); - let switches_column = Flex::row() - .align_children_center() - .with_child(super::toggle_replace_button( - search.replace_enabled, - theme.tooltip.clone(), - theme.search.option_button_component.clone(), - )) - .constrained() - .with_height(theme.search.search_bar_row_height) - .contained() - .with_style(theme.search.option_button_group); - let mode_column = - Flex::row() - .with_child(search_button_for_mode( - SearchMode::Text, - Some(Side::Left), - cx, - )) - .with_child(search_button_for_mode( - SearchMode::Regex, - if is_semantic_available { - None - } else { - Some(Side::Right) - }, - cx, - )) - .with_children(is_semantic_available.then(|| { - search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + })) + .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + })) + .on_action(cx.listener(|this, action, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_next(action, cx); + }) + } + })) + .on_action(cx.listener(|this, action, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.replace_all(action, cx); + }) + } + })) + .when(search.filters_enabled, |this| { + this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, cx| { + this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx); })) - .contained() - .with_style(theme.search.modes_container); - - let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { - render_nav_button( - label, - direction, - is_active, - move |_, this, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |search, cx| search.select_match(direction, cx)); - } - }, - cx, + }) + }) + .child( + h_stack() + .justify_between() + .child(query_column) + .child(mode_column) + .child(replace_column) + .child(actions_column), + ) + .when(search.filters_enabled, |this| { + this.child( + h_stack() + .flex_1() + .gap_2() + .justify_between() + .child( + h_stack() + .flex_1() + .h_full() + .px_2() + .py_1() + .border_1() + .border_color(search.border_color_for(InputPanel::Include, cx)) + .rounded_lg() + .child(self.render_text_input(&search.included_files_editor, cx)) + .when(search.current_mode != SearchMode::Semantic, |this| { + this.child( + SearchOptions::INCLUDE_IGNORED.as_button( + search + .search_options + .contains(SearchOptions::INCLUDE_IGNORED), + cx.listener(|this, _, cx| { + this.toggle_search_option( + SearchOptions::INCLUDE_IGNORED, + cx, + ); + }), + ), + ) + }), + ) + .child( + h_stack() + .flex_1() + .h_full() + .px_2() + .py_1() + .border_1() + .border_color(search.border_color_for(InputPanel::Exclude, cx)) + .rounded_lg() + .child(self.render_text_input(&search.excluded_files_editor, cx)), + ), ) - }; - - let nav_column = Flex::row() - .with_children(replace_next) - .with_children(replace_all) - .with_child(Flex::row().with_children(matches)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .constrained() - .with_height(theme.search.search_bar_row_height) - .flex_float(); - - Flex::row() - .with_child(query_column) - .with_child(mode_column) - .with_child(switches_column) - .with_children(replacement) - .with_child(nav_column) - .contained() - .with_style(theme.search.container) - .into_any_named("project search") - } else { - Empty::new().into_any() - } + }) } } +impl EventEmitter for ProjectSearchBar {} + impl ToolbarItemView for ProjectSearchBar { fn set_active_pane_item( &mut self, @@ -1989,15 +1902,13 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); - ToolbarItemLocation::PrimaryLeft { - flex: Some((1., true)), - } + ToolbarItemLocation::PrimaryLeft {} } else { ToolbarItemLocation::Hidden } } - fn row_count(&self, cx: &ViewContext) -> usize { + fn row_count(&self, cx: &WindowContext<'_>) -> usize { if let Some(search) = self.active_project_search.as_ref() { if search.read(cx).filters_enabled { return 2; @@ -2011,19 +1922,17 @@ impl ToolbarItemView for ProjectSearchBar { pub mod tests { use super::*; use editor::DisplayPoint; - use gpui::{color::Color, executor::Deterministic, TestAppContext}; + use gpui::{Action, TestAppContext}; use project::FakeFs; use semantic_index::semantic_index_settings::SemanticIndexSettings; use serde_json::json; - use settings::SettingsStore; - use std::sync::Arc; - use theme::ThemeSettings; + use settings::{Settings, SettingsStore}; #[gpui::test] - async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_project_search(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/dir", json!({ @@ -2035,18 +1944,18 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); - let search_view = cx - .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)) - .root(cx); + let search = cx.new_model(|cx| ProjectSearch::new(project, cx)); + let search_view = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)); - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - deterministic.run_until_parked(); + search_view + .update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }) + .unwrap(); + cx.background_executor.run_until_parked(); search_view.update(cx, |search_view, cx| { assert_eq!( search_view @@ -2054,6 +1963,7 @@ pub mod tests { .update(cx, |editor, cx| editor.display_text(cx)), "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" ); + let match_background_color = cx.theme().colors().search_match_background; assert_eq!( search_view .results_editor @@ -2061,15 +1971,15 @@ pub mod tests { &[ ( DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), - Color::red() + match_background_color ), ( DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), - Color::red() + match_background_color ), ( DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), - Color::red() + match_background_color ) ] ); @@ -2082,68 +1992,78 @@ pub mod tests { ); search_view.select_match(Direction::Next, cx); - }); + }).unwrap(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(1)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] - ); - search_view.select_match(Direction::Next, cx); - }); + search_view + .update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + search_view.select_match(Direction::Next, cx); + }) + .unwrap(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(2)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] - ); - search_view.select_match(Direction::Next, cx); - }); + search_view + .update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(Direction::Next, cx); + }) + .unwrap(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(0)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] - ); - search_view.select_match(Direction::Prev, cx); - }); + search_view + .update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + search_view.select_match(Direction::Prev, cx); + }) + .unwrap(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(2)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] - ); - search_view.select_match(Direction::Prev, cx); - }); + search_view + .update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(Direction::Prev, cx); + }) + .unwrap(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(1)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] - ); - }); + search_view + .update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + }) + .unwrap(); } #[gpui::test] - async fn test_project_search_focus(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_project_search_focus(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/dir", json!({ @@ -2156,11 +2076,13 @@ pub mod tests { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); + let workspace = window.clone(); + let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); let active_item = cx.read(|cx| { workspace .read(cx) + .unwrap() .active_pane() .read(cx) .active_item() @@ -2168,16 +2090,25 @@ pub mod tests { }); assert!( active_item.is_none(), - "Expected no search panel to be active, but got: {active_item:?}" + "Expected no search panel to be active" ); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }); + window + .update(cx, move |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) + }); + + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }) + .unwrap(); let Some(search_view) = cx.read(|cx| { workspace .read(cx) + .unwrap() .active_pane() .read(cx) .active_item() @@ -2185,111 +2116,148 @@ pub mod tests { }) else { panic!("Search view expected to appear after new search event trigger") }; - let search_view_id = search_view.id(); cx.spawn(|mut cx| async move { - window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + window + .update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + .unwrap(); }) .detach(); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.is_focused(cx), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); + cx.background_executor.run_until_parked(); - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.is_focused(cx), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "Empty search view should be focused after the toggle focus event: no results panel to focus on", + ); + }); + }).unwrap(); - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) - }); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + let query_editor = &search_view.query_editor; + assert!( + query_editor.focus_handle(cx).is_focused(cx), + "Search view should be focused after the new search view is activated", + ); + let query_text = query_editor.read(cx).text(cx); + assert!( + query_text.is_empty(), + "New search query should be empty but got '{query_text}'", + ); + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Empty search view should have no results but got '{results_text}'" + ); + }); + }) + .unwrap(); + + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) + }); + search_view.search(cx); + }); + }) + .unwrap(); + + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( results_text.is_empty(), "Search view for mismatching query should have no results but got '{results_text}'" ); - assert!( - search_view.query_editor.is_focused(cx), + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), "Search view should be focused after mismatching query had been used in search", ); - }); - cx.spawn( - |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) }, - ) - .detach(); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.is_focused(cx), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.is_focused(cx), - "Search view with mismatching query should be focused after search results are available", - ); - }); + }); + }) + .unwrap(); cx.spawn(|mut cx| async move { - window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + window.update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) }) .detach(); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.is_focused(cx), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", + ); + }); + }).unwrap(); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }); - deterministic.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Search view results should match the query" + ); + assert!( + search_view.results_editor.focus_handle(cx).is_focused(cx), + "Search view with mismatching query should be focused after search results are available", + ); + })).unwrap(); + cx.spawn(|mut cx| async move { + window + .update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + .unwrap(); + }) + .detach(); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.focus_handle(cx).is_focused(cx), + "Search view with matching query should still have its results editor focused after the toggle focus event", + ); + }); + }).unwrap(); + + workspace + .update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }) + .unwrap(); + cx.background_executor.run_until_parked(); let Some(search_view_2) = cx.read(|cx| { workspace .read(cx) + .unwrap() .active_pane() .read(cx) .active_item() @@ -2297,88 +2265,100 @@ pub mod tests { }) else { panic!("Search view expected to appear after new search event trigger") }; - let search_view_id_2 = search_view_2.id(); - assert_ne!( - search_view_2, search_view, + assert!( + search_view_2 != search_view, "New search view should be open after `workspace::NewSearch` event" ); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results of the first search view should not update too" - ); - assert!( - !search_view.query_editor.is_focused(cx), - "Focus should be moved away from the first search view" - ); - }); + window.update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Results of the first search view should not update too" + ); + assert!( + !search_view.query_editor.focus_handle(cx).is_focused(cx), + "Focus should be moved away from the first search view" + ); + }); + }).unwrap(); - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2.query_editor.read(cx).text(cx), - "two", - "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" - ); - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "", - "No search results should be in the 2nd view yet, as we did not spawn a search for it" - ); - assert!( - search_view_2.query_editor.is_focused(cx), - "Focus should be moved into query editor fo the new window" - ); - }); + window.update(cx, |_, cx| { + search_view_2.update(cx, |search_view_2, cx| { + assert_eq!( + search_view_2.query_editor.read(cx).text(cx), + "two", + "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" + ); + assert_eq!( + search_view_2 + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "", + "No search results should be in the 2nd view yet, as we did not spawn a search for it" + ); + assert!( + search_view_2.query_editor.focus_handle(cx).is_focused(cx), + "Focus should be moved into query editor fo the new window" + ); + }); + }).unwrap(); - search_view_2.update(cx, |search_view_2, cx| { - search_view_2 - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx)); - search_view_2.search(cx); - }); - deterministic.run_until_parked(); - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "New search view with the updated query should have new search results" - ); - assert!( - search_view_2.results_editor.is_focused(cx), - "Search view with mismatching query should be focused after search results are available", - ); - }); + window + .update(cx, |_, cx| { + search_view_2.update(cx, |search_view_2, cx| { + search_view_2 + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx)); + search_view_2.search(cx); + }); + }) + .unwrap(); + + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view_2.update(cx, |search_view_2, cx| { + assert_eq!( + search_view_2 + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst FOUR: usize = one::ONE + three::THREE;", + "New search view with the updated query should have new search results" + ); + assert!( + search_view_2.results_editor.focus_handle(cx).is_focused(cx), + "Search view with mismatching query should be focused after search results are available", + ); + }); + }).unwrap(); cx.spawn(|mut cx| async move { - window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx); + window + .update(&mut cx, |_, cx| { + cx.dispatch_action(ToggleFocus.boxed_clone()) + }) + .unwrap(); }) .detach(); - deterministic.run_until_parked(); - search_view_2.update(cx, |search_view_2, cx| { - assert!( - search_view_2.results_editor.is_focused(cx), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - }); + cx.background_executor.run_until_parked(); + window.update(cx, |_, cx| { + search_view_2.update(cx, |search_view_2, cx| { + assert!( + search_view_2.results_editor.focus_handle(cx).is_focused(cx), + "Search view with matching query should switch focus to the results editor after the toggle focus event", + ); + });}).unwrap(); } #[gpui::test] - async fn test_new_project_search_in_directory( - deterministic: Arc, - cx: &mut TestAppContext, - ) { + async fn test_new_project_search_in_directory(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/dir", json!({ @@ -2395,11 +2375,11 @@ pub mod tests { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() + project.worktrees().next().unwrap().read(cx).id() }); - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx).unwrap(); + let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); let active_item = cx.read(|cx| { workspace @@ -2411,9 +2391,19 @@ pub mod tests { }); assert!( active_item.is_none(), - "Expected no search panel to be active, but got: {active_item:?}" + "Expected no search panel to be active" ); + window + .update(cx, move |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) + }); + }) + .unwrap(); + let one_file_entry = cx.update(|cx| { workspace .read(cx) @@ -2423,9 +2413,11 @@ pub mod tests { .expect("no entry for /a/one.rs file") }); assert!(one_file_entry.is_file()); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) - }); + window + .update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) + }) + .unwrap(); let active_search_entry = cx.read(|cx| { workspace .read(cx) @@ -2448,9 +2440,11 @@ pub mod tests { .expect("no entry for /a/ directory") }); assert!(a_dir_entry.is_dir()); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) - }); + window + .update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) + }) + .unwrap(); let Some(search_view) = cx.read(|cx| { workspace @@ -2462,50 +2456,61 @@ pub mod tests { }) else { panic!("Search view expected to appear after new search in directory event trigger") }; - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.is_focused(cx), - "On new search in directory, focus should be moved into query editor" - ); - search_view.excluded_files_editor.update(cx, |editor, cx| { - assert!( - editor.display_text(cx).is_empty(), - "New search in directory should not have any excluded files" - ); - }); - search_view.included_files_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - a_dir_entry.path.to_str().unwrap(), - "New search in directory should have included dir entry path" - ); - }); - }); - - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); - search_view.search(cx); - }); - deterministic.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!( + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.focus_handle(cx).is_focused(cx), + "On new search in directory, focus should be moved into query editor" + ); + search_view.excluded_files_editor.update(cx, |editor, cx| { + assert!( + editor.display_text(cx).is_empty(), + "New search in directory should not have any excluded files" + ); + }); + search_view.included_files_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + a_dir_entry.path.to_str().unwrap(), + "New search in directory should have included dir entry path" + ); + }); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); + search_view.search(cx); + }); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!( search_view .results_editor .update(cx, |editor, cx| editor.display_text(cx)), "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", "New search in directory should have a filter that matches a certain directory" ); - }); + }) + }) + .unwrap(); } #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/dir", json!({ @@ -2518,10 +2523,23 @@ pub mod tests { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - workspace.update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }); + let workspace = window.root(cx).unwrap(); + let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); + + window + .update(cx, { + let search_bar = search_bar.clone(); + move |workspace, cx| { + assert_eq!(workspace.panes().len(), 1); + workspace.panes()[0].update(cx, move |pane, cx| { + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) + }); + + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + } + }) + .unwrap(); let search_view = cx.read(|cx| { workspace @@ -2533,178 +2551,287 @@ pub mod tests { .expect("Search view expected to appear after new search event trigger") }); - let search_bar = window.add_view(cx, |cx| { - let mut search_bar = ProjectSearchBar::new(); - search_bar.set_active_pane_item(Some(&search_view), cx); - // search_bar.show(cx); - search_bar - }); - // Add 3 search items into the history + another unsubmitted one. - search_view.update(cx, |search_view, cx| { - search_view.search_options = SearchOptions::CASE_SENSITIVE; - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("JUST_TEXT_INPUT", cx) - }); - }); - cx.foreground().run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view.search_options = SearchOptions::CASE_SENSITIVE; + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); + search_view.search(cx); + }); + }) + .unwrap(); + + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); + search_view.search(cx); + }) + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("JUST_TEXT_INPUT", cx) + }); + }) + }) + .unwrap(); + cx.background_executor.run_until_parked(); // Ensure that the latest input with search settings is active. - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view.query_editor.read(cx).text(cx), - "JUST_TEXT_INPUT" - ); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view.query_editor.read(cx).text(cx), + "JUST_TEXT_INPUT" + ); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); // Next history query after the latest should set the query to the empty string. - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }) + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }) + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); // First previous query for empty current query should set the query to the latest submitted one. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); // Further previous items should go over the history in reverse order. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); // Previous items should never go behind the first history item. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); // Next items should go over the history in the original order. - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); - search_view.search(cx); - }); - cx.foreground().run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); + search_view.search(cx); + }); + }) + .unwrap(); + cx.background_executor.run_until_parked(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); // New search input should add another entry to history and move the selection to the end of the history. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + }) + .unwrap(); } pub fn init_test(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); - theme.search.match_background = Color::red(); - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); + let settings = SettingsStore::test(cx); + cx.set_global(settings); cx.set_global(ActiveSearches::default()); - settings::register::(cx); + SemanticIndexSettings::register(cx); - theme::init((), cx); - cx.update_global::(|store, _| { - let mut settings = store.get::(None).clone(); - settings.theme = Arc::new(theme); - store.override_global(settings) - }); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); client::init_settings(cx); diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index db39455dca..f0301a5bcc 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,16 +1,11 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; -use gpui::{ - actions, - elements::{Component, SafeStylable, TooltipStyle}, - Action, AnyElement, AppContext, Element, View, -}; +use gpui::{actions, Action, AppContext, IntoElement}; pub use mode::SearchMode; use project::search::SearchQuery; -pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{ - action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, -}; +pub use project_search::ProjectSearchView; +use ui::{prelude::*, Tooltip}; +use ui::{ButtonStyle, IconButton}; pub mod buffer_search; mod history; @@ -19,6 +14,7 @@ pub mod project_search; pub(crate) mod search_bar; pub fn init(cx: &mut AppContext) { + menu::init(); buffer_search::init(cx); project_search::init(cx); } @@ -57,28 +53,28 @@ bitflags! { impl SearchOptions { pub fn label(&self) -> &'static str { match *self { - Self::WHOLE_WORD => "Match Whole Word", - Self::CASE_SENSITIVE => "Match Case", - Self::INCLUDE_IGNORED => "Include Ignored", - _ => panic!("{self:?} is not a named SearchOption"), + SearchOptions::WHOLE_WORD => "Match Whole Word", + SearchOptions::CASE_SENSITIVE => "Match Case", + SearchOptions::INCLUDE_IGNORED => "Include ignored", + _ => panic!("{:?} is not a named SearchOption", self), } } - pub fn icon(&self) -> &'static str { + pub fn icon(&self) -> ui::Icon { match *self { - Self::WHOLE_WORD => "icons/word_search.svg", - Self::CASE_SENSITIVE => "icons/case_insensitive.svg", - Self::INCLUDE_IGNORED => "icons/case_insensitive.svg", - _ => panic!("{self:?} is not a named SearchOption"), + SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, + SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, + SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit, + _ => panic!("{:?} is not a named SearchOption", self), } } - pub fn to_toggle_action(&self) -> Box { + pub fn to_toggle_action(&self) -> Box { match *self { - Self::WHOLE_WORD => Box::new(ToggleWholeWord), - Self::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), - Self::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored), - _ => panic!("{self:?} is not a named SearchOption"), + SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), + SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), + SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored), + _ => panic!("{:?} is not a named SearchOption", self), } } @@ -94,47 +90,19 @@ impl SearchOptions { options } - pub fn as_button( + pub fn as_button( &self, active: bool, - tooltip_style: TooltipStyle, - button_style: ToggleIconButtonStyle, - ) -> AnyElement { - Button::dynamic_action(self.to_toggle_action()) - .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) - .with_contents(Svg::new(self.icon())) - .toggleable(active) - .with_style(button_style) - .element() - .into_any() + action: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static, + ) -> impl IntoElement { + IconButton::new(self.label(), self.icon()) + .on_click(action) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) + .tooltip({ + let action = self.to_toggle_action(); + let label: SharedString = format!("Toggle {}", self.label()).into(); + move |cx| Tooltip::for_action(label.clone(), &*action, cx) + }) } } - -fn toggle_replace_button( - active: bool, - tooltip_style: TooltipStyle, - button_style: ToggleIconButtonStyle, -) -> AnyElement { - Button::dynamic_action(Box::new(ToggleReplace)) - .with_tooltip("Toggle Replace", tooltip_style) - .with_contents(theme::components::svg::Svg::new("icons/replace.svg")) - .toggleable(active) - .with_style(button_style) - .element() - .into_any() -} - -fn replace_action( - action: impl Action, - name: &'static str, - icon_path: &'static str, - tooltip_style: TooltipStyle, - button_style: IconButtonStyle, -) -> AnyElement { - Button::dynamic_action(Box::new(action)) - .with_tooltip(name, tooltip_style) - .with_contents(theme::components::svg::Svg::new(icon_path)) - .with_style(button_style) - .element() - .into_any() -} diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index d1a5a0380a..628be3112e 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -1,174 +1,18 @@ -use std::borrow::Cow; +use gpui::{Action, IntoElement}; +use ui::IconButton; +use ui::{prelude::*, Tooltip}; -use gpui::{ - elements::{Label, MouseEventHandler, Svg}, - platform::{CursorStyle, MouseButton}, - scene::{CornerRadii, MouseClick}, - Action, AnyElement, Element, EventContext, View, ViewContext, -}; -use workspace::searchable::Direction; - -use crate::{ - mode::{SearchMode, Side}, - SelectNextMatch, SelectPrevMatch, -}; - -pub(super) fn render_nav_button( - icon: &'static str, - direction: Direction, +pub(super) fn render_nav_button( + icon: ui::Icon, active: bool, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - cx: &mut ViewContext, -) -> AnyElement { - let action: Box; - let tooltip; - - match direction { - Direction::Prev => { - action = Box::new(SelectPrevMatch); - tooltip = "Select Previous Match"; - } - Direction::Next => { - action = Box::new(SelectNextMatch); - tooltip = "Select Next Match"; - } - }; - let tooltip_style = theme::current(cx).tooltip.clone(); - let cursor_style = if active { - CursorStyle::PointingHand - } else { - CursorStyle::default() - }; - enum NavButton {} - MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .nav_button - .in_state(active) - .style_for(state) - .clone(); - let mut container_style = style.container.clone(); - let label = Label::new(icon, style.label.clone()).aligned().contained(); - container_style.corner_radii = match direction { - Direction::Prev => CornerRadii { - bottom_right: 0., - top_right: 0., - ..container_style.corner_radii - }, - Direction::Next => CornerRadii { - bottom_left: 0., - top_left: 0., - ..container_style.corner_radii - }, - }; - if direction == Direction::Prev { - // Remove right border so that when both Next and Prev buttons are - // next to one another, there's no double border between them. - container_style.border.right = false; - } - label.with_style(container_style) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(cursor_style) - .with_tooltip::( - direction as usize, - tooltip.to_string(), - Some(action), - tooltip_style, - cx, + tooltip: &'static str, + action: &'static dyn Action, +) -> impl IntoElement { + IconButton::new( + SharedString::from(format!("search-nav-button-{}", action.name())), + icon, ) - .into_any() -} - -pub(crate) fn render_search_mode_button( - mode: SearchMode, - side: Option, - is_active: bool, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - cx: &mut ViewContext, -) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - enum SearchModeButton {} - MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .mode_button - .in_state(is_active) - .style_for(state) - .clone(); - - let mut container_style = style.container; - if let Some(button_side) = side { - if button_side == Side::Left { - container_style.border.left = true; - container_style.corner_radii = CornerRadii { - bottom_right: 0., - top_right: 0., - ..container_style.corner_radii - }; - } else { - container_style.border.left = false; - container_style.corner_radii = CornerRadii { - bottom_left: 0., - top_left: 0., - ..container_style.corner_radii - }; - } - } else { - container_style.border.left = false; - container_style.corner_radii = CornerRadii::default(); - } - - Label::new(mode.label(), style.text) - .aligned() - .contained() - .with_style(container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::( - mode.region_id(), - mode.tooltip_text().to_owned(), - Some(mode.activate_action()), - tooltip_style, - cx, - ) - .into_any() -} - -pub(crate) fn render_option_button_icon( - is_active: bool, - icon: &'static str, - id: usize, - label: impl Into>, - action: Box, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - cx: &mut ViewContext, -) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(id, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .option_button - .in_state(is_active) - .style_for(state); - Svg::new(icon) - .with_color(style.color.clone()) - .constrained() - .with_width(style.icon_width) - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.search.option_button_height) - .with_width(style.button_width) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(id, label, Some(action), tooltip_style, cx) - .into_any() + .on_click(|_, cx| cx.dispatch_action(action.boxed_clone())) + .tooltip(move |cx| Tooltip::for_action(tooltip, action, cx)) + .disabled(!active) } diff --git a/crates/search2/Cargo.toml b/crates/search2/Cargo.toml deleted file mode 100644 index 34158e7416..0000000000 --- a/crates/search2/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "search2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/search.rs" -doctest = false - -[dependencies] -bitflags = "1" -collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -menu = { package = "menu2", path = "../menu2" } -project = { package = "project2", path = "../project2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -util = { path = "../util" } -ui = {package = "ui2", path = "../ui2"} -workspace = { package = "workspace2", path = "../workspace2" } -semantic_index = { package = "semantic_index2", path = "../semantic_index2" } -anyhow.workspace = true -futures.workspace = true -log.workspace = true -postage.workspace = true -serde.workspace = true -serde_derive.workspace = true -smallvec.workspace = true -smol.workspace = true -serde_json.workspace = true -[dev-dependencies] -client = { package = "client2", path = "../client2", features = ["test-support"] } -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } - -workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -unindent.workspace = true diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs deleted file mode 100644 index 67aa4955bc..0000000000 --- a/crates/search2/src/buffer_search.rs +++ /dev/null @@ -1,1858 +0,0 @@ -use crate::{ - history::SearchHistory, - mode::{next_mode, SearchMode}, - search_bar::render_nav_button, - ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, - ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, - ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, -}; -use collections::HashMap; -use editor::{Editor, EditorElement, EditorStyle, Tab}; -use futures::channel::oneshot; -use gpui::{ - actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, - FontStyle, FontWeight, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, - Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, - WhiteSpace, WindowContext, -}; -use project::search::SearchQuery; -use serde::Deserialize; -use settings::Settings; -use std::{any::Any, sync::Arc}; -use theme::ThemeSettings; - -use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, ToggleButton, Tooltip}; -use util::ResultExt; -use workspace::{ - item::ItemHandle, - searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, -}; - -#[derive(PartialEq, Clone, Deserialize)] -pub struct Deploy { - pub focus: bool, -} - -impl_actions!(buffer_search, [Deploy]); - -actions!(buffer_search, [Dismiss, FocusEditor]); - -pub enum Event { - UpdateLocation, -} - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|editor: &mut Workspace, _| BufferSearchBar::register(editor)) - .detach(); -} - -pub struct BufferSearchBar { - query_editor: View, - replacement_editor: View, - active_searchable_item: Option>, - active_match_index: Option, - active_searchable_item_subscription: Option, - active_search: Option>, - searchable_items_with_matches: - HashMap, Vec>>, - pending_search: Option>, - search_options: SearchOptions, - default_options: SearchOptions, - query_contains_error: bool, - dismissed: bool, - search_history: SearchHistory, - current_mode: SearchMode, - replace_enabled: bool, -} - -impl BufferSearchBar { - fn render_text_input(&self, editor: &View, cx: &ViewContext) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if editor.read(cx).read_only() { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(1.3).into(), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }; - - EditorElement::new( - &editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - } -} - -impl EventEmitter for BufferSearchBar {} -impl EventEmitter for BufferSearchBar {} -impl Render for BufferSearchBar { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - if self.dismissed { - return div(); - } - - let supported_options = self.supported_options(); - - if self.query_editor.read(cx).placeholder_text().is_none() { - let query_focus_handle = self.query_editor.focus_handle(cx); - let up_keystrokes = cx - .bindings_for_action_in(&PreviousHistoryQuery {}, &query_focus_handle) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let down_keystrokes = cx - .bindings_for_action_in(&NextHistoryQuery {}, &query_focus_handle) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - - let placeholder_text = - up_keystrokes - .zip(down_keystrokes) - .map(|(up_keystrokes, down_keystrokes)| { - Arc::from(format!( - "Search ({}/{} for previous/next query)", - up_keystrokes.join(" "), - down_keystrokes.join(" ") - )) - }); - - if let Some(placeholder_text) = placeholder_text { - self.query_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(placeholder_text, cx); - }); - } - } - - self.replacement_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Replace with...", cx); - }); - - let match_count = self - .active_searchable_item - .as_ref() - .and_then(|searchable_item| { - if self.query(cx).is_empty() { - return None; - } - let matches = self - .searchable_items_with_matches - .get(&searchable_item.downgrade())?; - let message = if let Some(match_ix) = self.active_match_index { - format!("{}/{}", match_ix + 1, matches.len()) - } else { - "No matches".to_string() - }; - - Some(ui::Label::new(message)) - }); - let should_show_replace_input = self.replace_enabled && supported_options.replacement; - let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); - - let mut key_context = KeyContext::default(); - key_context.add("BufferSearchBar"); - if in_replace { - key_context.add("in_replace"); - } - let editor_border = if self.query_contains_error { - Color::Error.color(cx) - } else { - cx.theme().colors().border - }; - h_stack() - .w_full() - .gap_2() - .key_context(key_context) - .capture_action(cx.listener(Self::tab)) - .on_action(cx.listener(Self::previous_history_query)) - .on_action(cx.listener(Self::next_history_query)) - .on_action(cx.listener(Self::dismiss)) - .on_action(cx.listener(Self::select_next_match)) - .on_action(cx.listener(Self::select_prev_match)) - .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| { - this.activate_search_mode(SearchMode::Regex, cx); - })) - .on_action(cx.listener(|this, _: &ActivateTextMode, cx| { - this.activate_search_mode(SearchMode::Text, cx); - })) - .when(self.supported_options().replacement, |this| { - this.on_action(cx.listener(Self::toggle_replace)) - .when(in_replace, |this| { - this.on_action(cx.listener(Self::replace_next)) - .on_action(cx.listener(Self::replace_all)) - }) - }) - .when(self.supported_options().case, |this| { - this.on_action(cx.listener(Self::toggle_case_sensitive)) - }) - .when(self.supported_options().word, |this| { - this.on_action(cx.listener(Self::toggle_whole_word)) - }) - .child( - h_stack() - .flex_1() - .px_2() - .py_1() - .gap_2() - .border_1() - .border_color(editor_border) - .rounded_lg() - .child(IconElement::new(Icon::MagnifyingGlass)) - .child(self.render_text_input(&self.query_editor, cx)) - .children(supported_options.case.then(|| { - self.render_search_option_button( - SearchOptions::CASE_SENSITIVE, - cx.listener(|this, _, cx| { - this.toggle_case_sensitive(&ToggleCaseSensitive, cx) - }), - ) - })) - .children(supported_options.word.then(|| { - self.render_search_option_button( - SearchOptions::WHOLE_WORD, - cx.listener(|this, _, cx| this.toggle_whole_word(&ToggleWholeWord, cx)), - ) - })), - ) - .child( - h_stack() - .gap_2() - .flex_none() - .child( - h_stack() - .child( - ToggleButton::new("search-mode-text", SearchMode::Text.label()) - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .selected(self.current_mode == SearchMode::Text) - .on_click(cx.listener(move |_, _event, cx| { - cx.dispatch_action(SearchMode::Text.action()) - })) - .tooltip(|cx| { - Tooltip::for_action( - SearchMode::Text.tooltip(), - &*SearchMode::Text.action(), - cx, - ) - }) - .first(), - ) - .child( - ToggleButton::new("search-mode-regex", SearchMode::Regex.label()) - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .selected(self.current_mode == SearchMode::Regex) - .on_click(cx.listener(move |_, _event, cx| { - cx.dispatch_action(SearchMode::Regex.action()) - })) - .tooltip(|cx| { - Tooltip::for_action( - SearchMode::Regex.tooltip(), - &*SearchMode::Regex.action(), - cx, - ) - }) - .last(), - ), - ) - .when(supported_options.replacement, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-replace-button", - Icon::Replace, - ) - .style(ButtonStyle::Subtle) - .when(self.replace_enabled, |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, cx| { - this.toggle_replace(&ToggleReplace, cx); - })) - .tooltip(|cx| { - Tooltip::for_action("Toggle replace", &ToggleReplace, cx) - }), - ) - }), - ) - .child( - h_stack() - .gap_0p5() - .flex_1() - .when(self.replace_enabled, |this| { - this.child( - h_stack() - .flex_1() - // We're giving this a fixed height to match the height of the search input, - // which has an icon inside that is increasing its height. - .h_8() - .px_2() - .py_1() - .gap_2() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_lg() - .child(self.render_text_input(&self.replacement_editor, cx)), - ) - .when(should_show_replace_input, |this| { - this.child( - IconButton::new("search-replace-next", ui::Icon::ReplaceNext) - .tooltip(move |cx| { - Tooltip::for_action("Replace next", &ReplaceNext, cx) - }) - .on_click(cx.listener(|this, _, cx| { - this.replace_next(&ReplaceNext, cx) - })), - ) - .child( - IconButton::new("search-replace-all", ui::Icon::ReplaceAll) - .tooltip(move |cx| { - Tooltip::for_action("Replace all", &ReplaceAll, cx) - }) - .on_click( - cx.listener(|this, _, cx| { - this.replace_all(&ReplaceAll, cx) - }), - ), - ) - }) - }), - ) - .child( - h_stack() - .gap_0p5() - .flex_none() - .child( - IconButton::new("select-all", ui::Icon::SelectAll) - .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone())) - .tooltip(|cx| { - Tooltip::for_action("Select all matches", &SelectAllMatches, cx) - }), - ) - .children(match_count) - .child(render_nav_button( - ui::Icon::ChevronLeft, - self.active_match_index.is_some(), - "Select previous match", - &SelectPrevMatch, - )) - .child(render_nav_button( - ui::Icon::ChevronRight, - self.active_match_index.is_some(), - "Select next match", - &SelectNextMatch, - )), - ) - } -} - -impl FocusableView for BufferSearchBar { - fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { - self.query_editor.focus_handle(cx) - } -} - -impl ToolbarItemView for BufferSearchBar { - fn set_active_pane_item( - &mut self, - item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_searchable_item_subscription.take(); - self.active_searchable_item.take(); - - self.pending_search.take(); - - if let Some(searchable_item_handle) = - item.and_then(|item| item.to_searchable_item_handle(cx)) - { - let this = cx.view().downgrade(); - - searchable_item_handle - .subscribe_to_search_events( - cx, - Box::new(move |search_event, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_active_searchable_item_event(search_event, cx) - }); - } - }), - ) - .detach(); - - self.active_searchable_item = Some(searchable_item_handle); - let _ = self.update_matches(cx); - if !self.dismissed { - return ToolbarItemLocation::Secondary; - } - } - ToolbarItemLocation::Hidden - } - - fn row_count(&self, _: &WindowContext<'_>) -> usize { - 1 - } -} - -impl BufferSearchBar { - fn register(workspace: &mut Workspace) { - workspace.register_action(move |workspace, deploy: &Deploy, cx| { - let pane = workspace.active_pane(); - - pane.update(cx, |this, cx| { - this.toolbar().update(cx, |this, cx| { - if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, |this, cx| { - this.deploy(deploy, cx); - }); - return; - } - let view = cx.new_view(|cx| BufferSearchBar::new(cx)); - this.add_item(view.clone(), cx); - view.update(cx, |this, cx| this.deploy(deploy, cx)); - cx.notify(); - }) - }); - }); - fn register_action( - workspace: &mut Workspace, - update: fn(&mut BufferSearchBar, &A, &mut ViewContext), - ) { - workspace.register_action(move |workspace, action: &A, cx| { - let pane = workspace.active_pane(); - pane.update(cx, move |this, cx| { - this.toolbar().update(cx, move |this, cx| { - if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, move |this, cx| update(this, action, cx)); - cx.notify(); - } - }) - }); - }); - } - - register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { - if this.supported_options().case { - this.toggle_case_sensitive(action, cx); - } - }); - register_action(workspace, |this, action: &ToggleWholeWord, cx| { - if this.supported_options().word { - this.toggle_whole_word(action, cx); - } - }); - register_action(workspace, |this, action: &ToggleReplace, cx| { - if this.supported_options().replacement { - this.toggle_replace(action, cx); - } - }); - register_action(workspace, |this, _: &ActivateRegexMode, cx| { - if this.supported_options().regex { - this.activate_search_mode(SearchMode::Regex, cx); - } - }); - register_action(workspace, |this, _: &ActivateTextMode, cx| { - this.activate_search_mode(SearchMode::Text, cx); - }); - register_action(workspace, |this, action: &CycleMode, cx| { - if this.supported_options().regex { - // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting - // cycling. - this.cycle_mode(action, cx) - } - }); - register_action(workspace, |this, action: &SelectNextMatch, cx| { - this.select_next_match(action, cx); - }); - register_action(workspace, |this, action: &SelectPrevMatch, cx| { - this.select_prev_match(action, cx); - }); - register_action(workspace, |this, action: &SelectAllMatches, cx| { - this.select_all_matches(action, cx); - }); - register_action(workspace, |this, _: &editor::Cancel, cx| { - if !this.dismissed { - this.dismiss(&Dismiss, cx); - return; - } - cx.propagate(); - }); - } - pub fn new(cx: &mut ViewContext) -> Self { - let query_editor = cx.new_view(|cx| Editor::single_line(cx)); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); - let replacement_editor = cx.new_view(|cx| Editor::single_line(cx)); - cx.subscribe(&replacement_editor, Self::on_query_editor_event) - .detach(); - Self { - query_editor, - replacement_editor, - active_searchable_item: None, - active_searchable_item_subscription: None, - active_match_index: None, - searchable_items_with_matches: Default::default(), - default_options: SearchOptions::NONE, - search_options: SearchOptions::NONE, - pending_search: None, - query_contains_error: false, - dismissed: true, - search_history: SearchHistory::default(), - current_mode: SearchMode::default(), - active_search: None, - replace_enabled: false, - } - } - - pub fn is_dismissed(&self) -> bool { - self.dismissed - } - - pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { - self.dismissed = true; - for searchable_item in self.searchable_items_with_matches.keys() { - if let Some(searchable_item) = - WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) - { - searchable_item.clear_matches(cx); - } - } - if let Some(active_editor) = self.active_searchable_item.as_ref() { - let handle = active_editor.focus_handle(cx); - cx.focus(&handle); - } - cx.emit(Event::UpdateLocation); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )); - cx.notify(); - } - - pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext) -> bool { - if self.show(cx) { - self.search_suggested(cx); - if deploy.focus { - self.select_query(cx); - let handle = self.query_editor.focus_handle(cx); - cx.focus(&handle); - } - return true; - } - - false - } - - pub fn toggle(&mut self, action: &Deploy, cx: &mut ViewContext) { - if self.is_dismissed() { - self.deploy(action, cx); - } else { - self.dismiss(&Dismiss, cx); - } - } - - pub fn show(&mut self, cx: &mut ViewContext) -> bool { - if self.active_searchable_item.is_none() { - return false; - } - self.dismissed = false; - cx.notify(); - cx.emit(Event::UpdateLocation); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Secondary, - )); - true - } - - fn supported_options(&self) -> workspace::searchable::SearchOptions { - self.active_searchable_item - .as_deref() - .map(SearchableItemHandle::supported_options) - .unwrap_or_default() - } - pub fn search_suggested(&mut self, cx: &mut ViewContext) { - let search = self - .query_suggestion(cx) - .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx)); - - if let Some(search) = search { - cx.spawn(|this, mut cx| async move { - search.await?; - this.update(&mut cx, |this, cx| this.activate_current_match(cx)) - }) - .detach_and_log_err(cx); - } - } - - pub fn activate_current_match(&mut self, cx: &mut ViewContext) { - if let Some(match_ix) = self.active_match_index { - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, cx) - } - } - } - } - - pub fn select_query(&mut self, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&Default::default(), cx); - }); - } - - pub fn query(&self, cx: &WindowContext) -> String { - self.query_editor.read(cx).text(cx) - } - pub fn replacement(&self, cx: &WindowContext) -> String { - self.replacement_editor.read(cx).text(cx) - } - pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { - self.active_searchable_item - .as_ref() - .map(|searchable_item| searchable_item.query_suggestion(cx)) - .filter(|suggestion| !suggestion.is_empty()) - } - - pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { - if replacement.is_none() { - self.replace_enabled = false; - return; - } - self.replace_enabled = true; - self.replacement_editor - .update(cx, |replacement_editor, cx| { - replacement_editor - .buffer() - .update(cx, |replacement_buffer, cx| { - let len = replacement_buffer.len(cx); - replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx); - }); - }); - } - - pub fn search( - &mut self, - query: &str, - options: Option, - cx: &mut ViewContext, - ) -> oneshot::Receiver<()> { - let options = options.unwrap_or(self.default_options); - if query != self.query(cx) || self.search_options != options { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); - }); - self.search_options = options; - self.query_contains_error = false; - self.clear_matches(cx); - cx.notify(); - } - self.update_matches(cx) - } - - fn render_search_option_button( - &self, - option: SearchOptions, - action: impl Fn(&ClickEvent, &mut WindowContext) + 'static, - ) -> impl IntoElement { - let is_active = self.search_options.contains(option); - option.as_button(is_active, action) - } - pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { - assert_ne!( - mode, - SearchMode::Semantic, - "Semantic search is not supported in buffer search" - ); - if mode == self.current_mode { - return; - } - self.current_mode = mode; - let _ = self.update_matches(cx); - cx.notify(); - } - - pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { - if let Some(active_editor) = self.active_searchable_item.as_ref() { - let handle = active_editor.focus_handle(cx); - cx.focus(&handle); - } - } - - fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { - self.search_options.toggle(search_option); - self.default_options = self.search_options; - let _ = self.update_matches(cx); - cx.notify(); - } - - pub fn set_search_options( - &mut self, - search_options: SearchOptions, - cx: &mut ViewContext, - ) { - self.search_options = search_options; - cx.notify(); - } - - fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { - self.select_match(Direction::Next, 1, cx); - } - - fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { - self.select_match(Direction::Prev, 1, cx); - } - - fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { - if !self.dismissed && self.active_match_index.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - searchable_item.select_matches(matches, cx); - self.focus_editor(&FocusEditor, cx); - } - } - } - } - - pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext) { - if let Some(index) = self.active_match_index { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, cx); - - searchable_item.update_matches(matches, cx); - searchable_item.activate_match(new_match_index, matches, cx); - } - } - } - } - - pub fn select_last_match(&mut self, cx: &mut ViewContext) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if matches.len() == 0 { - return; - } - let new_match_index = matches.len() - 1; - searchable_item.update_matches(matches, cx); - searchable_item.activate_match(new_match_index, matches, cx); - } - } - } - - fn on_query_editor_event( - &mut self, - _: View, - event: &editor::EditorEvent, - cx: &mut ViewContext, - ) { - if let editor::EditorEvent::Edited { .. } = event { - self.query_contains_error = false; - self.clear_matches(cx); - let search = self.update_matches(cx); - cx.spawn(|this, mut cx| async move { - search.await?; - this.update(&mut cx, |this, cx| this.activate_current_match(cx)) - }) - .detach_and_log_err(cx); - } - } - - fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext) { - match event { - SearchEvent::MatchesInvalidated => { - let _ = self.update_matches(cx); - } - SearchEvent::ActiveMatchChanged => self.update_match_index(cx), - } - } - - fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext) { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) - } - fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext) { - self.toggle_search_option(SearchOptions::WHOLE_WORD, cx) - } - fn clear_matches(&mut self, cx: &mut ViewContext) { - let mut active_item_matches = None; - for (searchable_item, matches) in self.searchable_items_with_matches.drain() { - if let Some(searchable_item) = - WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) - { - if Some(&searchable_item) == self.active_searchable_item.as_ref() { - active_item_matches = Some((searchable_item.downgrade(), matches)); - } else { - searchable_item.clear_matches(cx); - } - } - } - - self.searchable_items_with_matches - .extend(active_item_matches); - } - - fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { - let (done_tx, done_rx) = oneshot::channel(); - let query = self.query(cx); - self.pending_search.take(); - - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - if query.is_empty() { - self.active_match_index.take(); - active_searchable_item.clear_matches(cx); - let _ = done_tx.send(()); - cx.notify(); - } else { - let query: Arc<_> = if self.current_mode == SearchMode::Regex { - match SearchQuery::regex( - query, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - false, - Vec::new(), - Vec::new(), - ) { - Ok(query) => query.with_replacement(self.replacement(cx)), - Err(_) => { - self.query_contains_error = true; - self.active_match_index = None; - cx.notify(); - return done_rx; - } - } - } else { - match SearchQuery::text( - query, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - false, - Vec::new(), - Vec::new(), - ) { - Ok(query) => query.with_replacement(self.replacement(cx)), - Err(_) => { - self.query_contains_error = true; - self.active_match_index = None; - cx.notify(); - return done_rx; - } - } - } - .into(); - self.active_search = Some(query.clone()); - let query_text = query.as_str().to_string(); - - let matches = active_searchable_item.find_matches(query, cx); - - let active_searchable_item = active_searchable_item.downgrade(); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - let matches = matches.await; - - this.update(&mut cx, |this, cx| { - if let Some(active_searchable_item) = - WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) - { - this.searchable_items_with_matches - .insert(active_searchable_item.downgrade(), matches); - - this.update_match_index(cx); - this.search_history.add(query_text); - if !this.dismissed { - let matches = this - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - .unwrap(); - active_searchable_item.update_matches(matches, cx); - let _ = done_tx.send(()); - } - cx.notify(); - } - }) - .log_err(); - })); - } - } - done_rx - } - - fn update_match_index(&mut self, cx: &mut ViewContext) { - let new_index = self - .active_searchable_item - .as_ref() - .and_then(|searchable_item| { - let matches = self - .searchable_items_with_matches - .get(&searchable_item.downgrade())?; - searchable_item.active_match_index(matches, cx) - }); - if new_index != self.active_match_index { - self.active_match_index = new_index; - cx.notify(); - } - } - - fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if let Some(item) = self.active_searchable_item.as_ref() { - let focus_handle = item.focus_handle(cx); - cx.focus(&focus_handle); - cx.stop_propagation(); - } - } - - fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { - if let Some(new_query) = self.search_history.next().map(str::to_string) { - let _ = self.search(&new_query, Some(self.search_options), cx); - } else { - self.search_history.reset_selection(); - let _ = self.search("", Some(self.search_options), cx); - } - } - - fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { - if self.query(cx).is_empty() { - if let Some(new_query) = self.search_history.current().map(str::to_string) { - let _ = self.search(&new_query, Some(self.search_options), cx); - return; - } - } - - if let Some(new_query) = self.search_history.previous().map(str::to_string) { - let _ = self.search(&new_query, Some(self.search_options), cx); - } - } - fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { - self.activate_search_mode(next_mode(&self.current_mode, false), cx); - } - fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { - if let Some(_) = &self.active_searchable_item { - self.replace_enabled = !self.replace_enabled; - if !self.replace_enabled { - let handle = self.query_editor.focus_handle(cx); - cx.focus(&handle); - } - cx.notify(); - } - } - fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { - let mut should_propagate = true; - if !self.dismissed && self.active_search.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(query) = self.active_search.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if let Some(active_index) = self.active_match_index { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace(&matches[active_index], &query, cx); - self.select_next_match(&SelectNextMatch, cx); - } - should_propagate = false; - self.focus_editor(&FocusEditor, cx); - } - } - } - } - if !should_propagate { - cx.stop_propagation(); - } - } - pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { - if !self.dismissed && self.active_search.is_some() { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(query) = self.active_search.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - for m in matches { - searchable_item.replace(m, &query, cx); - } - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use std::ops::Range; - - use super::*; - use editor::{DisplayPoint, Editor}; - use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext}; - use language::Buffer; - use smol::stream::StreamExt as _; - use unindent::Unindent as _; - - fn init_globals(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = settings::SettingsStore::test(cx); - cx.set_global(store); - editor::init(cx); - - language::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - }); - } - fn init_test( - cx: &mut TestAppContext, - ) -> ( - View, - View, - &mut VisualTestContext<'_>, - ) { - init_globals(cx); - let buffer = cx.new_model(|cx| { - Buffer::new( - 0, - cx.entity_id().as_u64(), - r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - ) - }); - let (_, cx) = cx.add_window_view(|_| EmptyView {}); - let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); - - let search_bar = cx.new_view(|cx| { - let mut search_bar = BufferSearchBar::new(cx); - search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(cx); - search_bar - }); - - (editor, search_bar, cx) - } - - #[gpui::test] - async fn test_search_simple(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - // todo! osiewicz: these tests asserted on background color as well, that should be brought back. - let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { - background_highlights - .into_iter() - .map(|(range, _)| range) - .collect::>() - }; - // Search for a string that appears with different casing. - // By default, search is case-insensitive. - search_bar - .update(cx, |search_bar, cx| search_bar.search("us", None, cx)) - .await - .unwrap(); - editor.update(cx, |editor, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(cx)), - &[ - DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - ] - ); - }); - - // Switch to a case sensitive search. - search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update(cx, |editor, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(cx)), - &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] - ); - }); - - // Search for a string that appears both as a whole word and - // within other words. By default, all results are found. - search_bar - .update(cx, |search_bar, cx| search_bar.search("or", None, cx)) - .await - .unwrap(); - editor.update(cx, |editor, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(cx)), - &[ - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), - ] - ); - }); - - // Switch to a whole word search. - search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update(cx, |editor, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(cx)), - &[ - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - ] - ); - }); - - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) - }); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.select_next_match(&SelectNextMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_next_match(&SelectNextMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_next_match(&SelectNextMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_next_match(&SelectNextMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPrevMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPrevMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPrevMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the previous match selects - // the closest match to the left. - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) - }); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.select_prev_match(&SelectPrevMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the next match selects the - // closest match to the right. - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) - }); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.select_next_match(&SelectNextMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - // Park the cursor after the last match and ensure that going to the previous match selects - // the last match. - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)]) - }); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.select_prev_match(&SelectPrevMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - // Park the cursor after the last match and ensure that going to the next match selects the - // first match. - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)]) - }); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.select_next_match(&SelectNextMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor before the first match and ensure that going to the previous match - // selects the last match. - editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) - }); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.select_prev_match(&SelectPrevMatch, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - } - - #[gpui::test] - async fn test_search_option_handling(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - // show with options should make current search case sensitive - search_bar - .update(cx, |search_bar, cx| { - search_bar.show(cx); - search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx) - }) - .await - .unwrap(); - // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back. - let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { - background_highlights - .into_iter() - .map(|(range, _)| range) - .collect::>() - }; - editor.update(cx, |editor, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(cx)), - &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] - ); - }); - - // search_suggested should restore default options - search_bar.update(cx, |search_bar, cx| { - search_bar.search_suggested(cx); - assert_eq!(search_bar.search_options, SearchOptions::NONE) - }); - - // toggling a search option should update the defaults - search_bar - .update(cx, |search_bar, cx| { - search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx) - }) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update(cx, |editor, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(cx)), - &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),] - ); - }); - - // defaults should still include whole word - search_bar.update(cx, |search_bar, cx| { - search_bar.search_suggested(cx); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD - ) - }); - } - - #[gpui::test] - async fn test_search_select_all_matches(cx: &mut TestAppContext) { - init_globals(cx); - let buffer_text = r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(); - let expected_query_matches_count = buffer_text - .chars() - .filter(|c| c.to_ascii_lowercase() == 'a') - .count(); - assert!( - expected_query_matches_count > 1, - "Should pick a query with multiple results" - ); - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); - let window = cx.add_window(|_| EmptyView {}); - - let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); - - let search_bar = window.build_view(cx, |cx| { - let mut search_bar = BufferSearchBar::new(cx); - search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(cx); - search_bar - }); - - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx)) - }) - .unwrap() - .await - .unwrap(); - let initial_selections = window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - let handle = search_bar.query_editor.focus_handle(cx); - cx.focus(&handle); - search_bar.activate_current_match(cx); - }); - assert!( - !editor.read(cx).is_focused(cx), - "Initially, the editor should not be focused" - ); - let initial_selections = editor.update(cx, |editor, cx| { - let initial_selections = editor.selections.display_ranges(cx); - assert_eq!( - initial_selections.len(), 1, - "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", - ); - initial_selections - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - let handle = search_bar.query_editor.focus_handle(cx); - cx.focus(&handle); - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - assert!( - editor.read(cx).is_focused(cx), - "Should focus editor after successful SelectAllMatches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx)); - initial_selections - }).unwrap(); - - window - .update(cx, |_, cx| { - assert!( - editor.read(cx).is_focused(cx), - "Should still have editor focused after SelectNextMatch" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - 1, - "On next match, should deselect items and select the next match" - ); - assert_ne!( - all_selections, initial_selections, - "Next match should be different from the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should be updated to the next one" - ); - let handle = search_bar.query_editor.focus_handle(cx); - cx.focus(&handle); - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - assert!( - editor.read(cx).is_focused(cx), - "Should focus editor after successful SelectAllMatches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should not change after selecting all matches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPrevMatch, cx); - }); - }) - .unwrap(); - let last_match_selections = window - .update(cx, |_, cx| { - assert!( - editor.read(cx).is_focused(&cx), - "Should still have editor focused after SelectPrevMatch" - ); - - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - 1, - "On previous match, should deselect items and select the previous item" - ); - assert_eq!( - all_selections, initial_selections, - "Previous match should be the same as the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should be updated to the previous one" - ); - all_selections - }) - }) - .unwrap(); - - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - let handle = search_bar.query_editor.focus_handle(cx); - cx.focus(&handle); - search_bar.search("abas_nonexistent_match", None, cx) - }) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - assert!( - editor.update(cx, |this, cx| !this.is_focused(cx.window_context())), - "Should not switch focus to editor if SelectAllMatches does not find any matches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections, last_match_selections, - "Should not select anything new if there are no matches" - ); - assert!( - search_bar.active_match_index.is_none(), - "For no matches, there should be no active match index" - ); - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - //crate::project_search::tests::init_test(cx); - init_globals(cx); - let buffer_text = r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(); - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); - let (_, cx) = cx.add_window_view(|_| EmptyView {}); - - let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); - - let search_bar = cx.new_view(|cx| { - let mut search_bar = BufferSearchBar::new(cx); - search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(cx); - search_bar - }); - - // Add 3 search items into the history. - search_bar - .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) - .await - .unwrap(); - search_bar - .update(cx, |search_bar, cx| search_bar.search("b", None, cx)) - .await - .unwrap(); - search_bar - .update(cx, |search_bar, cx| { - search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx) - }) - .await - .unwrap(); - // Ensure that the latest search is active. - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next history query after the latest should set the query to the empty string. - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // First previous query for empty current query should set the query to the latest. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Further previous items should go over the history in reverse order. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Previous items should never go behind the first history item. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "a"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "a"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next items should go over the history in the original order. - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - search_bar - .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "ba"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - - // New search input should add another entry to history and move the selection to the end of the history. - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "ba"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - } - - #[gpui::test] - async fn test_replace_simple(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - search_bar - .update(cx, |search_bar, cx| { - search_bar.search("expression", None, cx) - }) - .await - .unwrap(); - - search_bar.update(cx, |search_bar, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. - editor.set_text("expr$1", cx); - }); - search_bar.replace_all(&ReplaceAll, cx) - }); - assert_eq!( - editor.update(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex or regexp;[1] also referred to as - rational expr$1[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - - // Search for word boundaries and replace just a single one. - search_bar - .update(cx, |search_bar, cx| { - search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx) - }) - .await - .unwrap(); - - search_bar.update(cx, |search_bar, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("banana", cx); - }); - search_bar.replace_next(&ReplaceNext, cx) - }); - // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. - assert_eq!( - editor.update(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;[1] also referred to as - rational expr$1[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - // Let's turn on regex mode. - search_bar - .update(cx, |search_bar, cx| { - search_bar.activate_search_mode(SearchMode::Regex, cx); - search_bar.search("\\[([^\\]]+)\\]", None, cx) - }) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("${1}number", cx); - }); - search_bar.replace_all(&ReplaceAll, cx) - }); - assert_eq!( - editor.update(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;1number also referred to as - rational expr$12number3number) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - // Now with a whole-word twist. - search_bar - .update(cx, |search_bar, cx| { - search_bar.activate_search_mode(SearchMode::Regex, cx); - search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx) - }) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("things", cx); - }); - search_bar.replace_all(&ReplaceAll, cx) - }); - // The only word affected by this edit should be `algorithms`, even though there's a bunch - // of words in this text that would match this regex if not for WHOLE_WORD. - assert_eq!( - editor.update(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;1number also referred to as - rational expr$12number3number) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching things - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - } -} diff --git a/crates/search2/src/history.rs b/crates/search2/src/history.rs deleted file mode 100644 index 6b06c60293..0000000000 --- a/crates/search2/src/history.rs +++ /dev/null @@ -1,184 +0,0 @@ -use smallvec::SmallVec; -const SEARCH_HISTORY_LIMIT: usize = 20; - -#[derive(Default, Debug, Clone)] -pub struct SearchHistory { - history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, - selected: Option, -} - -impl SearchHistory { - pub fn add(&mut self, search_string: String) { - if let Some(i) = self.selected { - if search_string == self.history[i] { - return; - } - } - - if let Some(previously_searched) = self.history.last_mut() { - if search_string.find(previously_searched.as_str()).is_some() { - *previously_searched = search_string; - self.selected = Some(self.history.len() - 1); - return; - } - } - - self.history.push(search_string); - if self.history.len() > SEARCH_HISTORY_LIMIT { - self.history.remove(0); - } - self.selected = Some(self.history.len() - 1); - } - - pub fn next(&mut self) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - - let selected = self.selected?; - if selected == history_size - 1 { - return None; - } - let next_index = selected + 1; - self.selected = Some(next_index); - Some(&self.history[next_index]) - } - - pub fn current(&self) -> Option<&str> { - Some(&self.history[self.selected?]) - } - - pub fn previous(&mut self) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - - let prev_index = match self.selected { - Some(selected_index) => { - if selected_index == 0 { - return None; - } else { - selected_index - 1 - } - } - None => history_size - 1, - }; - - self.selected = Some(prev_index); - Some(&self.history[prev_index]) - } - - pub fn reset_selection(&mut self) { - self.selected = None; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_add() { - let mut search_history = SearchHistory::default(); - assert_eq!( - search_history.current(), - None, - "No current selection should be set fo the default search history" - ); - - search_history.add("rust".to_string()); - assert_eq!( - search_history.current(), - Some("rust"), - "Newly added item should be selected" - ); - - // check if duplicates are not added - search_history.add("rust".to_string()); - assert_eq!( - search_history.history.len(), - 1, - "Should not add a duplicate" - ); - assert_eq!(search_history.current(), Some("rust")); - - // check if new string containing the previous string replaces it - search_history.add("rustlang".to_string()); - assert_eq!( - search_history.history.len(), - 1, - "Should replace previous item if it's a substring" - ); - assert_eq!(search_history.current(), Some("rustlang")); - - // push enough items to test SEARCH_HISTORY_LIMIT - for i in 0..SEARCH_HISTORY_LIMIT * 2 { - search_history.add(format!("item{i}")); - } - assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); - } - - #[test] - fn test_next_and_previous() { - let mut search_history = SearchHistory::default(); - assert_eq!( - search_history.next(), - None, - "Default search history should not have a next item" - ); - - search_history.add("Rust".to_string()); - assert_eq!(search_history.next(), None); - search_history.add("JavaScript".to_string()); - assert_eq!(search_history.next(), None); - search_history.add("TypeScript".to_string()); - assert_eq!(search_history.next(), None); - - assert_eq!(search_history.current(), Some("TypeScript")); - - assert_eq!(search_history.previous(), Some("JavaScript")); - assert_eq!(search_history.current(), Some("JavaScript")); - - assert_eq!(search_history.previous(), Some("Rust")); - assert_eq!(search_history.current(), Some("Rust")); - - assert_eq!(search_history.previous(), None); - assert_eq!(search_history.current(), Some("Rust")); - - assert_eq!(search_history.next(), Some("JavaScript")); - assert_eq!(search_history.current(), Some("JavaScript")); - - assert_eq!(search_history.next(), Some("TypeScript")); - assert_eq!(search_history.current(), Some("TypeScript")); - - assert_eq!(search_history.next(), None); - assert_eq!(search_history.current(), Some("TypeScript")); - } - - #[test] - fn test_reset_selection() { - let mut search_history = SearchHistory::default(); - search_history.add("Rust".to_string()); - search_history.add("JavaScript".to_string()); - search_history.add("TypeScript".to_string()); - - assert_eq!(search_history.current(), Some("TypeScript")); - search_history.reset_selection(); - assert_eq!(search_history.current(), None); - assert_eq!( - search_history.previous(), - Some("TypeScript"), - "Should start from the end after reset on previous item query" - ); - - search_history.previous(); - assert_eq!(search_history.current(), Some("JavaScript")); - search_history.previous(); - assert_eq!(search_history.current(), Some("Rust")); - - search_history.reset_selection(); - assert_eq!(search_history.current(), None); - } -} diff --git a/crates/search2/src/mode.rs b/crates/search2/src/mode.rs deleted file mode 100644 index 3fd53cee49..0000000000 --- a/crates/search2/src/mode.rs +++ /dev/null @@ -1,46 +0,0 @@ -use gpui::{Action, SharedString}; - -use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; - -// TODO: Update the default search mode to get from config -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub enum SearchMode { - #[default] - Text, - Semantic, - Regex, -} - -impl SearchMode { - pub(crate) fn label(&self) -> &'static str { - match self { - SearchMode::Text => "Text", - SearchMode::Semantic => "Semantic", - SearchMode::Regex => "Regex", - } - } - pub(crate) fn tooltip(&self) -> SharedString { - format!("Activate {} Mode", self.label()).into() - } - pub(crate) fn action(&self) -> Box { - match self { - SearchMode::Text => ActivateTextMode.boxed_clone(), - SearchMode::Semantic => ActivateSemanticMode.boxed_clone(), - SearchMode::Regex => ActivateRegexMode.boxed_clone(), - } - } -} - -pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { - match mode { - SearchMode::Text => SearchMode::Regex, - SearchMode::Regex => { - if semantic_enabled { - SearchMode::Semantic - } else { - SearchMode::Text - } - } - SearchMode::Semantic => SearchMode::Text, - } -} diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs deleted file mode 100644 index 9a91d619a4..0000000000 --- a/crates/search2/src/project_search.rs +++ /dev/null @@ -1,2844 +0,0 @@ -use crate::{ - history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode, - ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, - ToggleReplace, ToggleWholeWord, -}; -use anyhow::{Context as _, Result}; -use collections::HashMap; -use editor::{ - items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent, - MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, -}; -use editor::{EditorElement, EditorStyle}; -use gpui::{ - actions, div, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, EventEmitter, - FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement, IntoElement, - KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled, - Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView, - WhiteSpace, WindowContext, -}; -use menu::Confirm; -use project::{ - search::{SearchInputs, SearchQuery}, - Entry, Project, -}; -use semantic_index::{SemanticIndex, SemanticIndexStatus}; - -use settings::Settings; -use smol::stream::StreamExt; -use std::{ - any::{Any, TypeId}, - collections::HashSet, - mem, - ops::{Not, Range}, - path::PathBuf, - time::{Duration, Instant}, -}; -use theme::ThemeSettings; - -use ui::{ - h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon, - LabelSize, Selectable, Tooltip, -}; -use util::{paths::PathMatcher, ResultExt as _}; -use workspace::{ - item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, - searchable::{Direction, SearchableItem, SearchableItemHandle}, - ItemNavHistory, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - WorkspaceId, -}; - -actions!( - project_search, - [SearchInNew, ToggleFocus, NextField, ToggleFilters] -); - -#[derive(Default)] -struct ActiveSearches(HashMap, WeakView>); - -#[derive(Default)] -struct ActiveSettings(HashMap, ProjectSearchSettings>); - -pub fn init(cx: &mut AppContext) { - // todo!() po - cx.set_global(ActiveSearches::default()); - cx.set_global(ActiveSettings::default()); - cx.observe_new_views(|workspace: &mut Workspace, _cx| { - workspace - .register_action(ProjectSearchView::deploy) - .register_action(ProjectSearchBar::search_in_new); - }) - .detach(); -} - -struct ProjectSearch { - project: Model, - excerpts: Model, - pending_search: Option>>, - match_ranges: Vec>, - active_query: Option, - search_id: usize, - search_history: SearchHistory, - no_results: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum InputPanel { - Query, - Exclude, - Include, -} - -pub struct ProjectSearchView { - focus_handle: FocusHandle, - model: Model, - query_editor: View, - replacement_editor: View, - results_editor: View, - semantic_state: Option, - semantic_permissioned: Option, - search_options: SearchOptions, - panels_with_errors: HashSet, - active_match_index: Option, - search_id: usize, - query_editor_was_focused: bool, - included_files_editor: View, - excluded_files_editor: View, - filters_enabled: bool, - replace_enabled: bool, - current_mode: SearchMode, - _subscriptions: Vec, -} - -struct SemanticState { - index_status: SemanticIndexStatus, - maintain_rate_limit: Option>, - _subscription: Subscription, -} - -#[derive(Debug, Clone)] -struct ProjectSearchSettings { - search_options: SearchOptions, - filters_enabled: bool, - current_mode: SearchMode, -} - -pub struct ProjectSearchBar { - active_project_search: Option>, - subscription: Option, -} - -impl ProjectSearch { - fn new(project: Model, cx: &mut ModelContext) -> Self { - let replica_id = project.read(cx).replica_id(); - Self { - project, - excerpts: cx.new_model(|_| MultiBuffer::new(replica_id)), - pending_search: Default::default(), - match_ranges: Default::default(), - active_query: None, - search_id: 0, - search_history: SearchHistory::default(), - no_results: None, - } - } - - fn clone(&self, cx: &mut ModelContext) -> Model { - cx.new_model(|cx| Self { - project: self.project.clone(), - excerpts: self - .excerpts - .update(cx, |excerpts, cx| cx.new_model(|cx| excerpts.clone(cx))), - pending_search: Default::default(), - match_ranges: self.match_ranges.clone(), - active_query: self.active_query.clone(), - search_id: self.search_id, - search_history: self.search_history.clone(), - no_results: self.no_results.clone(), - }) - } - - fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { - let search = self - .project - .update(cx, |project, cx| project.search(query.clone(), cx)); - self.search_id += 1; - self.search_history.add(query.as_str().to_string()); - self.active_query = Some(query); - self.match_ranges.clear(); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - let mut matches = search; - let this = this.upgrade()?; - this.update(&mut cx, |this, cx| { - this.match_ranges.clear(); - this.excerpts.update(cx, |this, cx| this.clear(cx)); - this.no_results = Some(true); - }) - .ok()?; - - while let Some((buffer, anchors)) = matches.next().await { - let mut ranges = this - .update(&mut cx, |this, cx| { - this.no_results = Some(false); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) - }) - }) - .ok()?; - - while let Some(range) = ranges.next().await { - this.update(&mut cx, |this, _| this.match_ranges.push(range)) - .ok()?; - } - this.update(&mut cx, |_, cx| cx.notify()).ok()?; - } - - this.update(&mut cx, |this, cx| { - this.pending_search.take(); - cx.notify(); - }) - .ok()?; - - None - })); - cx.notify(); - } - - fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { - let search = SemanticIndex::global(cx).map(|index| { - index.update(cx, |semantic_index, cx| { - semantic_index.search_project( - self.project.clone(), - inputs.as_str().to_owned(), - 10, - inputs.files_to_include().to_vec(), - inputs.files_to_exclude().to_vec(), - cx, - ) - }) - }); - self.search_id += 1; - self.match_ranges.clear(); - self.search_history.add(inputs.as_str().to_string()); - self.no_results = None; - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - let results = search?.await.log_err()?; - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])); - - this.update(&mut cx, |this, cx| { - this.no_results = Some(true); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - }); - }) - .ok()?; - for (buffer, ranges) in matches { - let mut match_ranges = this - .update(&mut cx, |this, cx| { - this.no_results = Some(false); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) - }) - }) - .ok()?; - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } - cx.notify(); - }) - .ok()?; - } - } - - this.update(&mut cx, |this, cx| { - this.pending_search.take(); - cx.notify(); - }) - .ok()?; - - None - })); - cx.notify(); - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ViewEvent { - UpdateTab, - Activate, - EditorEvent(editor::EditorEvent), - Dismiss, -} - -impl EventEmitter for ProjectSearchView {} - -impl Render for ProjectSearchView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - if self.has_matches() { - div() - .flex_1() - .size_full() - .track_focus(&self.focus_handle) - .child(self.results_editor.clone()) - .into_any() - } else { - let model = self.model.read(cx); - let has_no_results = model.no_results.unwrap_or(false); - let is_search_underway = model.pending_search.is_some(); - let mut major_text = if is_search_underway { - Label::new("Searching...") - } else if has_no_results { - Label::new("No results") - } else { - Label::new(format!("{} search all files", self.current_mode.label())) - }; - - let mut show_minor_text = true; - let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { - let status = semantic.index_status; - match status { - SemanticIndexStatus::NotAuthenticated => { - major_text = Label::new("Not Authenticated"); - show_minor_text = false; - Some( - "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()) - } - SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()), - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { - if remaining_files == 0 { - Some("Indexing...".to_string()) - } else { - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = - rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) { - Some(format!( - "Remaining files to index (rate limit resets in {}s): {}", - remaining_seconds.as_secs(), - remaining_files - )) - } else { - Some(format!("Remaining files to index: {}", remaining_files)) - } - } else { - Some(format!("Remaining files to index: {}", remaining_files)) - } - } - } - SemanticIndexStatus::NotIndexed => None, - } - }); - let major_text = div().justify_center().max_w_96().child(major_text); - - let minor_text: Option = if let Some(no_results) = model.no_results { - if model.pending_search.is_none() && no_results { - Some("No results found in this project for the provided query".into()) - } else { - None - } - } else { - if let Some(mut semantic_status) = semantic_status { - semantic_status.extend(self.landing_text_minor().chars()); - Some(semantic_status.into()) - } else { - Some(self.landing_text_minor()) - } - }; - let minor_text = minor_text.map(|text| { - div() - .items_center() - .max_w_96() - .child(Label::new(text).size(LabelSize::Small)) - }); - v_stack() - .flex_1() - .size_full() - .justify_center() - .track_focus(&self.focus_handle) - .child( - h_stack() - .size_full() - .justify_center() - .child(h_stack().flex_1()) - .child(v_stack().child(major_text).children(minor_text)) - .child(h_stack().flex_1()), - ) - .into_any() - } - } -} - -impl FocusableView for ProjectSearchView { - fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ProjectSearchView { - type Event = ViewEvent; - fn tab_tooltip_text(&self, cx: &AppContext) -> Option { - let query_text = self.query_editor.read(cx).text(cx); - - query_text - .is_empty() - .not() - .then(|| query_text.into()) - .or_else(|| Some("Project Search".into())) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a View, - _: &'a AppContext, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.clone().into()) - } else if type_id == TypeId::of::() { - Some(self.results_editor.clone().into()) - } else { - None - } - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.deactivated(cx)); - } - - fn tab_content(&self, _: Option, selected: bool, cx: &WindowContext<'_>) -> AnyElement { - let last_query: Option = self - .model - .read(cx) - .search_history - .current() - .as_ref() - .map(|query| { - let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); - query_text.into() - }); - let tab_name = last_query - .filter(|query| !query.is_empty()) - .unwrap_or_else(|| "Project search".into()); - h_stack() - .gap_2() - .child(IconElement::new(Icon::MagnifyingGlass).color(if selected { - Color::Default - } else { - Color::Muted - })) - .child(Label::new(tab_name).color(if selected { - Color::Default - } else { - Color::Muted - })) - .into_any() - } - - fn for_each_project_item( - &self, - cx: &AppContext, - f: &mut dyn FnMut(EntityId, &dyn project::Item), - ) { - self.results_editor.for_each_project_item(cx, f) - } - - fn is_singleton(&self, _: &AppContext) -> bool { - false - } - - fn can_save(&self, _: &AppContext) -> bool { - true - } - - fn is_dirty(&self, cx: &AppContext) -> bool { - self.results_editor.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - self.results_editor.read(cx).has_conflict(cx) - } - - fn save( - &mut self, - project: Model, - cx: &mut ViewContext, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.save(project, cx)) - } - - fn save_as( - &mut self, - _: Model, - _: PathBuf, - _: &mut ViewContext, - ) -> Task> { - unreachable!("save_as should not have been called") - } - - fn reload( - &mut self, - project: Model, - cx: &mut ViewContext, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.reload(project, cx)) - } - - fn clone_on_split( - &self, - _workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Option> - where - Self: Sized, - { - let model = self.model.update(cx, |model, cx| model.clone(cx)); - Some(cx.new_view(|cx| Self::new(model, cx, None))) - } - - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); - } - - fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { - self.results_editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { - self.results_editor - .update(cx, |editor, cx| editor.navigate(data, cx)) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - ViewEvent::UpdateTab => { - f(ItemEvent::UpdateBreadcrumbs); - f(ItemEvent::UpdateTab); - } - ViewEvent::EditorEvent(editor_event) => { - Editor::to_item_events(editor_event, f); - } - ViewEvent::Dismiss => f(ItemEvent::CloseItem), - _ => {} - } - } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - if self.has_matches() { - ToolbarItemLocation::Secondary - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { - self.results_editor.breadcrumbs(theme, cx) - } - - fn serialized_item_kind() -> Option<&'static str> { - None - } - - fn deserialize( - _project: Model, - _workspace: WeakView, - _workspace_id: workspace::WorkspaceId, - _item_id: workspace::ItemId, - _cx: &mut ViewContext, - ) -> Task>> { - unimplemented!() - } -} - -impl ProjectSearchView { - fn toggle_filters(&mut self, cx: &mut ViewContext) { - self.filters_enabled = !self.filters_enabled; - cx.update_global(|state: &mut ActiveSettings, cx| { - state.0.insert( - self.model.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - } - - fn current_settings(&self) -> ProjectSearchSettings { - ProjectSearchSettings { - search_options: self.search_options, - filters_enabled: self.filters_enabled, - current_mode: self.current_mode, - } - } - fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) { - self.search_options.toggle(option); - cx.update_global(|state: &mut ActiveSettings, cx| { - state.0.insert( - self.model.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - } - - fn index_project(&mut self, cx: &mut ViewContext) { - if let Some(semantic_index) = SemanticIndex::global(cx) { - // Semantic search uses no options - self.search_options = SearchOptions::none(); - - let project = self.model.read(cx).project.clone(); - - semantic_index.update(cx, |semantic_index, cx| { - semantic_index - .index_project(project.clone(), cx) - .detach_and_log_err(cx); - }); - - self.semantic_state = Some(SemanticState { - index_status: semantic_index.read(cx).status(&project), - maintain_rate_limit: None, - _subscription: cx.observe(&semantic_index, Self::semantic_index_changed), - }); - self.semantic_index_changed(semantic_index, cx); - } - } - - fn semantic_index_changed( - &mut self, - semantic_index: Model, - cx: &mut ViewContext, - ) { - let project = self.model.read(cx).project.clone(); - if let Some(semantic_state) = self.semantic_state.as_mut() { - cx.notify(); - semantic_state.index_status = semantic_index.read(cx).status(&project); - if let SemanticIndexStatus::Indexing { - rate_limit_expiry: Some(_), - .. - } = &semantic_state.index_status - { - if semantic_state.maintain_rate_limit.is_none() { - semantic_state.maintain_rate_limit = - Some(cx.spawn(|this, mut cx| async move { - loop { - cx.background_executor().timer(Duration::from_secs(1)).await; - this.update(&mut cx, |_, cx| cx.notify()).log_err(); - } - })); - return; - } - } else { - semantic_state.maintain_rate_limit = None; - } - } - } - - fn clear_search(&mut self, cx: &mut ViewContext) { - self.model.update(cx, |model, cx| { - model.pending_search = None; - model.no_results = None; - model.match_ranges.clear(); - - model.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - }); - }); - } - - fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { - let previous_mode = self.current_mode; - if previous_mode == mode { - return; - } - - self.clear_search(cx); - self.current_mode = mode; - self.active_match_index = None; - - match mode { - SearchMode::Semantic => { - let has_permission = self.semantic_permissioned(cx); - self.active_match_index = None; - cx.spawn(|this, mut cx| async move { - let has_permission = has_permission.await?; - - if !has_permission { - let answer = this.update(&mut cx, |this, cx| { - let project = this.model.read(cx).project.clone(); - let project_name = project - .read(cx) - .worktree_root_names(cx) - .collect::>() - .join("/"); - let is_plural = - project_name.chars().filter(|letter| *letter == '/').count() > 0; - let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name, - if is_plural { - "s" - } else {""}); - cx.prompt( - PromptLevel::Info, - prompt_text.as_str(), - &["Continue", "Cancel"], - ) - })?; - - if answer.await? == 0 { - this.update(&mut cx, |this, _| { - this.semantic_permissioned = Some(true); - })?; - } else { - this.update(&mut cx, |this, cx| { - this.semantic_permissioned = Some(false); - debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected"); - this.activate_search_mode(previous_mode, cx); - })?; - return anyhow::Ok(()); - } - } - - this.update(&mut cx, |this, cx| { - this.index_project(cx); - })?; - - anyhow::Ok(()) - }).detach_and_log_err(cx); - } - SearchMode::Regex | SearchMode::Text => { - self.semantic_state = None; - self.active_match_index = None; - self.search(cx); - } - } - - cx.update_global(|state: &mut ActiveSettings, cx| { - state.0.insert( - self.model.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - - cx.notify(); - } - fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { - let model = self.model.read(cx); - if let Some(query) = model.active_query.as_ref() { - if model.match_ranges.is_empty() { - return; - } - if let Some(active_index) = self.active_match_index { - let query = query.clone().with_replacement(self.replacement(cx)); - self.results_editor.replace( - &(Box::new(model.match_ranges[active_index].clone()) as _), - &query, - cx, - ); - self.select_match(Direction::Next, cx) - } - } - } - pub fn replacement(&self, cx: &AppContext) -> String { - self.replacement_editor.read(cx).text(cx) - } - fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { - let model = self.model.read(cx); - if let Some(query) = model.active_query.as_ref() { - if model.match_ranges.is_empty() { - return; - } - if self.active_match_index.is_some() { - let query = query.clone().with_replacement(self.replacement(cx)); - let matches = model - .match_ranges - .iter() - .map(|item| Box::new(item.clone()) as _) - .collect::>(); - for item in matches { - self.results_editor.replace(&item, &query, cx); - } - } - } - } - - fn new( - model: Model, - cx: &mut ViewContext, - settings: Option, - ) -> Self { - let project; - let excerpts; - let mut replacement_text = None; - let mut query_text = String::new(); - let mut subscriptions = Vec::new(); - - // Read in settings if available - let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings { - ( - settings.search_options, - settings.current_mode, - settings.filters_enabled, - ) - } else { - (SearchOptions::NONE, Default::default(), false) - }; - - { - let model = model.read(cx); - project = model.project.clone(); - excerpts = model.excerpts.clone(); - if let Some(active_query) = model.active_query.as_ref() { - query_text = active_query.as_str().to_string(); - replacement_text = active_query.replacement().map(ToOwned::to_owned); - options = SearchOptions::from_query(active_query); - } - } - subscriptions.push(cx.observe(&model, |this, _, cx| this.model_changed(cx))); - - let query_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Text search all files", cx); - editor.set_text(query_text, cx); - editor - }); - // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&query_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - let replacement_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Replace in project..", cx); - if let Some(text) = replacement_text { - editor.set_text(text, cx); - } - editor - }); - let results_editor = cx.new_view(|cx| { - let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); - editor.set_searchable(false); - editor - }); - subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))); - - subscriptions.push( - cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| { - if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) { - this.update_match_index(cx); - } - // Reraise editor events for workspace item activation purposes - cx.emit(ViewEvent::EditorEvent(event.clone())); - }), - ); - - let included_files_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Include: crates/**/*.toml", cx); - - editor - }); - // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - - let excluded_files_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx); - - editor - }); - // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - - let focus_handle = cx.focus_handle(); - subscriptions.push(cx.on_focus_in(&focus_handle, |this, cx| { - if this.focus_handle.is_focused(cx) { - if this.has_matches() { - this.results_editor.focus_handle(cx).focus(cx); - } else { - this.query_editor.focus_handle(cx).focus(cx); - } - } - })); - - // Check if Worktrees have all been previously indexed - let mut this = ProjectSearchView { - focus_handle, - replacement_editor, - search_id: model.read(cx).search_id, - model, - query_editor, - results_editor, - semantic_state: None, - semantic_permissioned: None, - search_options: options, - panels_with_errors: HashSet::new(), - active_match_index: None, - query_editor_was_focused: false, - included_files_editor, - excluded_files_editor, - filters_enabled, - current_mode, - replace_enabled: false, - _subscriptions: subscriptions, - }; - this.model_changed(cx); - this - } - - fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { - if let Some(value) = self.semantic_permissioned { - return Task::ready(Ok(value)); - } - - SemanticIndex::global(cx) - .map(|semantic| { - let project = self.model.read(cx).project.clone(); - semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) - }) - .unwrap_or(Task::ready(Ok(false))) - } - - pub fn new_search_in_directory( - workspace: &mut Workspace, - dir_entry: &Entry, - cx: &mut ViewContext, - ) { - if !dir_entry.is_dir() { - return; - } - let Some(filter_str) = dir_entry.path.to_str() else { - return; - }; - - let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, None)); - workspace.add_item(Box::new(search.clone()), cx); - search.update(cx, |search, cx| { - search - .included_files_editor - .update(cx, |editor, cx| editor.set_text(filter_str, cx)); - search.filters_enabled = true; - search.focus_query_editor(cx) - }); - } - - // Add another search tab to the workspace. - fn deploy( - workspace: &mut Workspace, - _: &workspace::NewSearch, - cx: &mut ViewContext, - ) { - // Clean up entries for dropped projects - cx.update_global(|state: &mut ActiveSearches, _cx| { - state.0.retain(|project, _| project.is_upgradable()) - }); - - let query = workspace.active_item(cx).and_then(|item| { - let editor = item.act_as::(cx)?; - let query = editor.query_suggestion(cx); - if query.is_empty() { - None - } else { - Some(query) - } - }); - - let settings = cx - .global::() - .0 - .get(&workspace.project().downgrade()); - - let settings = if let Some(settings) = settings { - Some(settings.clone()) - } else { - None - }; - - let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings)); - - workspace.add_item(Box::new(search.clone()), cx); - - search.update(cx, |search, cx| { - if let Some(query) = query { - search.set_query(&query, cx); - } - search.focus_query_editor(cx) - }); - } - - fn search(&mut self, cx: &mut ViewContext) { - let mode = self.current_mode; - match mode { - SearchMode::Semantic => { - if self.semantic_state.is_some() { - if let Some(query) = self.build_search_query(cx) { - self.model - .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx)); - } - } - } - - _ => { - if let Some(query) = self.build_search_query(cx) { - self.model.update(cx, |model, cx| model.search(query, cx)); - } - } - } - } - - fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { - // Do not bail early in this function, as we want to fill out `self.panels_with_errors`. - let text = self.query_editor.read(cx).text(cx); - let included_files = - match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) { - Ok(included_files) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Include); - if should_unmark_error { - cx.notify(); - } - included_files - } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Include); - if should_mark_error { - cx.notify(); - } - vec![] - } - }; - let excluded_files = - match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) { - Ok(excluded_files) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Exclude); - if should_unmark_error { - cx.notify(); - } - - excluded_files - } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Exclude); - if should_mark_error { - cx.notify(); - } - vec![] - } - }; - - let current_mode = self.current_mode; - let query = match current_mode { - SearchMode::Regex => { - match SearchQuery::regex( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - self.search_options.contains(SearchOptions::INCLUDE_IGNORED), - included_files, - excluded_files, - ) { - Ok(query) => { - let should_unmark_error = - self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error { - cx.notify(); - } - - Some(query) - } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { - cx.notify(); - } - - None - } - } - } - _ => match SearchQuery::text( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - self.search_options.contains(SearchOptions::INCLUDE_IGNORED), - included_files, - excluded_files, - ) { - Ok(query) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error { - cx.notify(); - } - - Some(query) - } - Err(_e) => { - let should_mark_error = self.panels_with_errors.insert(InputPanel::Query); - if should_mark_error { - cx.notify(); - } - - None - } - }, - }; - if !self.panels_with_errors.is_empty() { - return None; - } - query - } - - fn parse_path_matches(text: &str) -> anyhow::Result> { - text.split(',') - .map(str::trim) - .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) - .map(|maybe_glob_str| { - PathMatcher::new(maybe_glob_str) - .with_context(|| format!("parsing {maybe_glob_str} as path matcher")) - }) - .collect() - } - - fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { - if let Some(index) = self.active_match_index { - let match_ranges = self.model.read(cx).match_ranges.clone(); - let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, 1, cx) - }); - - let range_to_select = match_ranges[new_index].clone(); - self.results_editor.update(cx, |editor, cx| { - let range_to_select = editor.range_for_match(&range_to_select); - editor.unfold_ranges([range_to_select.clone()], false, true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range_to_select]) - }); - }); - } - } - - fn focus_query_editor(&mut self, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&SelectAll, cx); - }); - self.query_editor_was_focused = true; - let editor_handle = self.query_editor.focus_handle(cx); - cx.focus(&editor_handle); - } - - fn set_query(&mut self, query: &str, cx: &mut ViewContext) { - self.query_editor - .update(cx, |query_editor, cx| query_editor.set_text(query, cx)); - } - - fn focus_results_editor(&mut self, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - let cursor = query_editor.selections.newest_anchor().head(); - query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor])); - }); - self.query_editor_was_focused = false; - let results_handle = self.results_editor.focus_handle(cx); - cx.focus(&results_handle); - } - - fn model_changed(&mut self, cx: &mut ViewContext) { - let match_ranges = self.model.read(cx).match_ranges.clone(); - if match_ranges.is_empty() { - self.active_match_index = None; - } else { - self.active_match_index = Some(0); - self.update_match_index(cx); - let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); - let is_new_search = self.search_id != prev_search_id; - self.results_editor.update(cx, |editor, cx| { - if is_new_search { - let range_to_select = match_ranges - .first() - .clone() - .map(|range| editor.range_for_match(range)); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges(range_to_select) - }); - } - editor.highlight_background::( - match_ranges, - |theme| theme.search_match_background, - cx, - ); - }); - if is_new_search && self.query_editor.focus_handle(cx).is_focused(cx) { - self.focus_results_editor(cx); - } - } - - cx.emit(ViewEvent::UpdateTab); - cx.notify(); - } - - fn update_match_index(&mut self, cx: &mut ViewContext) { - let results_editor = self.results_editor.read(cx); - let new_index = active_match_index( - &self.model.read(cx).match_ranges, - &results_editor.selections.newest_anchor().head(), - &results_editor.buffer().read(cx).snapshot(cx), - ); - if self.active_match_index != new_index { - self.active_match_index = new_index; - cx.notify(); - } - } - - pub fn has_matches(&self) -> bool { - self.active_match_index.is_some() - } - - fn landing_text_minor(&self) -> SharedString { - match self.current_mode { - SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(), - SearchMode::Semantic => "\nSimply explain the code you are looking to find. ex. 'prompt user for permissions to index their project'".into() - } - } - fn border_color_for(&self, panel: InputPanel, cx: &WindowContext) -> Hsla { - if self.panels_with_errors.contains(&panel) { - Color::Error.color(cx) - } else { - cx.theme().colors().border - } - } - fn move_focus_to_results(&mut self, cx: &mut ViewContext) { - if !self.results_editor.focus_handle(cx).is_focused(cx) - && !self.model.read(cx).match_ranges.is_empty() - { - cx.stop_propagation(); - return self.focus_results_editor(cx); - } - } -} - -impl Default for ProjectSearchBar { - fn default() -> Self { - Self::new() - } -} - -impl ProjectSearchBar { - pub fn new() -> Self { - Self { - active_project_search: Default::default(), - subscription: Default::default(), - } - } - - fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext) { - if let Some(view) = self.active_project_search.as_ref() { - view.update(cx, |this, cx| { - let new_mode = - crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); - this.activate_search_mode(new_mode, cx); - let editor_handle = this.query_editor.focus_handle(cx); - cx.focus(&editor_handle); - }); - } - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - if !search_view - .replacement_editor - .focus_handle(cx) - .is_focused(cx) - { - cx.stop_propagation(); - search_view.search(cx); - } - }); - } - } - - fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let new_query = search_view.update(cx, |search_view, cx| { - let new_query = search_view.build_search_query(cx); - if new_query.is_some() { - if let Some(old_query) = search_view.model.read(cx).active_query.clone() { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), cx); - }); - search_view.search_options = SearchOptions::from_query(&old_query); - } - } - new_query - }); - if let Some(new_query) = new_query { - let model = cx.new_model(|cx| { - let mut model = ProjectSearch::new(workspace.project().clone(), cx); - model.search(new_query, cx); - model - }); - workspace.add_item( - Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))), - cx, - ); - } - } - } - - fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { - self.cycle_field(Direction::Next, cx); - } - - fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext) { - self.cycle_field(Direction::Prev, cx); - } - - fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext) { - let active_project_search = match &self.active_project_search { - Some(active_project_search) => active_project_search, - - None => { - return; - } - }; - - active_project_search.update(cx, |project_view, cx| { - let mut views = vec![&project_view.query_editor]; - if project_view.filters_enabled { - views.extend([ - &project_view.included_files_editor, - &project_view.excluded_files_editor, - ]); - } - if project_view.replace_enabled { - views.push(&project_view.replacement_editor); - } - let current_index = match views - .iter() - .enumerate() - .find(|(_, view)| view.focus_handle(cx).is_focused(cx)) - { - Some((index, _)) => index, - - None => { - return; - } - }; - - let new_index = match direction { - Direction::Next => (current_index + 1) % views.len(), - Direction::Prev if current_index == 0 => views.len() - 1, - Direction::Prev => (current_index - 1) % views.len(), - }; - let next_focus_handle = views[new_index].focus_handle(cx); - cx.focus(&next_focus_handle); - cx.stop_propagation(); - }); - } - - fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.toggle_search_option(option, cx); - search_view.search(cx); - }); - - cx.notify(); - true - } else { - false - } - } - - fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { - if let Some(search) = &self.active_project_search { - search.update(cx, |this, cx| { - this.replace_enabled = !this.replace_enabled; - let editor_to_focus = if !this.replace_enabled { - this.query_editor.focus_handle(cx) - } else { - this.replacement_editor.focus_handle(cx) - }; - cx.focus(&editor_to_focus); - cx.notify(); - }); - } - } - - fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.toggle_filters(cx); - search_view - .included_files_editor - .update(cx, |_, cx| cx.notify()); - search_view - .excluded_files_editor - .update(cx, |_, cx| cx.notify()); - cx.refresh(); - cx.notify(); - }); - cx.notify(); - true - } else { - false - } - } - - fn move_focus_to_results(&self, cx: &mut ViewContext) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.move_focus_to_results(cx); - }); - cx.notify(); - } - } - - fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { - // Update Current Mode - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.activate_search_mode(mode, cx); - }); - cx.notify(); - } - } - - fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { - if let Some(search) = self.active_project_search.as_ref() { - search.read(cx).search_options.contains(option) - } else { - false - } - } - - fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - let new_query = search_view.model.update(cx, |model, _| { - if let Some(new_query) = model.search_history.next().map(str::to_string) { - new_query - } else { - model.search_history.reset_selection(); - String::new() - } - }); - search_view.set_query(&new_query, cx); - }); - } - } - - fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - if search_view.query_editor.read(cx).text(cx).is_empty() { - if let Some(new_query) = search_view - .model - .read(cx) - .search_history - .current() - .map(str::to_string) - { - search_view.set_query(&new_query, cx); - return; - } - } - - if let Some(new_query) = search_view.model.update(cx, |model, _| { - model.search_history.previous().map(str::to_string) - }) { - search_view.set_query(&new_query, cx); - } - }); - } - } - - fn new_placeholder_text(&self, cx: &mut ViewContext) -> Option { - let previous_query_keystrokes = cx - .bindings_for_action(&PreviousHistoryQuery {}) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let next_query_keystrokes = cx - .bindings_for_action(&NextHistoryQuery {}) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(|k| k.to_string()) - .collect::>() - }); - let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { - (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => Some(format!( - "Search ({}/{} for previous/next query)", - previous_query_keystrokes.join(" "), - next_query_keystrokes.join(" ") - )), - (None, Some(next_query_keystrokes)) => Some(format!( - "Search ({} for next query)", - next_query_keystrokes.join(" ") - )), - (Some(previous_query_keystrokes), None) => Some(format!( - "Search ({} for previous query)", - previous_query_keystrokes.join(" ") - )), - (None, None) => None, - }; - new_placeholder_text - } - - fn render_text_input(&self, editor: &View, cx: &ViewContext) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if editor.read(cx).read_only() { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(1.3).into(), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }; - - EditorElement::new( - &editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - } -} - -impl Render for ProjectSearchBar { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let Some(search) = self.active_project_search.clone() else { - return div(); - }; - let mut key_context = KeyContext::default(); - key_context.add("ProjectSearchBar"); - if let Some(placeholder_text) = self.new_placeholder_text(cx) { - search.update(cx, |search, cx| { - search.query_editor.update(cx, |this, cx| { - this.set_placeholder_text(placeholder_text, cx) - }) - }); - } - let search = search.read(cx); - let semantic_is_available = SemanticIndex::enabled(cx); - - let query_column = v_stack().child( - h_stack() - .min_w(rems(512. / 16.)) - .px_2() - .py_1() - .gap_2() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(search.border_color_for(InputPanel::Query, cx)) - .rounded_lg() - .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) - .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx))) - .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) - .child(IconElement::new(Icon::MagnifyingGlass)) - .child(self.render_text_input(&search.query_editor, cx)) - .child( - h_stack() - .child( - IconButton::new("project-search-filter-button", Icon::Filter) - .tooltip(|cx| { - Tooltip::for_action("Toggle filters", &ToggleFilters, cx) - }) - .on_click(cx.listener(|this, _, cx| { - this.toggle_filters(cx); - })) - .selected( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).filters_enabled) - .unwrap_or_default(), - ), - ) - .when(search.current_mode != SearchMode::Semantic, |this| { - this.child( - IconButton::new( - "project-search-case-sensitive", - Icon::CaseSensitive, - ) - .tooltip(|cx| { - Tooltip::for_action( - "Toggle case sensitive", - &ToggleCaseSensitive, - cx, - ) - }) - .selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx)) - .on_click(cx.listener( - |this, _, cx| { - this.toggle_search_option( - SearchOptions::CASE_SENSITIVE, - cx, - ); - }, - )), - ) - .child( - IconButton::new("project-search-whole-word", Icon::WholeWord) - .tooltip(|cx| { - Tooltip::for_action( - "Toggle whole word", - &ToggleWholeWord, - cx, - ) - }) - .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx)) - .on_click(cx.listener(|this, _, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - })), - ) - }), - ), - ); - - let mode_column = v_stack().items_start().justify_start().child( - h_stack() - .child( - h_stack() - .child( - Button::new("project-search-text-button", "Text") - .selected(search.current_mode == SearchMode::Text) - .on_click(cx.listener(|this, _, cx| { - this.activate_search_mode(SearchMode::Text, cx) - })) - .tooltip(|cx| { - Tooltip::for_action("Toggle text search", &ActivateTextMode, cx) - }), - ) - .child( - Button::new("project-search-regex-button", "Regex") - .selected(search.current_mode == SearchMode::Regex) - .on_click(cx.listener(|this, _, cx| { - this.activate_search_mode(SearchMode::Regex, cx) - })) - .tooltip(|cx| { - Tooltip::for_action( - "Toggle regular expression search", - &ActivateRegexMode, - cx, - ) - }), - ) - .when(semantic_is_available, |this| { - this.child( - Button::new("project-search-semantic-button", "Semantic") - .selected(search.current_mode == SearchMode::Semantic) - .on_click(cx.listener(|this, _, cx| { - this.activate_search_mode(SearchMode::Semantic, cx) - })) - .tooltip(|cx| { - Tooltip::for_action( - "Toggle semantic search", - &ActivateSemanticMode, - cx, - ) - }), - ) - }), - ) - .child( - IconButton::new("project-search-toggle-replace", Icon::Replace) - .on_click(cx.listener(|this, _, cx| { - this.toggle_replace(&ToggleReplace, cx); - })) - .tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)), - ), - ); - let replace_column = if search.replace_enabled { - h_stack() - .flex_1() - .h_full() - .gap_2() - .px_2() - .py_1() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_lg() - .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small)) - .child(self.render_text_input(&search.replacement_editor, cx)) - } else { - // Fill out the space if we don't have a replacement editor. - h_stack().flex_1() - }; - let actions_column = h_stack() - .when(search.replace_enabled, |this| { - this.child( - IconButton::new("project-search-replace-next", Icon::ReplaceNext) - .on_click(cx.listener(|this, _, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(&ReplaceNext, cx); - }) - } - })) - .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), - ) - .child( - IconButton::new("project-search-replace-all", Icon::ReplaceAll) - .on_click(cx.listener(|this, _, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(&ReplaceAll, cx); - }) - } - })) - .tooltip(|cx| Tooltip::for_action("Replace all matches", &ReplaceAll, cx)), - ) - }) - .when_some(search.active_match_index, |mut this, index| { - let index = index + 1; - let match_quantity = search.model.read(cx).match_ranges.len(); - if match_quantity > 0 { - debug_assert!(match_quantity >= index); - this = this.child(Label::new(format!("{index}/{match_quantity}"))) - } - this - }) - .child( - IconButton::new("project-search-prev-match", Icon::ChevronLeft) - .disabled(search.active_match_index.is_none()) - .on_click(cx.listener(|this, _, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Prev, cx); - }) - } - })) - .tooltip(|cx| { - Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx) - }), - ) - .child( - IconButton::new("project-search-next-match", Icon::ChevronRight) - .disabled(search.active_match_index.is_none()) - .on_click(cx.listener(|this, _, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Next, cx); - }) - } - })) - .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)), - ); - - v_stack() - .key_context(key_context) - .flex_grow() - .gap_2() - .on_action(cx.listener(|this, _: &ToggleFocus, cx| this.move_focus_to_results(cx))) - .on_action(cx.listener(|this, _: &ToggleFilters, cx| { - this.toggle_filters(cx); - })) - .on_action(cx.listener(|this, _: &ActivateTextMode, cx| { - this.activate_search_mode(SearchMode::Text, cx) - })) - .on_action(cx.listener(|this, _: &ActivateRegexMode, cx| { - this.activate_search_mode(SearchMode::Regex, cx) - })) - .on_action(cx.listener(|this, _: &ActivateSemanticMode, cx| { - this.activate_search_mode(SearchMode::Semantic, cx) - })) - .capture_action(cx.listener(|this, action, cx| { - this.tab(action, cx); - cx.stop_propagation(); - })) - .capture_action(cx.listener(|this, action, cx| { - this.tab_previous(action, cx); - cx.stop_propagation(); - })) - .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) - .on_action(cx.listener(|this, action, cx| { - this.cycle_mode(action, cx); - })) - .when(search.current_mode != SearchMode::Semantic, |this| { - this.on_action(cx.listener(|this, action, cx| { - this.toggle_replace(action, cx); - })) - .on_action(cx.listener(|this, _: &ToggleWholeWord, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx); - })) - .on_action(cx.listener(|this, _: &ToggleCaseSensitive, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - })) - .on_action(cx.listener(|this, action, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(action, cx); - }) - } - })) - .on_action(cx.listener(|this, action, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(action, cx); - }) - } - })) - .when(search.filters_enabled, |this| { - this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx); - })) - }) - }) - .child( - h_stack() - .justify_between() - .child(query_column) - .child(mode_column) - .child(replace_column) - .child(actions_column), - ) - .when(search.filters_enabled, |this| { - this.child( - h_stack() - .flex_1() - .gap_2() - .justify_between() - .child( - h_stack() - .flex_1() - .h_full() - .px_2() - .py_1() - .border_1() - .border_color(search.border_color_for(InputPanel::Include, cx)) - .rounded_lg() - .child(self.render_text_input(&search.included_files_editor, cx)) - .when(search.current_mode != SearchMode::Semantic, |this| { - this.child( - SearchOptions::INCLUDE_IGNORED.as_button( - search - .search_options - .contains(SearchOptions::INCLUDE_IGNORED), - cx.listener(|this, _, cx| { - this.toggle_search_option( - SearchOptions::INCLUDE_IGNORED, - cx, - ); - }), - ), - ) - }), - ) - .child( - h_stack() - .flex_1() - .h_full() - .px_2() - .py_1() - .border_1() - .border_color(search.border_color_for(InputPanel::Exclude, cx)) - .rounded_lg() - .child(self.render_text_input(&search.excluded_files_editor, cx)), - ), - ) - }) - } -} - -impl EventEmitter for ProjectSearchBar {} - -impl ToolbarItemView for ProjectSearchBar { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation { - cx.notify(); - self.subscription = None; - self.active_project_search = None; - if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { - search.update(cx, |search, cx| { - if search.current_mode == SearchMode::Semantic { - search.index_project(cx); - } - }); - - self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); - self.active_project_search = Some(search); - ToolbarItemLocation::PrimaryLeft {} - } else { - ToolbarItemLocation::Hidden - } - } - - fn row_count(&self, cx: &WindowContext<'_>) -> usize { - if let Some(search) = self.active_project_search.as_ref() { - if search.read(cx).filters_enabled { - return 2; - } - } - 1 - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use editor::DisplayPoint; - use gpui::{Action, TestAppContext}; - use project::FakeFs; - use semantic_index::semantic_index_settings::SemanticIndexSettings; - use serde_json::json; - use settings::{Settings, SettingsStore}; - - #[gpui::test] - async fn test_project_search(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let search = cx.new_model(|cx| ProjectSearch::new(project, cx)); - let search_view = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)); - - search_view - .update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" - ); - let match_background_color = cx.theme().colors().search_match_background; - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), - &[ - ( - DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), - match_background_color - ), - ( - DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), - match_background_color - ), - ( - DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), - match_background_color - ) - ] - ); - assert_eq!(search_view.active_match_index, Some(0)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] - ); - - search_view.select_match(Direction::Next, cx); - }).unwrap(); - - search_view - .update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(1)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] - ); - search_view.select_match(Direction::Next, cx); - }) - .unwrap(); - - search_view - .update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(2)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] - ); - search_view.select_match(Direction::Next, cx); - }) - .unwrap(); - - search_view - .update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(0)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] - ); - search_view.select_match(Direction::Prev, cx); - }) - .unwrap(); - - search_view - .update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(2)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] - ); - search_view.select_match(Direction::Prev, cx); - }) - .unwrap(); - - search_view - .update(cx, |search_view, cx| { - assert_eq!(search_view.active_match_index, Some(1)); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.selections.display_ranges(cx)), - [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] - ); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_project_search_focus(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.clone(); - let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) - }); - - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, cx| { - cx.dispatch_action(ToggleFocus.boxed_clone()) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - - window.update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(cx), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.focus_handle(cx).is_focused(cx), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); - }) - .unwrap(); - - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for mismatching query should have no results but got '{results_text}'" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(cx), - "Search view should be focused after mismatching query had been used in search", - ); - }); - }) - .unwrap(); - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, cx| { - cx.dispatch_action(ToggleFocus.boxed_clone()) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(cx), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, cx| - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.focus_handle(cx).is_focused(cx), - "Search view with mismatching query should be focused after search results are available", - ); - })).unwrap(); - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, cx| { - cx.dispatch_action(ToggleFocus.boxed_clone()) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(cx), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); - }).unwrap(); - - workspace - .update(cx, |workspace, cx| { - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - let Some(search_view_2) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - assert!( - search_view_2 != search_view, - "New search view should be open after `workspace::NewSearch` event" - ); - - window.update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results of the first search view should not update too" - ); - assert!( - !search_view.query_editor.focus_handle(cx).is_focused(cx), - "Focus should be moved away from the first search view" - ); - }); - }).unwrap(); - - window.update(cx, |_, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2.query_editor.read(cx).text(cx), - "two", - "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" - ); - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "", - "No search results should be in the 2nd view yet, as we did not spawn a search for it" - ); - assert!( - search_view_2.query_editor.focus_handle(cx).is_focused(cx), - "Focus should be moved into query editor fo the new window" - ); - }); - }).unwrap(); - - window - .update(cx, |_, cx| { - search_view_2.update(cx, |search_view_2, cx| { - search_view_2 - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx)); - search_view_2.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window.update(cx, |_, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "New search view with the updated query should have new search results" - ); - assert!( - search_view_2.results_editor.focus_handle(cx).is_focused(cx), - "Search view with mismatching query should be focused after search results are available", - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, cx| { - cx.dispatch_action(ToggleFocus.boxed_clone()) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert!( - search_view_2.results_editor.focus_handle(cx).is_focused(cx), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - });}).unwrap(); - } - - #[gpui::test] - async fn test_new_project_search_in_directory(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "a": { - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - }, - "b": { - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }, - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees().next().unwrap().read(cx).id() - }); - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx).unwrap(); - let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) - }); - }) - .unwrap(); - - let one_file_entry = cx.update(|cx| { - workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&(worktree_id, "a/one.rs").into(), cx) - .expect("no entry for /a/one.rs file") - }); - assert!(one_file_entry.is_file()); - window - .update(cx, |workspace, cx| { - ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) - }) - .unwrap(); - let active_search_entry = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_search_entry.is_none(), - "Expected no search panel to be active for file entry" - ); - - let a_dir_entry = cx.update(|cx| { - workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&(worktree_id, "a").into(), cx) - .expect("no entry for /a/ directory") - }); - assert!(a_dir_entry.is_dir()); - window - .update(cx, |workspace, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search in directory event trigger") - }; - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(cx), - "On new search in directory, focus should be moved into query editor" - ); - search_view.excluded_files_editor.update(cx, |editor, cx| { - assert!( - editor.display_text(cx).is_empty(), - "New search in directory should not have any excluded files" - ); - }); - search_view.included_files_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - a_dir_entry.path.to_str().unwrap(), - "New search in directory should have included dir entry path" - ); - }); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "New search in directory should have a filter that matches a certain directory" - ); - }) - }) - .unwrap(); - } - - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx).unwrap(); - let search_bar = window.build_view(cx, |_| ProjectSearchBar::new()); - - window - .update(cx, { - let search_bar = search_bar.clone(); - move |workspace, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, cx)) - }); - - ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) - } - }) - .unwrap(); - - let search_view = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - // Add 3 search items into the history + another unsubmitted one. - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view.search_options = SearchOptions::CASE_SENSITIVE; - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); - search_view.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); - search_view.search(cx); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("JUST_TEXT_INPUT", cx) - }); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - - // Ensure that the latest input with search settings is active. - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view.query_editor.read(cx).text(cx), - "JUST_TEXT_INPUT" - ); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Next history query after the latest should set the query to the empty string. - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }) - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }) - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // First previous query for empty current query should set the query to the latest submitted one. - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Further previous items should go over the history in reverse order. - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Previous items should never go behind the first history item. - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Next items should go over the history in the original order. - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - search_view - .query_editor - .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // New search input should add another entry to history and move the selection to the end of the history. - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.next_history_query(&NextHistoryQuery, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - } - - pub fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - cx.set_global(ActiveSearches::default()); - SemanticIndexSettings::register(cx); - - theme::init(theme::LoadThemes::JustBase, cx); - - language::init(cx); - client::init_settings(cx); - editor::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - super::init(cx); - }); - } -} diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs deleted file mode 100644 index f0301a5bcc..0000000000 --- a/crates/search2/src/search.rs +++ /dev/null @@ -1,108 +0,0 @@ -use bitflags::bitflags; -pub use buffer_search::BufferSearchBar; -use gpui::{actions, Action, AppContext, IntoElement}; -pub use mode::SearchMode; -use project::search::SearchQuery; -pub use project_search::ProjectSearchView; -use ui::{prelude::*, Tooltip}; -use ui::{ButtonStyle, IconButton}; - -pub mod buffer_search; -mod history; -mod mode; -pub mod project_search; -pub(crate) mod search_bar; - -pub fn init(cx: &mut AppContext) { - menu::init(); - buffer_search::init(cx); - project_search::init(cx); -} - -actions!( - search, - [ - CycleMode, - ToggleWholeWord, - ToggleCaseSensitive, - ToggleIncludeIgnored, - ToggleReplace, - SelectNextMatch, - SelectPrevMatch, - SelectAllMatches, - NextHistoryQuery, - PreviousHistoryQuery, - ActivateTextMode, - ActivateSemanticMode, - ActivateRegexMode, - ReplaceAll, - ReplaceNext, - ] -); - -bitflags! { - #[derive(Default)] - pub struct SearchOptions: u8 { - const NONE = 0b000; - const WHOLE_WORD = 0b001; - const CASE_SENSITIVE = 0b010; - const INCLUDE_IGNORED = 0b100; - } -} - -impl SearchOptions { - pub fn label(&self) -> &'static str { - match *self { - SearchOptions::WHOLE_WORD => "Match Whole Word", - SearchOptions::CASE_SENSITIVE => "Match Case", - SearchOptions::INCLUDE_IGNORED => "Include ignored", - _ => panic!("{:?} is not a named SearchOption", self), - } - } - - pub fn icon(&self) -> ui::Icon { - match *self { - SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, - SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, - SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit, - _ => panic!("{:?} is not a named SearchOption", self), - } - } - - pub fn to_toggle_action(&self) -> Box { - match *self { - SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), - SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), - SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored), - _ => panic!("{:?} is not a named SearchOption", self), - } - } - - pub fn none() -> SearchOptions { - SearchOptions::NONE - } - - pub fn from_query(query: &SearchQuery) -> SearchOptions { - let mut options = SearchOptions::NONE; - options.set(SearchOptions::WHOLE_WORD, query.whole_word()); - options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); - options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored()); - options - } - - pub fn as_button( - &self, - active: bool, - action: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static, - ) -> impl IntoElement { - IconButton::new(self.label(), self.icon()) - .on_click(action) - .style(ButtonStyle::Subtle) - .when(active, |button| button.style(ButtonStyle::Filled)) - .tooltip({ - let action = self.to_toggle_action(); - let label: SharedString = format!("Toggle {}", self.label()).into(); - move |cx| Tooltip::for_action(label.clone(), &*action, cx) - }) - } -} diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs deleted file mode 100644 index 628be3112e..0000000000 --- a/crates/search2/src/search_bar.rs +++ /dev/null @@ -1,18 +0,0 @@ -use gpui::{Action, IntoElement}; -use ui::IconButton; -use ui::{prelude::*, Tooltip}; - -pub(super) fn render_nav_button( - icon: ui::Icon, - active: bool, - tooltip: &'static str, - action: &'static dyn Action, -) -> impl IntoElement { - IconButton::new( - SharedString::from(format!("search-nav-button-{}", action.name())), - icon, - ) - .on_click(|_, cx| cx.dispatch_action(action.boxed_clone())) - .tooltip(move |cx| Tooltip::for_action(tooltip, action, cx)) - .disabled(!active) -} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 85de173604..390cd0545f 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -9,19 +9,19 @@ path = "src/terminal_view.rs" doctest = false [dependencies] -context_menu = { path = "../context_menu" } -editor = { path = "../editor" } -language = { path = "../language" } -gpui = { path = "../gpui" } -project = { path = "../project" } -search = { path = "../search" } -settings = { path = "../settings" } -theme = { path = "../theme" } +editor = { package = "editor2", path = "../editor2" } +language = { package = "language2", path = "../language2" } +gpui = { package = "gpui2", path = "../gpui2" } +project = { package = "project2", path = "../project2" } +# search = { path = "../search" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } -workspace = { path = "../workspace" } -db = { path = "../db" } +workspace = { package = "workspace2", path = "../workspace2" } +db = { package = "db2", path = "../db2" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } -terminal = { path = "../terminal" } +terminal = { package = "terminal2", path = "../terminal2" } +ui = { package = "ui2", path = "../ui2" } smallvec.workspace = true smol.workspace = true mio-extras = "2.0.6" @@ -38,9 +38,9 @@ serde.workspace = true serde_derive.workspace = true [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } -client = { path = "../client", features = ["test-support"]} -project = { path = "../project", features = ["test-support"]} -workspace = { path = "../workspace", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"]} +project = { package = "project2", path = "../project2", features = ["test-support"]} +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } rand.workspace = true diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 30dbccf455..8be10f9469 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,52 +1,44 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ - color::Color, - elements::{Empty, Overlay}, - fonts::{HighlightStyle, Properties, Style::Italic, TextStyle, Underline, Weight}, - geometry::{ - rect::RectF, - vector::{vec2f, Vector2F}, - }, - platform::{CursorStyle, MouseButton}, - serde_json::json, - text_layout::{Line, RunStyle}, - AnyElement, Element, EventContext, FontCache, ModelContext, MouseRegion, Quad, SizeConstraint, - TextLayoutCache, ViewContext, WeakModelHandle, WindowContext, + div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace, + BorrowWindow, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, + FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, + Interactivity, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, + Pixels, PlatformInputHandler, Point, ShapedLine, StatefulInteractiveElement, StyleRefinement, + Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; -use ordered_float::OrderedFloat; +use settings::Settings; use terminal::{ + alacritty_terminal::ansi::NamedColor, alacritty_terminal::{ - ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, + ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape}, grid::Dimensions, - index::Point, + index::Point as AlacPoint, term::{cell::Flags, TermMode}, }, - mappings::colors::convert_color, terminal_settings::TerminalSettings, IndexedCell, Terminal, TerminalContent, TerminalSize, }; -use theme::{TerminalStyle, ThemeSettings}; -use util::ResultExt; +use theme::{ActiveTheme, Theme, ThemeSettings}; +use ui::Tooltip; +use std::{any::TypeId, mem}; use std::{fmt::Debug, ops::RangeInclusive}; -use std::{mem, ops::Range}; - -use crate::TerminalView; ///The information generated during layout that is necessary for painting pub struct LayoutState { cells: Vec, rects: Vec, - relative_highlighted_ranges: Vec<(RangeInclusive, Color)>, + relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, cursor: Option, - background_color: Color, - size: TerminalSize, + background_color: Hsla, + dimensions: TerminalSize, mode: TermMode, display_offset: usize, - hyperlink_tooltip: Option>, - gutter: f32, + hyperlink_tooltip: Option, + gutter: Pixels, } ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points @@ -56,7 +48,7 @@ struct DisplayCursor { } impl DisplayCursor { - fn from(cursor_point: Point, display_offset: usize) -> Self { + fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { Self { line: cursor_point.line.0 + display_offset as i32, col: cursor_point.column.0, @@ -72,47 +64,46 @@ impl DisplayCursor { } } -#[derive(Clone, Debug, Default)] +#[derive(Debug, Default)] struct LayoutCell { - point: Point, - text: Line, + point: AlacPoint, + text: gpui::ShapedLine, } impl LayoutCell { - fn new(point: Point, text: Line) -> LayoutCell { + fn new(point: AlacPoint, text: gpui::ShapedLine) -> LayoutCell { LayoutCell { point, text } } fn paint( &self, - origin: Vector2F, + origin: Point, layout: &LayoutState, - visible_bounds: RectF, - _view: &mut TerminalView, + _visible_bounds: Bounds, cx: &mut WindowContext, ) { let pos = { let point = self.point; - vec2f( - (origin.x() + point.column as f32 * layout.size.cell_width).floor(), - origin.y() + point.line as f32 * layout.size.line_height, + + Point::new( + (origin.x + point.column as f32 * layout.dimensions.cell_width).floor(), + origin.y + point.line as f32 * layout.dimensions.line_height, ) }; - self.text - .paint(pos, visible_bounds, layout.size.line_height, cx); + self.text.paint(pos, layout.dimensions.line_height, cx).ok(); } } #[derive(Clone, Debug, Default)] struct LayoutRect { - point: Point, + point: AlacPoint, num_of_cells: usize, - color: Color, + color: Hsla, } impl LayoutRect { - fn new(point: Point, num_of_cells: usize, color: Color) -> LayoutRect { + fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { LayoutRect { point, num_of_cells, @@ -128,46 +119,47 @@ impl LayoutRect { } } - fn paint( - &self, - origin: Vector2F, - layout: &LayoutState, - _view: &mut TerminalView, - cx: &mut ViewContext, - ) { + fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut WindowContext) { let position = { - let point = self.point; - vec2f( - (origin.x() + point.column as f32 * layout.size.cell_width).floor(), - origin.y() + point.line as f32 * layout.size.line_height, + let alac_point = self.point; + point( + (origin.x + alac_point.column as f32 * layout.dimensions.cell_width).floor(), + origin.y + alac_point.line as f32 * layout.dimensions.line_height, ) }; - let size = vec2f( - (layout.size.cell_width * self.num_of_cells as f32).ceil(), - layout.size.line_height, - ); + let size = point( + (layout.dimensions.cell_width * self.num_of_cells as f32).ceil(), + layout.dimensions.line_height, + ) + .into(); - cx.scene().push_quad(Quad { - bounds: RectF::new(position, size), - background: Some(self.color), - border: Default::default(), - corner_radii: Default::default(), - }) + cx.paint_quad(fill(Bounds::new(position, size), self.color)); } } ///The GPUI element that paints the terminal. ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalElement { - terminal: WeakModelHandle, + terminal: Model, + focus: FocusHandle, focused: bool, cursor_visible: bool, can_navigate_to_selected_word: bool, + interactivity: Interactivity, } +impl InteractiveElement for TerminalElement { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl StatefulInteractiveElement for TerminalElement {} + impl TerminalElement { pub fn new( - terminal: WeakModelHandle, + terminal: Model, + focus: FocusHandle, focused: bool, cursor_visible: bool, can_navigate_to_selected_word: bool, @@ -175,21 +167,26 @@ impl TerminalElement { TerminalElement { terminal, focused, + focus: focus.clone(), cursor_visible, can_navigate_to_selected_word, + interactivity: Default::default(), } + .track_focus(&focus) + .element } - //Vec> -> Clip out the parts of the ranges + //Vec> -> Clip out the parts of the ranges fn layout_grid( grid: &Vec, text_style: &TextStyle, - terminal_theme: &TerminalStyle, - text_layout_cache: &TextLayoutCache, - font_cache: &FontCache, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + // terminal_theme: &TerminalStyle, + text_system: &TextSystem, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + cx: &WindowContext<'_>, ) -> (Vec, Vec) { + let theme = cx.theme(); let mut cells = vec![]; let mut rects = vec![]; @@ -225,18 +222,21 @@ impl TerminalElement { rects.push(cur_rect.take().unwrap()); } cur_rect = Some(LayoutRect::new( - Point::new(line_index as i32, cell.point.column.0 as i32), + AlacPoint::new( + line_index as i32, + cell.point.column.0 as i32, + ), 1, - convert_color(&bg, &terminal_theme), + convert_color(&bg, theme), )); } } None => { cur_alac_color = Some(bg); cur_rect = Some(LayoutRect::new( - Point::new(line_index as i32, cell.point.column.0 as i32), + AlacPoint::new(line_index as i32, cell.point.column.0 as i32), 1, - convert_color(&bg, &terminal_theme), + convert_color(&bg, &theme), )); } } @@ -245,25 +245,21 @@ impl TerminalElement { //Layout current cell text { - let cell_text = &cell.c.to_string(); + let cell_text = cell.c.to_string(); if !is_blank(&cell) { - let cell_style = TerminalElement::cell_style( - &cell, - fg, - terminal_theme, - text_style, - font_cache, - hyperlink, - ); + let cell_style = + TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink); - let layout_cell = text_layout_cache.layout_str( - cell_text, - text_style.font_size, - &[(cell_text.len(), cell_style)], - ); + let layout_cell = text_system + .shape_line( + cell_text.into(), + text_style.font_size.to_pixels(cx.rem_size()), + &[cell_style], + ) + .unwrap(); cells.push(LayoutCell::new( - Point::new(line_index as i32, cell.point.column.0 as i32), + AlacPoint::new(line_index as i32, cell.point.column.0 as i32), layout_cell, )) }; @@ -282,19 +278,19 @@ impl TerminalElement { fn shape_cursor( cursor_point: DisplayCursor, size: TerminalSize, - text_fragment: &Line, - ) -> Option<(Vector2F, f32)> { + text_fragment: &ShapedLine, + ) -> Option<(Point, Pixels)> { if cursor_point.line() < size.total_lines() as i32 { - let cursor_width = if text_fragment.width() == 0. { + let cursor_width = if text_fragment.width == Pixels::ZERO { size.cell_width() } else { - text_fragment.width() + text_fragment.width }; - //Cursor should always surround as much of the text as possible, - //hence when on pixel boundaries round the origin down and the width up + // Cursor should always surround as much of the text as possible, + // hence when on pixel boundaries round the origin down and the width up Some(( - vec2f( + point( (cursor_point.col() as f32 * size.cell_width()).floor(), (cursor_point.line() as f32 * size.line_height()).floor(), ), @@ -305,55 +301,55 @@ impl TerminalElement { } } - ///Convert the Alacritty cell styles to GPUI text styles and background color + /// Convert the Alacritty cell styles to GPUI text styles and background color fn cell_style( indexed: &IndexedCell, fg: terminal::alacritty_terminal::ansi::Color, - style: &TerminalStyle, + // bg: terminal::alacritty_terminal::ansi::Color, + colors: &Theme, text_style: &TextStyle, - font_cache: &FontCache, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - ) -> RunStyle { + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + ) -> TextRun { let flags = indexed.cell.flags; - let fg = convert_color(&fg, &style); + let fg = convert_color(&fg, &colors); + // let bg = convert_color(&bg, &colors); - let mut underline = flags - .intersects(Flags::ALL_UNDERLINES) - .then(|| Underline { - color: Some(fg), - squiggly: flags.contains(Flags::UNDERCURL), - thickness: OrderedFloat(1.), - }) - .unwrap_or_default(); + let underline = (flags.intersects(Flags::ALL_UNDERLINES) + || indexed.cell.hyperlink().is_some()) + .then(|| UnderlineStyle { + color: Some(fg), + thickness: Pixels::from(1.0), + wavy: flags.contains(Flags::UNDERCURL), + }); - if indexed.cell.hyperlink().is_some() { - if underline.thickness == OrderedFloat(0.) { - underline.thickness = OrderedFloat(1.); - } - } + let weight = if flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { + FontWeight::BOLD + } else { + FontWeight::NORMAL + }; - let mut properties = Properties::new(); - if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { - properties = *properties.weight(Weight::BOLD); - } - if indexed.flags.intersects(Flags::ITALIC) { - properties = *properties.style(Italic); - } + let style = if flags.intersects(Flags::ITALIC) { + FontStyle::Italic + } else { + FontStyle::Normal + }; - let font_id = font_cache - .select_font(text_style.font_family_id, &properties) - .unwrap_or(text_style.font_id); - - let mut result = RunStyle { + let mut result = TextRun { + len: indexed.c.len_utf8() as usize, color: fg, - font_id, + background_color: None, + font: Font { + weight, + style, + ..text_style.font() + }, underline, }; if let Some((style, range)) = hyperlink { if range.contains(&indexed.point) { if let Some(underline) = style.underline { - result.underline = underline; + result.underline = Some(underline); } if let Some(color) = style.color { @@ -365,229 +361,86 @@ impl TerminalElement { result } - fn generic_button_handler( - connection: WeakModelHandle, - origin: Vector2F, - f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext), - ) -> impl Fn(E, &mut TerminalView, &mut EventContext) { - move |event, _: &mut TerminalView, cx| { - cx.focus_parent(); - if let Some(conn_handle) = connection.upgrade(cx) { - conn_handle.update(cx, |terminal, cx| { - f(terminal, origin, event, cx); + fn compute_layout(&self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { + let settings = ThemeSettings::get_global(cx).clone(); - cx.notify(); - }) - } - } - } + let buffer_font_size = settings.buffer_font_size(cx); - fn attach_mouse_handlers( - &self, - origin: Vector2F, - visible_bounds: RectF, - mode: TermMode, - cx: &mut ViewContext, - ) { - let connection = self.terminal; - - let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); - - // Terminal Emulator controlled behavior: - region = region - // Start selections - .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { - let terminal_view = cx.handle(); - cx.focus(&terminal_view); - v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); - if let Some(conn_handle) = connection.upgrade(cx) { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_down(&event, origin); - - cx.notify(); - }) - } - }) - // Update drag selections - .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { - if event.end { - return; - } - - if cx.is_self_focused() { - if let Some(conn_handle) = connection.upgrade(cx) { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_drag(event, origin); - cx.notify(); - }) - } - } - }) - // Copy on up behavior - .on_up( - MouseButton::Left, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ) - // Context menu - .on_click( - MouseButton::Right, - move |event, view: &mut TerminalView, cx| { - let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx) { - conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift)) - } else { - // If we can't get the model handle, probably can't deploy the context menu - true - }; - if !mouse_mode { - view.deploy_context_menu(event.position, cx); - } - }, - ) - .on_move(move |event, _: &mut TerminalView, cx| { - if cx.is_self_focused() { - if let Some(conn_handle) = connection.upgrade(cx) { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_move(&event, origin); - cx.notify(); - }) - } - } - }) - .on_scroll(move |event, _: &mut TerminalView, cx| { - if let Some(conn_handle) = connection.upgrade(cx) { - conn_handle.update(cx, |terminal, cx| { - terminal.scroll_wheel(event, origin); - cx.notify(); - }) - } - }); - - // Mouse mode handlers: - // All mouse modes need the extra click handlers - if mode.intersects(TermMode::MOUSE_MODE) { - region = region - .on_down( - MouseButton::Right, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, _cx| { - terminal.mouse_down(&e, origin); - }, - ), - ) - .on_down( - MouseButton::Middle, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, _cx| { - terminal.mouse_down(&e, origin); - }, - ), - ) - .on_up( - MouseButton::Right, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ) - .on_up( - MouseButton::Middle, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ) - } - - cx.scene().push_mouse_region(region); - } -} - -impl Element for TerminalElement { - type LayoutState = LayoutState; - type PaintState = (); - - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - view: &mut TerminalView, - cx: &mut ViewContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - let settings = settings::get::(cx); - let terminal_settings = settings::get::(cx); - - //Setup layout information - let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. - let link_style = settings.theme.editor.link_definition; - let tooltip_style = settings.theme.tooltip.clone(); - - let font_cache = cx.font_cache(); - let font_size = terminal_settings - .font_size(cx) - .unwrap_or(settings.buffer_font_size(cx)); - let font_family_name = terminal_settings + let terminal_settings = TerminalSettings::get_global(cx); + let font_family = terminal_settings .font_family .as_ref() - .unwrap_or(&settings.buffer_font_family_name); + .map(|string| string.clone().into()) + .unwrap_or(settings.buffer_font.family); + let font_features = terminal_settings .font_features - .as_ref() - .unwrap_or(&settings.buffer_font_features); - let family_id = font_cache - .load_family(&[font_family_name], &font_features) - .log_err() - .unwrap_or(settings.buffer_font_family); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); + .clone() + .unwrap_or(settings.buffer_font.features.clone()); + + let line_height = terminal_settings.line_height.value(); + let font_size = terminal_settings.font_size.clone(); + + let font_size = + font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); + + let theme = cx.theme().clone(); + + let link_style = HighlightStyle { + color: Some(theme.colors().link_text_hover), + font_weight: None, + font_style: None, + background_color: None, + underline: Some(UnderlineStyle { + thickness: px(1.0), + color: Some(theme.colors().link_text_hover), + wavy: false, + }), + fade_out: None, + }; let text_style = TextStyle { - color: settings.theme.editor.text_color, - font_family_id: family_id, - font_family_name: font_cache.family_name(family_id).unwrap(), - font_id, - font_size, - font_properties: Default::default(), - underline: Default::default(), - soft_wrap: false, + font_family, + font_features, + font_size: font_size.into(), + font_style: FontStyle::Normal, + line_height: line_height.into(), + background_color: None, + white_space: WhiteSpace::Normal, + // These are going to be overridden per-cell + underline: None, + color: theme.colors().text, + font_weight: FontWeight::NORMAL, }; - let selection_color = settings.theme.editor.selection.selection; - let match_color = settings.theme.search.match_background; + + let text_system = cx.text_system(); + let selection_color = theme.players().local(); + let match_color = theme.colors().search_match_background; let gutter; let dimensions = { - let line_height = text_style.font_size * terminal_settings.line_height.value(); - let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); + let rem_size = cx.rem_size(); + let font_pixels = text_style.font_size.to_pixels(rem_size); + let line_height = font_pixels * line_height.to_pixels(rem_size); + let font_id = cx.text_system().font_id(&text_style.font()).unwrap(); + + // todo!(do we need to keep this unwrap?) + let cell_width = text_system + .advance(font_id, font_pixels, 'm') + .unwrap() + .width; gutter = cell_width; - let size = constraint.max - vec2f(gutter, 0.); + let mut size = bounds.size.clone(); + size.width -= gutter; + TerminalSize::new(line_height, cell_width, size) }; - let search_matches = if let Some(terminal_model) = self.terminal.upgrade(cx) { - terminal_model.read(cx).matches.clone() - } else { - Default::default() - }; + let search_matches = self.terminal.read(cx).matches.clone(); - let background_color = terminal_theme.background; - let terminal_handle = self.terminal.upgrade(cx).unwrap(); + let background_color = theme.colors().terminal_background; - let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { + let last_hovered_word = self.terminal.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { @@ -598,29 +451,11 @@ impl Element for TerminalElement { }); let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { - let mut tooltip = Overlay::new( - Empty::new() - .contained() - .constrained() - .with_width(dimensions.width()) - .with_height(dimensions.height()) - .with_tooltip::( - hovered_word.id, - hovered_word.word, - None, - tooltip_style, - cx, - ), - ) - .with_position_mode(gpui::elements::OverlayPositionMode::Local) - .into_any(); - - tooltip.layout( - SizeConstraint::new(Vector2F::zero(), cx.window_size()), - view, - cx, - ); - tooltip + div() + .size_full() + .id("terminal-element") + .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) + .into_any_element() }); let TerminalContent { @@ -631,7 +466,7 @@ impl Element for TerminalElement { selection, cursor, .. - } = { &terminal_handle.read(cx).last_content }; + } = &self.terminal.read(cx).last_content; // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); @@ -639,7 +474,8 @@ impl Element for TerminalElement { relative_highlighted_ranges.push((search_match, match_color)) } if let Some(selection) = selection { - relative_highlighted_ranges.push((selection.start..=selection.end, selection_color)); + relative_highlighted_ranges + .push((selection.start..=selection.end, selection_color.cursor)); } // then have that representation be converted to the appropriate highlight data structure @@ -647,12 +483,11 @@ impl Element for TerminalElement { let (cells, rects) = TerminalElement::layout_grid( cells, &text_style, - &terminal_theme, - cx.text_layout_cache(), - cx.font_cache(), + &cx.text_system(), last_hovered_word .as_ref() .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), + cx, ); //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -663,25 +498,21 @@ impl Element for TerminalElement { let cursor_point = DisplayCursor::from(cursor.point, *display_offset); let cursor_text = { let str_trxt = cursor_char.to_string(); - - let color = if self.focused { - terminal_theme.background - } else { - terminal_theme.foreground - }; - - cx.text_layout_cache().layout_str( - &str_trxt, - text_style.font_size, - &[( - str_trxt.len(), - RunStyle { - font_id: text_style.font_id, - color, + let len = str_trxt.len(); + cx.text_system() + .shape_line( + str_trxt.into(), + text_style.font_size.to_pixels(cx.rem_size()), + &[TextRun { + len, + font: text_style.font(), + color: theme.colors().terminal_background, + background_color: None, underline: Default::default(), - }, - )], - ) + }], + ) + //todo!(do we need to keep this unwrap?) + .unwrap() }; let focused = self.focused; @@ -701,7 +532,7 @@ impl Element for TerminalElement { cursor_position, block_width, dimensions.line_height, - terminal_theme.cursor, + theme.players().local().cursor, shape, text, ) @@ -710,144 +541,377 @@ impl Element for TerminalElement { }; //Done! - ( - constraint.max, - LayoutState { - cells, - cursor, - background_color, - size: dimensions, - rects, - relative_highlighted_ranges, - mode: *mode, - display_offset: *display_offset, - hyperlink_tooltip, - gutter, - }, - ) + LayoutState { + cells, + cursor, + background_color, + dimensions, + rects, + relative_highlighted_ranges, + mode: *mode, + display_offset: *display_offset, + hyperlink_tooltip, + gutter, + } + } + + fn generic_button_handler( + connection: Model, + origin: Point, + focus_handle: FocusHandle, + f: impl Fn(&mut Terminal, Point, &E, &mut ModelContext), + ) -> impl Fn(&E, &mut WindowContext) { + move |event, cx| { + cx.focus(&focus_handle); + connection.update(cx, |terminal, cx| { + f(terminal, origin, event, cx); + + cx.notify(); + }) + } + } + + fn register_key_listeners(&self, cx: &mut WindowContext) { + cx.on_key_event({ + let this = self.terminal.clone(); + move |event: &ModifiersChangedEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + let handled = + this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); + + if handled { + cx.notify(); + } + } + }); + } + + fn register_mouse_listeners( + &mut self, + origin: Point, + mode: TermMode, + bounds: Bounds, + cx: &mut WindowContext, + ) { + let focus = self.focus.clone(); + let terminal = self.terminal.clone(); + + self.interactivity.on_mouse_down(MouseButton::Left, { + let terminal = terminal.clone(); + let focus = focus.clone(); + move |e, cx| { + cx.focus(&focus); + //todo!(context menu) + // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); + terminal.update(cx, |terminal, cx| { + terminal.mouse_down(&e, origin); + + cx.notify(); + }) + } + }); + self.interactivity.on_mouse_move({ + let terminal = terminal.clone(); + let focus = focus.clone(); + move |e, cx| { + if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() { + terminal.update(cx, |terminal, cx| { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + }) + } + } + }); + self.interactivity.on_mouse_up( + MouseButton::Left, + TerminalElement::generic_button_handler( + terminal.clone(), + origin, + focus.clone(), + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ); + self.interactivity.on_click({ + let terminal = terminal.clone(); + move |e, cx| { + if e.down.button == MouseButton::Right { + let mouse_mode = terminal.update(cx, |terminal, _cx| { + terminal.mouse_mode(e.down.modifiers.shift) + }); + + if !mouse_mode { + //todo!(context menu) + // view.deploy_context_menu(e.position, cx); + } + } + } + }); + + self.interactivity.on_mouse_move({ + let terminal = terminal.clone(); + let focus = focus.clone(); + move |e, cx| { + if focus.is_focused(cx) { + terminal.update(cx, |terminal, cx| { + terminal.mouse_move(&e, origin); + cx.notify(); + }) + } + } + }); + self.interactivity.on_scroll_wheel({ + let terminal = terminal.clone(); + move |e, cx| { + terminal.update(cx, |terminal, cx| { + terminal.scroll_wheel(e, origin); + cx.notify(); + }) + } + }); + + self.interactivity.drag_over_styles.push(( + TypeId::of::(), + StyleRefinement::default().bg(cx.theme().colors().drop_target_background), + )); + self.interactivity.on_drop::({ + let focus = focus.clone(); + let terminal = terminal.clone(); + move |external_paths, cx| { + cx.focus(&focus); + let mut new_text = external_paths + .paths() + .iter() + .map(|path| format!(" {path:?}")) + .join(""); + new_text.push(' '); + terminal.update(cx, |terminal, _| { + // todo!() long paths are not displayed properly albeit the text is there + terminal.paste(&new_text); + }); + } + }); + + // Mouse mode handlers: + // All mouse modes need the extra click handlers + if mode.intersects(TermMode::MOUSE_MODE) { + self.interactivity.on_mouse_down( + MouseButton::Right, + TerminalElement::generic_button_handler( + terminal.clone(), + origin, + focus.clone(), + move |terminal, origin, e, _cx| { + terminal.mouse_down(&e, origin); + }, + ), + ); + self.interactivity.on_mouse_down( + MouseButton::Middle, + TerminalElement::generic_button_handler( + terminal.clone(), + origin, + focus.clone(), + move |terminal, origin, e, _cx| { + terminal.mouse_down(&e, origin); + }, + ), + ); + self.interactivity.on_mouse_up( + MouseButton::Right, + TerminalElement::generic_button_handler( + terminal.clone(), + origin, + focus.clone(), + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ); + self.interactivity.on_mouse_up( + MouseButton::Middle, + TerminalElement::generic_button_handler( + terminal, + origin, + focus, + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ); + } + } +} + +impl Element for TerminalElement { + type State = InteractiveElementState; + + fn request_layout( + &mut self, + element_state: Option, + cx: &mut WindowContext<'_>, + ) -> (LayoutId, Self::State) { + let (layout_id, interactive_state) = + self.interactivity + .layout(element_state, cx, |mut style, cx| { + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + let layout_id = cx.request_layout(&style, None); + + layout_id + }); + + (layout_id, interactive_state) } fn paint( &mut self, - bounds: RectF, - visible_bounds: RectF, - layout: &mut Self::LayoutState, - view: &mut TerminalView, - cx: &mut ViewContext, - ) -> Self::PaintState { - let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + bounds: Bounds, + state: &mut Self::State, + cx: &mut WindowContext<'_>, + ) { + let mut layout = self.compute_layout(bounds, cx); - //Setup element stuff - let clip_bounds = Some(visible_bounds); + cx.paint_quad(fill(bounds, layout.background_color)); + let origin = bounds.origin + Point::new(layout.gutter, px(0.)); - cx.paint_layer(clip_bounds, |cx| { - let origin = bounds.origin() + vec2f(layout.gutter, 0.); + let terminal_input_handler = TerminalInputHandler { + cx: cx.to_async(), + terminal: self.terminal.clone(), + cursor_bounds: layout + .cursor + .as_ref() + .map(|cursor| cursor.bounding_rect(origin)), + }; - // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - self.attach_mouse_handlers(origin, visible_bounds, layout.mode, cx); + self.register_mouse_listeners(origin, layout.mode, bounds, cx); - cx.scene().push_cursor_region(gpui::CursorRegion { - bounds, - style: if layout.hyperlink_tooltip.is_some() { - CursorStyle::PointingHand - } else { - CursorStyle::IBeam - }, - }); + let mut interactivity = mem::take(&mut self.interactivity); + interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { + cx.handle_input(&self.focus, terminal_input_handler); - cx.paint_layer(clip_bounds, |cx| { - //Start with a background color - cx.scene().push_quad(Quad { - bounds: RectF::new(bounds.origin(), bounds.size()), - background: Some(layout.background_color), - border: Default::default(), - corner_radii: Default::default(), - }); + self.register_key_listeners(cx); - for rect in &layout.rects { - rect.paint(origin, layout, view, cx); - } - }); + for rect in &layout.rects { + rect.paint(origin, &layout, cx); + } - //Draw Highlighted Backgrounds - cx.paint_layer(clip_bounds, |cx| { + cx.with_z_index(1, |cx| { for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() { if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, layout, origin) + to_highlighted_range_lines(relative_highlighted_range, &layout, origin) { let hr = HighlightedRange { start_y, //Need to change this - line_height: layout.size.line_height, + line_height: layout.dimensions.line_height, lines: highlighted_range_lines, color: color.clone(), //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.size.line_height, + corner_radius: 0.15 * layout.dimensions.line_height, }; hr.paint(bounds, cx); } } }); - //Draw the text cells - cx.paint_layer(clip_bounds, |cx| { + cx.with_z_index(2, |cx| { for cell in &layout.cells { - cell.paint(origin, layout, visible_bounds, view, cx); + cell.paint(origin, &layout, bounds, cx); } }); - //Draw cursor if self.cursor_visible { - if let Some(cursor) = &layout.cursor { - cx.paint_layer(clip_bounds, |cx| { + cx.with_z_index(3, |cx| { + if let Some(cursor) = &layout.cursor { cursor.paint(origin, cx); - }) - } + } + }); } - if let Some(element) = &mut layout.hyperlink_tooltip { - element.paint(origin, visible_bounds, view, cx) + if let Some(mut element) = layout.hyperlink_tooltip.take() { + element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx) } }); } +} - fn metadata(&self) -> Option<&dyn std::any::Any> { +impl IntoElement for TerminalElement { + type Element = Self; + + fn element_id(&self) -> Option { + Some("terminal".into()) + } + + fn into_element(self) -> Self::Element { + self + } +} + +struct TerminalInputHandler { + cx: AsyncWindowContext, + terminal: Model, + cursor_bounds: Option>, +} + +impl PlatformInputHandler for TerminalInputHandler { + fn selected_text_range(&mut self) -> Option> { + self.cx + .update(|_, cx| { + if self + .terminal + .read(cx) + .last_content + .mode + .contains(TermMode::ALT_SCREEN) + { + None + } else { + Some(0..0) + } + }) + .ok() + .flatten() + } + + fn marked_text_range(&mut self) -> Option> { None } - fn debug( - &self, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &TerminalView, - _: &gpui::ViewContext, - ) -> gpui::serde_json::Value { - json!({ - "type": "TerminalElement", - }) + fn text_for_range(&mut self, _: std::ops::Range) -> Option { + None } - fn rect_for_text_range( - &self, - _: Range, - bounds: RectF, - _: RectF, - layout: &Self::LayoutState, - _: &Self::PaintState, - _: &TerminalView, - _: &gpui::ViewContext, - ) -> Option { - // Use the same origin that's passed to `Cursor::paint` in the paint - // method bove. - let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.); + fn replace_text_in_range( + &mut self, + _replacement_range: Option>, + text: &str, + ) { + self.cx + .update(|_, cx| { + self.terminal.update(cx, |terminal, _| { + terminal.input(text.into()); + }) + }) + .ok(); + } - // TODO - Why is it necessary to move downward one line to get correct - // positioning? I would think that we'd want the same rect that is - // painted for the cursor. - origin += vec2f(0., layout.size.line_height); + fn replace_and_mark_text_in_range( + &mut self, + _range_utf16: Option>, + _new_text: &str, + _new_selected_range: Option>, + ) { + } - Some(layout.cursor.as_ref()?.bounding_rect(origin)) + fn unmark_text(&mut self) {} + + fn bounds_for_range(&mut self, _range_utf16: std::ops::Range) -> Option> { + self.cursor_bounds } } @@ -875,10 +939,10 @@ fn is_blank(cell: &IndexedCell) -> bool { } fn to_highlighted_range_lines( - range: &RangeInclusive, + range: &RangeInclusive, layout: &LayoutState, - origin: Vector2F, -) -> Option<(f32, Vec)> { + origin: Point, +) -> Option<(Pixels, Vec)> { // Step 1. Normalize the points to be viewport relative. // When display_offset = 1, here's how the grid is arranged: //-2,0 -2,1... @@ -897,28 +961,32 @@ fn to_highlighted_range_lines( // of the grid data we should be looking at. But for the rendering step, we don't // want negatives. We want things relative to the 'viewport' (the area of the grid // which is currently shown according to the display offset) - let unclamped_start = Point::new( + let unclamped_start = AlacPoint::new( range.start().line + layout.display_offset, range.start().column, ); - let unclamped_end = Point::new(range.end().line + layout.display_offset, range.end().column); + let unclamped_end = + AlacPoint::new(range.end().line + layout.display_offset, range.end().column); // Step 2. Clamp range to viewport, and return None if it doesn't overlap - if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 { + if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 { return None; } let clamped_start_line = unclamped_start.line.0.max(0) as usize; - let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize; + let clamped_end_line = unclamped_end + .line + .0 + .min(layout.dimensions.num_lines() as i32) as usize; //Convert the start of the range to pixels - let start_y = origin.y() + clamped_start_line as f32 * layout.size.line_height; + let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height; // Step 3. Expand ranges that cross lines into a collection of single-line ranges. // (also convert to pixels) let mut highlighted_range_lines = Vec::new(); for line in clamped_start_line..=clamped_end_line { let mut line_start = 0; - let mut line_end = layout.size.columns(); + let mut line_end = layout.dimensions.columns(); if line == clamped_start_line { line_start = unclamped_start.column.0 as usize; @@ -928,10 +996,59 @@ fn to_highlighted_range_lines( } highlighted_range_lines.push(HighlightedRangeLine { - start_x: origin.x() + line_start as f32 * layout.size.cell_width, - end_x: origin.x() + line_end as f32 * layout.size.cell_width, + start_x: origin.x + line_start as f32 * layout.dimensions.cell_width, + end_x: origin.x + line_end as f32 * layout.dimensions.cell_width, }); } Some((start_y, highlighted_range_lines)) } + +///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla { + let colors = theme.colors(); + match fg { + //Named and theme defined colors + terminal::alacritty_terminal::ansi::Color::Named(n) => match n { + NamedColor::Black => colors.terminal_ansi_black, + NamedColor::Red => colors.terminal_ansi_red, + NamedColor::Green => colors.terminal_ansi_green, + NamedColor::Yellow => colors.terminal_ansi_yellow, + NamedColor::Blue => colors.terminal_ansi_blue, + NamedColor::Magenta => colors.terminal_ansi_magenta, + NamedColor::Cyan => colors.terminal_ansi_cyan, + NamedColor::White => colors.terminal_ansi_white, + NamedColor::BrightBlack => colors.terminal_ansi_bright_black, + NamedColor::BrightRed => colors.terminal_ansi_bright_red, + NamedColor::BrightGreen => colors.terminal_ansi_bright_green, + NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow, + NamedColor::BrightBlue => colors.terminal_ansi_bright_blue, + NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta, + NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan, + NamedColor::BrightWhite => colors.terminal_ansi_bright_white, + NamedColor::Foreground => colors.text, + NamedColor::Background => colors.background, + NamedColor::Cursor => theme.players().local().cursor, + + // todo!(more colors) + NamedColor::DimBlack => red(), + NamedColor::DimRed => red(), + NamedColor::DimGreen => red(), + NamedColor::DimYellow => red(), + NamedColor::DimBlue => red(), + NamedColor::DimMagenta => red(), + NamedColor::DimCyan => red(), + NamedColor::DimWhite => red(), + NamedColor::BrightForeground => red(), + NamedColor::DimForeground => red(), + }, + //'True' colors + terminal::alacritty_terminal::ansi::Color::Spec(rgb) => { + terminal::rgba_color(rgb.r, rgb.g, rgb.b) + } + //8 bit, indexed colors + terminal::alacritty_terminal::ansi::Color::Indexed(i) => { + terminal::get_color_at_index(*i as usize, theme) + } + } +} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 0bfa84e754..9e193e83b7 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -3,110 +3,107 @@ use std::{path::PathBuf, sync::Arc}; use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ - actions, anyhow::Result, elements::*, serde_json, Action, AppContext, AsyncAppContext, Entity, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, + FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, + Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use project::Fs; use serde::{Deserialize, Serialize}; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; +use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; use util::{ResultExt, TryFutureExt}; use workspace::{ - dock::{DockPosition, Panel}, + dock::{DockPosition, Panel, PanelEvent}, item::Item, - pane, DraggedItem, Pane, Workspace, + pane, + ui::Icon, + Pane, Workspace, }; +use anyhow::Result; + const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; actions!(terminal_panel, [ToggleFocus]); pub fn init(cx: &mut AppContext) { - cx.add_action(TerminalPanel::new_terminal); - cx.add_action(TerminalPanel::open_terminal); -} - -#[derive(Debug)] -pub enum Event { - Close, - DockPositionChanged, - ZoomIn, - ZoomOut, - Focus, + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + workspace.register_action(TerminalPanel::new_terminal); + workspace.register_action(TerminalPanel::open_terminal); + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }, + ) + .detach(); } pub struct TerminalPanel { - pane: ViewHandle, + pane: View, fs: Arc, - workspace: WeakViewHandle, - width: Option, - height: Option, + workspace: WeakView, + width: Option, + height: Option, pending_serialization: Task>, _subscriptions: Vec, } impl TerminalPanel { fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let weak_self = cx.weak_handle(); - let pane = cx.add_view(|cx| { - let window = cx.window(); + let terminal_panel = cx.view().clone(); + let pane = cx.new_view(|cx| { let mut pane = Pane::new( workspace.weak_handle(), workspace.project().clone(), - workspace.app_state().background_actions, Default::default(), + Some(Arc::new(|a, cx| { + if let Some(tab) = a.downcast_ref::() { + if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) { + return item.downcast::().is_some(); + } + } + if a.downcast_ref::().is_some() { + return true; + } + + false + })), cx, ); pane.set_can_split(false, cx); pane.set_can_navigate(false, cx); - pane.on_can_drop(move |drag_and_drop, cx| { - drag_and_drop - .currently_dragged::(window) - .map_or(false, |(_, item)| { - item.handle.act_as::(cx).is_some() - }) - }); + pane.display_nav_history_buttons(false); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - let this = weak_self.clone(); - Flex::row() - .with_child(Pane::render_tab_bar_button( - 0, - "icons/plus.svg", - false, - Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))), - cx, - move |_, cx| { - let this = this.clone(); - cx.window_context().defer(move |cx| { - if let Some(this) = this.upgrade(cx) { - this.update(cx, |this, cx| { - this.add_terminal(None, cx); - }); - } + h_stack() + .gap_2() + .child( + IconButton::new("plus", Icon::Plus) + .icon_size(IconSize::Small) + .on_click(cx.listener_for(&terminal_panel, |terminal_panel, _, cx| { + terminal_panel.add_terminal(None, cx); + })) + .tooltip(|cx| Tooltip::text("New Terminal", cx)), + ) + .child({ + let zoomed = pane.is_zoomed(); + IconButton::new("toggle_zoom", Icon::Maximize) + .icon_size(IconSize::Small) + .selected(zoomed) + .selected_icon(Icon::Minimize) + .on_click(cx.listener(|pane, _, cx| { + pane.toggle_zoom(&workspace::ToggleZoom, cx); + })) + .tooltip(move |cx| { + Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx) }) - }, - |_, _| {}, - None, - )) - .with_child(Pane::render_tab_bar_button( - 1, - if pane.is_zoomed() { - "icons/minimize.svg" - } else { - "icons/maximize.svg" - }, - pane.is_zoomed(), - Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), - cx, - move |pane, cx| pane.toggle_zoom(&Default::default(), cx), - |_, _| {}, - None, - )) - .into_any() + }) + .into_any_element() }); - let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + // let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); + // pane.toolbar() + // .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); pane }); let subscriptions = vec![ @@ -123,105 +120,103 @@ impl TerminalPanel { _subscriptions: subscriptions, }; let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this, cx| { + cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); + cx.emit(PanelEvent::ChangePosition); } }) .detach(); this } - pub fn load( - workspace: WeakViewHandle, - cx: AsyncAppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let serialized_panel = if let Some(panel) = cx - .background() - .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) }) - .await - .log_err() - .flatten() - { - Some(serde_json::from_str::(&panel)?) - } else { - None - }; - let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| { - let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx)); - let items = if let Some(serialized_panel) = serialized_panel.as_ref() { - panel.update(cx, |panel, cx| { - cx.notify(); - panel.height = serialized_panel.height; - panel.width = serialized_panel.width; - panel.pane.update(cx, |_, cx| { - serialized_panel - .items - .iter() - .map(|item_id| { - TerminalView::deserialize( - workspace.project().clone(), - workspace.weak_handle(), - workspace.database_id(), - *item_id, - cx, - ) - }) - .collect::>() - }) - }) - } else { - Default::default() - }; - let pane = panel.read(cx).pane.clone(); - (panel, pane, items) - })?; + pub async fn load( + workspace: WeakView, + mut cx: AsyncWindowContext, + ) -> Result> { + let serialized_panel = cx + .background_executor() + .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) }) + .await + .log_err() + .flatten() + .map(|panel| serde_json::from_str::(&panel)) + .transpose() + .log_err() + .flatten(); - let pane = pane.downgrade(); - let items = futures::future::join_all(items).await; - pane.update(&mut cx, |pane, cx| { - let active_item_id = serialized_panel - .as_ref() - .and_then(|panel| panel.active_item_id); - let mut active_ix = None; - for item in items { - if let Some(item) = item.log_err() { - let item_id = item.id(); - pane.add_item(Box::new(item), false, false, None, cx); - if Some(item_id) == active_item_id { - active_ix = Some(pane.items_len() - 1); - } + let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| { + let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx)); + let items = if let Some(serialized_panel) = serialized_panel.as_ref() { + panel.update(cx, |panel, cx| { + cx.notify(); + panel.height = serialized_panel.height; + panel.width = serialized_panel.width; + panel.pane.update(cx, |_, cx| { + serialized_panel + .items + .iter() + .map(|item_id| { + TerminalView::deserialize( + workspace.project().clone(), + workspace.weak_handle(), + workspace.database_id(), + *item_id, + cx, + ) + }) + .collect::>() + }) + }) + } else { + Default::default() + }; + let pane = panel.read(cx).pane.clone(); + (panel, pane, items) + })?; + + let pane = pane.downgrade(); + let items = futures::future::join_all(items).await; + pane.update(&mut cx, |pane, cx| { + let active_item_id = serialized_panel + .as_ref() + .and_then(|panel| panel.active_item_id); + let mut active_ix = None; + for item in items { + if let Some(item) = item.log_err() { + let item_id = item.entity_id().as_u64(); + pane.add_item(Box::new(item), false, false, None, cx); + if Some(item_id) == active_item_id { + active_ix = Some(pane.items_len() - 1); } } + } - if let Some(active_ix) = active_ix { - pane.activate_item(active_ix, false, false, cx) - } - })?; + if let Some(active_ix) = active_ix { + pane.activate_item(active_ix, false, false, cx) + } + })?; - Ok(panel) - }) + Ok(panel) } fn handle_pane_event( &mut self, - _pane: ViewHandle, + _pane: View, event: &pane::Event, cx: &mut ViewContext, ) { match event { pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::RemoveItem { .. } => self.serialize(cx), - pane::Event::Remove => cx.emit(Event::Close), - pane::Event::ZoomIn => cx.emit(Event::ZoomIn), - pane::Event::ZoomOut => cx.emit(Event::ZoomOut), - pane::Event::Focus => cx.emit(Event::Focus), + pane::Event::Remove => cx.emit(PanelEvent::Close), + pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), + pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), + pane::Event::Focus => cx.emit(PanelEvent::Focus), pane::Event::AddItem { item } => { - if let Some(workspace) = self.workspace.upgrade(cx) { + if let Some(workspace) = self.workspace.upgrade() { let pane = self.pane.clone(); workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx)) } @@ -261,24 +256,23 @@ impl TerminalPanel { fn add_terminal(&mut self, working_directory: Option, cx: &mut ViewContext) { let workspace = self.workspace.clone(); cx.spawn(|this, mut cx| async move { - let pane = this.read_with(&cx, |this, _| this.pane.clone())?; + let pane = this.update(&mut cx, |this, _| this.pane.clone())?; workspace.update(&mut cx, |workspace, cx| { let working_directory = if let Some(working_directory) = working_directory { Some(working_directory) } else { - let working_directory_strategy = settings::get::(cx) - .working_directory - .clone(); + let working_directory_strategy = + TerminalSettings::get_global(cx).working_directory.clone(); crate::get_working_directory(workspace, cx, working_directory_strategy) }; - let window = cx.window(); + let window = cx.window_handle(); if let Some(terminal) = workspace.project().update(cx, |project, cx| { project .create_terminal(working_directory, window, cx) .log_err() }) { - let terminal = Box::new(cx.add_view(|cx| { + let terminal = Box::new(cx.new_view(|cx| { TerminalView::new( terminal, workspace.weak_handle(), @@ -287,7 +281,7 @@ impl TerminalPanel { ) })); pane.update(cx, |pane, cx| { - let focus = pane.has_focus(); + let focus = pane.has_focus(cx); pane.add_item(terminal, true, focus, None, cx); }); } @@ -303,12 +297,16 @@ impl TerminalPanel { .pane .read(cx) .items() - .map(|item| item.id()) + .map(|item| item.item_id().as_u64()) .collect::>(); - let active_item_id = self.pane.read(cx).active_item().map(|item| item.id()); + let active_item_id = self + .pane + .read(cx) + .active_item() + .map(|item| item.item_id().as_u64()); let height = self.height; let width = self.width; - self.pending_serialization = cx.background().spawn( + self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( @@ -328,29 +326,23 @@ impl TerminalPanel { } } -impl Entity for TerminalPanel { - type Event = Event; +impl EventEmitter for TerminalPanel {} + +impl Render for TerminalPanel { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().size_full().child(self.pane.clone()) + } } -impl View for TerminalPanel { - fn ui_name() -> &'static str { - "TerminalPanel" - } - - fn render(&mut self, cx: &mut ViewContext) -> gpui::AnyElement { - ChildView::new(&self.pane, cx).into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.pane); - } +impl FocusableView for TerminalPanel { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.pane.focus_handle(cx) } } impl Panel for TerminalPanel { fn position(&self, cx: &WindowContext) -> DockPosition { - match settings::get::(cx).dock { + match TerminalSettings::get_global(cx).dock { TerminalDockPosition::Left => DockPosition::Left, TerminalDockPosition::Bottom => DockPosition::Bottom, TerminalDockPosition::Right => DockPosition::Right, @@ -372,8 +364,8 @@ impl Panel for TerminalPanel { }); } - fn size(&self, cx: &WindowContext) -> f32 { - let settings = settings::get::(cx); + fn size(&self, cx: &WindowContext) -> Pixels { + let settings = TerminalSettings::get_global(cx); match self.position(cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or_else(|| settings.default_width) @@ -382,7 +374,7 @@ impl Panel for TerminalPanel { } } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { match self.position(cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, @@ -391,14 +383,6 @@ impl Panel for TerminalPanel { cx.notify(); } - fn should_zoom_in_on_event(event: &Event) -> bool { - matches!(event, Event::ZoomIn) - } - - fn should_zoom_out_on_event(event: &Event) -> bool { - matches!(event, Event::ZoomOut) - } - fn is_zoomed(&self, cx: &WindowContext) -> bool { self.pane.read(cx).is_zoomed() } @@ -413,14 +397,6 @@ impl Panel for TerminalPanel { } } - fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { - Some("icons/terminal.svg") - } - - fn icon_tooltip(&self) -> (String, Option>) { - ("Terminal Panel".into(), Some(Box::new(ToggleFocus))) - } - fn icon_label(&self, cx: &WindowContext) -> Option { let count = self.pane.read(cx).items_len(); if count == 0 { @@ -430,31 +406,32 @@ impl Panel for TerminalPanel { } } - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, Event::DockPositionChanged) + fn persistent_name() -> &'static str { + "TerminalPanel" } - fn should_activate_on_event(_: &Self::Event) -> bool { - false + // todo!() + // fn icon_tooltip(&self) -> (String, Option>) { + // ("Terminal Panel".into(), Some(Box::new(ToggleFocus))) + // } + + fn icon(&self, _cx: &WindowContext) -> Option { + Some(Icon::Terminal) } - fn should_close_on_event(event: &Event) -> bool { - matches!(event, Event::Close) + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + Some("Terminal Panel") } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus() - } - - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, Event::Focus) + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) } } #[derive(Serialize, Deserialize)] struct SerializedTerminalPanel { - items: Vec, - active_item_id: Option, - width: Option, - height: Option, + items: Vec, + active_item_id: Option, + width: Option, + height: Option, } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index dda976b2cd..d4dea29b49 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2,48 +2,47 @@ mod persistence; pub mod terminal_element; pub mod terminal_panel; -use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; -use anyhow::Context; -use context_menu::{ContextMenu, ContextMenuItem}; -use dirs::home_dir; +// todo!() +// use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, - elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack}, - geometry::vector::Vector2F, - impl_actions, - keymap_matcher::{KeymapContext, Keystroke}, - platform::{KeyDownEvent, ModifiersChangedEvent}, - AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, + FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels, + Render, Styled, Subscription, Task, View, VisualContext, WeakView, }; use language::Bias; +use persistence::TERMINAL_DB; use project::{search::SearchQuery, LocalWorktree, Project}; -use serde::Deserialize; -use smallvec::{smallvec, SmallVec}; -use smol::Timer; -use std::{ - borrow::Cow, - ops::RangeInclusive, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; use terminal::{ alacritty_terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, - Event, MaybeNavigationTarget, Terminal, + Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal, }; +use terminal_element::TerminalElement; +use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, notifications::NotifyResultExt, - pane, register_deserializable_item, - searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, - NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, + register_deserializable_item, + searchable::{SearchEvent, SearchOptions, SearchableItem}, + CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, +}; + +use anyhow::Context; +use dirs::home_dir; +use serde::Deserialize; +use settings::Settings; +use smol::Timer; + +use std::{ + ops::RangeInclusive, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -52,18 +51,13 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); #[derive(Clone, Debug, PartialEq)] pub struct ScrollTerminal(pub i32); -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SendText(String); -#[derive(Clone, Default, Deserialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct SendKeystroke(String); -actions!( - terminal, - [Clear, Copy, Paste, ShowCharacterPalette, SearchTest] -); - -impl_actions!(terminal, [SendText, SendKeystroke]); +impl_actions!(terminal_view, [SendText, SendKeystroke]); pub fn init(cx: &mut AppContext) { terminal_panel::init(cx); @@ -71,35 +65,37 @@ pub fn init(cx: &mut AppContext) { register_deserializable_item::(cx); - cx.add_action(TerminalView::deploy); - - //Useful terminal views - cx.add_action(TerminalView::send_text); - cx.add_action(TerminalView::send_keystroke); - cx.add_action(TerminalView::copy); - cx.add_action(TerminalView::paste); - cx.add_action(TerminalView::clear); - cx.add_action(TerminalView::show_character_palette); - cx.add_action(TerminalView::select_all) + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(TerminalView::deploy); + }) + .detach(); } ///A terminal view, maintains the PTY's file handles and communicates with the terminal pub struct TerminalView { - terminal: ModelHandle, + terminal: Model, + focus_handle: FocusHandle, has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, - context_menu: ViewHandle, + context_menu: Option<(View, gpui::Point, Subscription)>, blink_state: bool, blinking_on: bool, blinking_paused: bool, blink_epoch: usize, can_navigate_to_selected_word: bool, workspace_id: WorkspaceId, + _subscriptions: Vec, } -impl Entity for TerminalView { - type Event = Event; +impl EventEmitter for TerminalView {} +impl EventEmitter for TerminalView {} +impl EventEmitter for TerminalView {} + +impl FocusableView for TerminalView { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } } impl TerminalView { @@ -109,11 +105,11 @@ impl TerminalView { _: &NewCenterTerminal, cx: &mut ViewContext, ) { - let strategy = settings::get::(cx); + let strategy = TerminalSettings::get_global(cx); let working_directory = get_working_directory(workspace, cx, strategy.working_directory.clone()); - let window = cx.window(); + let window = cx.window_handle(); let terminal = workspace .project() .update(cx, |project, cx| { @@ -122,7 +118,7 @@ impl TerminalView { .notify_err(workspace, cx); if let Some(terminal) = terminal { - let view = cx.add_view(|cx| { + let view = cx.new_view(|cx| { TerminalView::new( terminal, workspace.weak_handle(), @@ -135,20 +131,21 @@ impl TerminalView { } pub fn new( - terminal: ModelHandle, - workspace: WeakViewHandle, + terminal: Model, + workspace: WeakView, workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { - let view_id = cx.view_id(); cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); cx.subscribe(&terminal, move |this, _, event, cx| match event { Event::Wakeup => { - if !cx.is_self_focused() { + if !this.focus_handle.is_focused(cx) { this.has_new_content = true; } cx.notify(); cx.emit(Event::Wakeup); + cx.emit(ItemEvent::UpdateTab); + cx.emit(SearchEvent::MatchesInvalidated); } Event::Bell => { @@ -159,15 +156,16 @@ impl TerminalView { Event::BlinkChanged => this.blinking_on = !this.blinking_on, Event::TitleChanged => { + cx.emit(ItemEvent::UpdateTab); if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { let cwd = foreground_info.cwd.clone(); - let item_id = cx.view_id(); + let item_id = cx.entity_id(); let workspace_id = this.workspace_id; - cx.background() + cx.background_executor() .spawn(async move { TERMINAL_DB - .save_working_directory(item_id, workspace_id, cwd) + .save_working_directory(item_id.as_u64(), workspace_id, cwd) .await .log_err(); }) @@ -186,7 +184,7 @@ impl TerminalView { } Event::Open(maybe_navigation_target) => match maybe_navigation_target { - MaybeNavigationTarget::Url(url) => cx.platform().open_url(url), + MaybeNavigationTarget::Url(url) => cx.open_url(url), MaybeNavigationTarget::PathLike(maybe_path) => { if !this.can_navigate_to_selected_word { @@ -252,26 +250,37 @@ impl TerminalView { } } }, - - _ => cx.emit(event.clone()), + Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), + Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), + Event::SelectionsChanged => cx.emit(SearchEvent::ActiveMatchChanged), }) .detach(); + let focus_handle = cx.focus_handle(); + let focus_in = cx.on_focus_in(&focus_handle, |terminal_view, cx| { + terminal_view.focus_in(cx); + }); + let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, cx| { + terminal_view.focus_out(cx); + }); + Self { terminal, has_new_content: true, has_bell: false, - context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + focus_handle: cx.focus_handle(), + context_menu: None, blink_state: true, blinking_on: false, blinking_paused: false, blink_epoch: 0, can_navigate_to_selected_word: false, workspace_id, + _subscriptions: vec![focus_in, focus_out], } } - pub fn model(&self) -> &ModelHandle { + pub fn model(&self) -> &Model { &self.terminal } @@ -288,17 +297,29 @@ impl TerminalView { cx.emit(Event::Wakeup); } - pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext) { - let menu_entries = vec![ - ContextMenuItem::action("Clear", Clear), - ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }), - ]; - - self.context_menu.update(cx, |menu, cx| { - menu.show(position, AnchorCorner::TopLeft, menu_entries, cx) + pub fn deploy_context_menu( + &mut self, + position: gpui::Point, + cx: &mut ViewContext, + ) { + let context_menu = ContextMenu::build(cx, |menu, _| { + menu.action("Clear", Box::new(Clear)) + .action("Close", Box::new(CloseActiveItem { save_intent: None })) }); - cx.notify(); + 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)); } fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { @@ -314,7 +335,7 @@ impl TerminalView { self.terminal.update(cx, |term, cx| { term.try_keystroke( &Keystroke::parse("ctrl-cmd-space").unwrap(), - settings::get::(cx).option_as_meta, + TerminalSettings::get_global(cx).option_as_meta, ) }); } @@ -345,7 +366,7 @@ impl TerminalView { return true; } - match settings::get::(cx).blinking { + match TerminalSettings::get_global(cx).blinking { //If the user requested to never blink, don't blink it. TerminalBlink::Off => true, //If the terminal is controlling it, check terminal mode @@ -392,11 +413,11 @@ impl TerminalView { self.terminal .update(cx, |term, cx| term.find_matches(searcher, cx)) } else { - cx.background().spawn(async { Vec::new() }) + cx.background_executor().spawn(async { Vec::new() }) } } - pub fn terminal(&self) -> &ModelHandle { + pub fn terminal(&self) -> &Model { &self.terminal } @@ -436,19 +457,91 @@ impl TerminalView { if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { self.clear_bel(cx); self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &keystroke, - settings::get::(cx).option_as_meta, - ); + term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta); }); } } + + fn dispatch_context(&self, cx: &AppContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("Terminal"); + + let mode = self.terminal.read(cx).last_content.mode; + dispatch_context.set( + "screen", + if mode.contains(TermMode::ALT_SCREEN) { + "alt" + } else { + "normal" + }, + ); + + if mode.contains(TermMode::APP_CURSOR) { + dispatch_context.add("DECCKM"); + } + if mode.contains(TermMode::APP_KEYPAD) { + dispatch_context.add("DECPAM"); + } else { + dispatch_context.add("DECPNM"); + } + if mode.contains(TermMode::SHOW_CURSOR) { + dispatch_context.add("DECTCEM"); + } + if mode.contains(TermMode::LINE_WRAP) { + dispatch_context.add("DECAWM"); + } + if mode.contains(TermMode::ORIGIN) { + dispatch_context.add("DECOM"); + } + if mode.contains(TermMode::INSERT) { + dispatch_context.add("IRM"); + } + //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html + if mode.contains(TermMode::LINE_FEED_NEW_LINE) { + dispatch_context.add("LNM"); + } + if mode.contains(TermMode::FOCUS_IN_OUT) { + dispatch_context.add("report_focus"); + } + if mode.contains(TermMode::ALTERNATE_SCROLL) { + dispatch_context.add("alternate_scroll"); + } + if mode.contains(TermMode::BRACKETED_PASTE) { + dispatch_context.add("bracketed_paste"); + } + if mode.intersects(TermMode::MOUSE_MODE) { + dispatch_context.add("any_mouse_reporting"); + } + { + let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { + "click" + } else if mode.contains(TermMode::MOUSE_DRAG) { + "drag" + } else if mode.contains(TermMode::MOUSE_MOTION) { + "motion" + } else { + "off" + }; + dispatch_context.set("mouse_reporting", mouse_reporting); + } + { + let format = if mode.contains(TermMode::SGR_MOUSE) { + "sgr" + } else if mode.contains(TermMode::UTF8_MOUSE) { + "utf8" + } else { + "normal" + }; + dispatch_context.set("mouse_format", format); + }; + dispatch_context + } } fn possible_open_targets( - workspace: &WeakViewHandle, + workspace: &WeakView, maybe_path: &String, - cx: &mut ViewContext<'_, '_, TerminalView>, + cx: &mut ViewContext<'_, TerminalView>, ) -> Vec> { let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| { Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) @@ -467,7 +560,7 @@ fn possible_open_targets( } else { Vec::new() } - } else if let Some(workspace) = workspace.upgrade(cx) { + } else if let Some(workspace) = workspace.upgrade() { workspace.update(cx, |workspace, cx| { workspace .worktrees(cx) @@ -495,198 +588,102 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option &'static str { - "Terminal" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { - let terminal_handle = self.terminal.clone().downgrade(); - - let self_id = cx.view_id(); - let focused = cx - .focused_view_id() - .filter(|view_id| *view_id == self_id) - .is_some(); - - Stack::new() - .with_child( - TerminalElement::new( - terminal_handle, - focused, - self.should_show_cursor(focused, cx), - self.can_navigate_to_selected_word, - ) - .contained(), - ) - .with_child(ChildView::new(&self.context_menu, cx)) - .into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_new_content = false; - self.terminal.read(cx).focus_in(); - self.blink_cursors(self.blink_epoch, cx); - cx.notify(); - } - - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.terminal.update(cx, |terminal, _| { - terminal.focus_out(); - }); - cx.notify(); - } - - fn modifiers_changed( - &mut self, - event: &ModifiersChangedEvent, - cx: &mut ViewContext, - ) -> bool { - let handled = self - .terminal() - .update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); - if handled { - cx.notify(); - } - handled - } - - fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext) -> bool { +impl TerminalView { + fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext) { self.clear_bel(cx); self.pause_cursor_blinking(cx); self.terminal.update(cx, |term, cx| { term.try_keystroke( &event.keystroke, - settings::get::(cx).option_as_meta, + TerminalSettings::get_global(cx).option_as_meta, ) - }) - } - - //IME stuff - fn selected_text_range(&self, cx: &AppContext) -> Option> { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(0..0) - } - } - - fn replace_text_in_range( - &mut self, - _: Option>, - text: &str, - cx: &mut ViewContext, - ) { - self.terminal.update(cx, |terminal, _| { - terminal.input(text.into()); }); } - fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) { - Self::reset_to_default_keymap_context(keymap); + fn focus_in(&mut self, cx: &mut ViewContext) { + self.has_new_content = false; + self.terminal.read(cx).focus_in(); + self.blink_cursors(self.blink_epoch, cx); + cx.notify(); + } - let mode = self.terminal.read(cx).last_content.mode; - keymap.add_key( - "screen", - if mode.contains(TermMode::ALT_SCREEN) { - "alt" - } else { - "normal" - }, - ); + fn focus_out(&mut self, cx: &mut ViewContext) { + self.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); + cx.notify(); + } +} - if mode.contains(TermMode::APP_CURSOR) { - keymap.add_identifier("DECCKM"); - } - if mode.contains(TermMode::APP_KEYPAD) { - keymap.add_identifier("DECPAM"); - } else { - keymap.add_identifier("DECPNM"); - } - if mode.contains(TermMode::SHOW_CURSOR) { - keymap.add_identifier("DECTCEM"); - } - if mode.contains(TermMode::LINE_WRAP) { - keymap.add_identifier("DECAWM"); - } - if mode.contains(TermMode::ORIGIN) { - keymap.add_identifier("DECOM"); - } - if mode.contains(TermMode::INSERT) { - keymap.add_identifier("IRM"); - } - //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html - if mode.contains(TermMode::LINE_FEED_NEW_LINE) { - keymap.add_identifier("LNM"); - } - if mode.contains(TermMode::FOCUS_IN_OUT) { - keymap.add_identifier("report_focus"); - } - if mode.contains(TermMode::ALTERNATE_SCROLL) { - keymap.add_identifier("alternate_scroll"); - } - if mode.contains(TermMode::BRACKETED_PASTE) { - keymap.add_identifier("bracketed_paste"); - } - if mode.intersects(TermMode::MOUSE_MODE) { - keymap.add_identifier("any_mouse_reporting"); - } - { - let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { - "click" - } else if mode.contains(TermMode::MOUSE_DRAG) { - "drag" - } else if mode.contains(TermMode::MOUSE_MOTION) { - "motion" - } else { - "off" - }; - keymap.add_key("mouse_reporting", mouse_reporting); - } - { - let format = if mode.contains(TermMode::SGR_MOUSE) { - "sgr" - } else if mode.contains(TermMode::UTF8_MOUSE) { - "utf8" - } else { - "normal" - }; - keymap.add_key("mouse_format", format); - } +impl Render for TerminalView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let terminal_handle = self.terminal.clone(); + + let focused = self.focus_handle.is_focused(cx); + + div() + .size_full() + .relative() + .track_focus(&self.focus_handle) + .key_context(self.dispatch_context(cx)) + .on_action(cx.listener(TerminalView::send_text)) + .on_action(cx.listener(TerminalView::send_keystroke)) + .on_action(cx.listener(TerminalView::copy)) + .on_action(cx.listener(TerminalView::paste)) + .on_action(cx.listener(TerminalView::clear)) + .on_action(cx.listener(TerminalView::show_character_palette)) + .on_action(cx.listener(TerminalView::select_all)) + .on_key_down(cx.listener(Self::key_down)) + .on_mouse_down( + MouseButton::Right, + cx.listener(|this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, cx); + cx.notify(); + }), + ) + .child( + // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu + div().size_full().child(TerminalElement::new( + terminal_handle, + self.focus_handle.clone(), + focused, + self.should_show_cursor(focused, cx), + self.can_navigate_to_selected_word, + )), + ) + .children(self.context_menu.as_ref().map(|(menu, positon, _)| { + overlay() + .position(*positon) + .anchor(gpui::AnchorCorner::TopLeft) + .child(menu.clone()) + })) } } impl Item for TerminalView { - fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + type Event = ItemEvent; + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(self.terminal().read(cx).title().into()) } - fn tab_content( + fn tab_content( &self, _detail: Option, - tab_theme: &theme::Tab, - cx: &gpui::AppContext, - ) -> AnyElement { + selected: bool, + cx: &WindowContext, + ) -> AnyElement { let title = self.terminal().read(cx).title(); - Flex::row() - .with_child( - gpui::elements::Svg::new("icons/terminal.svg") - .with_color(tab_theme.label.text.color) - .constrained() - .with_width(tab_theme.type_icon_width) - .aligned() - .contained() - .with_margin_right(tab_theme.spacing), - ) - .with_child(Label::new(title, tab_theme.label.clone()).aligned()) + h_stack() + .gap_2() + .child(IconElement::new(Icon::Terminal)) + .child(Label::new(title).color(if selected { + Color::Default + } else { + Color::Muted + })) .into_any() } @@ -694,7 +691,7 @@ impl Item for TerminalView { &self, _workspace_id: WorkspaceId, _cx: &mut ViewContext, - ) -> Option { + ) -> Option> { //From what I can tell, there's no way to tell the current working //Directory of the terminal from outside the shell. There might be //solutions to this, but they are non-trivial and require more IPC @@ -717,21 +714,13 @@ impl Item for TerminalView { false } - fn as_searchable(&self, handle: &ViewHandle) -> Option> { - Some(Box::new(handle.clone())) - } - - fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { - match event { - Event::BreadcrumbsChanged => smallvec![ItemEvent::UpdateBreadcrumbs], - Event::TitleChanged | Event::Wakeup => smallvec![ItemEvent::UpdateTab], - Event::CloseTerminal => smallvec![ItemEvent::CloseItem], - _ => smallvec![], - } - } + // todo!(search) + // fn as_searchable(&self, handle: &View) -> Option> { + // Some(Box::new(handle.clone())) + // } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option> { @@ -746,51 +735,55 @@ impl Item for TerminalView { } fn deserialize( - project: ModelHandle, - workspace: WeakViewHandle, + project: Model, + workspace: WeakView, workspace_id: workspace::WorkspaceId, item_id: workspace::ItemId, cx: &mut ViewContext, - ) -> Task>> { - let window = cx.window(); + ) -> Task>> { + let window = cx.window_handle(); cx.spawn(|pane, mut cx| async move { let cwd = TERMINAL_DB .get_working_directory(item_id, workspace_id) .log_err() .flatten() .or_else(|| { - cx.read(|cx| { - let strategy = settings::get::(cx) - .working_directory - .clone(); + cx.update(|_, cx| { + let strategy = TerminalSettings::get_global(cx).working_directory.clone(); workspace - .upgrade(cx) + .upgrade() .map(|workspace| { get_working_directory(workspace.read(cx), cx, strategy) }) .flatten() }) + .ok() + .flatten() }); let terminal = project.update(&mut cx, |project, cx| { project.create_terminal(cwd, window, cx) - })?; - Ok(pane.update(&mut cx, |_, cx| { - cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx)) - })?) + })??; + pane.update(&mut cx, |_, cx| { + cx.new_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx)) + }) }) } fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - cx.background() + cx.background_executor() .spawn(TERMINAL_DB.update_workspace_id( workspace.database_id(), self.workspace_id, - cx.view_id(), + cx.entity_id().as_u64(), )) .detach(); self.workspace_id = workspace.database_id(); } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } } impl SearchableItem for TerminalView { @@ -805,19 +798,6 @@ impl SearchableItem for TerminalView { } } - /// Convert events raised by this item into search-relevant events (if applicable) - fn to_search_event( - &mut self, - event: &Self::Event, - _: &mut ViewContext, - ) -> Option { - match event { - Event::Wakeup => Some(SearchEvent::MatchesInvalidated), - Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), - _ => None, - } - } - /// Clear stored matches fn clear_matches(&mut self, cx: &mut ViewContext) { self.terminal().update(cx, |term, _| term.matches.clear()) @@ -1074,12 +1054,10 @@ mod tests { } /// Creates a worktree with 1 file: /root.txt - pub async fn init_test( - cx: &mut TestAppContext, - ) -> (ModelHandle, ViewHandle) { + pub async fn init_test(cx: &mut TestAppContext) -> (Model, View) { let params = cx.update(AppState::test); cx.update(|cx| { - theme::init((), cx); + theme::init(theme::LoadThemes::JustBase, cx); Project::init_settings(cx); language::init(cx); }); @@ -1087,35 +1065,36 @@ mod tests { let project = Project::test(params.fs.clone(), [], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); + .root_view(cx) + .unwrap(); (project, workspace) } /// Creates a worktree with 1 folder: /root{suffix}/ async fn create_folder_wt( - project: ModelHandle, + project: Model, path: impl AsRef, cx: &mut TestAppContext, - ) -> (ModelHandle, Entry) { + ) -> (Model, Entry) { create_wt(project, true, path, cx).await } /// Creates a worktree with 1 file: /root{suffix}.txt async fn create_file_wt( - project: ModelHandle, + project: Model, path: impl AsRef, cx: &mut TestAppContext, - ) -> (ModelHandle, Entry) { + ) -> (Model, Entry) { create_wt(project, false, path, cx).await } async fn create_wt( - project: ModelHandle, + project: Model, is_dir: bool, path: impl AsRef, cx: &mut TestAppContext, - ) -> (ModelHandle, Entry) { + ) -> (Model, Entry) { let (wt, _) = project .update(cx, |project, cx| { project.find_or_create_local_worktree(path, true, cx) @@ -1139,9 +1118,9 @@ mod tests { } pub fn insert_active_entry_for( - wt: ModelHandle, + wt: Model, entry: Entry, - project: ModelHandle, + project: Model, cx: &mut TestAppContext, ) { cx.update(|cx| { diff --git a/crates/terminal_view2/Cargo.toml b/crates/terminal_view2/Cargo.toml deleted file mode 100644 index 9654bed7f5..0000000000 --- a/crates/terminal_view2/Cargo.toml +++ /dev/null @@ -1,46 +0,0 @@ -[package] -name = "terminal_view2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/terminal_view.rs" -doctest = false - -[dependencies] -editor = { package = "editor2", path = "../editor2" } -language = { package = "language2", path = "../language2" } -gpui = { package = "gpui2", path = "../gpui2" } -project = { package = "project2", path = "../project2" } -# search = { package = "search2", path = "../search2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -util = { path = "../util" } -workspace = { package = "workspace2", path = "../workspace2" } -db = { package = "db2", path = "../db2" } -procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } -terminal = { package = "terminal2", path = "../terminal2" } -ui = { package = "ui2", path = "../ui2" } -smallvec.workspace = true -smol.workspace = true -mio-extras = "2.0.6" -futures.workspace = true -ordered-float.workspace = true -itertools = "0.10" -dirs = "4.0.0" -shellexpand = "2.1.0" -libc = "0.2" -anyhow.workspace = true -thiserror.workspace = true -lazy_static.workspace = true -serde.workspace = true -serde_derive.workspace = true - -[dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -client = { package = "client2", path = "../client2", features = ["test-support"]} -project = { package = "project2", path = "../project2", features = ["test-support"]} -workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -rand.workspace = true diff --git a/crates/terminal_view2/README.md b/crates/terminal_view2/README.md deleted file mode 100644 index ca48f54542..0000000000 --- a/crates/terminal_view2/README.md +++ /dev/null @@ -1,23 +0,0 @@ -Design notes: - -This crate is split into two conceptual halves: -- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here. -- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file. - -ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context. - -The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors. - -#Input - -There are currently many distinct paths for getting keystrokes to the terminal: - -1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal - -2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings - -3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`. - -4. Pasted text has a separate pathway. - -Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal diff --git a/crates/terminal_view2/scripts/print256color.sh b/crates/terminal_view2/scripts/print256color.sh deleted file mode 100755 index 8a53f3bc02..0000000000 --- a/crates/terminal_view2/scripts/print256color.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash - -# Tom Hale, 2016. MIT Licence. -# Print out 256 colours, with each number printed in its corresponding colour -# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163 - -set -eu # Fail on errors or undeclared variables - -printable_colours=256 - -# Return a colour that contrasts with the given colour -# Bash only does integer division, so keep it integral -function contrast_colour { - local r g b luminance - colour="$1" - - if (( colour < 16 )); then # Initial 16 ANSI colours - (( colour == 0 )) && printf "15" || printf "0" - return - fi - - # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8 - if (( colour > 231 )); then # Greyscale ramp - (( colour < 244 )) && printf "15" || printf "0" - return - fi - - # All other colours: - # 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5] - # See http://stackoverflow.com/a/27165165/5353461 - - # r=$(( (colour-16) / 36 )) - g=$(( ((colour-16) % 36) / 6 )) - # b=$(( (colour-16) % 6 )) - - # If luminance is bright, print number in black, white otherwise. - # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601 - (( g > 2)) && printf "0" || printf "15" - return - - # Uncomment the below for more precise luminance calculations - - # # Calculate perceived brightness - # # See https://www.w3.org/TR/AERT#color-contrast - # # and http://www.itu.int/rec/R-REC-BT.601 - # # Luminance is in range 0..5000 as each value is 0..5 - # luminance=$(( (r * 299) + (g * 587) + (b * 114) )) - # (( $luminance > 2500 )) && printf "0" || printf "15" -} - -# Print a coloured block with the number of that colour -function print_colour { - local colour="$1" contrast - contrast=$(contrast_colour "$1") - printf "\e[48;5;%sm" "$colour" # Start block of colour - printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number - printf "\e[0m " # Reset colour -} - -# Starting at $1, print a run of $2 colours -function print_run { - local i - for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do - print_colour "$i" - done - printf " " -} - -# Print blocks of colours -function print_blocks { - local start="$1" i - local end="$2" # inclusive - local block_cols="$3" - local block_rows="$4" - local blocks_per_line="$5" - local block_length=$((block_cols * block_rows)) - - # Print sets of blocks - for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do - printf "\n" # Space before each set of blocks - # For each block row - for (( row = 0; row < block_rows; row++ )) do - # Print block columns for all blocks on the line - for (( block = 0; block < blocks_per_line; block++ )) do - print_run $(( i + (block * block_length) )) "$block_cols" - done - (( i += block_cols )) # Prepare to print the next row - printf "\n" - done - done -} - -print_run 0 16 # The first 16 colours are spread over the whole spectrum -printf "\n" -print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive -print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey diff --git a/crates/terminal_view2/scripts/truecolor.sh b/crates/terminal_view2/scripts/truecolor.sh deleted file mode 100755 index 14e5d81308..0000000000 --- a/crates/terminal_view2/scripts/truecolor.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# Copied from: https://unix.stackexchange.com/a/696756 -# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213 - -awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{ - s="/\\"; - total_cols=term_cols*term_lines; - for (colnum = 0; colnum255) g = 510-g; - printf "\033[48;2;%d;%d;%dm", r,g,b; - printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b; - printf "%s\033[0m", substr(s,colnum%2+1,1); - if (colnum%term_cols==term_cols) printf "\n"; - } - printf "\n"; -}' \ No newline at end of file diff --git a/crates/terminal_view2/src/persistence.rs b/crates/terminal_view2/src/persistence.rs deleted file mode 100644 index 0da9ed4729..0000000000 --- a/crates/terminal_view2/src/persistence.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::path::PathBuf; - -use db::{define_connection, query, sqlez_macros::sql}; -use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - -define_connection! { - pub static ref TERMINAL_DB: TerminalDb = - &[sql!( - CREATE TABLE terminals ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - ), - // Remove the unique constraint on the item_id table - // SQLite doesn't have a way of doing this automatically, so - // we have to do this silly copying. - sql!( - CREATE TABLE terminals2 ( - workspace_id INTEGER, - item_id INTEGER, - working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - - INSERT INTO terminals2 (workspace_id, item_id, working_directory) - SELECT workspace_id, item_id, working_directory FROM terminals; - - DROP TABLE terminals; - - ALTER TABLE terminals2 RENAME TO terminals; - )]; -} - -impl TerminalDb { - query! { - pub async fn update_workspace_id( - new_id: WorkspaceId, - old_id: WorkspaceId, - item_id: ItemId - ) -> Result<()> { - UPDATE terminals - SET workspace_id = ? - WHERE workspace_id = ? AND item_id = ? - } - } - - query! { - pub async fn save_working_directory( - item_id: ItemId, - workspace_id: WorkspaceId, - working_directory: PathBuf - ) -> Result<()> { - INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) - VALUES (?, ?, ?) - } - } - - query! { - pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT working_directory - FROM terminals - WHERE item_id = ? AND workspace_id = ? - } - } -} diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs deleted file mode 100644 index 8be10f9469..0000000000 --- a/crates/terminal_view2/src/terminal_element.rs +++ /dev/null @@ -1,1054 +0,0 @@ -use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; -use gpui::{ - div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace, - BorrowWindow, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, - FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, - Interactivity, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, - Pixels, PlatformInputHandler, Point, ShapedLine, StatefulInteractiveElement, StyleRefinement, - Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext, -}; -use itertools::Itertools; -use language::CursorShape; -use settings::Settings; -use terminal::{ - alacritty_terminal::ansi::NamedColor, - alacritty_terminal::{ - ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape}, - grid::Dimensions, - index::Point as AlacPoint, - term::{cell::Flags, TermMode}, - }, - terminal_settings::TerminalSettings, - IndexedCell, Terminal, TerminalContent, TerminalSize, -}; -use theme::{ActiveTheme, Theme, ThemeSettings}; -use ui::Tooltip; - -use std::{any::TypeId, mem}; -use std::{fmt::Debug, ops::RangeInclusive}; - -///The information generated during layout that is necessary for painting -pub struct LayoutState { - cells: Vec, - rects: Vec, - relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, - cursor: Option, - background_color: Hsla, - dimensions: TerminalSize, - mode: TermMode, - display_offset: usize, - hyperlink_tooltip: Option, - gutter: Pixels, -} - -///Helper struct for converting data between alacritty's cursor points, and displayed cursor points -struct DisplayCursor { - line: i32, - col: usize, -} - -impl DisplayCursor { - fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { - Self { - line: cursor_point.line.0 + display_offset as i32, - col: cursor_point.column.0, - } - } - - pub fn line(&self) -> i32 { - self.line - } - - pub fn col(&self) -> usize { - self.col - } -} - -#[derive(Debug, Default)] -struct LayoutCell { - point: AlacPoint, - text: gpui::ShapedLine, -} - -impl LayoutCell { - fn new(point: AlacPoint, text: gpui::ShapedLine) -> LayoutCell { - LayoutCell { point, text } - } - - fn paint( - &self, - origin: Point, - layout: &LayoutState, - _visible_bounds: Bounds, - cx: &mut WindowContext, - ) { - let pos = { - let point = self.point; - - Point::new( - (origin.x + point.column as f32 * layout.dimensions.cell_width).floor(), - origin.y + point.line as f32 * layout.dimensions.line_height, - ) - }; - - self.text.paint(pos, layout.dimensions.line_height, cx).ok(); - } -} - -#[derive(Clone, Debug, Default)] -struct LayoutRect { - point: AlacPoint, - num_of_cells: usize, - color: Hsla, -} - -impl LayoutRect { - fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { - LayoutRect { - point, - num_of_cells, - color, - } - } - - fn extend(&self) -> Self { - LayoutRect { - point: self.point, - num_of_cells: self.num_of_cells + 1, - color: self.color, - } - } - - fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut WindowContext) { - let position = { - let alac_point = self.point; - point( - (origin.x + alac_point.column as f32 * layout.dimensions.cell_width).floor(), - origin.y + alac_point.line as f32 * layout.dimensions.line_height, - ) - }; - let size = point( - (layout.dimensions.cell_width * self.num_of_cells as f32).ceil(), - layout.dimensions.line_height, - ) - .into(); - - cx.paint_quad(fill(Bounds::new(position, size), self.color)); - } -} - -///The GPUI element that paints the terminal. -///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? -pub struct TerminalElement { - terminal: Model, - focus: FocusHandle, - focused: bool, - cursor_visible: bool, - can_navigate_to_selected_word: bool, - interactivity: Interactivity, -} - -impl InteractiveElement for TerminalElement { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -} - -impl StatefulInteractiveElement for TerminalElement {} - -impl TerminalElement { - pub fn new( - terminal: Model, - focus: FocusHandle, - focused: bool, - cursor_visible: bool, - can_navigate_to_selected_word: bool, - ) -> TerminalElement { - TerminalElement { - terminal, - focused, - focus: focus.clone(), - cursor_visible, - can_navigate_to_selected_word, - interactivity: Default::default(), - } - .track_focus(&focus) - .element - } - - //Vec> -> Clip out the parts of the ranges - - fn layout_grid( - grid: &Vec, - text_style: &TextStyle, - // terminal_theme: &TerminalStyle, - text_system: &TextSystem, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - cx: &WindowContext<'_>, - ) -> (Vec, Vec) { - let theme = cx.theme(); - let mut cells = vec![]; - let mut rects = vec![]; - - let mut cur_rect: Option = None; - let mut cur_alac_color = None; - - let linegroups = grid.into_iter().group_by(|i| i.point.line); - for (line_index, (_, line)) in linegroups.into_iter().enumerate() { - for cell in line { - let mut fg = cell.fg; - let mut bg = cell.bg; - if cell.flags.contains(Flags::INVERSE) { - mem::swap(&mut fg, &mut bg); - } - - //Expand background rect range - { - if matches!(bg, Named(NamedColor::Background)) { - //Continue to next cell, resetting variables if necessary - cur_alac_color = None; - if let Some(rect) = cur_rect { - rects.push(rect); - cur_rect = None - } - } else { - match cur_alac_color { - Some(cur_color) => { - if bg == cur_color { - cur_rect = cur_rect.take().map(|rect| rect.extend()); - } else { - cur_alac_color = Some(bg); - if cur_rect.is_some() { - rects.push(cur_rect.take().unwrap()); - } - cur_rect = Some(LayoutRect::new( - AlacPoint::new( - line_index as i32, - cell.point.column.0 as i32, - ), - 1, - convert_color(&bg, theme), - )); - } - } - None => { - cur_alac_color = Some(bg); - cur_rect = Some(LayoutRect::new( - AlacPoint::new(line_index as i32, cell.point.column.0 as i32), - 1, - convert_color(&bg, &theme), - )); - } - } - } - } - - //Layout current cell text - { - let cell_text = cell.c.to_string(); - if !is_blank(&cell) { - let cell_style = - TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink); - - let layout_cell = text_system - .shape_line( - cell_text.into(), - text_style.font_size.to_pixels(cx.rem_size()), - &[cell_style], - ) - .unwrap(); - - cells.push(LayoutCell::new( - AlacPoint::new(line_index as i32, cell.point.column.0 as i32), - layout_cell, - )) - }; - } - } - - if cur_rect.is_some() { - rects.push(cur_rect.take().unwrap()); - } - } - (cells, rects) - } - - // Compute the cursor position and expected block width, may return a zero width if x_for_index returns - // the same position for sequential indexes. Use em_width instead - fn shape_cursor( - cursor_point: DisplayCursor, - size: TerminalSize, - text_fragment: &ShapedLine, - ) -> Option<(Point, Pixels)> { - if cursor_point.line() < size.total_lines() as i32 { - let cursor_width = if text_fragment.width == Pixels::ZERO { - size.cell_width() - } else { - text_fragment.width - }; - - // Cursor should always surround as much of the text as possible, - // hence when on pixel boundaries round the origin down and the width up - Some(( - point( - (cursor_point.col() as f32 * size.cell_width()).floor(), - (cursor_point.line() as f32 * size.line_height()).floor(), - ), - cursor_width.ceil(), - )) - } else { - None - } - } - - /// Convert the Alacritty cell styles to GPUI text styles and background color - fn cell_style( - indexed: &IndexedCell, - fg: terminal::alacritty_terminal::ansi::Color, - // bg: terminal::alacritty_terminal::ansi::Color, - colors: &Theme, - text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - ) -> TextRun { - let flags = indexed.cell.flags; - let fg = convert_color(&fg, &colors); - // let bg = convert_color(&bg, &colors); - - let underline = (flags.intersects(Flags::ALL_UNDERLINES) - || indexed.cell.hyperlink().is_some()) - .then(|| UnderlineStyle { - color: Some(fg), - thickness: Pixels::from(1.0), - wavy: flags.contains(Flags::UNDERCURL), - }); - - let weight = if flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { - FontWeight::BOLD - } else { - FontWeight::NORMAL - }; - - let style = if flags.intersects(Flags::ITALIC) { - FontStyle::Italic - } else { - FontStyle::Normal - }; - - let mut result = TextRun { - len: indexed.c.len_utf8() as usize, - color: fg, - background_color: None, - font: Font { - weight, - style, - ..text_style.font() - }, - underline, - }; - - if let Some((style, range)) = hyperlink { - if range.contains(&indexed.point) { - if let Some(underline) = style.underline { - result.underline = Some(underline); - } - - if let Some(color) = style.color { - result.color = color; - } - } - } - - result - } - - fn compute_layout(&self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { - let settings = ThemeSettings::get_global(cx).clone(); - - let buffer_font_size = settings.buffer_font_size(cx); - - let terminal_settings = TerminalSettings::get_global(cx); - let font_family = terminal_settings - .font_family - .as_ref() - .map(|string| string.clone().into()) - .unwrap_or(settings.buffer_font.family); - - let font_features = terminal_settings - .font_features - .clone() - .unwrap_or(settings.buffer_font.features.clone()); - - let line_height = terminal_settings.line_height.value(); - let font_size = terminal_settings.font_size.clone(); - - let font_size = - font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); - - let theme = cx.theme().clone(); - - let link_style = HighlightStyle { - color: Some(theme.colors().link_text_hover), - font_weight: None, - font_style: None, - background_color: None, - underline: Some(UnderlineStyle { - thickness: px(1.0), - color: Some(theme.colors().link_text_hover), - wavy: false, - }), - fade_out: None, - }; - - let text_style = TextStyle { - font_family, - font_features, - font_size: font_size.into(), - font_style: FontStyle::Normal, - line_height: line_height.into(), - background_color: None, - white_space: WhiteSpace::Normal, - // These are going to be overridden per-cell - underline: None, - color: theme.colors().text, - font_weight: FontWeight::NORMAL, - }; - - let text_system = cx.text_system(); - let selection_color = theme.players().local(); - let match_color = theme.colors().search_match_background; - let gutter; - let dimensions = { - let rem_size = cx.rem_size(); - let font_pixels = text_style.font_size.to_pixels(rem_size); - let line_height = font_pixels * line_height.to_pixels(rem_size); - let font_id = cx.text_system().font_id(&text_style.font()).unwrap(); - - // todo!(do we need to keep this unwrap?) - let cell_width = text_system - .advance(font_id, font_pixels, 'm') - .unwrap() - .width; - gutter = cell_width; - - let mut size = bounds.size.clone(); - size.width -= gutter; - - TerminalSize::new(line_height, cell_width, size) - }; - - let search_matches = self.terminal.read(cx).matches.clone(); - - let background_color = theme.colors().terminal_background; - - let last_hovered_word = self.terminal.update(cx, |terminal, cx| { - terminal.set_size(dimensions); - terminal.try_sync(cx); - if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { - terminal.last_content.last_hovered_word.clone() - } else { - None - } - }); - - let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { - div() - .size_full() - .id("terminal-element") - .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) - .into_any_element() - }); - - let TerminalContent { - cells, - mode, - display_offset, - cursor_char, - selection, - cursor, - .. - } = &self.terminal.read(cx).last_content; - - // searches, highlights to a single range representations - let mut relative_highlighted_ranges = Vec::new(); - for search_match in search_matches { - relative_highlighted_ranges.push((search_match, match_color)) - } - if let Some(selection) = selection { - relative_highlighted_ranges - .push((selection.start..=selection.end, selection_color.cursor)); - } - - // then have that representation be converted to the appropriate highlight data structure - - let (cells, rects) = TerminalElement::layout_grid( - cells, - &text_style, - &cx.text_system(), - last_hovered_word - .as_ref() - .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), - cx, - ); - - //Layout cursor. Rectangle is used for IME, so we should lay it out even - //if we don't end up showing it. - let cursor = if let AlacCursorShape::Hidden = cursor.shape { - None - } else { - let cursor_point = DisplayCursor::from(cursor.point, *display_offset); - let cursor_text = { - let str_trxt = cursor_char.to_string(); - let len = str_trxt.len(); - cx.text_system() - .shape_line( - str_trxt.into(), - text_style.font_size.to_pixels(cx.rem_size()), - &[TextRun { - len, - font: text_style.font(), - color: theme.colors().terminal_background, - background_color: None, - underline: Default::default(), - }], - ) - //todo!(do we need to keep this unwrap?) - .unwrap() - }; - - let focused = self.focused; - TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - let (shape, text) = match cursor.shape { - AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), - AlacCursorShape::Underline => (CursorShape::Underscore, None), - AlacCursorShape::Beam => (CursorShape::Bar, None), - AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), - //This case is handled in the if wrapping the whole cursor layout - AlacCursorShape::Hidden => unreachable!(), - }; - - Cursor::new( - cursor_position, - block_width, - dimensions.line_height, - theme.players().local().cursor, - shape, - text, - ) - }, - ) - }; - - //Done! - LayoutState { - cells, - cursor, - background_color, - dimensions, - rects, - relative_highlighted_ranges, - mode: *mode, - display_offset: *display_offset, - hyperlink_tooltip, - gutter, - } - } - - fn generic_button_handler( - connection: Model, - origin: Point, - focus_handle: FocusHandle, - f: impl Fn(&mut Terminal, Point, &E, &mut ModelContext), - ) -> impl Fn(&E, &mut WindowContext) { - move |event, cx| { - cx.focus(&focus_handle); - connection.update(cx, |terminal, cx| { - f(terminal, origin, event, cx); - - cx.notify(); - }) - } - } - - fn register_key_listeners(&self, cx: &mut WindowContext) { - cx.on_key_event({ - let this = self.terminal.clone(); - move |event: &ModifiersChangedEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - let handled = - this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); - - if handled { - cx.notify(); - } - } - }); - } - - fn register_mouse_listeners( - &mut self, - origin: Point, - mode: TermMode, - bounds: Bounds, - cx: &mut WindowContext, - ) { - let focus = self.focus.clone(); - let terminal = self.terminal.clone(); - - self.interactivity.on_mouse_down(MouseButton::Left, { - let terminal = terminal.clone(); - let focus = focus.clone(); - move |e, cx| { - cx.focus(&focus); - //todo!(context menu) - // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); - terminal.update(cx, |terminal, cx| { - terminal.mouse_down(&e, origin); - - cx.notify(); - }) - } - }); - self.interactivity.on_mouse_move({ - let terminal = terminal.clone(); - let focus = focus.clone(); - move |e, cx| { - if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() { - terminal.update(cx, |terminal, cx| { - terminal.mouse_drag(e, origin, bounds); - cx.notify(); - }) - } - } - }); - self.interactivity.on_mouse_up( - MouseButton::Left, - TerminalElement::generic_button_handler( - terminal.clone(), - origin, - focus.clone(), - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ); - self.interactivity.on_click({ - let terminal = terminal.clone(); - move |e, cx| { - if e.down.button == MouseButton::Right { - let mouse_mode = terminal.update(cx, |terminal, _cx| { - terminal.mouse_mode(e.down.modifiers.shift) - }); - - if !mouse_mode { - //todo!(context menu) - // view.deploy_context_menu(e.position, cx); - } - } - } - }); - - self.interactivity.on_mouse_move({ - let terminal = terminal.clone(); - let focus = focus.clone(); - move |e, cx| { - if focus.is_focused(cx) { - terminal.update(cx, |terminal, cx| { - terminal.mouse_move(&e, origin); - cx.notify(); - }) - } - } - }); - self.interactivity.on_scroll_wheel({ - let terminal = terminal.clone(); - move |e, cx| { - terminal.update(cx, |terminal, cx| { - terminal.scroll_wheel(e, origin); - cx.notify(); - }) - } - }); - - self.interactivity.drag_over_styles.push(( - TypeId::of::(), - StyleRefinement::default().bg(cx.theme().colors().drop_target_background), - )); - self.interactivity.on_drop::({ - let focus = focus.clone(); - let terminal = terminal.clone(); - move |external_paths, cx| { - cx.focus(&focus); - let mut new_text = external_paths - .paths() - .iter() - .map(|path| format!(" {path:?}")) - .join(""); - new_text.push(' '); - terminal.update(cx, |terminal, _| { - // todo!() long paths are not displayed properly albeit the text is there - terminal.paste(&new_text); - }); - } - }); - - // Mouse mode handlers: - // All mouse modes need the extra click handlers - if mode.intersects(TermMode::MOUSE_MODE) { - self.interactivity.on_mouse_down( - MouseButton::Right, - TerminalElement::generic_button_handler( - terminal.clone(), - origin, - focus.clone(), - move |terminal, origin, e, _cx| { - terminal.mouse_down(&e, origin); - }, - ), - ); - self.interactivity.on_mouse_down( - MouseButton::Middle, - TerminalElement::generic_button_handler( - terminal.clone(), - origin, - focus.clone(), - move |terminal, origin, e, _cx| { - terminal.mouse_down(&e, origin); - }, - ), - ); - self.interactivity.on_mouse_up( - MouseButton::Right, - TerminalElement::generic_button_handler( - terminal.clone(), - origin, - focus.clone(), - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ); - self.interactivity.on_mouse_up( - MouseButton::Middle, - TerminalElement::generic_button_handler( - terminal, - origin, - focus, - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ); - } - } -} - -impl Element for TerminalElement { - type State = InteractiveElementState; - - fn request_layout( - &mut self, - element_state: Option, - cx: &mut WindowContext<'_>, - ) -> (LayoutId, Self::State) { - let (layout_id, interactive_state) = - self.interactivity - .layout(element_state, cx, |mut style, cx| { - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - let layout_id = cx.request_layout(&style, None); - - layout_id - }); - - (layout_id, interactive_state) - } - - fn paint( - &mut self, - bounds: Bounds, - state: &mut Self::State, - cx: &mut WindowContext<'_>, - ) { - let mut layout = self.compute_layout(bounds, cx); - - cx.paint_quad(fill(bounds, layout.background_color)); - let origin = bounds.origin + Point::new(layout.gutter, px(0.)); - - let terminal_input_handler = TerminalInputHandler { - cx: cx.to_async(), - terminal: self.terminal.clone(), - cursor_bounds: layout - .cursor - .as_ref() - .map(|cursor| cursor.bounding_rect(origin)), - }; - - self.register_mouse_listeners(origin, layout.mode, bounds, cx); - - let mut interactivity = mem::take(&mut self.interactivity); - interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { - cx.handle_input(&self.focus, terminal_input_handler); - - self.register_key_listeners(cx); - - for rect in &layout.rects { - rect.paint(origin, &layout, cx); - } - - cx.with_z_index(1, |cx| { - for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() - { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, &layout, origin) - { - let hr = HighlightedRange { - start_y, //Need to change this - line_height: layout.dimensions.line_height, - lines: highlighted_range_lines, - color: color.clone(), - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.dimensions.line_height, - }; - hr.paint(bounds, cx); - } - } - }); - - cx.with_z_index(2, |cx| { - for cell in &layout.cells { - cell.paint(origin, &layout, bounds, cx); - } - }); - - if self.cursor_visible { - cx.with_z_index(3, |cx| { - if let Some(cursor) = &layout.cursor { - cursor.paint(origin, cx); - } - }); - } - - if let Some(mut element) = layout.hyperlink_tooltip.take() { - element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx) - } - }); - } -} - -impl IntoElement for TerminalElement { - type Element = Self; - - fn element_id(&self) -> Option { - Some("terminal".into()) - } - - fn into_element(self) -> Self::Element { - self - } -} - -struct TerminalInputHandler { - cx: AsyncWindowContext, - terminal: Model, - cursor_bounds: Option>, -} - -impl PlatformInputHandler for TerminalInputHandler { - fn selected_text_range(&mut self) -> Option> { - self.cx - .update(|_, cx| { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(0..0) - } - }) - .ok() - .flatten() - } - - fn marked_text_range(&mut self) -> Option> { - None - } - - fn text_for_range(&mut self, _: std::ops::Range) -> Option { - None - } - - fn replace_text_in_range( - &mut self, - _replacement_range: Option>, - text: &str, - ) { - self.cx - .update(|_, cx| { - self.terminal.update(cx, |terminal, _| { - terminal.input(text.into()); - }) - }) - .ok(); - } - - fn replace_and_mark_text_in_range( - &mut self, - _range_utf16: Option>, - _new_text: &str, - _new_selected_range: Option>, - ) { - } - - fn unmark_text(&mut self) {} - - fn bounds_for_range(&mut self, _range_utf16: std::ops::Range) -> Option> { - self.cursor_bounds - } -} - -fn is_blank(cell: &IndexedCell) -> bool { - if cell.c != ' ' { - return false; - } - - if cell.bg != AnsiColor::Named(NamedColor::Background) { - return false; - } - - if cell.hyperlink().is_some() { - return false; - } - - if cell - .flags - .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) - { - return false; - } - - return true; -} - -fn to_highlighted_range_lines( - range: &RangeInclusive, - layout: &LayoutState, - origin: Point, -) -> Option<(Pixels, Vec)> { - // Step 1. Normalize the points to be viewport relative. - // When display_offset = 1, here's how the grid is arranged: - //-2,0 -2,1... - //--- Viewport top - //-1,0 -1,1... - //--------- Terminal Top - // 0,0 0,1... - // 1,0 1,1... - //--- Viewport Bottom - // 2,0 2,1... - //--------- Terminal Bottom - - // Normalize to viewport relative, from terminal relative. - // lines are i32s, which are negative above the top left corner of the terminal - // If the user has scrolled, we use the display_offset to tell us which offset - // of the grid data we should be looking at. But for the rendering step, we don't - // want negatives. We want things relative to the 'viewport' (the area of the grid - // which is currently shown according to the display offset) - let unclamped_start = AlacPoint::new( - range.start().line + layout.display_offset, - range.start().column, - ); - let unclamped_end = - AlacPoint::new(range.end().line + layout.display_offset, range.end().column); - - // Step 2. Clamp range to viewport, and return None if it doesn't overlap - if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 { - return None; - } - - let clamped_start_line = unclamped_start.line.0.max(0) as usize; - let clamped_end_line = unclamped_end - .line - .0 - .min(layout.dimensions.num_lines() as i32) as usize; - //Convert the start of the range to pixels - let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height; - - // Step 3. Expand ranges that cross lines into a collection of single-line ranges. - // (also convert to pixels) - let mut highlighted_range_lines = Vec::new(); - for line in clamped_start_line..=clamped_end_line { - let mut line_start = 0; - let mut line_end = layout.dimensions.columns(); - - if line == clamped_start_line { - line_start = unclamped_start.column.0 as usize; - } - if line == clamped_end_line { - line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive - } - - highlighted_range_lines.push(HighlightedRangeLine { - start_x: origin.x + line_start as f32 * layout.dimensions.cell_width, - end_x: origin.x + line_end as f32 * layout.dimensions.cell_width, - }); - } - - Some((start_y, highlighted_range_lines)) -} - -///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent -fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla { - let colors = theme.colors(); - match fg { - //Named and theme defined colors - terminal::alacritty_terminal::ansi::Color::Named(n) => match n { - NamedColor::Black => colors.terminal_ansi_black, - NamedColor::Red => colors.terminal_ansi_red, - NamedColor::Green => colors.terminal_ansi_green, - NamedColor::Yellow => colors.terminal_ansi_yellow, - NamedColor::Blue => colors.terminal_ansi_blue, - NamedColor::Magenta => colors.terminal_ansi_magenta, - NamedColor::Cyan => colors.terminal_ansi_cyan, - NamedColor::White => colors.terminal_ansi_white, - NamedColor::BrightBlack => colors.terminal_ansi_bright_black, - NamedColor::BrightRed => colors.terminal_ansi_bright_red, - NamedColor::BrightGreen => colors.terminal_ansi_bright_green, - NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow, - NamedColor::BrightBlue => colors.terminal_ansi_bright_blue, - NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta, - NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan, - NamedColor::BrightWhite => colors.terminal_ansi_bright_white, - NamedColor::Foreground => colors.text, - NamedColor::Background => colors.background, - NamedColor::Cursor => theme.players().local().cursor, - - // todo!(more colors) - NamedColor::DimBlack => red(), - NamedColor::DimRed => red(), - NamedColor::DimGreen => red(), - NamedColor::DimYellow => red(), - NamedColor::DimBlue => red(), - NamedColor::DimMagenta => red(), - NamedColor::DimCyan => red(), - NamedColor::DimWhite => red(), - NamedColor::BrightForeground => red(), - NamedColor::DimForeground => red(), - }, - //'True' colors - terminal::alacritty_terminal::ansi::Color::Spec(rgb) => { - terminal::rgba_color(rgb.r, rgb.g, rgb.b) - } - //8 bit, indexed colors - terminal::alacritty_terminal::ansi::Color::Indexed(i) => { - terminal::get_color_at_index(*i as usize, theme) - } - } -} diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs deleted file mode 100644 index 9e193e83b7..0000000000 --- a/crates/terminal_view2/src/terminal_panel.rs +++ /dev/null @@ -1,437 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use crate::TerminalView; -use db::kvp::KEY_VALUE_STORE; -use gpui::{ - actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, - FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowContext, -}; -use project::Fs; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; -use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; -use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; -use util::{ResultExt, TryFutureExt}; -use workspace::{ - dock::{DockPosition, Panel, PanelEvent}, - item::Item, - pane, - ui::Icon, - Pane, Workspace, -}; - -use anyhow::Result; - -const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; - -actions!(terminal_panel, [ToggleFocus]); - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace.register_action(TerminalPanel::new_terminal); - workspace.register_action(TerminalPanel::open_terminal); - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }); - }, - ) - .detach(); -} - -pub struct TerminalPanel { - pane: View, - fs: Arc, - workspace: WeakView, - width: Option, - height: Option, - pending_serialization: Task>, - _subscriptions: Vec, -} - -impl TerminalPanel { - fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let terminal_panel = cx.view().clone(); - let pane = cx.new_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.project().clone(), - Default::default(), - Some(Arc::new(|a, cx| { - if let Some(tab) = a.downcast_ref::() { - if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) { - return item.downcast::().is_some(); - } - } - if a.downcast_ref::().is_some() { - return true; - } - - false - })), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(false, cx); - pane.display_nav_history_buttons(false); - pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - h_stack() - .gap_2() - .child( - IconButton::new("plus", Icon::Plus) - .icon_size(IconSize::Small) - .on_click(cx.listener_for(&terminal_panel, |terminal_panel, _, cx| { - terminal_panel.add_terminal(None, cx); - })) - .tooltip(|cx| Tooltip::text("New Terminal", cx)), - ) - .child({ - let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", Icon::Maximize) - .icon_size(IconSize::Small) - .selected(zoomed) - .selected_icon(Icon::Minimize) - .on_click(cx.listener(|pane, _, cx| { - pane.toggle_zoom(&workspace::ToggleZoom, cx); - })) - .tooltip(move |cx| { - Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx) - }) - }) - .into_any_element() - }); - // let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); - // pane.toolbar() - // .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); - pane - }); - let subscriptions = vec![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - ]; - let this = Self { - pane, - fs: workspace.app_state().fs.clone(), - workspace: workspace.weak_handle(), - pending_serialization: Task::ready(None), - width: None, - height: None, - _subscriptions: subscriptions, - }; - let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - }) - .detach(); - this - } - - pub async fn load( - workspace: WeakView, - mut cx: AsyncWindowContext, - ) -> Result> { - let serialized_panel = cx - .background_executor() - .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) }) - .await - .log_err() - .flatten() - .map(|panel| serde_json::from_str::(&panel)) - .transpose() - .log_err() - .flatten(); - - let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| { - let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx)); - let items = if let Some(serialized_panel) = serialized_panel.as_ref() { - panel.update(cx, |panel, cx| { - cx.notify(); - panel.height = serialized_panel.height; - panel.width = serialized_panel.width; - panel.pane.update(cx, |_, cx| { - serialized_panel - .items - .iter() - .map(|item_id| { - TerminalView::deserialize( - workspace.project().clone(), - workspace.weak_handle(), - workspace.database_id(), - *item_id, - cx, - ) - }) - .collect::>() - }) - }) - } else { - Default::default() - }; - let pane = panel.read(cx).pane.clone(); - (panel, pane, items) - })?; - - let pane = pane.downgrade(); - let items = futures::future::join_all(items).await; - pane.update(&mut cx, |pane, cx| { - let active_item_id = serialized_panel - .as_ref() - .and_then(|panel| panel.active_item_id); - let mut active_ix = None; - for item in items { - if let Some(item) = item.log_err() { - let item_id = item.entity_id().as_u64(); - pane.add_item(Box::new(item), false, false, None, cx); - if Some(item_id) == active_item_id { - active_ix = Some(pane.items_len() - 1); - } - } - } - - if let Some(active_ix) = active_ix { - pane.activate_item(active_ix, false, false, cx) - } - })?; - - Ok(panel) - } - - fn handle_pane_event( - &mut self, - _pane: View, - event: &pane::Event, - cx: &mut ViewContext, - ) { - match event { - pane::Event::ActivateItem { .. } => self.serialize(cx), - pane::Event::RemoveItem { .. } => self.serialize(cx), - pane::Event::Remove => cx.emit(PanelEvent::Close), - pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), - pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), - pane::Event::Focus => cx.emit(PanelEvent::Focus), - - pane::Event::AddItem { item } => { - if let Some(workspace) = self.workspace.upgrade() { - let pane = self.pane.clone(); - workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx)) - } - } - - _ => {} - } - } - - pub fn open_terminal( - workspace: &mut Workspace, - action: &workspace::OpenTerminal, - cx: &mut ViewContext, - ) { - let Some(this) = workspace.focus_panel::(cx) else { - return; - }; - - this.update(cx, |this, cx| { - this.add_terminal(Some(action.working_directory.clone()), cx) - }) - } - - ///Create a new Terminal in the current working directory or the user's home directory - fn new_terminal( - workspace: &mut Workspace, - _: &workspace::NewTerminal, - cx: &mut ViewContext, - ) { - let Some(this) = workspace.focus_panel::(cx) else { - return; - }; - - this.update(cx, |this, cx| this.add_terminal(None, cx)) - } - - fn add_terminal(&mut self, working_directory: Option, cx: &mut ViewContext) { - let workspace = self.workspace.clone(); - cx.spawn(|this, mut cx| async move { - let pane = this.update(&mut cx, |this, _| this.pane.clone())?; - workspace.update(&mut cx, |workspace, cx| { - let working_directory = if let Some(working_directory) = working_directory { - Some(working_directory) - } else { - let working_directory_strategy = - TerminalSettings::get_global(cx).working_directory.clone(); - crate::get_working_directory(workspace, cx, working_directory_strategy) - }; - - let window = cx.window_handle(); - if let Some(terminal) = workspace.project().update(cx, |project, cx| { - project - .create_terminal(working_directory, window, cx) - .log_err() - }) { - let terminal = Box::new(cx.new_view(|cx| { - TerminalView::new( - terminal, - workspace.weak_handle(), - workspace.database_id(), - cx, - ) - })); - pane.update(cx, |pane, cx| { - let focus = pane.has_focus(cx); - pane.add_item(terminal, true, focus, None, cx); - }); - } - })?; - this.update(&mut cx, |this, cx| this.serialize(cx))?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - fn serialize(&mut self, cx: &mut ViewContext) { - let items = self - .pane - .read(cx) - .items() - .map(|item| item.item_id().as_u64()) - .collect::>(); - let active_item_id = self - .pane - .read(cx) - .active_item() - .map(|item| item.item_id().as_u64()); - let height = self.height; - let width = self.width; - self.pending_serialization = cx.background_executor().spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - TERMINAL_PANEL_KEY.into(), - serde_json::to_string(&SerializedTerminalPanel { - items, - active_item_id, - height, - width, - })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); - } -} - -impl EventEmitter for TerminalPanel {} - -impl Render for TerminalPanel { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div().size_full().child(self.pane.clone()) - } -} - -impl FocusableView for TerminalPanel { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.pane.focus_handle(cx) - } -} - -impl Panel for TerminalPanel { - fn position(&self, cx: &WindowContext) -> DockPosition { - match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, _: DockPosition) -> bool { - true - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::(self.fs.clone(), cx, move |settings| { - let dock = match position { - DockPosition::Left => TerminalDockPosition::Left, - DockPosition::Bottom => TerminalDockPosition::Bottom, - DockPosition::Right => TerminalDockPosition::Right, - }; - settings.dock = Some(dock); - }); - } - - fn size(&self, cx: &WindowContext) -> Pixels { - let settings = TerminalSettings::get_global(cx); - match self.position(cx) { - DockPosition::Left | DockPosition::Right => { - self.width.unwrap_or_else(|| settings.default_width) - } - DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), - } - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - match self.position(cx) { - DockPosition::Left | DockPosition::Right => self.width = size, - DockPosition::Bottom => self.height = size, - } - self.serialize(cx); - cx.notify(); - } - - fn is_zoomed(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).is_zoomed() - } - - fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); - } - - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - if active && self.pane.read(cx).items_len() == 0 { - self.add_terminal(None, cx) - } - } - - fn icon_label(&self, cx: &WindowContext) -> Option { - let count = self.pane.read(cx).items_len(); - if count == 0 { - None - } else { - Some(count.to_string()) - } - } - - fn persistent_name() -> &'static str { - "TerminalPanel" - } - - // todo!() - // fn icon_tooltip(&self) -> (String, Option>) { - // ("Terminal Panel".into(), Some(Box::new(ToggleFocus))) - // } - - fn icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::Terminal) - } - - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { - Some("Terminal Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } -} - -#[derive(Serialize, Deserialize)] -struct SerializedTerminalPanel { - items: Vec, - active_item_id: Option, - width: Option, - height: Option, -} diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs deleted file mode 100644 index d4dea29b49..0000000000 --- a/crates/terminal_view2/src/terminal_view.rs +++ /dev/null @@ -1,1134 +0,0 @@ -mod persistence; -pub mod terminal_element; -pub mod terminal_panel; - -// todo!() -// use crate::terminal_element::TerminalElement; -use editor::{scroll::autoscroll::Autoscroll, Editor}; -use gpui::{ - div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, - FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels, - Render, Styled, Subscription, Task, View, VisualContext, WeakView, -}; -use language::Bias; -use persistence::TERMINAL_DB; -use project::{search::SearchQuery, LocalWorktree, Project}; -use terminal::{ - alacritty_terminal::{ - index::Point, - term::{search::RegexSearch, TermMode}, - }, - terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, - Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal, -}; -use terminal_element::TerminalElement; -use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; -use util::{paths::PathLikeWithPosition, ResultExt}; -use workspace::{ - item::{BreadcrumbText, Item, ItemEvent}, - notifications::NotifyResultExt, - register_deserializable_item, - searchable::{SearchEvent, SearchOptions, SearchableItem}, - CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, -}; - -use anyhow::Context; -use dirs::home_dir; -use serde::Deserialize; -use settings::Settings; -use smol::Timer; - -use std::{ - ops::RangeInclusive, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; - -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); - -///Event to transmit the scroll from the element to the view -#[derive(Clone, Debug, PartialEq)] -pub struct ScrollTerminal(pub i32); - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -pub struct SendText(String); - -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] -pub struct SendKeystroke(String); - -impl_actions!(terminal_view, [SendText, SendKeystroke]); - -pub fn init(cx: &mut AppContext) { - terminal_panel::init(cx); - terminal::init(cx); - - register_deserializable_item::(cx); - - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(TerminalView::deploy); - }) - .detach(); -} - -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct TerminalView { - terminal: Model, - focus_handle: FocusHandle, - has_new_content: bool, - //Currently using iTerm bell, show bell emoji in tab until input is received - has_bell: bool, - context_menu: Option<(View, gpui::Point, Subscription)>, - blink_state: bool, - blinking_on: bool, - blinking_paused: bool, - blink_epoch: usize, - can_navigate_to_selected_word: bool, - workspace_id: WorkspaceId, - _subscriptions: Vec, -} - -impl EventEmitter for TerminalView {} -impl EventEmitter for TerminalView {} -impl EventEmitter for TerminalView {} - -impl FocusableView for TerminalView { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl TerminalView { - ///Create a new Terminal in the current working directory or the user's home directory - pub fn deploy( - workspace: &mut Workspace, - _: &NewCenterTerminal, - cx: &mut ViewContext, - ) { - let strategy = TerminalSettings::get_global(cx); - let working_directory = - get_working_directory(workspace, cx, strategy.working_directory.clone()); - - let window = cx.window_handle(); - let terminal = workspace - .project() - .update(cx, |project, cx| { - project.create_terminal(working_directory, window, cx) - }) - .notify_err(workspace, cx); - - if let Some(terminal) = terminal { - let view = cx.new_view(|cx| { - TerminalView::new( - terminal, - workspace.weak_handle(), - workspace.database_id(), - cx, - ) - }); - workspace.add_item(Box::new(view), cx) - } - } - - pub fn new( - terminal: Model, - workspace: WeakView, - workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Self { - cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&terminal, move |this, _, event, cx| match event { - Event::Wakeup => { - if !this.focus_handle.is_focused(cx) { - this.has_new_content = true; - } - cx.notify(); - cx.emit(Event::Wakeup); - cx.emit(ItemEvent::UpdateTab); - cx.emit(SearchEvent::MatchesInvalidated); - } - - Event::Bell => { - this.has_bell = true; - cx.emit(Event::Wakeup); - } - - Event::BlinkChanged => this.blinking_on = !this.blinking_on, - - Event::TitleChanged => { - cx.emit(ItemEvent::UpdateTab); - if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info { - let cwd = foreground_info.cwd.clone(); - - let item_id = cx.entity_id(); - let workspace_id = this.workspace_id; - cx.background_executor() - .spawn(async move { - TERMINAL_DB - .save_working_directory(item_id.as_u64(), workspace_id, cwd) - .await - .log_err(); - }) - .detach(); - } - } - - Event::NewNavigationTarget(maybe_navigation_target) => { - this.can_navigate_to_selected_word = match maybe_navigation_target { - Some(MaybeNavigationTarget::Url(_)) => true, - Some(MaybeNavigationTarget::PathLike(maybe_path)) => { - !possible_open_targets(&workspace, maybe_path, cx).is_empty() - } - None => false, - } - } - - Event::Open(maybe_navigation_target) => match maybe_navigation_target { - MaybeNavigationTarget::Url(url) => cx.open_url(url), - - MaybeNavigationTarget::PathLike(maybe_path) => { - if !this.can_navigate_to_selected_word { - return; - } - let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); - if let Some(path) = potential_abs_paths.into_iter().next() { - let is_dir = path.path_like.is_dir(); - let task_workspace = workspace.clone(); - cx.spawn(|_, mut cx| async move { - let opened_items = task_workspace - .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![path.path_like], is_dir, cx) - }) - .context("workspace update")? - .await; - anyhow::ensure!( - opened_items.len() == 1, - "For a single path open, expected single opened item" - ); - let opened_item = opened_items - .into_iter() - .next() - .unwrap() - .transpose() - .context("path open")?; - if is_dir { - task_workspace.update(&mut cx, |workspace, cx| { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::ActivateProjectPanel); - }) - })?; - } else { - if let Some(row) = path.row { - let col = path.column.unwrap_or(0); - if let Some(active_editor) = - opened_item.and_then(|item| item.downcast::()) - { - active_editor - .downgrade() - .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - Bias::Left, - ); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }) - .log_err(); - } - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - }, - Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), - Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), - Event::SelectionsChanged => cx.emit(SearchEvent::ActiveMatchChanged), - }) - .detach(); - - let focus_handle = cx.focus_handle(); - let focus_in = cx.on_focus_in(&focus_handle, |terminal_view, cx| { - terminal_view.focus_in(cx); - }); - let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, cx| { - terminal_view.focus_out(cx); - }); - - Self { - terminal, - has_new_content: true, - has_bell: false, - focus_handle: cx.focus_handle(), - context_menu: None, - blink_state: true, - blinking_on: false, - blinking_paused: false, - blink_epoch: 0, - can_navigate_to_selected_word: false, - workspace_id, - _subscriptions: vec![focus_in, focus_out], - } - } - - pub fn model(&self) -> &Model { - &self.terminal - } - - pub fn has_new_content(&self) -> bool { - self.has_new_content - } - - pub fn has_bell(&self) -> bool { - self.has_bell - } - - pub fn clear_bel(&mut self, cx: &mut ViewContext) { - self.has_bell = false; - cx.emit(Event::Wakeup); - } - - pub fn deploy_context_menu( - &mut self, - position: gpui::Point, - cx: &mut ViewContext, - ) { - let context_menu = ContextMenu::build(cx, |menu, _| { - menu.action("Clear", Box::new(Clear)) - .action("Close", Box::new(CloseActiveItem { save_intent: None })) - }); - - 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)); - } - - fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext) { - if !self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - cx.show_character_palette(); - } else { - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &Keystroke::parse("ctrl-cmd-space").unwrap(), - TerminalSettings::get_global(cx).option_as_meta, - ) - }); - } - } - - fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| term.select_all()); - cx.notify(); - } - - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| term.clear()); - cx.notify(); - } - - pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext) -> bool { - //Don't blink the cursor when not focused, blinking is disabled, or paused - if !focused - || !self.blinking_on - || self.blinking_paused - || self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - return true; - } - - match TerminalSettings::get_global(cx).blinking { - //If the user requested to never blink, don't blink it. - TerminalBlink::Off => true, - //If the terminal is controlling it, check terminal mode - TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state, - } - } - - fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch && !self.blinking_paused { - self.blink_state = !self.blink_state; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)) - .log_err(); - }) - .detach(); - } - } - - pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { - self.blink_state = true; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) - .ok(); - }) - .detach(); - } - - pub fn find_matches( - &mut self, - query: Arc, - cx: &mut ViewContext, - ) -> Task>> { - let searcher = regex_search_for_query(&query); - - if let Some(searcher) = searcher { - self.terminal - .update(cx, |term, cx| term.find_matches(searcher, cx)) - } else { - cx.background_executor().spawn(async { Vec::new() }) - } - } - - pub fn terminal(&self) -> &Model { - &self.terminal - } - - fn next_blink_epoch(&mut self) -> usize { - self.blink_epoch += 1; - self.blink_epoch - } - - fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch { - self.blinking_paused = false; - self.blink_cursors(epoch, cx); - } - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - self.terminal.update(cx, |term, _| term.copy()) - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.read_from_clipboard() { - self.terminal - .update(cx, |terminal, _cx| terminal.paste(item.text())); - } - } - - fn send_text(&mut self, text: &SendText, cx: &mut ViewContext) { - self.clear_bel(cx); - self.terminal.update(cx, |term, _| { - term.input(text.0.to_string()); - }); - } - - fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext) { - if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { - self.clear_bel(cx); - self.terminal.update(cx, |term, cx| { - term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta); - }); - } - } - - fn dispatch_context(&self, cx: &AppContext) -> KeyContext { - let mut dispatch_context = KeyContext::default(); - dispatch_context.add("Terminal"); - - let mode = self.terminal.read(cx).last_content.mode; - dispatch_context.set( - "screen", - if mode.contains(TermMode::ALT_SCREEN) { - "alt" - } else { - "normal" - }, - ); - - if mode.contains(TermMode::APP_CURSOR) { - dispatch_context.add("DECCKM"); - } - if mode.contains(TermMode::APP_KEYPAD) { - dispatch_context.add("DECPAM"); - } else { - dispatch_context.add("DECPNM"); - } - if mode.contains(TermMode::SHOW_CURSOR) { - dispatch_context.add("DECTCEM"); - } - if mode.contains(TermMode::LINE_WRAP) { - dispatch_context.add("DECAWM"); - } - if mode.contains(TermMode::ORIGIN) { - dispatch_context.add("DECOM"); - } - if mode.contains(TermMode::INSERT) { - dispatch_context.add("IRM"); - } - //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html - if mode.contains(TermMode::LINE_FEED_NEW_LINE) { - dispatch_context.add("LNM"); - } - if mode.contains(TermMode::FOCUS_IN_OUT) { - dispatch_context.add("report_focus"); - } - if mode.contains(TermMode::ALTERNATE_SCROLL) { - dispatch_context.add("alternate_scroll"); - } - if mode.contains(TermMode::BRACKETED_PASTE) { - dispatch_context.add("bracketed_paste"); - } - if mode.intersects(TermMode::MOUSE_MODE) { - dispatch_context.add("any_mouse_reporting"); - } - { - let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { - "click" - } else if mode.contains(TermMode::MOUSE_DRAG) { - "drag" - } else if mode.contains(TermMode::MOUSE_MOTION) { - "motion" - } else { - "off" - }; - dispatch_context.set("mouse_reporting", mouse_reporting); - } - { - let format = if mode.contains(TermMode::SGR_MOUSE) { - "sgr" - } else if mode.contains(TermMode::UTF8_MOUSE) { - "utf8" - } else { - "normal" - }; - dispatch_context.set("mouse_format", format); - }; - dispatch_context - } -} - -fn possible_open_targets( - workspace: &WeakView, - maybe_path: &String, - cx: &mut ViewContext<'_, TerminalView>, -) -> Vec> { - let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| { - Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) - }) - .expect("infallible"); - let maybe_path = path_like.path_like; - let potential_abs_paths = if maybe_path.is_absolute() { - vec![maybe_path] - } else if maybe_path.starts_with("~") { - if let Some(abs_path) = maybe_path - .strip_prefix("~") - .ok() - .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path))) - { - vec![abs_path] - } else { - Vec::new() - } - } else if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - workspace - .worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) - .collect() - }) - } else { - Vec::new() - }; - - potential_abs_paths - .into_iter() - .filter(|path| path.exists()) - .map(|path| PathLikeWithPosition { - path_like: path, - row: path_like.row, - column: path_like.column, - }) - .collect() -} - -pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { - let query = query.as_str(); - let searcher = RegexSearch::new(&query); - searcher.ok() -} - -impl TerminalView { - fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext) { - self.clear_bel(cx); - self.pause_cursor_blinking(cx); - - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &event.keystroke, - TerminalSettings::get_global(cx).option_as_meta, - ) - }); - } - - fn focus_in(&mut self, cx: &mut ViewContext) { - self.has_new_content = false; - self.terminal.read(cx).focus_in(); - self.blink_cursors(self.blink_epoch, cx); - cx.notify(); - } - - fn focus_out(&mut self, cx: &mut ViewContext) { - self.terminal.update(cx, |terminal, _| { - terminal.focus_out(); - }); - cx.notify(); - } -} - -impl Render for TerminalView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let terminal_handle = self.terminal.clone(); - - let focused = self.focus_handle.is_focused(cx); - - div() - .size_full() - .relative() - .track_focus(&self.focus_handle) - .key_context(self.dispatch_context(cx)) - .on_action(cx.listener(TerminalView::send_text)) - .on_action(cx.listener(TerminalView::send_keystroke)) - .on_action(cx.listener(TerminalView::copy)) - .on_action(cx.listener(TerminalView::paste)) - .on_action(cx.listener(TerminalView::clear)) - .on_action(cx.listener(TerminalView::show_character_palette)) - .on_action(cx.listener(TerminalView::select_all)) - .on_key_down(cx.listener(Self::key_down)) - .on_mouse_down( - MouseButton::Right, - cx.listener(|this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, cx); - cx.notify(); - }), - ) - .child( - // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu - div().size_full().child(TerminalElement::new( - terminal_handle, - self.focus_handle.clone(), - focused, - self.should_show_cursor(focused, cx), - self.can_navigate_to_selected_word, - )), - ) - .children(self.context_menu.as_ref().map(|(menu, positon, _)| { - overlay() - .position(*positon) - .anchor(gpui::AnchorCorner::TopLeft) - .child(menu.clone()) - })) - } -} - -impl Item for TerminalView { - type Event = ItemEvent; - - fn tab_tooltip_text(&self, cx: &AppContext) -> Option { - Some(self.terminal().read(cx).title().into()) - } - - fn tab_content( - &self, - _detail: Option, - selected: bool, - cx: &WindowContext, - ) -> AnyElement { - let title = self.terminal().read(cx).title(); - - h_stack() - .gap_2() - .child(IconElement::new(Icon::Terminal)) - .child(Label::new(title).color(if selected { - Color::Default - } else { - Color::Muted - })) - .into_any() - } - - fn clone_on_split( - &self, - _workspace_id: WorkspaceId, - _cx: &mut ViewContext, - ) -> Option> { - //From what I can tell, there's no way to tell the current working - //Directory of the terminal from outside the shell. There might be - //solutions to this, but they are non-trivial and require more IPC - - // Some(TerminalContainer::new( - // Err(anyhow::anyhow!("failed to instantiate terminal")), - // workspace_id, - // cx, - // )) - - // TODO - None - } - - fn is_dirty(&self, _cx: &gpui::AppContext) -> bool { - self.has_bell() - } - - fn has_conflict(&self, _cx: &AppContext) -> bool { - false - } - - // todo!(search) - // fn as_searchable(&self, handle: &View) -> Option> { - // Some(Box::new(handle.clone())) - // } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option> { - Some(vec![BreadcrumbText { - text: self.terminal().read(cx).breadcrumb_text.clone(), - highlights: None, - }]) - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("Terminal") - } - - fn deserialize( - project: Model, - workspace: WeakView, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - cx: &mut ViewContext, - ) -> Task>> { - let window = cx.window_handle(); - cx.spawn(|pane, mut cx| async move { - let cwd = TERMINAL_DB - .get_working_directory(item_id, workspace_id) - .log_err() - .flatten() - .or_else(|| { - cx.update(|_, cx| { - let strategy = TerminalSettings::get_global(cx).working_directory.clone(); - workspace - .upgrade() - .map(|workspace| { - get_working_directory(workspace.read(cx), cx, strategy) - }) - .flatten() - }) - .ok() - .flatten() - }); - - let terminal = project.update(&mut cx, |project, cx| { - project.create_terminal(cwd, window, cx) - })??; - pane.update(&mut cx, |_, cx| { - cx.new_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx)) - }) - }) - } - - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - cx.background_executor() - .spawn(TERMINAL_DB.update_workspace_id( - workspace.database_id(), - self.workspace_id, - cx.entity_id().as_u64(), - )) - .detach(); - self.workspace_id = workspace.database_id(); - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } -} - -impl SearchableItem for TerminalView { - type Match = RangeInclusive; - - fn supported_options() -> SearchOptions { - SearchOptions { - case: false, - word: false, - regex: false, - replacement: false, - } - } - - /// Clear stored matches - fn clear_matches(&mut self, cx: &mut ViewContext) { - self.terminal().update(cx, |term, _| term.matches.clear()) - } - - /// Store matches returned from find_matches somewhere for rendering - fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - self.terminal().update(cx, |term, _| term.matches = matches) - } - - /// Return the selection content to pre-load into this search - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - self.terminal() - .read(cx) - .last_content - .selection_text - .clone() - .unwrap_or_default() - } - - /// Focus match at given index into the Vec of matches - fn activate_match(&mut self, index: usize, _: Vec, cx: &mut ViewContext) { - self.terminal() - .update(cx, |term, _| term.activate_match(index)); - cx.notify(); - } - - /// Add selections for all matches given. - fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - self.terminal() - .update(cx, |term, _| term.select_matches(matches)); - cx.notify(); - } - - /// Get all of the matches for this query, should be done on the background - fn find_matches( - &mut self, - query: Arc, - cx: &mut ViewContext, - ) -> Task> { - if let Some(searcher) = regex_search_for_query(&query) { - self.terminal() - .update(cx, |term, cx| term.find_matches(searcher, cx)) - } else { - Task::ready(vec![]) - } - } - - /// Reports back to the search toolbar what the active match should be (the selection) - fn active_match_index( - &mut self, - matches: Vec, - cx: &mut ViewContext, - ) -> Option { - // Selection head might have a value if there's a selection that isn't - // associated with a match. Therefore, if there are no matches, we should - // report None, no matter the state of the terminal - let res = if matches.len() > 0 { - if let Some(selection_head) = self.terminal().read(cx).selection_head { - // If selection head is contained in a match. Return that match - if let Some(ix) = matches - .iter() - .enumerate() - .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() > &selection_head - }) - .map(|(ix, _)| ix) - { - Some(ix) - } else { - // If no selection after selection head, return the last match - Some(matches.len().saturating_sub(1)) - } - } else { - // Matches found but no active selection, return the first last one (closest to cursor) - Some(matches.len().saturating_sub(1)) - } - } else { - None - }; - - res - } - fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { - // Replacement is not supported in terminal view, so this is a no-op. - } -} - -///Get's the working directory for the given workspace, respecting the user's settings. -pub fn get_working_directory( - workspace: &Workspace, - cx: &AppContext, - strategy: WorkingDirectory, -) -> Option { - let res = match strategy { - WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) - .or_else(|| first_project_directory(workspace, cx)), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) - } - }; - res.or_else(home_dir) -} - -///Get's the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - workspace - .worktrees(cx) - .next() - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -///Gets the intuitively correct working directory from the given workspace -///If there is an active entry for this project, returns that entry's worktree root. -///If there's no active entry but there is a worktree, returns that worktrees root. -///If either of these roots are files, or if there are any other query failures, -/// returns the user's home directory -fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option { - let project = workspace.project().read(cx); - - project - .active_entry() - .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) - .or_else(|| workspace.worktrees(cx).next()) - .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) - .and_then(get_path_from_wt) -} - -fn get_path_from_wt(wt: &LocalWorktree) -> Option { - wt.root_entry() - .filter(|re| re.is_dir()) - .map(|_| wt.abs_path().to_path_buf()) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{Entry, Project, ProjectPath, Worktree}; - use std::path::Path; - use workspace::AppState; - - // Working directory calculation tests - - // No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure environment is as expected - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - // No active entry, but a worktree, worktree is a file -> home_dir() - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - create_file_wt(project.clone(), "/root.txt", cx).await; - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure environment is as expected - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - // No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); - } - - // Active entry with a work tree, worktree is a file -> home_dir() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - // Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = current_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - /// Creates a worktree with 1 file: /root.txt - pub async fn init_test(cx: &mut TestAppContext) -> (Model, View) { - let params = cx.update(AppState::test); - cx.update(|cx| { - theme::init(theme::LoadThemes::JustBase, cx); - Project::init_settings(cx); - language::init(cx); - }); - - let project = Project::test(params.fs.clone(), [], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root_view(cx) - .unwrap(); - - (project, workspace) - } - - /// Creates a worktree with 1 folder: /root{suffix}/ - async fn create_folder_wt( - project: Model, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Model, Entry) { - create_wt(project, true, path, cx).await - } - - /// Creates a worktree with 1 file: /root{suffix}.txt - async fn create_file_wt( - project: Model, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Model, Entry) { - create_wt(project, false, path, cx).await - } - - async fn create_wt( - project: Model, - is_dir: bool, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Model, Entry) { - let (wt, _) = project - .update(cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) - }) - .await - .unwrap(); - - let entry = cx - .update(|cx| { - wt.update(cx, |wt, cx| { - wt.as_local() - .unwrap() - .create_entry(Path::new(""), is_dir, cx) - }) - }) - .await - .unwrap() - .unwrap(); - - (wt, entry) - } - - pub fn insert_active_entry_for( - wt: Model, - entry: Entry, - project: Model, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt.read(cx).id(), - path: entry.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); - } -} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 9262cff318..223f5442a3 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -29,7 +29,7 @@ command_palette = { path = "../command_palette" } editor = { package = "editor2", path = "../editor2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } -search = { package = "search2", path = "../search2" } +search = { path = "../search" } settings = { package = "settings2", path = "../settings2" } workspace = { package = "workspace2", path = "../workspace2" } theme = { package = "theme2", path = "../theme2" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3b854902a3..830e61d524 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" ai = { package = "ai2", path = "../ai2"} audio = { package = "audio2", path = "../audio2" } activity_indicator = { path = "../activity_indicator"} -auto_update = { package = "auto_update2", path = "../auto_update2" } +auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { package = "call2", path = "../call2" } channel = { package = "channel2", path = "../channel2" } @@ -36,7 +36,7 @@ db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } feedback = { package="feedback2", path = "../feedback2" } file_finder = { path = "../file_finder" } -search = { package = "search2", path = "../search2" } +search = { path = "../search" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } go_to_line = { path = "../go_to_line" } @@ -47,7 +47,7 @@ language = { package = "language2", path = "../language2" } language_selector = { path = "../language_selector" } lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } -language_tools = { package = "language_tools2", path = "../language_tools2" } +language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } notifications = { package = "notifications2", path = "../notifications2" } assistant = { package = "assistant2", path = "../assistant2" } @@ -65,7 +65,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" } sum_tree = { path = "../sum_tree" } shellexpand = "2.1.0" text = { package = "text2", path = "../text2" } -terminal_view = { package = "terminal_view2", path = "../terminal_view2" } +terminal_view = { path = "../terminal_view" } theme = { package = "theme2", path = "../theme2" } theme_selector = { path = "../theme_selector" } util = { path = "../util" }