diff --git a/Cargo.lock b/Cargo.lock index d1b0e62ca7..1f67be5e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "activity_indicator" +version = "0.1.0" +dependencies = [ + "auto_update", + "editor", + "futures", + "gpui", + "language", + "project", + "settings", + "smallvec", + "util", + "workspace", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -43,6 +59,45 @@ dependencies = [ "memchr", ] +[[package]] +name = "alacritty_config_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "alacritty_terminal" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f" +dependencies = [ + "alacritty_config_derive", + "base64 0.13.0", + "bitflags", + "dirs 3.0.2", + "libc", + "log", + "mio 0.6.23", + "mio-anonymous-pipes", + "mio-extras", + "miow 0.3.7", + "nix", + "parking_lot 0.11.2", + "regex-automata", + "serde", + "serde_yaml", + "signal-hook", + "signal-hook-mio", + "unicode-width", + "vte", + "winapi 0.3.9", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -2500,6 +2555,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lipsum" version = "0.8.2" @@ -2566,21 +2627,6 @@ dependencies = [ "url", ] -[[package]] -name = "lsp_status" -version = "0.1.0" -dependencies = [ - "editor", - "futures", - "gpui", - "language", - "project", - "settings", - "smallvec", - "util", - "workspace", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -2724,7 +2770,7 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", @@ -2742,6 +2788,42 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mio-anonymous-pipes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b" +dependencies = [ + "mio 0.6.23", + "miow 0.3.7", + "parking_lot 0.11.2", + "spsc-buffer", + "winapi 0.3.9", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio 0.6.23", + "slab", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + [[package]] name = "miow" version = "0.2.2" @@ -2754,6 +2836,15 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "multimap" version = "0.8.3" @@ -2798,6 +2889,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.1" @@ -4252,6 +4356,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "servo-fontconfig" version = "0.5.1" @@ -4364,6 +4480,18 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio 0.6.23", + "mio-uds", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4492,6 +4620,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spsc-buffer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b" + [[package]] name = "sqlformat" version = "0.1.8" @@ -4739,6 +4873,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal" +version = "0.1.0" +dependencies = [ + "alacritty_terminal", + "editor", + "futures", + "gpui", + "itertools", + "mio-extras", + "ordered-float", + "project", + "settings", + "smallvec", + "theme", + "util", + "workspace", +] + [[package]] name = "text" version = "0.1.0" @@ -5531,6 +5684,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "util" version = "0.1.0" @@ -5610,12 +5769,33 @@ dependencies = [ "language", "log", "project", + "search", "serde", "settings", "util", "workspace", ] +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -5967,10 +6147,20 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zed" version = "0.42.0" dependencies = [ + "activity_indicator", "anyhow", "assets", "async-compression", @@ -6011,7 +6201,6 @@ dependencies = [ "libc", "log", "lsp", - "lsp_status", "num_cpus", "outline", "parking_lot 0.11.2", @@ -6034,6 +6223,7 @@ dependencies = [ "smol", "sum_tree", "tempdir", + "terminal", "text", "theme", "theme_selector", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 0f1e005891..bea53ece45 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -13,6 +13,8 @@ "ctrl-c": "menu::Cancel", "shift-cmd-{": "pane::ActivatePrevItem", "shift-cmd-}": "pane::ActivateNextItem", + "alt-cmd-left": "pane::ActivatePrevItem", + "alt-cmd-right": "pane::ActivateNextItem", "cmd-w": "pane::CloseActiveItem", "cmd-shift-W": "workspace::CloseWindow", "alt-cmd-t": "pane::CloseInactiveItems", @@ -210,6 +212,43 @@ { "context": "Pane", "bindings": { + "ctrl-1": [ + "pane::ActivateItem", + 0 + ], + "ctrl-2": [ + "pane::ActivateItem", + 1 + ], + "ctrl-3": [ + "pane::ActivateItem", + 2 + ], + "ctrl-4": [ + "pane::ActivateItem", + 3 + ], + "ctrl-5": [ + "pane::ActivateItem", + 4 + ], + "ctrl-6": [ + "pane::ActivateItem", + 5 + ], + "ctrl-7": [ + "pane::ActivateItem", + 6 + ], + "ctrl-8": [ + "pane::ActivateItem", + 7 + ], + "ctrl-9": [ + "pane::ActivateItem", + 8 + ], + "ctrl-0": "pane::ActivateLastItem", "ctrl--": "pane::GoBack", "shift-ctrl-_": "pane::GoForward", "cmd-shift-T": "pane::ReopenClosedItem", @@ -219,6 +258,43 @@ { "context": "Workspace", "bindings": { + "cmd-1": [ + "workspace::ActivatePane", + 0 + ], + "cmd-2": [ + "workspace::ActivatePane", + 1 + ], + "cmd-3": [ + "workspace::ActivatePane", + 2 + ], + "cmd-4": [ + "workspace::ActivatePane", + 3 + ], + "cmd-5": [ + "workspace::ActivatePane", + 4 + ], + "cmd-6": [ + "workspace::ActivatePane", + 5 + ], + "cmd-7": [ + "workspace::ActivatePane", + 6 + ], + "cmd-8": [ + "workspace::ActivatePane", + 7 + ], + "cmd-9": [ + "workspace::ActivatePane", + 8 + ], + "cmd-b": "workspace::ToggleLeftSidebar", "cmd-shift-F": "project_search::Deploy", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", @@ -226,6 +302,7 @@ "cmd-p": "file_finder::Toggle", "cmd-shift-P": "command_palette::Toggle", "cmd-shift-M": "diagnostics::Deploy", + "cmd-shift-E": "project_panel::Toggle", "cmd-alt-s": "workspace::SaveAll" } }, @@ -310,34 +387,8 @@ { "context": "Workspace", "bindings": { - "cmd-1": [ - "workspace::ToggleSidebarItemFocus", - { - "side": "Left", - "item_index": 0 - } - ], - "cmd-shift-!": [ - "workspace::ToggleSidebarItem", - { - "side": "Left", - "item_index": 0 - } - ], - "cmd-9": [ - "workspace::ToggleSidebarItemFocus", - { - "side": "Right", - "item_index": 0 - } - ], - "cmd-shift-(": [ - "workspace::ToggleSidebarItem", - { - "side": "Right", - "item_index": 0 - } - ] + "cmd-shift-C": "contacts_panel::Toggle", + "cmd-shift-B": "workspace::ToggleRightSidebar" } }, { @@ -352,5 +403,21 @@ "f2": "project_panel::Rename", "backspace": "project_panel::Delete" } + }, + { + "context": "Terminal", + "bindings": { + "ctrl-c": "terminal::Sigint", + "escape": "terminal::Escape", + "ctrl-d": "terminal::Quit", + "backspace": "terminal::Del", + "enter": "terminal::Return", + "left": "terminal::Left", + "right": "terminal::Right", + "up": "terminal::Up", + "down": "terminal::Down", + "tab": "terminal::Tab", + "cmd-v": "terminal::Paste" + } } ] \ No newline at end of file diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 6fcd5d3d12..0d2a611d46 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -37,16 +37,12 @@ "ignorePunctuation": true } ], - "escape": [ - "vim::SwitchMode", - "Normal" - ] + "escape": "editor::Cancel" } }, { - "context": "Editor && vim_mode == normal", + "context": "Editor && vim_mode == normal && vim_operator == none", "bindings": { - "escape": "editor::Cancel", "c": [ "vim::PushOperator", "Change" @@ -92,7 +88,13 @@ "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", - "ctrl-o": "pane::GoBack" + "ctrl-o": "pane::GoBack", + "/": [ + "buffer_search::Deploy", + { + "focus": true + } + ] } }, { @@ -146,11 +148,5 @@ "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore" } - }, - { - "context": "Editor && mode == singleline", - "bindings": { - "escape": "editor::Cancel" - } } ] \ No newline at end of file diff --git a/crates/lsp_status/Cargo.toml b/crates/activity_indicator/Cargo.toml similarity index 78% rename from crates/lsp_status/Cargo.toml rename to crates/activity_indicator/Cargo.toml index 19d428d3b2..63998fa47b 100644 --- a/crates/lsp_status/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -1,13 +1,14 @@ [package] -name = "lsp_status" +name = "activity_indicator" version = "0.1.0" edition = "2021" [lib] -path = "src/lsp_status.rs" +path = "src/activity_indicator.rs" doctest = false [dependencies] +auto_update = { path = "../auto_update" } editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } diff --git a/crates/lsp_status/src/lsp_status.rs b/crates/activity_indicator/src/activity_indicator.rs similarity index 60% rename from crates/lsp_status/src/lsp_status.rs rename to crates/activity_indicator/src/activity_indicator.rs index fedf5299de..8bc84f911c 100644 --- a/crates/lsp_status/src/lsp_status.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,7 +1,8 @@ +use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; use editor::Editor; use futures::StreamExt; use gpui::{ - actions, elements::*, platform::CursorStyle, AppContext, Entity, EventContext, ModelHandle, + actions, elements::*, platform::CursorStyle, Action, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; @@ -14,13 +15,18 @@ use workspace::{ItemHandle, StatusItemView, Workspace}; actions!(lsp_status, [ShowErrorMessage]); +const DOWNLOAD_ICON: &'static str = "icons/download-solid-14.svg"; +const WARNING_ICON: &'static str = "icons/warning-solid-14.svg"; +const DONE_ICON: &'static str = "icons/accept.svg"; + pub enum Event { ShowError { lsp_name: Arc, error: String }, } -pub struct LspStatusItem { +pub struct ActivityIndicator { statuses: Vec, project: ModelHandle, + auto_updater: Option>, } struct LspStatus { @@ -29,15 +35,16 @@ struct LspStatus { } pub fn init(cx: &mut MutableAppContext) { - cx.add_action(LspStatusItem::show_error_message); + cx.add_action(ActivityIndicator::show_error_message); + cx.add_action(ActivityIndicator::dismiss_error_message); } -impl LspStatusItem { +impl ActivityIndicator { pub fn new( workspace: &mut Workspace, languages: Arc, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> ViewHandle { let project = workspace.project().clone(); let this = cx.add_view(|cx: &mut ViewContext| { let mut status_events = languages.language_server_binary_statuses(); @@ -63,6 +70,7 @@ impl LspStatusItem { Self { statuses: Default::default(), project: project.clone(), + auto_updater: AutoUpdater::get(cx), } }); cx.subscribe(&this, move |workspace, _, event, cx| match event { @@ -106,6 +114,15 @@ impl LspStatusItem { cx.notify(); } + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + if let Some(updater) = &self.auto_updater { + updater.update(cx, |updater, cx| { + updater.dismiss_error(cx); + }); + } + cx.notify(); + } + fn pending_language_server_work<'a>( &self, cx: &'a AppContext, @@ -129,25 +146,15 @@ impl LspStatusItem { }) .flatten() } -} - -impl Entity for LspStatusItem { - type Event = Event; -} - -impl View for LspStatusItem { - fn ui_name() -> &'static str { - "LspStatus" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let mut message; - let mut icon = None; - let mut handler = None; + fn content_to_render( + &mut self, + cx: &mut RenderContext, + ) -> (Option<&'static str>, String, Option>) { + // Show any language server has pending activity. let mut pending_work = self.pending_language_server_work(cx); if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { - message = lang_server_name.to_string(); + let mut message = lang_server_name.to_string(); message.push_str(": "); if let Some(progress_message) = progress.message.as_ref() { @@ -164,38 +171,43 @@ impl View for LspStatusItem { if additional_work_count > 0 { write!(&mut message, " + {} more", additional_work_count).unwrap(); } - } else { - drop(pending_work); - let mut downloading = SmallVec::<[_; 3]>::new(); - let mut checking_for_update = SmallVec::<[_; 3]>::new(); - let mut failed = SmallVec::<[_; 3]>::new(); - for status in &self.statuses { - match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => { - checking_for_update.push(status.name.clone()); - } - LanguageServerBinaryStatus::Downloading => { - downloading.push(status.name.clone()); - } - LanguageServerBinaryStatus::Failed { .. } => { - failed.push(status.name.clone()); - } - LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => { - } + return (None, message, None); + } + + // Show any language server installation info. + let mut downloading = SmallVec::<[_; 3]>::new(); + let mut checking_for_update = SmallVec::<[_; 3]>::new(); + let mut failed = SmallVec::<[_; 3]>::new(); + for status in &self.statuses { + match status.status { + LanguageServerBinaryStatus::CheckingForUpdate => { + checking_for_update.push(status.name.clone()); } + LanguageServerBinaryStatus::Downloading => { + downloading.push(status.name.clone()); + } + LanguageServerBinaryStatus::Failed { .. } => { + failed.push(status.name.clone()); + } + LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} } + } - if !downloading.is_empty() { - icon = Some("icons/download-solid-14.svg"); - message = format!( + if !downloading.is_empty() { + return ( + Some(DOWNLOAD_ICON), + format!( "Downloading {} language server{}...", downloading.join(", "), if downloading.len() > 1 { "s" } else { "" } - ); - } else if !checking_for_update.is_empty() { - icon = Some("icons/download-solid-14.svg"); - message = format!( + ), + None, + ); + } else if !checking_for_update.is_empty() { + return ( + Some(DOWNLOAD_ICON), + format!( "Checking for updates to {} language server{}...", checking_for_update.join(", "), if checking_for_update.len() > 1 { @@ -203,20 +215,68 @@ impl View for LspStatusItem { } else { "" } - ); - } else if !failed.is_empty() { - icon = Some("icons/warning-solid-14.svg"); - message = format!( + ), + None, + ); + } else if !failed.is_empty() { + return ( + Some(WARNING_ICON), + format!( "Failed to download {} language server{}. Click to show error.", failed.join(", "), if failed.len() > 1 { "s" } else { "" } - ); - handler = Some(|_, _, cx: &mut EventContext| cx.dispatch_action(ShowErrorMessage)); - } else { - return Empty::new().boxed(); - } + ), + Some(Box::new(ShowErrorMessage)), + ); } + // Show any application auto-update info. + if let Some(updater) = &self.auto_updater { + // let theme = &cx.global::().theme.workspace.status_bar; + match &updater.read(cx).status() { + AutoUpdateStatus::Checking => ( + Some(DOWNLOAD_ICON), + "Checking for Zed updates…".to_string(), + None, + ), + AutoUpdateStatus::Downloading => ( + Some(DOWNLOAD_ICON), + "Downloading Zed update…".to_string(), + None, + ), + AutoUpdateStatus::Installing => ( + Some(DOWNLOAD_ICON), + "Installing Zed update…".to_string(), + None, + ), + AutoUpdateStatus::Updated => { + (Some(DONE_ICON), "Restart to update Zed".to_string(), None) + } + AutoUpdateStatus::Errored => ( + Some(WARNING_ICON), + "Auto update failed".to_string(), + Some(Box::new(DismissErrorMessage)), + ), + AutoUpdateStatus::Idle => Default::default(), + } + } else { + Default::default() + } + } +} + +impl Entity for ActivityIndicator { + type Event = Event; +} + +impl View for ActivityIndicator { + fn ui_name() -> &'static str { + "ActivityIndicator" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let (icon, message, action) = self.content_to_render(cx); + let mut element = MouseEventHandler::new::(0, cx, |state, cx| { let theme = &cx .global::() @@ -224,7 +284,7 @@ impl View for LspStatusItem { .workspace .status_bar .lsp_status; - let style = if state.hovered && handler.is_some() { + let style = if state.hovered && action.is_some() { theme.hover.as_ref().unwrap_or(&theme.default) } else { &theme.default @@ -238,9 +298,14 @@ impl View for LspStatusItem { .contained() .with_margin_right(style.icon_spacing) .aligned() - .named("warning-icon") + .named("activity-icon") })) - .with_child(Label::new(message, style.message.clone()).aligned().boxed()) + .with_child( + Text::new(message, style.message.clone()) + .with_soft_wrap(false) + .aligned() + .boxed(), + ) .constrained() .with_height(style.height) .contained() @@ -249,16 +314,16 @@ impl View for LspStatusItem { .boxed() }); - if let Some(handler) = handler { + if let Some(action) = action { element = element .with_cursor_style(CursorStyle::PointingHand) - .on_click(handler); + .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())); } element.boxed() } } -impl StatusItemView for LspStatusItem { +impl StatusItemView for ActivityIndicator { fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 44eb5fe2e8..6e4f171f60 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -3,19 +3,15 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; use gpui::{ - actions, - elements::{Empty, MouseEventHandler, Text}, - platform::AppVersion, - AppContext, AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, - Task, View, ViewContext, WeakViewHandle, + actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, + MutableAppContext, Task, WeakViewHandle, }; use lazy_static::lazy_static; use serde::Deserialize; -use settings::Settings; use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; use update_notification::UpdateNotification; -use workspace::{ItemHandle, StatusItemView, Workspace}; +use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &'static str = "auto-updater-should-show-updated-notification"; @@ -30,7 +26,7 @@ lazy_static! { actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum AutoUpdateStatus { Idle, Checking, @@ -49,10 +45,6 @@ pub struct AutoUpdater { server_url: String, } -pub struct AutoUpdateIndicator { - updater: Option>, -} - #[derive(Deserialize)] struct JsonRelease { version: String, @@ -84,7 +76,6 @@ pub fn init( cx.add_global_action(move |_: &ViewReleaseNotes, cx| { cx.platform().open_url(&format!("{server_url}/releases")); }); - cx.add_action(AutoUpdateIndicator::dismiss_error_message); cx.add_action(UpdateNotification::dismiss); } } @@ -120,7 +111,7 @@ pub fn notify_of_any_new_update( } impl AutoUpdater { - fn get(cx: &mut MutableAppContext) -> Option> { + pub fn get(cx: &mut MutableAppContext) -> Option> { cx.default_global::>>().clone() } @@ -170,6 +161,15 @@ impl AutoUpdater { })); } + 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: ModelHandle, mut cx: AsyncAppContext) -> Result<()> { let (client, server_url, current_version) = this.read_with(&cx, |this, _| { ( @@ -299,79 +299,3 @@ impl AutoUpdater { }) } } - -impl Entity for AutoUpdateIndicator { - type Event = (); -} - -impl View for AutoUpdateIndicator { - fn ui_name() -> &'static str { - "AutoUpdateIndicator" - } - - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - if let Some(updater) = &self.updater { - let theme = &cx.global::().theme.workspace.status_bar; - match &updater.read(cx).status { - AutoUpdateStatus::Checking => Text::new( - "Checking for updates…".to_string(), - theme.auto_update_progress_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Downloading => Text::new( - "Downloading update…".to_string(), - theme.auto_update_progress_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Installing => Text::new( - "Installing update…".to_string(), - theme.auto_update_progress_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Updated => Text::new( - "Restart to update Zed".to_string(), - theme.auto_update_done_message.clone(), - ) - .boxed(), - AutoUpdateStatus::Errored => { - MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &cx.global::().theme.workspace.status_bar; - Text::new( - "Auto update failed".to_string(), - theme.auto_update_done_message.clone(), - ) - .boxed() - }) - .on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage)) - .boxed() - } - AutoUpdateStatus::Idle => Empty::new().boxed(), - } - } else { - Empty::new().boxed() - } - } -} - -impl StatusItemView for AutoUpdateIndicator { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} -} - -impl AutoUpdateIndicator { - pub fn new(cx: &mut ViewContext) -> Self { - let updater = AutoUpdater::get(cx); - if let Some(updater) = &updater { - cx.observe(updater, |_, _, cx| cx.notify()).detach(); - } - Self { updater } - } - - fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { - if let Some(updater) = &self.updater { - updater.update(cx, |updater, cx| { - updater.status = AutoUpdateStatus::Idle; - cx.notify(); - }); - } - } -} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 084d43af1b..538b0fa4b0 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -28,10 +28,7 @@ use std::{ convert::TryFrom, fmt::Write as _, future::Future, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Weak, - }, + sync::{Arc, Weak}, time::{Duration, Instant}, }; use thiserror::Error; @@ -232,12 +229,8 @@ impl Drop for Subscription { impl Client { pub fn new(http: Arc) -> Arc { - lazy_static! { - static ref NEXT_CLIENT_ID: AtomicUsize = AtomicUsize::default(); - } - Arc::new(Self { - id: NEXT_CLIENT_ID.fetch_add(1, Ordering::SeqCst), + id: 0, peer: Peer::new(), http, state: Default::default(), @@ -257,6 +250,12 @@ impl Client { self.http.clone() } + #[cfg(any(test, feature = "test-support"))] + pub fn set_id(&mut self, id: usize) -> &Self { + self.id = id; + self + } + #[cfg(any(test, feature = "test-support"))] pub fn tear_down(&self) { let mut state = self.state.write(); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 6c4b37870c..b14f9b14e4 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -2282,7 +2282,7 @@ pub mod tests { Self { background, users: Default::default(), - next_user_id: Mutex::new(1), + next_user_id: Mutex::new(0), projects: Default::default(), worktree_extensions: Default::default(), next_project_id: Mutex::new(1), @@ -2346,6 +2346,7 @@ pub mod tests { } async fn get_user_by_id(&self, id: UserId) -> Result> { + self.background.simulate_random_delay().await; Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next()) } @@ -2360,6 +2361,7 @@ pub mod tests { } async fn get_user_by_github_login(&self, github_login: &str) -> Result> { + self.background.simulate_random_delay().await; Ok(self .users .lock() @@ -2393,6 +2395,7 @@ pub mod tests { } async fn get_invite_code_for_user(&self, _id: UserId) -> Result> { + self.background.simulate_random_delay().await; Ok(None) } @@ -2430,6 +2433,7 @@ pub mod tests { } async fn unregister_project(&self, project_id: ProjectId) -> Result<()> { + self.background.simulate_random_delay().await; self.projects .lock() .get_mut(&project_id) @@ -2543,6 +2547,7 @@ pub mod tests { requester_id: UserId, responder_id: UserId, ) -> Result<()> { + self.background.simulate_random_delay().await; let mut contacts = self.contacts.lock(); for contact in contacts.iter_mut() { if contact.requester_id == requester_id && contact.responder_id == responder_id { @@ -2572,6 +2577,7 @@ pub mod tests { } async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> { + self.background.simulate_random_delay().await; self.contacts.lock().retain(|contact| { !(contact.requester_id == requester_id && contact.responder_id == responder_id) }); @@ -2583,6 +2589,7 @@ pub mod tests { user_id: UserId, contact_user_id: UserId, ) -> Result<()> { + self.background.simulate_random_delay().await; let mut contacts = self.contacts.lock(); for contact in contacts.iter_mut() { if contact.requester_id == contact_user_id @@ -2609,6 +2616,7 @@ pub mod tests { requester_id: UserId, accept: bool, ) -> Result<()> { + self.background.simulate_random_delay().await; let mut contacts = self.contacts.lock(); for (ix, contact) in contacts.iter_mut().enumerate() { if contact.requester_id == requester_id && contact.responder_id == responder_id { @@ -2804,6 +2812,7 @@ pub mod tests { count: usize, before_id: Option, ) -> Result> { + self.background.simulate_random_delay().await; let mut messages = self .channel_messages .lock() diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 89b1f38782..ed429ea87c 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -50,7 +50,6 @@ use std::{ time::Duration, }; use theme::ThemeRegistry; -use tokio::sync::RwLockReadGuard; use workspace::{Item, SplitDirection, ToggleFollow, Workspace}; #[ctor::ctor] @@ -596,7 +595,7 @@ async fn test_offline_projects( deterministic.run_until_parked(); assert!(server .store - .read() + .lock() .await .project_metadata_for_user(user_a) .is_empty()); @@ -630,7 +629,7 @@ async fn test_offline_projects( cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); assert!(server .store - .read() + .lock() .await .project_metadata_for_user(user_a) .is_empty()); @@ -1491,7 +1490,7 @@ async fn test_collaborating_with_diagnostics( // Wait for server to see the diagnostics update. deterministic.run_until_parked(); { - let store = server.store.read().await; + let store = server.store.lock().await; let project = store.project(ProjectId::from_proto(project_id)).unwrap(); let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap(); assert!(!worktree.diagnostic_summaries.is_empty()); @@ -1517,6 +1516,7 @@ async fn test_collaborating_with_diagnostics( // Join project as client C and observe the diagnostics. let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; + deterministic.run_until_parked(); project_c.read_with(cx_c, |project, cx| { assert_eq!( project.diagnostic_summaries(cx).collect::>(), @@ -3216,7 +3216,7 @@ async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { assert_eq!( server - .state() + .store() .await .channel(channel_id) .unwrap() @@ -4470,8 +4470,16 @@ async fn test_random_collaboration( let mut server = TestServer::start(cx.foreground(), cx.background()).await; let db = server.app_state.db.clone(); let host_user_id = db.create_user("host", None, false).await.unwrap(); - for username in ["guest-1", "guest-2", "guest-3", "guest-4"] { + let mut available_guests = vec![ + "guest-1".to_string(), + "guest-2".to_string(), + "guest-3".to_string(), + "guest-4".to_string(), + ]; + + for username in &available_guests { let guest_user_id = db.create_user(username, None, false).await.unwrap(); + assert_eq!(*username, format!("guest-{}", guest_user_id)); server .app_state .db @@ -4665,12 +4673,7 @@ async fn test_random_collaboration( } else { max_operations }; - let mut available_guests = vec![ - "guest-1".to_string(), - "guest-2".to_string(), - "guest-3".to_string(), - "guest-4".to_string(), - ]; + let mut operations = 0; while operations < max_operations { if operations == disconnect_host_at { @@ -4701,7 +4704,7 @@ async fn test_random_collaboration( .unwrap(); let contacts = server .store - .read() + .lock() .await .build_initial_contacts_update(contacts) .contacts; @@ -4773,6 +4776,7 @@ async fn test_random_collaboration( server.disconnect_client(removed_guest_id); deterministic.advance_clock(RECEIVE_TIMEOUT); deterministic.start_waiting(); + log::info!("Waiting for guest {} to exit...", removed_guest_id); let (guest, guest_project, mut guest_cx, guest_err) = guest.await; deterministic.finish_waiting(); server.allow_connections(); @@ -4785,7 +4789,7 @@ async fn test_random_collaboration( let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap(); let contacts = server .store - .read() + .lock() .await .build_initial_contacts_update(contacts) .contacts; @@ -4989,6 +4993,7 @@ impl TestServer { Arc::get_mut(&mut client) .unwrap() + .set_id(user_id.0 as usize) .override_authenticate(move |cx| { cx.spawn(|_| async move { let access_token = "the-token".to_string(); @@ -5116,10 +5121,6 @@ impl TestServer { }) } - async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> { - self.server.store.read().await - } - async fn condition(&mut self, mut predicate: F) where F: FnMut(&Store) -> bool, @@ -5128,7 +5129,7 @@ impl TestServer { self.foreground.parking_forbidden(), "you must call forbid_parking to use server conditions so we don't block indefinitely" ); - while !(predicate)(&*self.server.store.read().await) { + while !(predicate)(&*self.server.store.lock().await) { self.foreground.start_waiting(); self.notifications.next().await; self.foreground.finish_waiting(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2a5aeb459e..4944761d58 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -51,7 +51,7 @@ use std::{ }; use time::OffsetDateTime; use tokio::{ - sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, + sync::{Mutex, MutexGuard}, time::Sleep, }; use tower::ServiceBuilder; @@ -97,7 +97,7 @@ impl Response { pub struct Server { peer: Arc, - pub(crate) store: RwLock, + pub(crate) store: Mutex, app_state: Arc, handlers: HashMap, notifications: Option>, @@ -115,13 +115,8 @@ pub struct RealExecutor; const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; -struct StoreReadGuard<'a> { - guard: RwLockReadGuard<'a, Store>, - _not_send: PhantomData>, -} - -struct StoreWriteGuard<'a> { - guard: RwLockWriteGuard<'a, Store>, +pub(crate) struct StoreGuard<'a> { + guard: MutexGuard<'a, Store>, _not_send: PhantomData>, } @@ -129,7 +124,7 @@ struct StoreWriteGuard<'a> { pub struct ServerSnapshot<'a> { peer: &'a Peer, #[serde(serialize_with = "serialize_deref")] - store: RwLockReadGuard<'a, Store>, + store: StoreGuard<'a>, } pub fn serialize_deref(value: &T, serializer: S) -> Result @@ -385,7 +380,7 @@ impl Server { ).await?; { - let mut store = this.store_mut().await; + let mut store = this.store().await; store.add_connection(connection_id, user_id, user.admin); this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?; @@ -472,7 +467,7 @@ impl Server { let mut projects_to_unregister = Vec::new(); let removed_user_id; { - let mut store = self.store_mut().await; + let mut store = self.store().await; let removed_connection = store.remove_connection(connection_id)?; for (project_id, project) in removed_connection.hosted_projects { @@ -606,7 +601,7 @@ impl Server { .await .user_id_for_connection(request.sender_id)?; let project_id = self.app_state.db.register_project(user_id).await?; - self.store_mut().await.register_project( + self.store().await.register_project( request.sender_id, project_id, request.payload.online, @@ -626,7 +621,7 @@ impl Server { ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let (user_id, project) = { - let mut state = self.store_mut().await; + let mut state = self.store().await; let project = state.unregister_project(project_id, request.sender_id)?; (state.user_id_for_connection(request.sender_id)?, project) }; @@ -728,7 +723,7 @@ impl Server { return Err(anyhow!("no such project"))?; } - self.store_mut().await.request_join_project( + self.store().await.request_join_project( guest_user_id, project_id, response.into_receipt(), @@ -750,7 +745,7 @@ impl Server { let host_user_id; { - let mut state = self.store_mut().await; + let mut state = self.store().await; let project_id = ProjectId::from_proto(request.payload.project_id); let project = state.project(project_id)?; if project.host_connection_id != request.sender_id { @@ -794,20 +789,10 @@ impl Server { let worktrees = project .worktrees .iter() - .filter_map(|(id, shared_worktree)| { - let worktree = project.worktrees.get(&id)?; - Some(proto::Worktree { - id: *id, - root_name: worktree.root_name.clone(), - entries: shared_worktree.entries.values().cloned().collect(), - diagnostic_summaries: shared_worktree - .diagnostic_summaries - .values() - .cloned() - .collect(), - visible: worktree.visible, - scan_id: shared_worktree.scan_id, - }) + .map(|(id, worktree)| proto::WorktreeMetadata { + id: *id, + root_name: worktree.root_name.clone(), + visible: worktree.visible, }) .collect::>(); @@ -843,14 +828,15 @@ impl Server { } } - for (receipt, replica_id) in receipts_with_replica_ids { + // First, we send the metadata associated with each worktree. + for (receipt, replica_id) in &receipts_with_replica_ids { self.peer.respond( - receipt, + receipt.clone(), proto::JoinProjectResponse { variant: Some(proto::join_project_response::Variant::Accept( proto::join_project_response::Accept { worktrees: worktrees.clone(), - replica_id: replica_id as u32, + replica_id: *replica_id as u32, collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), }, @@ -858,6 +844,43 @@ impl Server { }, )?; } + + for (worktree_id, worktree) in &project.worktrees { + #[cfg(any(test, feature = "test-support"))] + const MAX_CHUNK_SIZE: usize = 2; + #[cfg(not(any(test, feature = "test-support")))] + const MAX_CHUNK_SIZE: usize = 256; + + // Stream this worktree's entries. + let message = proto::UpdateWorktree { + project_id: project_id.to_proto(), + worktree_id: *worktree_id, + root_name: worktree.root_name.clone(), + updated_entries: worktree.entries.values().cloned().collect(), + removed_entries: Default::default(), + scan_id: worktree.scan_id, + is_last_update: worktree.is_complete, + }; + for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { + for (receipt, _) in &receipts_with_replica_ids { + self.peer.send(receipt.sender_id, update.clone())?; + } + } + + // Stream this worktree's diagnostics. + for summary in worktree.diagnostic_summaries.values() { + for (receipt, _) in &receipts_with_replica_ids { + self.peer.send( + receipt.sender_id, + proto::UpdateDiagnosticSummary { + project_id: project_id.to_proto(), + worktree_id: *worktree_id, + summary: Some(summary.clone()), + }, + )?; + } + } + } } self.update_user_contacts(host_user_id).await?; @@ -872,7 +895,7 @@ impl Server { let project_id = ProjectId::from_proto(request.payload.project_id); let project; { - let mut store = self.store_mut().await; + let mut store = self.store().await; project = store.leave_project(sender_id, project_id)?; tracing::info!( %project_id, @@ -923,7 +946,7 @@ impl Server { let project_id = ProjectId::from_proto(request.payload.project_id); let user_id; { - let mut state = self.store_mut().await; + let mut state = self.store().await; user_id = state.user_id_for_connection(request.sender_id)?; let guest_connection_ids = state .read_project(project_id, request.sender_id)? @@ -983,7 +1006,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - self.store_mut().await.register_project_activity( + self.store().await.register_project_activity( ProjectId::from_proto(request.payload.project_id), request.sender_id, )?; @@ -998,7 +1021,7 @@ impl Server { let project_id = ProjectId::from_proto(request.payload.project_id); let worktree_id = request.payload.worktree_id; let (connection_ids, metadata_changed) = { - let mut store = self.store_mut().await; + let mut store = self.store().await; let (connection_ids, metadata_changed) = store.update_worktree( request.sender_id, project_id, @@ -1007,6 +1030,7 @@ impl Server { &request.payload.removed_entries, &request.payload.updated_entries, request.payload.scan_id, + request.payload.is_last_update, )?; (connection_ids, metadata_changed) }; @@ -1054,7 +1078,7 @@ impl Server { .summary .clone() .ok_or_else(|| anyhow!("invalid summary"))?; - let receiver_ids = self.store_mut().await.update_diagnostic_summary( + let receiver_ids = self.store().await.update_diagnostic_summary( ProjectId::from_proto(request.payload.project_id), request.payload.worktree_id, request.sender_id, @@ -1072,7 +1096,7 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let receiver_ids = self.store_mut().await.start_language_server( + let receiver_ids = self.store().await.start_language_server( ProjectId::from_proto(request.payload.project_id), request.sender_id, request @@ -1111,20 +1135,23 @@ impl Server { where T: EntityMessage + RequestMessage, { + let project_id = ProjectId::from_proto(request.payload.remote_entity_id()); let host_connection_id = self .store() .await - .read_project( - ProjectId::from_proto(request.payload.remote_entity_id()), - request.sender_id, - )? + .read_project(project_id, request.sender_id)? .host_connection_id; + let payload = self + .peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?; - response.send( - self.peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?, - )?; + // Ensure project still exists by the time we get the response from the host. + self.store() + .await + .read_project(project_id, request.sender_id)?; + + response.send(payload)?; Ok(()) } @@ -1165,7 +1192,7 @@ impl Server { ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let receiver_ids = { - let mut store = self.store_mut().await; + let mut store = self.store().await; store.register_project_activity(project_id, request.sender_id)?; store.project_connection_ids(project_id, request.sender_id)? }; @@ -1232,7 +1259,7 @@ impl Server { let leader_id = ConnectionId(request.payload.leader_id); let follower_id = request.sender_id; { - let mut store = self.store_mut().await; + let mut store = self.store().await; if !store .project_connection_ids(project_id, follower_id)? .contains(&leader_id) @@ -1257,7 +1284,7 @@ impl Server { async fn unfollow(self: Arc, request: TypedEnvelope) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); let leader_id = ConnectionId(request.payload.leader_id); - let mut store = self.store_mut().await; + let mut store = self.store().await; if !store .project_connection_ids(project_id, request.sender_id)? .contains(&leader_id) @@ -1275,7 +1302,7 @@ impl Server { request: TypedEnvelope, ) -> Result<()> { let project_id = ProjectId::from_proto(request.payload.project_id); - let mut store = self.store_mut().await; + let mut store = self.store().await; store.register_project_activity(project_id, request.sender_id)?; let connection_ids = store.project_connection_ids(project_id, request.sender_id)?; let leader_id = request @@ -1533,7 +1560,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.store_mut() + self.store() .await .join_channel(request.sender_id, channel_id); let messages = self @@ -1575,7 +1602,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.store_mut() + self.store() .await .leave_channel(request.sender_id, channel_id); @@ -1683,25 +1710,13 @@ impl Server { Ok(()) } - async fn store<'a>(self: &'a Arc) -> StoreReadGuard<'a> { + pub(crate) async fn store<'a>(&'a self) -> StoreGuard<'a> { #[cfg(test)] tokio::task::yield_now().await; - let guard = self.store.read().await; + let guard = self.store.lock().await; #[cfg(test)] tokio::task::yield_now().await; - StoreReadGuard { - guard, - _not_send: PhantomData, - } - } - - async fn store_mut<'a>(self: &'a Arc) -> StoreWriteGuard<'a> { - #[cfg(test)] - tokio::task::yield_now().await; - let guard = self.store.write().await; - #[cfg(test)] - tokio::task::yield_now().await; - StoreWriteGuard { + StoreGuard { guard, _not_send: PhantomData, } @@ -1709,13 +1724,13 @@ impl Server { pub async fn snapshot<'a>(self: &'a Arc) -> ServerSnapshot<'a> { ServerSnapshot { - store: self.store.read().await, + store: self.store().await, peer: &self.peer, } } } -impl<'a> Deref for StoreReadGuard<'a> { +impl<'a> Deref for StoreGuard<'a> { type Target = Store; fn deref(&self) -> &Self::Target { @@ -1723,21 +1738,13 @@ impl<'a> Deref for StoreReadGuard<'a> { } } -impl<'a> Deref for StoreWriteGuard<'a> { - type Target = Store; - - fn deref(&self) -> &Self::Target { - &*self.guard - } -} - -impl<'a> DerefMut for StoreWriteGuard<'a> { +impl<'a> DerefMut for StoreGuard<'a> { fn deref_mut(&mut self) -> &mut Self::Target { &mut *self.guard } } -impl<'a> Drop for StoreWriteGuard<'a> { +impl<'a> Drop for StoreGuard<'a> { fn drop(&mut self) { #[cfg(test)] self.check_invariants(); diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 2ae7036ccb..f5cc87cca8 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -56,6 +56,7 @@ pub struct Worktree { #[serde(skip)] pub diagnostic_summaries: BTreeMap, pub scan_id: u64, + pub is_complete: bool, } #[derive(Default)] @@ -646,6 +647,7 @@ impl Store { removed_entries: &[u64], updated_entries: &[proto::Entry], scan_id: u64, + is_last_update: bool, ) -> Result<(Vec, bool)> { let project = self.write_project(project_id, connection_id)?; if !project.online { @@ -666,6 +668,7 @@ impl Store { } worktree.scan_id = scan_id; + worktree.is_complete = is_last_update; Ok((connection_ids, metadata_changed)) } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 3197e46431..f4010a1278 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -8,6 +8,7 @@ use contact_notification::ContactNotification; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ + actions, elements::*, geometry::{rect::RectF, vector::vec2f}, impl_actions, impl_internal_actions, @@ -24,6 +25,8 @@ use std::{ops::DerefMut, sync::Arc}; use theme::IconButton; use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace}; +actions!(contacts_panel, [Toggle]); + impl_actions!( contacts_panel, [RequestContact, RemoveContact, RespondToContactRequest] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 19226c6472..faa821f20d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -490,7 +490,7 @@ impl EditorElement { } let block_text = - if matches!(self.cursor_shape, CursorShape::Block) { + if let CursorShape::Block = self.cursor_shape { layout.snapshot.chars_at(cursor_position).next().and_then( |character| { let font_id = @@ -520,7 +520,7 @@ impl EditorElement { cursors.push(Cursor { color: selection_style.cursor, block_width, - origin: content_origin + vec2f(x, y), + origin: vec2f(x, y), line_height: layout.line_height, shape: self.cursor_shape, block_text, @@ -546,13 +546,12 @@ impl EditorElement { cx.scene.push_layer(Some(bounds)); for cursor in cursors { - cursor.paint(cx); + cursor.paint(content_origin, cx); } cx.scene.pop_layer(); if let Some((position, context_menu)) = layout.context_menu.as_mut() { cx.scene.push_stacking_context(None); - let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize]; let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; let y = (position.row() + 1) as f32 * layout.line_height - scroll_top; @@ -1630,7 +1629,7 @@ impl Default for CursorShape { } } -struct Cursor { +pub struct Cursor { origin: Vector2F, block_width: f32, line_height: f32, @@ -1640,14 +1639,33 @@ struct Cursor { } impl Cursor { - fn paint(&self, cx: &mut PaintContext) { + pub fn new( + origin: Vector2F, + block_width: f32, + line_height: f32, + color: Color, + shape: CursorShape, + block_text: Option, + ) -> Cursor { + Cursor { + origin, + block_width, + line_height, + color, + shape, + block_text, + } + } + + pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) { let bounds = match self.shape { - CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), - CursorShape::Block => { - RectF::new(self.origin, vec2f(self.block_width, self.line_height)) - } + CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)), + CursorShape::Block => RectF::new( + self.origin + origin, + vec2f(self.block_width, self.line_height), + ), CursorShape::Underscore => RectF::new( - self.origin + Vector2F::new(0.0, self.line_height - 2.0), + self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0), vec2f(self.block_width, 2.0), ), }; @@ -1660,7 +1678,7 @@ impl Cursor { }); if let Some(block_text) = &self.block_text { - block_text.paint(self.origin, bounds, self.line_height, cx); + block_text.paint(self.origin + origin, bounds, self.line_height, cx); } } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 20ec9c6db0..ad38520032 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1634,14 +1634,10 @@ impl MutableAppContext { pub fn default_global(&mut self) -> &T { let type_id = TypeId::of::(); self.update(|this| { - if !this.globals.contains_key(&type_id) { + if let Entry::Vacant(entry) = this.cx.globals.entry(type_id) { + entry.insert(Box::new(T::default())); this.notify_global(type_id); } - - this.cx - .globals - .entry(type_id) - .or_insert_with(|| Box::new(T::default())); }); self.globals.get(&type_id).unwrap().downcast_ref().unwrap() } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 88e4d0a498..6285b1be99 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -703,6 +703,20 @@ impl<'a> EventContext<'a> { self.view_stack.last().copied() } + pub fn is_parent_view_focused(&self) -> bool { + if let Some(parent_view_id) = self.view_stack.last() { + self.app.focused_view_id(self.window_id) == Some(*parent_view_id) + } else { + false + } + } + + pub fn focus_parent_view(&mut self) { + if let Some(parent_view_id) = self.view_stack.last() { + self.app.focus(self.window_id, Some(*parent_view_id)) + } + } + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { dispatcher_view_id: self.view_stack.last().copied(), diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 2d8672aab3..50f16cb995 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -164,7 +164,7 @@ impl<'a> Hash for CacheKeyRef<'a> { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct Line { layout: Arc, style_runs: SmallVec<[(u32, Color, Underline); 32]>, diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 0bef424104..8282f1c8ff 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -11,7 +11,7 @@ use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - process, + process::{self, Child}, }; use std::{ future::Future, @@ -44,6 +44,7 @@ pub struct LanguageServer { io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, + _server: Option, } pub struct Subscription { @@ -118,11 +119,20 @@ impl LanguageServer { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) + .kill_on_drop(true) .spawn()?; + let stdin = server.stdin.take().unwrap(); - let stdout = server.stdout.take().unwrap(); - let mut server = - Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| { + let stout = server.stdout.take().unwrap(); + + let mut server = Self::new_internal( + server_id, + stdin, + stout, + Some(server), + root_path, + cx, + |notification| { log::info!( "unhandled notification {}:\n{}", notification.method, @@ -131,7 +141,8 @@ impl LanguageServer { ) .unwrap() ); - }); + }, + ); if let Some(name) = binary_path.file_name() { server.name = name.to_string_lossy().to_string(); } @@ -142,6 +153,7 @@ impl LanguageServer { server_id: usize, stdin: Stdin, stdout: Stdout, + server: Option, root_path: &Path, cx: AsyncAppContext, mut on_unhandled_notification: F, @@ -242,6 +254,7 @@ impl LanguageServer { io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), + _server: server, } } @@ -480,6 +493,10 @@ impl LanguageServer { self.server_id } + pub fn root_path(&self) -> &PathBuf { + &self.root_path + } + pub fn request( &self, params: T::Params, @@ -608,6 +625,7 @@ impl LanguageServer { 0, stdin_writer, stdout_reader, + None, Path::new("/"), cx.clone(), |_| {}, @@ -617,6 +635,7 @@ impl LanguageServer { 0, stdout_writer, stdin_reader, + None, Path::new("/"), cx.clone(), move |msg| { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 3f4cd3450f..212edb9219 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -242,7 +242,7 @@ impl LspCommand for PerformRename { .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) - .cloned() + .map(|(adapter, server)| (adapter.clone(), server.clone())) }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; Project::deserialize_workspace_edit( @@ -359,7 +359,7 @@ impl LspCommand for GetDefinition { .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) - .cloned() + .map(|(adapter, server)| (adapter.clone(), server.clone())) }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; @@ -388,8 +388,8 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - lsp_adapter.clone(), - language_server.clone(), + language_server.server_id(), + lsp_adapter.name(), cx, ) }) @@ -599,7 +599,7 @@ impl LspCommand for GetReferences { .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) - .cloned() + .map(|(adapter, server)| (adapter.clone(), server.clone())) }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; @@ -609,8 +609,8 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - lsp_adapter.clone(), - language_server.clone(), + language_server.server_id(), + lsp_adapter.name(), cx, ) }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e1a7129ab0..e8ee4810a3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -70,14 +70,26 @@ pub struct ProjectStore { projects: Vec>, } +// Language server state is stored across 3 collections: +// language_servers => +// a mapping from unique server id to LanguageServerState which can either be a task for a +// server in the process of starting, or a running server with adapter and language server arcs +// language_server_ids => a mapping from worktreeId and server name to the unique server id +// language_server_statuses => a mapping from unique server id to the current server status +// +// Multiple worktrees can map to the same language server for example when you jump to the definition +// of a file in the standard library. So language_server_ids is used to look up which server is active +// for a given worktree and language server name +// +// When starting a language server, first the id map is checked to make sure a server isn't already available +// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and +// the Starting variant of LanguageServerState is stored in the language_servers map. pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: - HashMap<(WorktreeId, LanguageServerName), (Arc, Arc)>, - started_language_servers: - HashMap<(WorktreeId, LanguageServerName), Task>>>, + language_servers: HashMap, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>, language_server_statuses: BTreeMap, language_server_settings: Arc>, last_workspace_edits_by_language_server: HashMap, @@ -175,6 +187,14 @@ pub enum Event { ContactCancelledJoinRequest(Arc), } +pub enum LanguageServerState { + Starting(Task>>), + Running { + adapter: Arc, + server: Arc, + }, +} + #[derive(Serialize)] pub struct LanguageServerStatus { pub name: String, @@ -452,7 +472,7 @@ impl Project { next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), language_servers: Default::default(), - started_language_servers: Default::default(), + language_server_ids: Default::default(), language_server_statuses: Default::default(), last_workspace_edits_by_language_server: Default::default(), language_server_settings: Default::default(), @@ -502,10 +522,9 @@ impl Project { let mut worktrees = Vec::new(); for worktree in response.worktrees { - let (worktree, load_task) = cx + let worktree = cx .update(|cx| Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx)); worktrees.push(worktree); - load_task.detach(); } let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); @@ -551,7 +570,7 @@ impl Project { }), }, language_servers: Default::default(), - started_language_servers: Default::default(), + language_server_ids: Default::default(), language_server_settings: Default::default(), language_server_statuses: response .language_servers @@ -706,7 +725,7 @@ impl Project { if let Some(lsp_adapter) = language.lsp_adapter() { if !settings.enable_language_server(Some(&language.name())) { let lsp_name = lsp_adapter.name(); - for (worktree_id, started_lsp_name) in self.started_language_servers.keys() { + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { if lsp_name == *started_lsp_name { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } @@ -1135,7 +1154,7 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + worktree.as_remote_mut().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, @@ -1178,7 +1197,7 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + worktree.as_remote_mut().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, @@ -1221,7 +1240,7 @@ impl Project { .ok_or_else(|| anyhow!("missing entry in response"))?; worktree .update(&mut cx, |worktree, cx| { - worktree.as_remote().unwrap().insert_entry( + worktree.as_remote_mut().unwrap().insert_entry( entry, response.worktree_scan_id as usize, cx, @@ -1254,7 +1273,7 @@ impl Project { .await?; worktree .update(&mut cx, move |worktree, cx| { - worktree.as_remote().unwrap().delete_entry( + worktree.as_remote_mut().unwrap().delete_entry( entry_id, response.worktree_scan_id as usize, cx, @@ -1393,14 +1412,15 @@ impl Project { let client = self.client.clone(); cx.foreground() .spawn(async move { - if let Some(share) = share { - share.await?; - } client.send(proto::RespondToJoinProjectRequest { requester_id, project_id, allow, - }) + })?; + if let Some(share) = share { + share.await?; + } + anyhow::Ok(()) }) .detach_and_log_err(cx); } @@ -1601,8 +1621,8 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lsp_adapter: Arc, - lsp_server: Arc, + language_server_id: usize, + language_server_name: LanguageServerName, cx: &mut ModelContext, ) -> Task>> { cx.spawn(|this, mut cx| async move { @@ -1620,9 +1640,9 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.language_servers.insert( - (worktree.read(cx).id(), lsp_adapter.name()), - (lsp_adapter, lsp_server), + this.language_server_ids.insert( + (worktree.read(cx).id(), language_server_name), + language_server_id, ); }); (worktree, PathBuf::new()) @@ -1789,9 +1809,16 @@ impl Project { if let Some(adapter) = language.lsp_adapter() { language_id = adapter.id_for_language(language.name().as_ref()); language_server = self - .language_servers + .language_server_ids .get(&(worktree_id, adapter.name())) - .cloned(); + .and_then(|id| self.language_servers.get(&id)) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); } } @@ -1802,7 +1829,7 @@ impl Project { } } - if let Some((_, server)) = language_server { + if let Some(server) = language_server { server .notify::( lsp::DidOpenTextDocumentParams { @@ -1879,9 +1906,9 @@ impl Project { } } BufferEvent::Edited { .. } => { - let (_, language_server) = self - .language_server_for_buffer(buffer.read(cx), cx)? - .clone(); + let language_server = self + .language_server_for_buffer(buffer.read(cx), cx) + .map(|(_, server)| server.clone())?; let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; let abs_path = file.as_local()?.abs_path(cx); @@ -1970,16 +1997,19 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator, Arc)> { - self.language_servers.iter().filter_map( - move |((language_server_worktree_id, _), server)| { + ) -> impl Iterator, &Arc)> { + self.language_server_ids + .iter() + .filter_map(move |((language_server_worktree_id, _), id)| { if *language_server_worktree_id == worktree_id { - Some(server) - } else { - None + if let Some(LanguageServerState::Running { adapter, server }) = + self.language_servers.get(&id) + { + return Some((adapter, server)); + } } - }, - ) + None + }) } fn assign_language_to_buffer( @@ -2023,7 +2053,8 @@ impl Project { return; }; let key = (worktree_id, adapter.name()); - self.started_language_servers + + self.language_server_ids .entry(key.clone()) .or_insert_with(|| { let server_id = post_inc(&mut self.next_language_server_id); @@ -2034,252 +2065,298 @@ impl Project { self.client.http_client(), cx, ); - cx.spawn_weak(|this, mut cx| async move { - let language_server = language_server?.await.log_err()?; - let language_server = language_server - .initialize(adapter.initialization_options()) - .await - .log_err()?; - let this = this.upgrade(&cx)?; - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token(); + self.language_servers.insert( + server_id, + LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move { + let language_server = language_server?.await.log_err()?; + let language_server = language_server + .initialize(adapter.initialization_options()) + .await + .log_err()?; + let this = this.upgrade(&cx)?; + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token(); - language_server - .on_notification::({ - let this = this.downgrade(); - let adapter = adapter.clone(); - move |params, mut cx| { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.on_lsp_diagnostics_published( - server_id, params, &adapter, cx, - ); - }); - } - } - }) - .detach(); - - language_server - .on_request::({ - let settings = this - .read_with(&cx, |this, _| this.language_server_settings.clone()); - move |params, _| { - let settings = settings.lock().clone(); - async move { - Ok(params - .items - .into_iter() - .map(|item| { - if let Some(section) = &item.section { - settings - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null) - } else { - settings.clone() - } - }) - .collect()) - } - } - }) - .detach(); - - // Even though we don't have handling for these requests, respond to them to - // avoid stalling any language server like `gopls` which waits for a response - // to these requests when initializing. - language_server - .on_request::({ - let this = this.downgrade(); - move |params, mut cx| async move { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - if let Some(status) = - this.language_server_statuses.get_mut(&server_id) - { - if let lsp::NumberOrString::String(token) = params.token - { - status.progress_tokens.insert(token); - } - } - }); - } - Ok(()) - } - }) - .detach(); - language_server - .on_request::(|_, _| async { - Ok(()) - }) - .detach(); - - language_server - .on_request::({ - let this = this.downgrade(); - let adapter = adapter.clone(); - let language_server = language_server.clone(); - move |params, cx| { - Self::on_lsp_workspace_edit( - this, - params, - server_id, - adapter.clone(), - language_server.clone(), - cx, - ) - } - }) - .detach(); - - language_server - .on_notification::({ - let this = this.downgrade(); - move |params, mut cx| { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.on_lsp_progress( - params, - server_id, - disk_based_diagnostics_progress_token, - cx, - ); - }); - } - } - }) - .detach(); - - this.update(&mut cx, |this, cx| { - this.language_servers - .insert(key.clone(), (adapter.clone(), language_server.clone())); - this.language_server_statuses.insert( - server_id, - LanguageServerStatus { - name: language_server.name().to_string(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - }, - ); language_server - .notify::( - lsp::DidChangeConfigurationParams { - settings: this.language_server_settings.lock().clone(), - }, - ) - .ok(); - - if let Some(project_id) = this.shared_remote_id() { - this.client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id as u64, - name: language_server.name().to_string(), - }), - }) - .log_err(); - } - - // Tell the language server about every open buffer in the worktree that matches the language. - for buffer in this.opened_buffers.values() { - if let Some(buffer_handle) = buffer.upgrade(cx) { - let buffer = buffer_handle.read(cx); - let file = if let Some(file) = File::from_dyn(buffer.file()) { - file - } else { - continue; - }; - let language = if let Some(language) = buffer.language() { - language - } else { - continue; - }; - if file.worktree.read(cx).id() != key.0 - || language.lsp_adapter().map(|a| a.name()) - != Some(key.1.clone()) - { - continue; + .on_notification::({ + let this = this.downgrade(); + let adapter = adapter.clone(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_diagnostics_published( + server_id, params, &adapter, cx, + ); + }); + } } + }) + .detach(); - let file = file.as_local()?; - let versions = this - .buffer_snapshots - .entry(buffer.remote_id()) - .or_insert_with(|| vec![(0, buffer.text_snapshot())]); - let (version, initial_snapshot) = versions.last().unwrap(); - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - let language_id = adapter.id_for_language(language.name().as_ref()); - language_server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - language_id.unwrap_or_default(), - *version, - initial_snapshot.text(), - ), - }, - ) - .log_err()?; - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - language_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider.trigger_characters.clone() + language_server + .on_request::({ + let settings = this.read_with(&cx, |this, _| { + this.language_server_settings.clone() + }); + move |params, _| { + let settings = settings.lock().clone(); + async move { + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + settings + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + settings.clone() + } }) - .unwrap_or(Vec::new()), + .collect()) + } + } + }) + .detach(); + + // Even though we don't have handling for these requests, respond to them to + // avoid stalling any language server like `gopls` which waits for a response + // to these requests when initializing. + language_server + .on_request::({ + let this = this.downgrade(); + move |params, mut cx| async move { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + if let Some(status) = + this.language_server_statuses.get_mut(&server_id) + { + if let lsp::NumberOrString::String(token) = + params.token + { + status.progress_tokens.insert(token); + } + } + }); + } + Ok(()) + } + }) + .detach(); + language_server + .on_request::(|_, _| async { + Ok(()) + }) + .detach(); + + language_server + .on_request::({ + let this = this.downgrade(); + let adapter = adapter.clone(); + let language_server = language_server.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit( + this, + params, + server_id, + adapter.clone(), + language_server.clone(), cx, ) - }); + } + }) + .detach(); + + language_server + .on_notification::({ + let this = this.downgrade(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } + } + }) + .detach(); + + this.update(&mut cx, |this, cx| { + // If the language server for this key doesn't match the server id, don't store the + // server. Which will cause it to be dropped, killing the process + if this + .language_server_ids + .get(&key) + .map(|id| id != &server_id) + .unwrap_or(false) + { + return None; } - } - cx.notify(); - Some(()) - }); + // Update language_servers collection with Running variant of LanguageServerState + // indicating that the server is up and running and ready + this.language_servers.insert( + server_id, + LanguageServerState::Running { + adapter: adapter.clone(), + server: language_server.clone(), + }, + ); + this.language_server_statuses.insert( + server_id, + LanguageServerStatus { + name: language_server.name().to_string(), + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ); + language_server + .notify::( + lsp::DidChangeConfigurationParams { + settings: this.language_server_settings.lock().clone(), + }, + ) + .ok(); - Some(language_server) - }) + if let Some(project_id) = this.shared_remote_id() { + this.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id as u64, + name: language_server.name().to_string(), + }), + }) + .log_err(); + } + + // Tell the language server about every open buffer in the worktree that matches the language. + for buffer in this.opened_buffers.values() { + if let Some(buffer_handle) = buffer.upgrade(cx) { + let buffer = buffer_handle.read(cx); + let file = if let Some(file) = File::from_dyn(buffer.file()) { + file + } else { + continue; + }; + let language = if let Some(language) = buffer.language() { + language + } else { + continue; + }; + if file.worktree.read(cx).id() != key.0 + || language.lsp_adapter().map(|a| a.name()) + != Some(key.1.clone()) + { + continue; + } + + let file = file.as_local()?; + let versions = this + .buffer_snapshots + .entry(buffer.remote_id()) + .or_insert_with(|| vec![(0, buffer.text_snapshot())]); + let (version, initial_snapshot) = versions.last().unwrap(); + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let language_id = + adapter.id_for_language(language.name().as_ref()); + language_server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + language_id.unwrap_or_default(), + *version, + initial_snapshot.text(), + ), + }, + ) + .log_err()?; + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider.trigger_characters.clone() + }) + .unwrap_or(Vec::new()), + cx, + ) + }); + } + } + + cx.notify(); + Some(language_server) + }) + })), + ); + + server_id }); } + // Returns a list of all of the worktrees which no longer have a language server and the root path + // for the stopped server fn stop_language_server( &mut self, worktree_id: WorktreeId, adapter_name: LanguageServerName, cx: &mut ModelContext, - ) -> Task<()> { + ) -> Task<(Option, Vec)> { let key = (worktree_id, adapter_name); - if let Some((_, language_server)) = self.language_servers.remove(&key) { - self.language_server_statuses - .remove(&language_server.server_id()); - cx.notify(); - } + if let Some(server_id) = self.language_server_ids.remove(&key) { + // Remove other entries for this language server as well + let mut orphaned_worktrees = vec![worktree_id]; + let other_keys = self.language_server_ids.keys().cloned().collect::>(); + for other_key in other_keys { + if self.language_server_ids.get(&other_key) == Some(&server_id) { + self.language_server_ids.remove(&other_key); + orphaned_worktrees.push(other_key.0); + } + } - if let Some(started_language_server) = self.started_language_servers.remove(&key) { + self.language_server_statuses.remove(&server_id); + cx.notify(); + + let server_state = self.language_servers.remove(&server_id); cx.spawn_weak(|this, mut cx| async move { - if let Some(language_server) = started_language_server.await { - if let Some(shutdown) = language_server.shutdown() { + let mut root_path = None; + + let server = match server_state { + Some(LanguageServerState::Starting(started_language_server)) => { + started_language_server.await + } + Some(LanguageServerState::Running { server, .. }) => Some(server), + None => None, + }; + + if let Some(server) = server { + root_path = Some(server.root_path().clone()); + if let Some(shutdown) = server.shutdown() { shutdown.await; } - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.language_server_statuses - .remove(&language_server.server_id()); - cx.notify(); - }); - } } + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.language_server_statuses.remove(&server_id); + cx.notify(); + }); + } + + (root_path, orphaned_worktrees) }) } else { - Task::ready(()) + Task::ready((None, Vec::new())) } } @@ -2310,7 +2387,7 @@ impl Project { fn restart_language_server( &mut self, worktree_id: WorktreeId, - worktree_path: Arc, + fallback_path: Arc, language: Arc, cx: &mut ModelContext, ) { @@ -2320,12 +2397,33 @@ impl Project { return; }; - let stop = self.stop_language_server(worktree_id, adapter.name(), cx); + let server_name = adapter.name(); + let stop = self.stop_language_server(worktree_id, server_name.clone(), cx); cx.spawn_weak(|this, mut cx| async move { - stop.await; + let (original_root_path, orphaned_worktrees) = stop.await; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.start_language_server(worktree_id, worktree_path, language, cx); + // Attempt to restart using original server path. Fallback to passed in + // path if we could not retrieve the root path + let root_path = original_root_path + .map(|path_buf| Arc::from(path_buf.as_path())) + .unwrap_or(fallback_path); + + this.start_language_server(worktree_id, root_path, language, cx); + + // Lookup new server id and set it for each of the orphaned worktrees + if let Some(new_server_id) = this + .language_server_ids + .get(&(worktree_id, server_name.clone())) + .cloned() + { + for orphaned_worktree in orphaned_worktrees { + this.language_server_ids.insert( + (orphaned_worktree, server_name.clone()), + new_server_id.clone(), + ); + } + } }); } }) @@ -2561,14 +2659,16 @@ impl Project { } pub fn set_language_server_settings(&mut self, settings: serde_json::Value) { - for (_, server) in self.language_servers.values() { - server - .notify::( - lsp::DidChangeConfigurationParams { - settings: settings.clone(), - }, - ) - .ok(); + for server_state in self.language_servers.values() { + if let LanguageServerState::Running { server, .. } = server_state { + server + .notify::( + lsp::DidChangeConfigurationParams { + settings: settings.clone(), + }, + ) + .ok(); + } } *self.language_server_settings.lock() = settings; } @@ -3031,30 +3131,36 @@ impl Project { pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { if self.is_local() { let mut requests = Vec::new(); - for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() { + for ((worktree_id, _), server_id) in self.language_server_ids.iter() { let worktree_id = *worktree_id; if let Some(worktree) = self .worktree_for_id(worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) { - let lsp_adapter = lsp_adapter.clone(); - let worktree_abs_path = worktree.abs_path().clone(); - requests.push( - language_server - .request::(lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }) - .log_err() - .map(move |response| { - ( - lsp_adapter, - worktree_id, - worktree_abs_path, - response.unwrap_or_default(), + if let Some(LanguageServerState::Running { adapter, server }) = + self.language_servers.get(server_id) + { + let adapter = adapter.clone(); + let worktree_abs_path = worktree.abs_path().clone(); + requests.push( + server + .request::( + lsp::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, ) - }), - ); + .log_err() + .map(move |response| { + ( + adapter, + worktree_id, + worktree_abs_path, + response.unwrap_or_default(), + ) + }), + ); + } } } @@ -3137,11 +3243,11 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&( + let language_server_id = if let Some(id) = self.language_server_ids.get(&( symbol.source_worktree_id, symbol.language_server_name.clone(), )) { - server.clone() + *id } else { return Task::ready(Err(anyhow!( "language server for worktree and language not found" @@ -3164,7 +3270,12 @@ impl Project { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx) + self.open_local_buffer_via_lsp( + symbol_uri, + language_server_id, + symbol.language_server_name.clone(), + cx, + ) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::OpenBufferForSymbol { project_id, @@ -3215,8 +3326,8 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let (_, lang_server) = - if let Some(server) = self.language_server_for_buffer(source_buffer, cx) { + let lang_server = + if let Some((_, server)) = self.language_server_for_buffer(source_buffer, cx) { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -3373,7 +3484,7 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { server.clone() } else { @@ -3470,7 +3581,7 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { server.clone() } else { @@ -3557,8 +3668,8 @@ impl Project { if self.is_local() { let buffer = buffer_handle.read(cx); let (lsp_adapter, lang_server) = - if let Some(server) = self.language_server_for_buffer(buffer, cx) { - server.clone() + if let Some((adapter, server)) = self.language_server_for_buffer(buffer, cx) { + (adapter.clone(), server.clone()) } else { return Task::ready(Ok(Default::default())); }; @@ -3594,8 +3705,8 @@ impl Project { this, edit, push_to_history, - lsp_adapter, - lang_server, + lsp_adapter.clone(), + lang_server.clone(), &mut cx, ) .await @@ -3724,8 +3835,8 @@ impl Project { .update(cx, |this, cx| { this.open_local_buffer_via_lsp( op.text_document.uri, - lsp_adapter.clone(), - language_server.clone(), + language_server.server_id(), + lsp_adapter.name(), cx, ) }) @@ -4019,9 +4130,10 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, (_, language_server))) = - file.zip(self.language_server_for_buffer(buffer, cx).cloned()) - { + if let Some((file, language_server)) = file.zip( + self.language_server_for_buffer(buffer, cx) + .map(|(_, server)| server.clone()), + ) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); return cx.spawn(|this, cx| async move { if !request.check_capabilities(&language_server.capabilities()) { @@ -4503,18 +4615,9 @@ impl Project { { this.worktrees.push(WorktreeHandle::Strong(old_worktree)); } else { - let worktree = proto::Worktree { - id: worktree.id, - root_name: worktree.root_name, - entries: Default::default(), - diagnostic_summaries: Default::default(), - visible: worktree.visible, - scan_id: 0, - }; - let (worktree, load_task) = + let worktree = Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx); this.add_worktree(&worktree, cx); - load_task.detach(); } } @@ -4538,8 +4641,8 @@ impl Project { if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { worktree.update(cx, |worktree, _| { let worktree = worktree.as_remote_mut().unwrap(); - worktree.update_from_remote(envelope) - })?; + worktree.update_from_remote(envelope.payload); + }); } Ok(()) }) @@ -5637,14 +5740,21 @@ impl Project { &self, buffer: &Buffer, cx: &AppContext, - ) -> Option<&(Arc, Arc)> { + ) -> Option<(&Arc, &Arc)> { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); - self.language_servers - .get(&(worktree_id, language.lsp_adapter()?.name())) - } else { - None + let key = (worktree_id, language.lsp_adapter()?.name()); + + if let Some(server_id) = self.language_server_ids.get(&key) { + if let Some(LanguageServerState::Running { adapter, server }) = + self.language_servers.get(&server_id) + { + return Some((adapter, server)); + } + } } + + None } } @@ -5804,8 +5914,16 @@ impl Entity for Project { let shutdown_futures = self .language_servers .drain() - .filter_map(|(_, (_, server))| server.shutdown()) + .map(|(_, server_state)| async { + match server_state { + LanguageServerState::Running { server, .. } => server.shutdown()?.await, + LanguageServerState::Starting(starting_server) => { + starting_server.await?.shutdown()?.await + } + } + }) .collect::>(); + Some( async move { futures::future::join_all(shutdown_futures).await; @@ -7720,6 +7838,10 @@ mod tests { .await .unwrap(); + // Assert no new language server started + cx.foreground().run_until_parked(); + assert!(fake_servers.try_next().is_err()); + assert_eq!(definitions.len(), 1); let definition = definitions.pop().unwrap(); cx.update(|cx| { @@ -8057,7 +8179,10 @@ mod tests { } #[gpui::test(retries = 5)] - async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { + async fn test_rescan_and_remote_updates( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { let dir = temp_tree(json!({ "a": { "file1": "", @@ -8101,17 +8226,24 @@ mod tests { // Create a remote copy of this worktree. let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - let (remote, load_task) = cx.update(|cx| { + let remote = cx.update(|cx| { Worktree::remote( 1, 1, - initial_snapshot.to_proto(&Default::default(), true), + proto::WorktreeMetadata { + id: initial_snapshot.id().to_proto(), + root_name: initial_snapshot.root_name().into(), + visible: true, + }, rpc.clone(), cx, ) }); - // tree - load_task.await; + remote.update(cx, |remote, _| { + let update = initial_snapshot.build_initial_update(1); + remote.as_remote_mut().unwrap().update_from_remote(update); + }); + deterministic.run_until_parked(); cx.read(|cx| { assert!(!buffer2.read(cx).is_dirty()); @@ -8177,19 +8309,16 @@ mod tests { // Update the remote worktree. Check that it becomes consistent with the // local worktree. remote.update(cx, |remote, cx| { - let update_message = tree.read(cx).as_local().unwrap().snapshot().build_update( + let update = tree.read(cx).as_local().unwrap().snapshot().build_update( &initial_snapshot, 1, 1, true, ); - remote - .as_remote_mut() - .unwrap() - .snapshot - .apply_remote_update(update_message) - .unwrap(); - + remote.as_remote_mut().unwrap().update_from_remote(update); + }); + deterministic.run_until_parked(); + remote.read_with(cx, |remote, _| { assert_eq!( remote .paths() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9ad45751c3..b472a28771 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -7,9 +7,9 @@ use super::{ }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope}; +use client::{proto, Client}; use clock::ReplicaId; -use collections::HashMap; +use collections::{HashMap, VecDeque}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -40,11 +40,11 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - mem, ops::{Deref, DerefMut}, os::unix::prelude::{OsStrExt, OsStringExt}, path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, + task::Poll, time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap}; @@ -82,7 +82,7 @@ pub struct RemoteWorktree { project_id: u64, client: Arc, updates_tx: Option>, - last_scan_id_rx: watch::Receiver, + snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, diagnostic_summaries: TreeMap, visible: bool, @@ -96,6 +96,7 @@ pub struct Snapshot { entries_by_path: SumTree, entries_by_id: SumTree, scan_id: usize, + is_complete: bool, } #[derive(Clone)] @@ -125,13 +126,16 @@ impl DerefMut for LocalSnapshot { #[derive(Clone, Debug)] enum ScanState { Idle, - Scanning, + /// The worktree is performing its initial scan of the filesystem. + Initializing, + /// The worktree is updating in response to filesystem events. + Updating, Err(Arc), } struct ShareState { project_id: u64, - snapshots_tx: Sender, + snapshots_tx: watch::Sender, _maintain_remote_snapshot: Option>>, } @@ -172,10 +176,10 @@ impl Worktree { pub fn remote( project_remote_id: u64, replica_id: ReplicaId, - worktree: proto::Worktree, + worktree: proto::WorktreeMetadata, client: Arc, cx: &mut MutableAppContext, - ) -> (ModelHandle, Task<()>) { + ) -> ModelHandle { let remote_id = worktree.id; let root_char_bag: CharBag = worktree .root_name @@ -190,13 +194,13 @@ impl Worktree { root_char_bag, entries_by_path: Default::default(), entries_by_id: Default::default(), - scan_id: worktree.scan_id as usize, + scan_id: 0, + is_complete: false, }; let (updates_tx, mut updates_rx) = mpsc::unbounded(); let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); - let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize); let worktree_handle = cx.add_model(|_: &mut ModelContext| { Worktree::Remote(RemoteWorktree { project_id: project_remote_id, @@ -204,96 +208,50 @@ impl Worktree { snapshot: snapshot.clone(), background_snapshot: background_snapshot.clone(), updates_tx: Some(updates_tx), - last_scan_id_rx, + snapshot_subscriptions: Default::default(), client: client.clone(), - diagnostic_summaries: TreeMap::from_ordered_entries( - worktree.diagnostic_summaries.into_iter().map(|summary| { - ( - PathKey(PathBuf::from(summary.path).into()), - DiagnosticSummary { - language_server_id: summary.language_server_id as usize, - error_count: summary.error_count as usize, - warning_count: summary.warning_count as usize, - }, - ) - }), - ), + diagnostic_summaries: Default::default(), visible, }) }); - let deserialize_task = cx.spawn({ - let worktree_handle = worktree_handle.clone(); - |cx| async move { - let (entries_by_path, entries_by_id) = cx - .background() - .spawn(async move { - let mut entries_by_path_edits = Vec::new(); - let mut entries_by_id_edits = Vec::new(); - for entry in worktree.entries { - match Entry::try_from((&root_char_bag, entry)) { - Ok(entry) => { - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: 0, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - Err(err) => log::warn!("error for remote worktree entry {:?}", err), - } - } - - let mut entries_by_path = SumTree::new(); - let mut entries_by_id = SumTree::new(); - entries_by_path.edit(entries_by_path_edits, &()); - entries_by_id.edit(entries_by_id_edits, &()); - - (entries_by_path, entries_by_id) - }) - .await; - - { - let mut snapshot = background_snapshot.lock(); - snapshot.entries_by_path = entries_by_path; - snapshot.entries_by_id = entries_by_id; + cx.background() + .spawn(async move { + while let Some(update) = updates_rx.next().await { + if let Err(error) = background_snapshot.lock().apply_remote_update(update) { + log::error!("error applying worktree update: {}", error); + } snapshot_updated_tx.send(()).await.ok(); } + }) + .detach(); - cx.background() - .spawn(async move { - while let Some(update) = updates_rx.next().await { - if let Err(error) = - background_snapshot.lock().apply_remote_update(update) - { - log::error!("error applying worktree update: {}", error); + cx.spawn(|mut cx| { + let this = worktree_handle.downgrade(); + async move { + while let Some(_) = snapshot_updated_rx.recv().await { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.poll_snapshot(cx); + let this = this.as_remote_mut().unwrap(); + while let Some((scan_id, _)) = this.snapshot_subscriptions.front() { + if this.observed_snapshot(*scan_id) { + let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap(); + let _ = tx.send(()); + } else { + break; + } } - snapshot_updated_tx.send(()).await.ok(); - } - }) - .detach(); - - cx.spawn(|mut cx| { - let this = worktree_handle.downgrade(); - async move { - while let Some(_) = snapshot_updated_rx.recv().await { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - let this = this.as_remote_mut().unwrap(); - *last_scan_id_tx.borrow_mut() = this.snapshot.scan_id; - }); - } else { - break; - } - } + }); + } else { + break; } - }) - .detach(); + } } - }); - (worktree_handle, deserialize_task) + }) + .detach(); + + worktree_handle } pub fn as_local(&self) -> Option<&LocalWorktree> { @@ -377,38 +335,9 @@ impl Worktree { fn poll_snapshot(&mut self, cx: &mut ModelContext) { match self { - Self::Local(worktree) => { - let is_fake_fs = worktree.fs.is_fake(); - worktree.snapshot = worktree.background_snapshot.lock().clone(); - if worktree.is_scanning() { - if worktree.poll_task.is_none() { - worktree.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { - if is_fake_fs { - #[cfg(any(test, feature = "test-support"))] - cx.background().simulate_random_delay().await; - } else { - smol::Timer::after(Duration::from_millis(100)).await; - } - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.as_local_mut().unwrap().poll_task = None; - this.poll_snapshot(cx); - }); - } - })); - } - } else { - worktree.poll_task.take(); - cx.emit(Event::UpdatedEntries); - } - } - Self::Remote(worktree) => { - worktree.snapshot = worktree.background_snapshot.lock().clone(); - cx.emit(Event::UpdatedEntries); - } + Self::Local(worktree) => worktree.poll_snapshot(false, cx), + Self::Remote(worktree) => worktree.poll_snapshot(cx), }; - - cx.notify(); } } @@ -436,7 +365,8 @@ impl LocalWorktree { .context("failed to stat worktree path")?; let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); - let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); + let (mut last_scan_state_tx, last_scan_state_rx) = + watch::channel_with(ScanState::Initializing); let tree = cx.add_model(move |cx: &mut ModelContext| { let mut snapshot = LocalSnapshot { abs_path, @@ -450,6 +380,7 @@ impl LocalWorktree { entries_by_path: Default::default(), entries_by_id: Default::default(), scan_id: 0, + is_complete: true, }, extension_counts: Default::default(), }; @@ -481,11 +412,7 @@ impl LocalWorktree { while let Some(scan_state) = scan_states_rx.next().await { if let Some(this) = this.upgrade(&cx) { last_scan_state_tx.blocking_send(scan_state).ok(); - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); } else { break; } @@ -569,22 +496,53 @@ impl LocalWorktree { Ok(updated) } + fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext) { + self.poll_task.take(); + match self.scan_state() { + ScanState::Idle => { + self.snapshot = self.background_snapshot.lock().clone(); + if let Some(share) = self.share.as_mut() { + *share.snapshots_tx.borrow_mut() = self.snapshot.clone(); + } + cx.emit(Event::UpdatedEntries); + } + ScanState::Initializing => { + let is_fake_fs = self.fs.is_fake(); + self.snapshot = self.background_snapshot.lock().clone(); + self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { + if is_fake_fs { + #[cfg(any(test, feature = "test-support"))] + cx.background().simulate_random_delay().await; + } else { + smol::Timer::after(Duration::from_millis(100)).await; + } + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + } + })); + cx.emit(Event::UpdatedEntries); + } + _ => { + if force { + self.snapshot = self.background_snapshot.lock().clone(); + } + } + } + cx.notify(); + } + pub fn scan_complete(&self) -> impl Future { let mut scan_state_rx = self.last_scan_state_rx.clone(); async move { let mut scan_state = Some(scan_state_rx.borrow().clone()); - while let Some(ScanState::Scanning) = scan_state { + while let Some(ScanState::Initializing | ScanState::Updating) = scan_state { scan_state = scan_state_rx.recv().await; } } } - fn is_scanning(&self) -> bool { - if let ScanState::Scanning = *self.last_scan_state_rx.borrow() { - true - } else { - false - } + fn scan_state(&self) -> ScanState { + self.last_scan_state_rx.borrow().clone() } pub fn snapshot(&self) -> LocalSnapshot { @@ -614,7 +572,6 @@ impl LocalWorktree { .refresh_entry(path, abs_path, None, cx) }) .await?; - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); Ok(( File { entry_id: Some(entry.id), @@ -712,16 +669,14 @@ impl LocalWorktree { Some(cx.spawn(|this, mut cx| async move { delete.await?; - this.update(&mut cx, |this, _| { - let this = this.as_local_mut().unwrap(); - let mut snapshot = this.background_snapshot.lock(); - snapshot.delete_entry(entry_id); - }); this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; + let this = this.as_local_mut().unwrap(); + { + let mut snapshot = this.background_snapshot.lock(); + snapshot.delete_entry(entry_id); + } + this.poll_snapshot(true, cx); + }); Ok(()) })) } @@ -757,11 +712,6 @@ impl LocalWorktree { ) }) .await?; - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; Ok(entry) })) } @@ -797,11 +747,6 @@ impl LocalWorktree { ) }) .await?; - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; Ok(entry) })) } @@ -835,11 +780,6 @@ impl LocalWorktree { .refresh_entry(path, abs_path, None, cx) }) .await?; - this.update(&mut cx, |this, cx| { - this.poll_snapshot(cx); - this.as_local().unwrap().broadcast_snapshot() - }) - .await; Ok(entry) }) } @@ -872,61 +812,55 @@ impl LocalWorktree { let this = this .upgrade(&cx) .ok_or_else(|| anyhow!("worktree was dropped"))?; - let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| { - let this = this.as_local().unwrap(); - let mut snapshot = this.background_snapshot.lock(); - entry.is_ignored = snapshot - .ignore_stack_for_path(&path, entry.is_dir()) - .is_path_ignored(&path, entry.is_dir()); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); + this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + let inserted_entry; + { + let mut snapshot = this.background_snapshot.lock(); + entry.is_ignored = snapshot + .ignore_stack_for_path(&path, entry.is_dir()) + .is_path_ignored(&path, entry.is_dir()); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + inserted_entry = snapshot.insert_entry(entry, fs.as_ref()); + snapshot.scan_id += 1; } - let entry = snapshot.insert_entry(entry, fs.as_ref()); - snapshot.scan_id += 1; - let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone()); - (entry, snapshot.clone(), snapshots_tx) - }); - this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); - - if let Some(snapshots_tx) = snapshots_tx { - snapshots_tx.send(snapshot).await.ok(); - } - - Ok(entry) + this.poll_snapshot(true, cx); + Ok(inserted_entry) + }) }) } pub fn share(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { let (share_tx, share_rx) = oneshot::channel(); - let (snapshots_to_send_tx, snapshots_to_send_rx) = - smol::channel::unbounded::(); + if self.share.is_some() { let _ = share_tx.send(Ok(())); } else { + let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot()); let rpc = self.client.clone(); let worktree_id = cx.model_id() as u64; let maintain_remote_snapshot = cx.background().spawn({ let rpc = rpc.clone(); let diagnostic_summaries = self.diagnostic_summaries.clone(); async move { - let mut prev_snapshot = match snapshots_to_send_rx.recv().await { - Ok(snapshot) => { - if let Err(error) = rpc - .request(proto::UpdateWorktree { - project_id, - worktree_id, - root_name: snapshot.root_name().to_string(), - updated_entries: snapshot - .entries_by_path - .iter() - .filter(|e| !e.is_ignored) - .map(Into::into) - .collect(), - removed_entries: Default::default(), - scan_id: snapshot.scan_id as u64, - }) - .await - { + let mut prev_snapshot = match snapshots_rx.recv().await { + Some(snapshot) => { + let update = proto::UpdateWorktree { + project_id, + worktree_id, + root_name: snapshot.root_name().to_string(), + updated_entries: snapshot + .entries_by_path + .iter() + .map(Into::into) + .collect(), + removed_entries: Default::default(), + scan_id: snapshot.scan_id as u64, + is_last_update: true, + }; + if let Err(error) = send_worktree_update(&rpc, update).await { let _ = share_tx.send(Err(error)); return Err(anyhow!("failed to send initial update worktree")); } else { @@ -934,8 +868,10 @@ impl LocalWorktree { snapshot } } - Err(error) => { - let _ = share_tx.send(Err(error.into())); + None => { + share_tx + .send(Err(anyhow!("worktree dropped before share completed"))) + .ok(); return Err(anyhow!("failed to send initial update worktree")); } }; @@ -948,44 +884,12 @@ impl LocalWorktree { })?; } - // Stream ignored entries in chunks. - { - let mut ignored_entries = prev_snapshot - .entries_by_path - .iter() - .filter(|e| e.is_ignored); - let mut ignored_entries_to_send = Vec::new(); - loop { - #[cfg(any(test, feature = "test-support"))] - const CHUNK_SIZE: usize = 2; - #[cfg(not(any(test, feature = "test-support")))] - const CHUNK_SIZE: usize = 256; - - let entry = ignored_entries.next(); - if ignored_entries_to_send.len() >= CHUNK_SIZE || entry.is_none() { - rpc.request(proto::UpdateWorktree { - project_id, - worktree_id, - root_name: prev_snapshot.root_name().to_string(), - updated_entries: mem::take(&mut ignored_entries_to_send), - removed_entries: Default::default(), - scan_id: prev_snapshot.scan_id as u64, - }) - .await?; - } - - if let Some(entry) = entry { - ignored_entries_to_send.push(entry.into()); - } else { - break; - } - } - } - - while let Ok(snapshot) = snapshots_to_send_rx.recv().await { - let message = - snapshot.build_update(&prev_snapshot, project_id, worktree_id, true); - rpc.request(message).await?; + while let Some(snapshot) = snapshots_rx.recv().await { + send_worktree_update( + &rpc, + snapshot.build_update(&prev_snapshot, project_id, worktree_id, true), + ) + .await?; prev_snapshot = snapshot; } @@ -995,18 +899,12 @@ impl LocalWorktree { }); self.share = Some(ShareState { project_id, - snapshots_tx: snapshots_to_send_tx.clone(), + snapshots_tx, _maintain_remote_snapshot: Some(maintain_remote_snapshot), }); } - cx.spawn_weak(|this, cx| async move { - if let Some(this) = this.upgrade(&cx) { - this.read_with(&cx, |this, _| { - let this = this.as_local().unwrap(); - let _ = snapshots_to_send_tx.try_send(this.snapshot()); - }); - } + cx.foreground().spawn(async move { share_rx .await .unwrap_or_else(|_| Err(anyhow!("share ended"))) @@ -1021,23 +919,6 @@ impl LocalWorktree { self.share.is_some() } - fn broadcast_snapshot(&self) -> impl Future { - let mut to_send = None; - if !self.is_scanning() { - if let Some(share) = self.share.as_ref() { - to_send = Some((self.snapshot(), share.snapshots_tx.clone())); - } - } - - async move { - if let Some((snapshot, snapshots_to_send_tx)) = to_send { - if let Err(err) = snapshots_to_send_tx.send(snapshot).await { - log::error!("error submitting snapshot to send {}", err); - } - } - } - } - pub fn send_extension_counts(&self, project_id: u64) { let mut extensions = Vec::new(); let mut counts = Vec::new(); @@ -1063,31 +944,45 @@ impl RemoteWorktree { self.snapshot.clone() } + fn poll_snapshot(&mut self, cx: &mut ModelContext) { + self.snapshot = self.background_snapshot.lock().clone(); + cx.emit(Event::UpdatedEntries); + cx.notify(); + } + pub fn disconnected_from_host(&mut self) { self.updates_tx.take(); + self.snapshot_subscriptions.clear(); } - pub fn update_from_remote( - &mut self, - envelope: TypedEnvelope, - ) -> Result<()> { + pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { if let Some(updates_tx) = &self.updates_tx { updates_tx - .unbounded_send(envelope.payload) + .unbounded_send(update) .expect("consumer runs to completion"); } - Ok(()) } - fn wait_for_snapshot(&self, scan_id: usize) -> impl Future { - let mut rx = self.last_scan_id_rx.clone(); - async move { - while let Some(applied_scan_id) = rx.next().await { - if applied_scan_id >= scan_id { - return; - } + fn observed_snapshot(&self, scan_id: usize) -> bool { + self.scan_id > scan_id || (self.scan_id == scan_id && self.is_complete) + } + + fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future { + let (tx, rx) = oneshot::channel(); + if self.observed_snapshot(scan_id) { + let _ = tx.send(()); + } else { + match self + .snapshot_subscriptions + .binary_search_by_key(&scan_id, |probe| probe.0) + { + Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)), } } + + async move { + let _ = rx.await; + } } pub fn update_diagnostic_summary( @@ -1109,7 +1004,7 @@ impl RemoteWorktree { } pub fn insert_entry( - &self, + &mut self, entry: proto::Entry, scan_id: usize, cx: &mut ModelContext, @@ -1128,7 +1023,7 @@ impl RemoteWorktree { } pub(crate) fn delete_entry( - &self, + &mut self, id: ProjectEntryId, scan_id: usize, cx: &mut ModelContext, @@ -1204,7 +1099,7 @@ impl Snapshot { for entry_id in update.removed_entries { let entry = self .entry_for_id(ProjectEntryId::from_proto(entry_id)) - .ok_or_else(|| anyhow!("unknown entry"))?; + .ok_or_else(|| anyhow!("unknown entry {}", entry_id))?; entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); entries_by_id_edits.push(Edit::Remove(entry.id)); } @@ -1226,6 +1121,7 @@ impl Snapshot { self.entries_by_path.edit(entries_by_path_edits, &()); self.entries_by_id.edit(entries_by_id_edits, &()); self.scan_id = update.scan_id as usize; + self.is_complete = update.is_last_update; Ok(()) } @@ -1351,27 +1247,16 @@ impl LocalSnapshot { } #[cfg(test)] - pub(crate) fn to_proto( - &self, - diagnostic_summaries: &TreeMap, - visible: bool, - ) -> proto::Worktree { + pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree { let root_name = self.root_name.clone(); - proto::Worktree { - id: self.id.0 as u64, + proto::UpdateWorktree { + project_id, + worktree_id: self.id().to_proto(), root_name, - entries: self - .entries_by_path - .iter() - .filter(|e| !e.is_ignored) - .map(Into::into) - .collect(), - diagnostic_summaries: diagnostic_summaries - .iter() - .map(|(path, summary)| summary.to_proto(&path.0)) - .collect(), - visible, + updated_entries: self.entries_by_path.iter().map(Into::into).collect(), + removed_entries: Default::default(), scan_id: self.scan_id as u64, + is_last_update: true, } } @@ -1438,6 +1323,7 @@ impl LocalSnapshot { updated_entries, removed_entries, scan_id: self.scan_id as u64, + is_last_update: true, } } @@ -2109,7 +1995,7 @@ impl BackgroundScanner { } async fn run(mut self, events_rx: impl Stream>) { - if self.notify.unbounded_send(ScanState::Scanning).is_err() { + if self.notify.unbounded_send(ScanState::Initializing).is_err() { return; } @@ -2128,8 +2014,13 @@ impl BackgroundScanner { } futures::pin_mut!(events_rx); - while let Some(events) = events_rx.next().await { - if self.notify.unbounded_send(ScanState::Scanning).is_err() { + + while let Some(mut events) = events_rx.next().await { + while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) { + events.extend(additional_events); + } + + if self.notify.unbounded_send(ScanState::Updating).is_err() { break; } @@ -2781,6 +2672,19 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } +async fn send_worktree_update(client: &Arc, update: proto::UpdateWorktree) -> Result<()> { + #[cfg(any(test, feature = "test-support"))] + const MAX_CHUNK_SIZE: usize = 2; + #[cfg(not(any(test, feature = "test-support")))] + const MAX_CHUNK_SIZE: usize = 256; + + for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) { + client.request(update).await?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -2990,6 +2894,7 @@ mod tests { root_name: Default::default(), root_char_bag: Default::default(), scan_id: 0, + is_complete: true, }, extension_counts: Default::default(), }; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 78aed09dc6..eaa747f2ac 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -108,7 +108,8 @@ actions!( Cut, Paste, Delete, - Rename + Rename, + Toggle ] ); impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 10810d7a6b..6325e5107d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -172,7 +172,7 @@ message JoinProjectResponse { message Accept { uint32 replica_id = 1; - repeated Worktree worktrees = 2; + repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; } @@ -199,6 +199,7 @@ message UpdateWorktree { repeated Entry updated_entries = 4; repeated uint64 removed_entries = 5; uint64 scan_id = 6; + bool is_last_update = 7; } message UpdateWorktreeExtensions { @@ -776,15 +777,6 @@ message User { string avatar_url = 3; } -message Worktree { - uint64 id = 1; - string root_name = 2; - repeated Entry entries = 3; - repeated DiagnosticSummary diagnostic_summaries = 4; - bool visible = 5; - uint64 scan_id = 6; -} - message File { uint64 worktree_id = 1; optional uint64 entry_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 9429d9a6eb..e3844a8692 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -5,6 +5,7 @@ use futures::{SinkExt as _, StreamExt as _}; use prost::Message as _; use serde::Serialize; use std::any::{Any, TypeId}; +use std::{cmp, iter, mem}; use std::{ fmt::Debug, io, @@ -392,6 +393,31 @@ impl From for u128 { } } +pub fn split_worktree_update( + mut message: UpdateWorktree, + max_chunk_size: usize, +) -> impl Iterator { + let mut done = false; + iter::from_fn(move || { + if done { + return None; + } + + let chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size); + let updated_entries = message.updated_entries.drain(..chunk_size).collect(); + done = message.updated_entries.is_empty(); + Some(UpdateWorktree { + project_id: message.project_id, + worktree_id: message.worktree_id, + root_name: message.root_name.clone(), + updated_entries, + removed_entries: mem::take(&mut message.removed_entries), + scan_id: message.scan_id, + is_last_update: done && message.is_last_update, + }) + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index de9e8af9c4..59f78cdc33 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -58,7 +58,7 @@ fn add_toggle_option_action(option: SearchOption, cx: &mut MutableApp } pub struct BufferSearchBar { - query_editor: ViewHandle, + pub query_editor: ViewHandle, active_editor: Option>, active_match_index: Option, active_editor_subscription: Option, diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml new file mode 100644 index 0000000000..0bbc056922 --- /dev/null +++ b/crates/terminal/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "terminal" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/terminal.rs" +doctest = false + +[dependencies] +alacritty_terminal = "0.16.1" +editor = { path = "../editor" } +util = { path = "../util" } +gpui = { path = "../gpui" } +theme = { path = "../theme" } +settings = { path = "../settings" } +workspace = { path = "../workspace" } +project = { path = "../project" } +smallvec = { version = "1.6", features = ["union"] } +mio-extras = "2.0.6" +futures = "0.3" +ordered-float = "2.1.1" +itertools = "0.10" + + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal/print256color.sh b/crates/terminal/print256color.sh new file mode 100755 index 0000000000..99e3d8c9f9 --- /dev/null +++ b/crates/terminal/print256color.sh @@ -0,0 +1,96 @@ +#!/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 percieved 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/src/terminal.rs b/crates/terminal/src/terminal.rs new file mode 100644 index 0000000000..6ea4ca5f73 --- /dev/null +++ b/crates/terminal/src/terminal.rs @@ -0,0 +1,494 @@ +use alacritty_terminal::{ + config::{Config, Program, PtyConfig}, + event::{Event as AlacTermEvent, EventListener, Notify}, + event_loop::{EventLoop, Msg, Notifier}, + grid::Scroll, + sync::FairMutex, + term::{color::Rgb as AlacRgb, SizeInfo}, + tty, Term, +}; + +use futures::{ + channel::mpsc::{unbounded, UnboundedSender}, + StreamExt, +}; +use gpui::{ + actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle, + ClipboardItem, Entity, MutableAppContext, View, ViewContext, +}; +use project::{Project, ProjectPath}; +use settings::Settings; +use smallvec::SmallVec; +use std::{path::PathBuf, sync::Arc}; +use workspace::{Item, Workspace}; + +use crate::terminal_element::{get_color_at_index, TerminalEl}; + +//ASCII Control characters on a keyboard +const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c' +const TAB_CHAR: char = 9_u8 as char; +const CARRIAGE_RETURN_CHAR: char = 13_u8 as char; +const ESC_CHAR: char = 27_u8 as char; +const DEL_CHAR: char = 127_u8 as char; +const LEFT_SEQ: &str = "\x1b[D"; +const RIGHT_SEQ: &str = "\x1b[C"; +const UP_SEQ: &str = "\x1b[A"; +const DOWN_SEQ: &str = "\x1b[B"; +const DEFAULT_TITLE: &str = "Terminal"; + +pub mod terminal_element; + +///Action for carrying the input to the PTY +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Input(pub String); + +///Event to transmit the scroll from the element to the view +#[derive(Clone, Debug, PartialEq)] +pub struct ScrollTerminal(pub i32); + +actions!( + terminal, + [Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit] +); +impl_internal_actions!(terminal, [Input, ScrollTerminal]); + +///Initialize and register all of our action handlers +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(Terminal::deploy); + cx.add_action(Terminal::write_to_pty); + cx.add_action(Terminal::send_sigint); + cx.add_action(Terminal::escape); + cx.add_action(Terminal::quit); + cx.add_action(Terminal::del); + cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode? + cx.add_action(Terminal::left); + cx.add_action(Terminal::right); + cx.add_action(Terminal::up); + cx.add_action(Terminal::down); + cx.add_action(Terminal::tab); + cx.add_action(Terminal::paste); + cx.add_action(Terminal::scroll_terminal); +} + +///A translation struct for Alacritty to communicate with us from their event loop +#[derive(Clone)] +pub struct ZedListener(UnboundedSender); + +impl EventListener for ZedListener { + fn send_event(&self, event: AlacTermEvent) { + self.0.unbounded_send(event).ok(); + } +} + +///A terminal view, maintains the PTY's file handles and communicates with the terminal +pub struct Terminal { + pty_tx: Notifier, + term: Arc>>, + title: String, + has_new_content: bool, + has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received + cur_size: SizeInfo, +} + +///Upward flowing events, for changing the title and such +pub enum Event { + TitleChanged, + CloseTerminal, + Activate, +} + +impl Entity for Terminal { + type Event = Event; +} + +impl Terminal { + ///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices + fn new(cx: &mut ViewContext, working_directory: Option) -> Self { + //Spawn a task so the Alacritty EventLoop can communicate with us in a view context + let (events_tx, mut events_rx) = unbounded(); + cx.spawn_weak(|this, mut cx| async move { + while let Some(event) = events_rx.next().await { + match this.upgrade(&cx) { + Some(handle) => { + handle.update(&mut cx, |this, cx| { + this.process_terminal_event(event, cx); + cx.notify(); + }); + } + None => break, + } + } + }) + .detach(); + + let pty_config = PtyConfig { + shell: Some(Program::Just("zsh".to_string())), + working_directory, + hold: false, + }; + + let config = Config { + pty_config: pty_config.clone(), + ..Default::default() + }; + + //The details here don't matter, the terminal will be resized on the first layout + //Set to something small for easier debugging + let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false); + + //Set up the terminal... + let term = Term::new(&config, size_info, ZedListener(events_tx.clone())); + let term = Arc::new(FairMutex::new(term)); + + //Setup the pty... + let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty"); + + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx.clone()), + pty, + pty_config.hold, + false, + ); + + //Kick things off + let pty_tx = Notifier(event_loop.channel()); + let _io_thread = event_loop.spawn(); + Terminal { + title: DEFAULT_TITLE.to_string(), + term, + pty_tx, + has_new_content: false, + has_bell: false, + cur_size: size_info, + } + } + + ///Takes events from Alacritty and translates them to behavior on this view + fn process_terminal_event( + &mut self, + event: alacritty_terminal::event::Event, + cx: &mut ViewContext, + ) { + match event { + AlacTermEvent::Wakeup => { + if !cx.is_self_focused() { + self.has_new_content = true; //Change tab content + cx.emit(Event::TitleChanged); + } else { + cx.notify() + } + } + AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx), + AlacTermEvent::MouseCursorDirty => { + //Calculate new cursor style. + //TODO + //Check on correctly handling mouse events for terminals + cx.platform().set_cursor_style(CursorStyle::Arrow); //??? + } + AlacTermEvent::Title(title) => { + self.title = title; + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ResetTitle => { + self.title = DEFAULT_TITLE.to_string(); + cx.emit(Event::TitleChanged); + } + AlacTermEvent::ClipboardStore(_, data) => { + cx.write_to_clipboard(ClipboardItem::new(data)) + } + AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty( + &Input(format( + &cx.read_from_clipboard() + .map(|ci| ci.text().to_string()) + .unwrap_or("".to_string()), + )), + cx, + ), + AlacTermEvent::ColorRequest(index, format) => { + let color = self.term.lock().colors()[index].unwrap_or_else(|| { + let term_style = &cx.global::().theme.terminal; + match index { + 0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)), + //These additional values are required to match the Alacritty Colors object's behavior + 256 => to_alac_rgb(term_style.foreground), + 257 => to_alac_rgb(term_style.background), + 258 => to_alac_rgb(term_style.cursor), + 259 => to_alac_rgb(term_style.dim_black), + 260 => to_alac_rgb(term_style.dim_red), + 261 => to_alac_rgb(term_style.dim_green), + 262 => to_alac_rgb(term_style.dim_yellow), + 263 => to_alac_rgb(term_style.dim_blue), + 264 => to_alac_rgb(term_style.dim_magenta), + 265 => to_alac_rgb(term_style.dim_cyan), + 266 => to_alac_rgb(term_style.dim_white), + 267 => to_alac_rgb(term_style.bright_foreground), + 268 => to_alac_rgb(term_style.black), //Dim Background, non-standard + _ => AlacRgb { r: 0, g: 0, b: 0 }, + } + }); + self.write_to_pty(&Input(format(color)), cx) + } + AlacTermEvent::CursorBlinkingChange => { + //TODO: Set a timer to blink the cursor on and off + } + AlacTermEvent::Bell => { + self.has_bell = true; + cx.emit(Event::TitleChanged); + } + AlacTermEvent::Exit => self.quit(&Quit, cx), + } + } + + ///Resize the terminal and the PTY. This locks the terminal. + fn set_size(&mut self, new_size: SizeInfo) { + if new_size != self.cur_size { + self.pty_tx.0.send(Msg::Resize(new_size)).ok(); + self.term.lock().resize(new_size); + self.cur_size = new_size; + } + } + + ///Scroll the terminal. This locks the terminal + fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext) { + self.term.lock().scroll_display(Scroll::Delta(scroll.0)); + } + + ///Create a new Terminal in the current working directory or the user's home directory + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let project = workspace.project().read(cx); + let abs_path = project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .and_then(|worktree_handle| worktree_handle.read(cx).as_local()) + .map(|wt| wt.abs_path().to_path_buf()); + + workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx); + } + + ///Send the shutdown message to Alacritty + fn shutdown_pty(&mut self) { + self.pty_tx.0.send(Msg::Shutdown).ok(); + } + + ///Tell Zed to close us + fn quit(&mut self, _: &Quit, cx: &mut ViewContext) { + cx.emit(Event::CloseTerminal); + } + + ///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.write_to_pty(&Input(item.text().to_owned()), cx); + } + } + + ///Write the Input payload to the tty. This locks the terminal so we can scroll it. + fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext) { + //iTerm bell behavior, bell stays until terminal is interacted with + self.has_bell = false; + self.term.lock().scroll_display(Scroll::Bottom); + cx.emit(Event::TitleChanged); + self.pty_tx.notify(input.0.clone().into_bytes()); + } + + ///Send the `up` key + fn up(&mut self, _: &Up, cx: &mut ViewContext) { + self.write_to_pty(&Input(UP_SEQ.to_string()), cx); + } + + ///Send the `down` key + fn down(&mut self, _: &Down, cx: &mut ViewContext) { + self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx); + } + + ///Send the `tab` key + fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + self.write_to_pty(&Input(TAB_CHAR.to_string()), cx); + } + + ///Send `SIGINT` (`ctrl-c`) + fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext) { + self.write_to_pty(&Input(ETX_CHAR.to_string()), cx); + } + + ///Send the `escape` key + fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { + self.write_to_pty(&Input(ESC_CHAR.to_string()), cx); + } + + ///Send the `delete` key. TODO: Difference between this and backspace? + fn del(&mut self, _: &Del, cx: &mut ViewContext) { + self.write_to_pty(&Input(DEL_CHAR.to_string()), cx); + } + + ///Send a carriage return. TODO: May need to check the terminal mode. + fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext) { + self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx); + } + + //Send the `left` key + fn left(&mut self, _: &Left, cx: &mut ViewContext) { + self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx); + } + + //Send the `right` key + fn right(&mut self, _: &Right, cx: &mut ViewContext) { + self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx); + } +} + +impl Drop for Terminal { + fn drop(&mut self) { + self.shutdown_pty(); + } +} + +impl View for Terminal { + fn ui_name() -> &'static str { + "Terminal" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + TerminalEl::new(cx.handle()).contained().boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Activate); + self.has_new_content = false; + } +} + +impl Item for Terminal { + fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = cx.global::(); + let search_theme = &settings.theme.search; //TODO properly integrate themes + + let mut flex = Flex::row(); + + if self.has_bell { + flex.add_child( + Svg::new("icons/zap.svg") //TODO: Swap out for a better icon, or at least resize this + .with_color(tab_theme.label.text.color) + .constrained() + .with_width(search_theme.tab_icon_width) + .aligned() + .boxed(), + ); + }; + + flex.with_child( + Label::new(self.title.clone(), tab_theme.label.clone()) + .aligned() + .contained() + .with_margin_left(if self.has_bell { + search_theme.tab_icon_spacing + } else { + 0. + }) + .boxed(), + ) + .boxed() + } + + fn project_path(&self, _cx: &gpui::AppContext) -> Option { + None + } + + fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + SmallVec::new() + } + + fn is_singleton(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} + + fn can_save(&self, _cx: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save should not have been called"); + } + + fn save_as( + &mut self, + _project: gpui::ModelHandle, + _abs_path: std::path::PathBuf, + _cx: &mut ViewContext, + ) -> gpui::Task> { + unreachable!("save_as should not have been called"); + } + + fn reload( + &mut self, + _project: gpui::ModelHandle, + _cx: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn is_dirty(&self, _: &gpui::AppContext) -> bool { + self.has_new_content + } + + fn should_update_tab_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::TitleChanged) + } + + fn should_close_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::CloseTerminal) + } + + fn should_activate_item_on_event(event: &Self::Event) -> bool { + matches!(event, &Event::Activate) + } +} + +//Convenience method for less lines +fn to_alac_rgb(color: Color) -> AlacRgb { + AlacRgb { + r: color.r, + g: color.g, + b: color.g, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::terminal_element::{build_chunks, BuiltChunks}; + use gpui::TestAppContext; + + ///Basic integration test, can we get the terminal to show up, execute a command, + //and produce noticable output? + #[gpui::test] + async fn test_terminal(cx: &mut TestAppContext) { + let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None)); + + terminal.update(cx, |terminal, cx| { + terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx); + terminal.carriage_return(&Return, cx); + }); + + terminal + .condition(cx, |terminal, _cx| { + let term = terminal.term.clone(); + let BuiltChunks { chunks, .. } = build_chunks( + term.lock().renderable_content().display_iter, + &Default::default(), + Default::default(), + ); + let content = chunks.iter().map(|e| e.0.trim()).collect::(); + content.contains("7") + }) + .await; + } +} diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs new file mode 100644 index 0000000000..42d4386fa6 --- /dev/null +++ b/crates/terminal/src/terminal_element.rs @@ -0,0 +1,621 @@ +use alacritty_terminal::{ + ansi::Color as AnsiColor, + grid::{GridIterator, Indexed}, + index::Point, + term::{ + cell::{Cell, Flags}, + SizeInfo, + }, +}; +use editor::{Cursor, CursorShape}; +use gpui::{ + color::Color, + elements::*, + fonts::{HighlightStyle, TextStyle, Underline}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::json, + text_layout::{Line, RunStyle}, + Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle, +}; +use itertools::Itertools; +use ordered_float::OrderedFloat; +use settings::Settings; +use std::{iter, rc::Rc}; +use theme::TerminalStyle; + +use crate::{Input, ScrollTerminal, Terminal}; + +///Scrolling is unbearably sluggish by default. Alacritty supports a configurable +///Scroll multiplier that is set to 3 by default. This will be removed when I +///Implement scroll bars. +const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; + +///Used to display the grid as passed to Alacritty and the TTY. +///Useful for debugging inconsistencies between behavior and display +#[cfg(debug_assertions)] +const DEBUG_GRID: bool = false; + +///The GPUI element that paints the terminal. +pub struct TerminalEl { + view: WeakViewHandle, +} + +///Represents a span of cells in a single line in the terminal's grid. +///This is used for drawing background rectangles +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct RectSpan { + start: i32, + end: i32, + line: usize, + color: Color, +} + +///A background color span +impl RectSpan { + ///Creates a new LineSpan. `start` must be <= `end`. + ///If `start` == `end`, then this span is considered to be over a + /// single cell + fn new(start: i32, end: i32, line: usize, color: Color) -> RectSpan { + debug_assert!(start <= end); + RectSpan { + start, + end, + line, + color, + } + } +} + +///Helper types so I don't mix these two up +struct CellWidth(f32); +struct LineHeight(f32); + +///The information generated during layout that is nescessary for painting +pub struct LayoutState { + lines: Vec, + line_height: LineHeight, + em_width: CellWidth, + cursor: Option, + cur_size: SizeInfo, + background_color: Color, + background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan +} + +impl TerminalEl { + pub fn new(view: WeakViewHandle) -> TerminalEl { + TerminalEl { view } + } +} + +impl Element for TerminalEl { + type LayoutState = LayoutState; + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + cx: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + //Settings immutably borrows cx here for the settings and font cache + //and we need to modify the cx to resize the terminal. So instead of + //storing Settings or the font_cache(), we toss them ASAP and then reborrow later + let text_style = make_text_style(cx.font_cache(), cx.global::()); + let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size)); + let cell_width = CellWidth( + cx.font_cache() + .em_advance(text_style.font_id, text_style.font_size), + ); + let view_handle = self.view.upgrade(cx).unwrap(); + + //Tell the view our new size. Requires a mutable borrow of cx and the view + let cur_size = make_new_size(constraint, &cell_width, &line_height); + //Note that set_size locks and mutates the terminal. + //TODO: Would be nice to lock once for the whole of layout + view_handle.update(cx.app, |view, _cx| view.set_size(cur_size)); + + //Now that we're done with the mutable portion, grab the immutable settings and view again + let terminal_theme = &(cx.global::()).theme.terminal; + let term = view_handle.read(cx).term.lock(); + + let grid = term.grid(); + let cursor_point = grid.cursor.point; + let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string(); + + let content = term.renderable_content(); + + //And we're off! Begin layouting + let BuiltChunks { + chunks, + line_count, + cursor_index, + } = build_chunks(content.display_iter, &terminal_theme, cursor_point); + + let shaped_lines = layout_highlighted_chunks( + chunks + .iter() + .map(|(text, style, _)| (text.as_str(), *style)), + &text_style, + cx.text_layout_cache, + cx.font_cache(), + usize::MAX, + line_count, + ); + + let backgrounds = chunks + .iter() + .filter(|(_, _, line_span)| line_span != &RectSpan::default()) + .map(|(_, _, line_span)| *line_span) + .collect(); + let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height); + + let block_text = cx.text_layout_cache.layout_str( + &cursor_text, + text_style.font_size, + &[( + cursor_text.len(), + RunStyle { + font_id: text_style.font_id, + color: terminal_theme.background, + underline: Default::default(), + }, + )], + ); + + let cursor = get_cursor_position( + content.cursor.point.line.0 as usize, + cursor_index, + &shaped_lines, + content.display_offset, + &line_height, + ) + .map(move |(cursor_position, block_width)| { + let block_width = if block_width != 0.0 { + block_width + } else { + cell_width.0 + }; + + Cursor::new( + cursor_position, + block_width, + line_height.0, + terminal_theme.cursor, + CursorShape::Block, + Some(block_text.clone()), + ) + }); + + ( + constraint.max, + LayoutState { + lines: shaped_lines, + line_height, + em_width: cell_width, + cursor, + cur_size, + background_rects, + background_color: terminal_theme.background, + }, + ) + } + + fn paint( + &mut self, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + cx: &mut gpui::PaintContext, + ) -> Self::PaintState { + //Setup element stuff + cx.scene.push_layer(Some(visible_bounds)); + + //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse + cx.scene.push_mouse_region(MouseRegion { + view_id: self.view.id(), + mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())), + bounds: visible_bounds, + ..Default::default() + }); + + let origin = bounds.origin() + vec2f(layout.em_width.0, 0.); + + //Start us off with a nice simple background color + cx.scene.push_layer(Some(visible_bounds)); + cx.scene.push_quad(Quad { + bounds: RectF::new(bounds.origin(), bounds.size()), + background: Some(layout.background_color), + border: Default::default(), + corner_radius: 0., + }); + + //Draw cell backgrounds + for background_rect in &layout.background_rects { + let new_origin = origin + background_rect.0.origin(); + cx.scene.push_quad(Quad { + bounds: RectF::new(new_origin, background_rect.0.size()), + background: Some(background_rect.1), + border: Default::default(), + corner_radius: 0., + }) + } + cx.scene.pop_layer(); + + //Draw text + cx.scene.push_layer(Some(visible_bounds)); + let mut line_origin = origin.clone(); + for line in &layout.lines { + let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0)); + if boundaries.intersects(visible_bounds) { + line.paint(line_origin, visible_bounds, layout.line_height.0, cx); + } + line_origin.set_y(boundaries.max_y()); + } + cx.scene.pop_layer(); + + //Draw cursor + if let Some(cursor) = &layout.cursor { + cx.scene.push_layer(Some(visible_bounds)); + cursor.paint(origin, cx); + cx.scene.pop_layer(); + } + + #[cfg(debug_assertions)] + if DEBUG_GRID { + draw_debug_grid(bounds, layout, cx); + } + + cx.scene.pop_layer(); + } + + fn dispatch_event( + &mut self, + event: &gpui::Event, + _bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + layout: &mut Self::LayoutState, + _paint: &mut Self::PaintState, + cx: &mut gpui::EventContext, + ) -> bool { + match event { + Event::ScrollWheel { + delta, position, .. + } => visible_bounds + .contains_point(*position) + .then(|| { + let vertical_scroll = + (delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER; + cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32)); + }) + .is_some(), + Event::KeyDown { + input: Some(input), .. + } => cx + .is_parent_view_focused() + .then(|| { + cx.dispatch_action(Input(input.to_string())); + }) + .is_some(), + _ => false, + } + } + + fn debug( + &self, + _bounds: gpui::geometry::rect::RectF, + _layout: &Self::LayoutState, + _paint: &Self::PaintState, + _cx: &gpui::DebugContext, + ) -> gpui::serde_json::Value { + json!({ + "type": "TerminalElement", + }) + } +} + +///Configures a text style from the current settings. +fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle { + TextStyle { + color: settings.theme.editor.text_color, + font_family_id: settings.buffer_font_family, + font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(), + font_id: font_cache + .select_font(settings.buffer_font_family, &Default::default()) + .unwrap(), + font_size: settings.buffer_font_size, + font_properties: Default::default(), + underline: Default::default(), + } +} + +///Configures a size info object from the given information. +fn make_new_size( + constraint: SizeConstraint, + cell_width: &CellWidth, + line_height: &LineHeight, +) -> SizeInfo { + SizeInfo::new( + constraint.max.x() - cell_width.0, + constraint.max.y(), + cell_width.0, + line_height.0, + 0., + 0., + false, + ) +} + +pub struct BuiltChunks { + pub chunks: Vec<(String, Option, RectSpan)>, + pub line_count: usize, + pub cursor_index: usize, +} + +///In a single pass, this function generates the background and foreground color info for every item in the grid. +pub(crate) fn build_chunks( + grid_iterator: GridIterator, + theme: &TerminalStyle, + cursor_point: Point, +) -> BuiltChunks { + let mut line_count: usize = 0; + let mut cursor_index: usize = 0; + //Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so + //rust knows where to put everything. + //Start by grouping by lines + let lines = grid_iterator.group_by(|i| i.point.line.0); + let result = lines + .into_iter() + .map(|(_line_grid_index, line)| { + line_count += 1; + let mut col_index = 0; + //Setup a variable + + //Then group by style + let chunks = line.group_by(|i| cell_style(&i, theme)); + chunks + .into_iter() + .map(|(style, fragment)| { + //And assemble the styled fragment into it's background and foreground information + let mut str_fragment = String::new(); + for indexed_cell in fragment { + if cursor_point.line.0 == indexed_cell.point.line.0 + && indexed_cell.point.column < cursor_point.column.0 + { + cursor_index += indexed_cell.c.to_string().len(); + } + str_fragment.push(indexed_cell.c); + } + + let start = col_index; + let end = start + str_fragment.len() as i32; + + //munge it here + col_index = end; + ( + str_fragment, + Some(style.0), + RectSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index + ) + }) + //Add a \n to the end, as we're using text layouting rather than grid layouts + .chain(iter::once(("\n".to_string(), None, Default::default()))) + .collect::, RectSpan)>>() + }) + .flatten() + //We have a Vec> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks) + .collect::, RectSpan)>>(); + + BuiltChunks { + chunks: result, + line_count, + cursor_index, + } +} + +///Convert a RectSpan in terms of character offsets, into RectFs of exact offsets +fn make_background_rects( + backgrounds: Vec, + shaped_lines: &Vec, + line_height: &LineHeight, +) -> Vec<(RectF, Color)> { + backgrounds + .into_iter() + .map(|line_span| { + //This should always be safe, as the shaped lines and backgrounds where derived + //At the same time earlier + let line = shaped_lines + .get(line_span.line) + .expect("Background line_num did not correspond to a line number"); + let x = line.x_for_index(line_span.start as usize); + let width = line.x_for_index(line_span.end as usize) - x; + ( + RectF::new( + vec2f(x, line_span.line as f32 * line_height.0), + vec2f(width, line_height.0), + ), + line_span.color, + ) + }) + .collect::>() +} + +// 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 get_cursor_position( + line: usize, + line_index: usize, + shaped_lines: &Vec, + display_offset: usize, + line_height: &LineHeight, +) -> Option<(Vector2F, f32)> { + let cursor_line = line + display_offset; + shaped_lines.get(cursor_line).map(|layout_line| { + let cursor_x = layout_line.x_for_index(line_index); + let next_char_x = layout_line.x_for_index(line_index + 1); + ( + vec2f(cursor_x, cursor_line as f32 * line_height.0), + next_char_x - cursor_x, + ) + }) +} + +///Convert the Alacritty cell styles to GPUI text styles and background color +fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) { + let flags = indexed.cell.flags; + let fg = Some(convert_color(&indexed.cell.fg, style)); + let bg = convert_color(&indexed.cell.bg, style); + + let underline = flags.contains(Flags::UNDERLINE).then(|| Underline { + color: fg, + squiggly: false, + thickness: OrderedFloat(1.), + }); + + ( + HighlightStyle { + color: fg, + underline, + ..Default::default() + }, + bg, + ) +} + +///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { + match alac_color { + //Named and theme defined colors + alacritty_terminal::ansi::Color::Named(n) => match n { + alacritty_terminal::ansi::NamedColor::Black => style.black, + alacritty_terminal::ansi::NamedColor::Red => style.red, + alacritty_terminal::ansi::NamedColor::Green => style.green, + alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, + alacritty_terminal::ansi::NamedColor::Blue => style.blue, + alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, + alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, + alacritty_terminal::ansi::NamedColor::White => style.white, + alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, + alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, + alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, + alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, + alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, + alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, + alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, + alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, + alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, + alacritty_terminal::ansi::NamedColor::Background => style.background, + alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, + alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, + alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, + alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, + alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, + alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, + alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, + alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, + alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, + alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, + alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, + }, + //'True' colors + alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), + //8 bit, indexed colors + alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style), + } +} + +///Converts an 8 bit ANSI color to it's GPUI equivalent. +pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color { + match index { + //0-15 are the same as the named colors above + 0 => style.black, + 1 => style.red, + 2 => style.green, + 3 => style.yellow, + 4 => style.blue, + 5 => style.magenta, + 6 => style.cyan, + 7 => style.white, + 8 => style.bright_black, + 9 => style.bright_red, + 10 => style.bright_green, + 11 => style.bright_yellow, + 12 => style.bright_blue, + 13 => style.bright_magenta, + 14 => style.bright_cyan, + 15 => style.bright_white, + //16-231 are mapped to their RGB colors on a 0-5 range per channel + 16..=231 => { + let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow + Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color + } + //232-255 are a 24 step grayscale from black to white + 232..=255 => { + let i = index - 232; //Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks + Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale + } + } +} + +///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube +///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). +/// +///Wikipedia gives a formula for calculating the index for a given color: +/// +///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +/// +///This function does the reverse, calculating the r, g, and b components from a given index. +fn rgb_for_index(i: &u8) -> (u8, u8, u8) { + debug_assert!(i >= &16 && i <= &231); + let i = i - 16; + let r = (i - (i % 36)) / 36; + let g = ((i % 36) - (i % 6)) / 6; + let b = (i % 36) % 6; + (r, g, b) +} + +///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between +///Display and conceptual grid. +#[cfg(debug_assertions)] +fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) { + let width = layout.cur_size.width(); + let height = layout.cur_size.height(); + //Alacritty uses 'as usize', so shall we. + for col in 0..(width / layout.em_width.0).round() as usize { + cx.scene.push_quad(Quad { + bounds: RectF::new( + bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.), + vec2f(1., height), + ), + background: Some(Color::green()), + border: Default::default(), + corner_radius: 0., + }); + } + for row in 0..((height / layout.line_height.0) + 1.0).round() as usize { + cx.scene.push_quad(Quad { + bounds: RectF::new( + bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0), + vec2f(width, 1.), + ), + background: Some(Color::green()), + border: Default::default(), + corner_radius: 0., + }); + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_rgb_for_index() { + //Test every possible value in the color cube + for i in 16..=231 { + let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8)); + assert_eq!(i, 16 + 36 * r + 6 * g + b); + } + } +} diff --git a/crates/terminal/truecolor.sh b/crates/terminal/truecolor.sh new file mode 100755 index 0000000000..14e5d81308 --- /dev/null +++ b/crates/terminal/truecolor.sh @@ -0,0 +1,19 @@ +#!/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/theme/src/theme.rs b/crates/theme/src/theme.rs index ae269c00cb..184b1880f0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -33,6 +33,7 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub tooltip: TooltipStyle, + pub terminal: TerminalStyle, } #[derive(Deserialize, Default)] @@ -633,3 +634,36 @@ pub struct HoverPopover { pub prose: TextStyle, pub highlight: Color, } + +#[derive(Clone, Deserialize, Default)] +pub struct TerminalStyle { + pub black: Color, + pub red: Color, + pub green: Color, + pub yellow: Color, + pub blue: Color, + pub magenta: Color, + pub cyan: Color, + pub white: Color, + pub bright_black: Color, + pub bright_red: Color, + pub bright_green: Color, + pub bright_yellow: Color, + pub bright_blue: Color, + pub bright_magenta: Color, + pub bright_cyan: Color, + pub bright_white: Color, + pub foreground: Color, + pub background: Color, + pub cursor: Color, + pub dim_black: Color, + pub dim_red: Color, + pub dim_green: Color, + pub dim_yellow: Color, + pub dim_blue: Color, + pub dim_magenta: Color, + pub dim_cyan: Color, + pub dim_white: Color, + pub bright_foreground: Color, + pub dim_foreground: Color, +} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 56b0aec8cc..5bc32cd5bd 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -14,6 +14,7 @@ command_palette = { path = "../command_palette" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +search = { path = "../search" } serde = { version = "1.0", features = ["derive", "rc"] } settings = { path = "../settings" } workspace = { path = "../workspace" } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 8837f264d3..c68e5182b0 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) { fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { cx.update_default_global(|vim: &mut Vim, cx| { vim.editors.insert(editor.id(), editor.downgrade()); - vim.sync_editor_options(cx); + vim.sync_vim_settings(cx); }) } @@ -29,8 +29,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont } })); - if editor.read(cx).mode() != EditorMode::Full { - vim.switch_mode(Mode::Insert, cx); + if !vim.enabled { + return; + } + + let editor = editor.read(cx); + if editor.selections.newest::(cx).is_empty() { + if editor.mode() != EditorMode::Full { + vim.switch_mode(Mode::Insert, cx); + } + } else { + vim.switch_mode(Mode::Visual { line: false }, cx); } }); } @@ -42,7 +51,7 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont vim.active_editor = None; } } - vim.sync_editor_options(cx); + vim.sync_vim_settings(cx); }) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 55c9779581..4c6dfd2d60 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1165,7 +1165,7 @@ mod test { The quick brown fox [jump}s over the lazy dog"}, - Mode::Normal, + Mode::Visual { line: false }, ); cx.simulate_keystroke("y"); cx.set_state( diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index cea607e9f3..c5c823c79e 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -40,7 +40,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { mod test { use indoc::indoc; - use crate::vim_test_context::VimTestContext; + use crate::{state::Mode, vim_test_context::VimTestContext}; #[gpui::test] async fn test_delete_h(cx: &mut gpui::TestAppContext) { @@ -390,4 +390,42 @@ mod test { the lazy"}, ); } + + #[gpui::test] + async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + + // Canceling operator twice reverts to normal mode with no active operator + cx.simulate_keystrokes(["d", "escape", "k"]); + assert_eq!(cx.active_operator(), None); + assert_eq!(cx.mode(), Mode::Normal); + cx.assert_editor_state(indoc! {" + The qu|ick brown + fox jumps over + the lazy dog"}); + } + + #[gpui::test] + async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + + // Canceling operator twice reverts to normal mode with no active operator + cx.simulate_keystrokes(["d", "y"]); + assert_eq!(cx.active_operator(), None); + assert_eq!(cx.mode(), Mode::Normal); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index a08b8bd2d2..e36cb7203d 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -37,7 +37,14 @@ pub struct VimState { impl VimState { pub fn cursor_shape(&self) -> CursorShape { match self.mode { - Mode::Normal | Mode::Visual { .. } => CursorShape::Block, + Mode::Normal => { + if self.operator_stack.is_empty() { + CursorShape::Block + } else { + CursorShape::Underscore + } + } + Mode::Visual { .. } => CursorShape::Block, Mode::Insert => CursorShape::Bar, } } @@ -73,20 +80,20 @@ impl VimState { context.set.insert("VimControl".to_string()); } - if let Some(operator) = &self.operator_stack.last() { - operator.set_context(&mut context); - } + Operator::set_context(self.operator_stack.last(), &mut context); + context } } impl Operator { - pub fn set_context(&self, context: &mut Context) { - let operator_context = match self { - Operator::Namespace(Namespace::G) => "g", - Operator::Change => "c", - Operator::Delete => "d", - Operator::Yank => "y", + pub fn set_context(operator: Option<&Operator>, context: &mut Context) { + let operator_context = match operator { + Some(Operator::Namespace(Namespace::G)) => "g", + Some(Operator::Change) => "c", + Some(Operator::Delete) => "d", + Some(Operator::Yank) => "y", + None => "none", } .to_owned(); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 0ee1abc3ab..5655e51e29 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -11,7 +11,7 @@ mod visual; use collections::HashMap; use command_palette::CommandPaletteFilter; -use editor::{Bias, CursorShape, Editor, Input}; +use editor::{Bias, Cancel, CursorShape, Editor, Input}; use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle}; use serde::Deserialize; @@ -34,6 +34,7 @@ pub fn init(cx: &mut MutableAppContext) { insert::init(cx); motion::init(cx); + // Vim Actions cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| { Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)) }); @@ -42,7 +43,11 @@ pub fn init(cx: &mut MutableAppContext) { Vim::update(cx, |vim, cx| vim.push_operator(operator, cx)) }, ); + + // Editor Actions cx.add_action(|_: &mut Editor, _: &Input, cx| { + // If we have an unbound input with an active operator, cancel that operator. Otherwise forward + // the input to the editor if Vim::read(cx).active_operator().is_some() { // Defer without updating editor MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx))) @@ -50,7 +55,25 @@ pub fn init(cx: &mut MutableAppContext) { cx.propagate_action() } }); + cx.add_action(|_: &mut Editor, _: &Cancel, cx| { + // If we are in a non normal mode or have an active operator, swap to normal mode + // Otherwise forward cancel on to the editor + let vim = Vim::read(cx); + if vim.state.mode != Mode::Normal || vim.active_operator().is_some() { + MutableAppContext::defer(cx, |cx| { + Vim::update(cx, |state, cx| { + state.switch_mode(Mode::Normal, cx); + }); + }); + } else { + cx.propagate_action(); + } + }); + // Sync initial settings with the rest of the app + Vim::update(cx, |state, cx| state.sync_vim_settings(cx)); + + // Any time settings change, update vim mode to match cx.observe_global::(|cx| { Vim::update(cx, |state, cx| { state.set_enabled(cx.global::().vim_mode, cx) @@ -93,25 +116,62 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) { + let previous_mode = self.state.mode; self.state.mode = mode; self.state.operator_stack.clear(); - self.sync_editor_options(cx); + + // Sync editor settings like clip mode + self.sync_vim_settings(cx); + + // Adjust selections + for editor in self.editors.values() { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + // If empty selections + if self.state.empty_selections_only() { + let new_head = map.clip_point(selection.head(), Bias::Left); + selection.collapse_to(new_head, selection.goal) + } else { + if matches!(mode, Mode::Visual { line: false }) + && !matches!(previous_mode, Mode::Visual { .. }) + && !selection.reversed + && !selection.is_empty() + { + // Mode wasn't visual mode before, but is now. We need to move the end + // back by one character so that the region to be modifed stays the same + *selection.end.column_mut() = + selection.end.column().saturating_sub(1); + selection.end = map.clip_point(selection.end, Bias::Left); + } + + selection.set_head( + map.clip_point(selection.head(), Bias::Left), + selection.goal, + ); + } + }); + }) + }) + } + } } fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) { self.state.operator_stack.push(operator); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator { let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); popped_operator } fn clear_operator(&mut self, cx: &mut MutableAppContext) { self.state.operator_stack.clear(); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } fn active_operator(&self) -> Option { @@ -123,23 +183,24 @@ impl Vim { self.enabled = enabled; self.state = Default::default(); if enabled { - self.state.mode = Mode::Normal; + self.switch_mode(Mode::Normal, cx); } - cx.update_default_global::(|filter, _| { - if enabled { - filter.filtered_namespaces.remove("vim"); - } else { - filter.filtered_namespaces.insert("vim"); - } - }); - self.sync_editor_options(cx); + self.sync_vim_settings(cx); } } - fn sync_editor_options(&self, cx: &mut MutableAppContext) { + fn sync_vim_settings(&self, cx: &mut MutableAppContext) { let state = &self.state; let cursor_shape = state.cursor_shape(); + cx.update_default_global::(|filter, _| { + if self.enabled { + filter.filtered_namespaces.remove("vim"); + } else { + filter.filtered_namespaces.insert("vim"); + } + }); + for editor in self.editors.values() { if let Some(editor) = editor.upgrade(cx) { editor.update(cx, |editor, cx| { @@ -151,17 +212,6 @@ impl Vim { matches!(state.mode, Mode::Visual { line: true }); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer); - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - selection.set_head( - map.clip_point(selection.head(), Bias::Left), - selection.goal, - ); - if state.empty_selections_only() { - selection.collapse_to(selection.head(), selection.goal) - } - }); - }) } else { editor.set_cursor_shape(CursorShape::Bar, cx); editor.set_clip_at_line_ends(false, cx); @@ -177,6 +227,9 @@ impl Vim { #[cfg(test)] mod test { + use indoc::indoc; + use search::BufferSearchBar; + use crate::{state::Mode, vim_test_context::VimTestContext}; #[gpui::test] @@ -221,4 +274,34 @@ mod test { cx.enable_vim(); assert_eq!(cx.mode(), Mode::Normal); } + + #[gpui::test] + async fn test_buffer_search_switches_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + The quick brown + fox ju|mps over + the lazy dog"}, + Mode::Normal, + ); + cx.simulate_keystroke("/"); + + assert_eq!(cx.mode(), Mode::Visual { line: false }); + + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); + }) + } } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 52d4778b38..57d0174703 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -1,14 +1,16 @@ use std::ops::{Deref, DerefMut}; use editor::test::EditorTestContext; -use gpui::json::json; +use gpui::{json::json, AppContext, ViewHandle}; use project::Project; +use search::{BufferSearchBar, ProjectSearchBar}; use workspace::{pane, AppState, WorkspaceHandle}; use crate::{state::Operator, *}; pub struct VimTestContext<'a> { cx: EditorTestContext<'a>, + workspace: ViewHandle, } impl<'a> VimTestContext<'a> { @@ -16,6 +18,7 @@ impl<'a> VimTestContext<'a> { cx.update(|cx| { editor::init(cx); pane::init(cx); + search::init(cx); crate::init(cx); settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); @@ -37,6 +40,19 @@ impl<'a> VimTestContext<'a> { .await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx)); + + // Setup search toolbars + workspace.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + }) + }); + }); + project .update(cx, |project, cx| { project.find_or_create_local_worktree("/root", true, cx) @@ -64,9 +80,17 @@ impl<'a> VimTestContext<'a> { window_id, editor, }, + workspace, } } + pub fn workspace(&mut self, read: F) -> T + where + F: FnOnce(&Workspace, &AppContext) -> T, + { + self.workspace.read_with(self.cx.cx, read) + } + pub fn enable_vim(&mut self) { self.cx.update(|cx| { cx.update_global(|settings: &mut Settings, _| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 29770dc772..5e039b8cd0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -18,11 +18,15 @@ use settings::Settings; use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc}; use util::ResultExt; +#[derive(Clone, Deserialize, PartialEq)] +pub struct ActivateItem(pub usize); + actions!( pane, [ ActivatePrevItem, ActivateNextItem, + ActivateLastItem, CloseActiveItem, CloseInactiveItems, ReopenClosedItem, @@ -39,9 +43,6 @@ pub struct CloseItem { pub pane: WeakViewHandle, } -#[derive(Clone, Deserialize, PartialEq)] -pub struct ActivateItem(pub usize); - #[derive(Clone, Deserialize, PartialEq)] pub struct GoBack { #[serde(skip_deserializing)] @@ -54,8 +55,8 @@ pub struct GoForward { pub pane: Option>, } -impl_actions!(pane, [GoBack, GoForward]); -impl_internal_actions!(pane, [CloseItem, ActivateItem]); +impl_actions!(pane, [GoBack, GoForward, ActivateItem]); +impl_internal_actions!(pane, [CloseItem]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -63,6 +64,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { pane.activate_item(action.0, true, true, cx); }); + cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { + pane.activate_item(pane.items.len() - 1, true, true, cx); + }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); }); diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 0cfb7f7865..341026aecf 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -55,7 +55,8 @@ impl Into for &dyn SidebarItemHandle { pub struct Sidebar { side: Side, items: Vec, - active_item_ix: Option, + is_open: bool, + active_item_ix: usize, actual_width: Rc>, custom_width: Rc>, } @@ -83,25 +84,41 @@ pub struct ToggleSidebarItem { pub item_index: usize, } -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct ToggleSidebarItemFocus { - pub side: Side, - pub item_index: usize, -} - -impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]); +impl_actions!(workspace, [ToggleSidebarItem]); impl Sidebar { pub fn new(side: Side) -> Self { Self { side, items: Default::default(), - active_item_ix: None, + active_item_ix: 0, + is_open: false, actual_width: Rc::new(RefCell::new(260.)), custom_width: Rc::new(RefCell::new(260.)), } } + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn active_item_ix(&self) -> usize { + self.active_item_ix + } + + pub fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + if open != self.is_open { + self.is_open = open; + cx.notify(); + } + } + + pub fn toggle_open(&mut self, cx: &mut ViewContext) { + if self.is_open {} + self.is_open = !self.is_open; + cx.notify(); + } + pub fn add_item( &mut self, icon_path: &'static str, @@ -133,23 +150,25 @@ impl Sidebar { } pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - self.active_item_ix = Some(item_ix); + self.active_item_ix = item_ix; cx.notify(); } pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - if self.active_item_ix == Some(item_ix) { - self.active_item_ix = None; + if self.active_item_ix == item_ix { + self.is_open = false; } else { - self.active_item_ix = Some(item_ix); + self.active_item_ix = item_ix; } cx.notify(); } pub fn active_item(&self) -> Option<&Rc> { - self.active_item_ix - .and_then(|ix| self.items.get(ix)) - .map(|item| &item.view) + if self.is_open { + self.items.get(self.active_item_ix).map(|item| &item.view) + } else { + None + } } fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { @@ -249,6 +268,7 @@ impl View for SidebarButtons { let item_style = theme.item; let badge_style = theme.badge; let active_ix = sidebar.active_item_ix; + let is_open = sidebar.is_open; let side = sidebar.side; let group_style = match side { Side::Left => theme.group_left, @@ -267,7 +287,7 @@ impl View for SidebarButtons { item_index: ix, }; MouseEventHandler::new::(ix, cx, move |state, cx| { - let is_active = Some(ix) == active_ix; + let is_active = is_open && ix == active_ix; let style = item_style.style_for(state, is_active); Stack::new() .with_child(Svg::new(icon_path).with_color(style.icon_color).boxed()) diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs index 5052afcf50..e5d765d4d5 100644 --- a/crates/workspace/src/waiting_room.rs +++ b/crates/workspace/src/waiting_room.rs @@ -1,7 +1,4 @@ -use crate::{ - sidebar::{Side, ToggleSidebarItem}, - AppState, ToggleFollow, Workspace, -}; +use crate::{sidebar::Side, AppState, ToggleFollow, Workspace}; use anyhow::Result; use client::{proto, Client, Contact}; use gpui::{ @@ -104,13 +101,7 @@ impl WaitingRoom { &app_state, cx, ); - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - cx, - ); + workspace.toggle_sidebar(Side::Left, cx); if let Some((host_peer_id, _)) = workspace.project.read(cx).collaborators().iter().find( |(_, collaborator)| collaborator.replica_id == 0, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b0e4373ca4..68e5b3cb26 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -31,7 +31,7 @@ use postage::prelude::Stream; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use serde::Deserialize; use settings::Settings; -use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus}; +use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem}; use smallvec::SmallVec; use status_bar::StatusBar; pub use status_bar::StatusItemView; @@ -90,6 +90,8 @@ actions!( ActivatePreviousPane, ActivateNextPane, FollowNextCollaborator, + ToggleLeftSidebar, + ToggleRightSidebar, ] ); @@ -104,6 +106,9 @@ pub struct ToggleProjectOnline { pub project: Option>, } +#[derive(Clone, Deserialize, PartialEq)] +pub struct ActivatePane(pub usize); + #[derive(Clone, PartialEq)] pub struct ToggleFollow(pub PeerId); @@ -122,7 +127,7 @@ impl_internal_actions!( RemoveWorktreeFromProject ] ); -impl_actions!(workspace, [ToggleProjectOnline]); +impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -185,7 +190,6 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { }, ); cx.add_action(Workspace::toggle_sidebar_item); - cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_action(Workspace::focus_center); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { workspace.activate_previous_pane(cx) @@ -193,6 +197,13 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { workspace.activate_next_pane(cx) }); + cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| { + workspace.toggle_sidebar(Side::Left, cx); + }); + cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| { + workspace.toggle_sidebar(Side::Right, cx); + }); + cx.add_action(Workspace::activate_pane_at_index); let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); @@ -1248,17 +1259,39 @@ impl Workspace { } } + pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext) { + let sidebar = match side { + Side::Left => &mut self.left_sidebar, + Side::Right => &mut self.right_sidebar, + }; + sidebar.update(cx, |sidebar, cx| { + sidebar.set_open(!sidebar.is_open(), cx); + }); + cx.focus_self(); + cx.notify(); + } + pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext) { let sidebar = match action.side { Side::Left => &mut self.left_sidebar, Side::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, |sidebar, cx| { - sidebar.toggle_item(action.item_index, cx); - sidebar.active_item().map(|item| item.to_any()) + if sidebar.is_open() && sidebar.active_item_ix() == action.item_index { + sidebar.set_open(false, cx); + None + } else { + sidebar.set_open(true, cx); + sidebar.activate_item(action.item_index, cx); + sidebar.active_item().cloned() + } }); if let Some(active_item) = active_item { - cx.focus(active_item); + if active_item.is_focused(cx) { + cx.focus_self(); + } else { + cx.focus(active_item.to_any()); + } } else { cx.focus_self(); } @@ -1267,15 +1300,17 @@ impl Workspace { pub fn toggle_sidebar_item_focus( &mut self, - action: &ToggleSidebarItemFocus, + side: Side, + item_index: usize, cx: &mut ViewContext, ) { - let sidebar = match action.side { + let sidebar = match side { Side::Left => &mut self.left_sidebar, Side::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, |sidebar, cx| { - sidebar.activate_item(action.item_index, cx); + sidebar.set_open(true, cx); + sidebar.activate_item(item_index, cx); sidebar.active_item().cloned() }); if let Some(active_item) = active_item { @@ -1405,6 +1440,15 @@ impl Workspace { } } + fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { + let panes = self.center.panes(); + if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { + self.activate_pane(pane, cx); + } else { + self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx); + } + } + pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { let next_pane = { let panes = self.center.panes(); @@ -2481,13 +2525,7 @@ pub fn open_paths( let mut workspace = Workspace::new(project, cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); if contains_directory { - workspace.toggle_sidebar_item( - &ToggleSidebarItem { - side: Side::Left, - item_index: 0, - }, - cx, - ); + workspace.toggle_sidebar(Side::Left, cx); } workspace }) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 56472be040..eb34539c35 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,6 +15,7 @@ name = "Zed" path = "src/main.rs" [dependencies] +activity_indicator = { path = "../activity_indicator" } assets = { path = "../assets" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } @@ -37,7 +38,6 @@ gpui = { path = "../gpui" } journal = { path = "../journal" } language = { path = "../language" } lsp = { path = "../lsp" } -lsp_status = { path = "../lsp_status" } outline = { path = "../outline" } project = { path = "../project" } project_panel = { path = "../project_panel" } @@ -46,6 +46,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } +terminal = { path = "../terminal" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } util = { path = "../util" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 10aa717c0d..c7a7e24c5a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -36,6 +36,7 @@ use std::{ thread, time::Duration, }; +use terminal; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use util::{ResultExt, TryFutureExt}; use workspace::{self, AppState, NewFile, OpenPaths}; @@ -181,6 +182,7 @@ fn main() { diagnostics::init(cx); search::init(cx); vim::init(cx); + terminal::init(cx); let db = cx.background().block(db); let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index dfc3556604..cadedc118b 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -187,11 +187,42 @@ pub fn menus() -> Vec> { }, MenuItem::Separator, MenuItem::Action { - name: "Project Browser", - action: Box::new(workspace::sidebar::ToggleSidebarItemFocus { - side: workspace::sidebar::Side::Left, - item_index: 0, - }), + name: "Toggle Left Sidebar", + action: Box::new(workspace::ToggleLeftSidebar), + }, + MenuItem::Action { + name: "Toggle Right Sidebar", + action: Box::new(workspace::ToggleRightSidebar), + }, + MenuItem::Submenu(Menu { + name: "Editor Layout", + items: vec![ + MenuItem::Action { + name: "Split Up", + action: Box::new(workspace::SplitUp), + }, + MenuItem::Action { + name: "Split Down", + action: Box::new(workspace::SplitDown), + }, + MenuItem::Action { + name: "Split Left", + action: Box::new(workspace::SplitLeft), + }, + MenuItem::Action { + name: "Split Right", + action: Box::new(workspace::SplitRight), + }, + ], + }), + MenuItem::Separator, + MenuItem::Action { + name: "Project Panel", + action: Box::new(project_panel::Toggle), + }, + MenuItem::Action { + name: "Contacts Panel", + action: Box::new(contacts_panel::Toggle), }, MenuItem::Action { name: "Command Palette", diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 68a713e9af..548b726af9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -34,7 +34,7 @@ use std::{ }; use util::ResultExt; pub use workspace; -use workspace::{AppState, Workspace}; +use workspace::{sidebar::Side, AppState, Workspace}; #[derive(Deserialize, Clone, PartialEq)] struct OpenBrowser { @@ -97,6 +97,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + println!("open settings"); open_config_file(&SETTINGS_PATH, app_state.clone(), cx); } }); @@ -128,8 +129,18 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { } }, ); + cx.add_action( + |workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext| { + workspace.toggle_sidebar_item_focus(Side::Left, 0, cx); + }, + ); + cx.add_action( + |workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext| { + workspace.toggle_sidebar_item_focus(Side::Right, 0, cx); + }, + ); - lsp_status::init(cx); + activity_indicator::init(cx); settings::KeymapFileContent::load_defaults(cx); } @@ -212,15 +223,14 @@ pub fn initialize_workspace( let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); - let lsp_status = lsp_status::LspStatusItem::new(workspace, app_state.languages.clone(), cx); + let activity_indicator = + activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx)); let feedback_link = cx.add_view(|_| feedback::FeedbackLink); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); - status_bar.add_left_item(lsp_status, cx); + status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(cursor_position, cx); - status_bar.add_right_item(auto_update, cx); status_bar.add_right_item(feedback_link, cx); }); @@ -429,7 +439,7 @@ mod tests { let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); workspace_1.update(cx, |workspace, cx| { assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_sidebar().read(cx).active_item().is_some()); + assert!(workspace.left_sidebar().read(cx).is_open()); assert!(workspace.active_pane().is_focused(cx)); }); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index e015895e9c..fe67cf701d 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -14,6 +14,7 @@ import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; import updateNotification from "./updateNotification"; import tooltip from "./tooltip"; +import terminal from "./terminal"; export const panel = { padding: { top: 12, bottom: 12 }, @@ -41,5 +42,6 @@ export default function app(theme: Theme): Object { contactNotification: contactNotification(theme), updateNotification: updateNotification(theme), tooltip: tooltip(theme), + terminal: terminal(theme), }; } diff --git a/styles/src/styleTree/terminal.ts b/styles/src/styleTree/terminal.ts new file mode 100644 index 0000000000..ef9e4f93dd --- /dev/null +++ b/styles/src/styleTree/terminal.ts @@ -0,0 +1,35 @@ +import Theme from "../themes/common/theme"; + +export default function terminal(theme: Theme) { + return { + black: theme.ramps.neutral(0).hex(), + red: theme.ramps.red(0.5).hex(), + green: theme.ramps.green(0.5).hex(), + yellow: theme.ramps.yellow(0.5).hex(), + blue: theme.ramps.blue(0.5).hex(), + magenta: theme.ramps.magenta(0.5).hex(), + cyan: theme.ramps.cyan(0.5).hex(), + white: theme.ramps.neutral(7).hex(), + brightBlack: theme.ramps.neutral(2).hex(), + brightRed: theme.ramps.red(0.25).hex(), + brightGreen: theme.ramps.green(0.25).hex(), + brightYellow: theme.ramps.yellow(0.25).hex(), + brightBlue: theme.ramps.blue(0.25).hex(), + brightMagenta: theme.ramps.magenta(0.25).hex(), + brightCyan: theme.ramps.cyan(0.25).hex(), + brightWhite: theme.ramps.neutral(7).hex(), + foreground: theme.ramps.neutral(7).hex(), + background: theme.ramps.neutral(0).hex(), + cursor: theme.ramps.neutral(7).hex(), + dimBlack: theme.ramps.neutral(7).hex(), + dimRed: theme.ramps.red(0.75).hex(), + dimGreen: theme.ramps.green(0.75).hex(), + dimYellow: theme.ramps.yellow(0.75).hex(), + dimBlue: theme.ramps.blue(0.75).hex(), + dimMagenta: theme.ramps.magenta(0.75).hex(), + dimCyan: theme.ramps.cyan(0.75).hex(), + dimWhite: theme.ramps.neutral(5).hex(), + brightForeground: theme.ramps.neutral(7).hex(), + dimForeground: theme.ramps.neutral(0).hex(), + }; +} \ No newline at end of file diff --git a/styles/src/themes/cave.ts b/styles/src/themes/cave.ts index 2e66f4baf4..aab020d626 100644 --- a/styles/src/themes/cave.ts +++ b/styles/src/themes/cave.ts @@ -25,4 +25,4 @@ const ramps = { }; export const dark = createTheme(`${name}-dark`, false, ramps); -export const light = createTheme(`${name}-light`, true, ramps); +export const light = createTheme(`${name}-light`, true, ramps); \ No newline at end of file diff --git a/styles/src/themes/common/base16.ts b/styles/src/themes/common/base16.ts index 21a02cde25..729cf32ee5 100644 --- a/styles/src/themes/common/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -13,15 +13,25 @@ export function colorRamp(color: Color): Scale { export function createTheme( name: string, isLight: boolean, - ramps: { [rampName: string]: Scale }, + color_ramps: { [rampName: string]: Scale }, ): Theme { + let ramps: typeof color_ramps = {}; + // Chromajs mutates the underlying ramp when you call domain. This causes problems because + // we now store the ramps object in the theme so that we can pull colors out of them. + // So instead of calling domain and storing the result, we have to construct new ramps for each + // theme so that we don't modify the passed in ramps. + // This combined with an error in the type definitions for chroma js means we have to cast the colors + // function to any in order to get the colors back out from the original ramps. if (isLight) { - for (var rampName in ramps) { - ramps[rampName] = ramps[rampName].domain([1, 0]); + for (var rampName in color_ramps) { + ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([1, 0]); } - ramps.neutral = ramps.neutral.domain([7, 0]); + ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([7, 0]); } else { - ramps.neutral = ramps.neutral.domain([0, 7]); + for (var rampName in color_ramps) { + ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([0, 1]); + } + ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([0, 7]); } let blend = isLight ? 0.12 : 0.24; @@ -237,6 +247,7 @@ export function createTheme( return { name, + isLight, backgroundColor, borderColor, textColor, @@ -245,5 +256,6 @@ export function createTheme( syntax, player, shadow, + ramps, }; } diff --git a/styles/src/themes/common/theme.ts b/styles/src/themes/common/theme.ts index 92b1f8eff8..7f32f48974 100644 --- a/styles/src/themes/common/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -1,3 +1,4 @@ +import { Scale } from "chroma-js"; import { FontWeight } from "../../common"; import { withOpacity } from "../../utils/color"; @@ -60,6 +61,7 @@ export interface Syntax { export default interface Theme { name: string; + isLight: boolean, backgroundColor: { // Basically just Title Bar // Lowest background level @@ -155,4 +157,5 @@ export default interface Theme { 8: Player; }, shadow: string; + ramps: { [rampName: string]: Scale }; }