Detailed changes
@@ -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",
@@ -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"
+ }
}
]
@@ -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"
- }
}
]
@@ -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" }
@@ -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<str>, error: String },
}
-pub struct LspStatusItem {
+pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: ModelHandle<Project>,
+ auto_updater: Option<ModelHandle<AutoUpdater>>,
}
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<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
- ) -> ViewHandle<LspStatusItem> {
+ ) -> ViewHandle<ActivityIndicator> {
let project = workspace.project().clone();
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
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<Self>) {
+ 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<Self>) -> ElementBox {
- let mut message;
- let mut icon = None;
- let mut handler = None;
+ fn content_to_render(
+ &mut self,
+ cx: &mut RenderContext<Self>,
+ ) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
+ // 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,19 +215,67 @@ 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::<Settings>().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<Self>) -> ElementBox {
+ let (icon, message, action) = self.content_to_render(cx);
let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
let theme = &cx
@@ -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<Self>) {}
}
@@ -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<ModelHandle<AutoUpdater>>,
-}
-
#[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<ModelHandle<Self>> {
+ pub fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
cx.default_global::<Option<ModelHandle<Self>>>().clone()
}
@@ -170,6 +161,15 @@ impl AutoUpdater {
}));
}
+ pub fn status(&self) -> AutoUpdateStatus {
+ self.status
+ }
+
+ pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
+ self.status = AutoUpdateStatus::Idle;
+ cx.notify();
+ }
+
async fn update(this: ModelHandle<Self>, 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::<Settings>().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::<Self, _, _>(0, cx, |_, cx| {
- let theme = &cx.global::<Settings>().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<Self>) {}
-}
-
-impl AutoUpdateIndicator {
- pub fn new(cx: &mut ViewContext<Self>) -> 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<Self>) {
- if let Some(updater) = &self.updater {
- updater.update(cx, |updater, cx| {
- updater.status = AutoUpdateStatus::Idle;
- cx.notify();
- });
- }
- }
-}
@@ -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<dyn HttpClient>) -> Arc<Self> {
- 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();
@@ -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<Option<User>> {
+ 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<Option<User>> {
+ 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<Option<(String, u32)>> {
+ 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<MessageId>,
) -> Result<Vec<ChannelMessage>> {
+ self.background.simulate_random_delay().await;
let mut messages = self
.channel_messages
.lock()
@@ -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::<Vec<_>>(),
@@ -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<F>(&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();
@@ -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<R: RequestMessage> Response<R> {
pub struct Server {
peer: Arc<Peer>,
- pub(crate) store: RwLock<Store>,
+ pub(crate) store: Mutex<Store>,
app_state: Arc<AppState>,
handlers: HashMap<TypeId, MessageHandler>,
notifications: Option<mpsc::UnboundedSender<()>>,
@@ -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<Rc<()>>,
-}
-
-struct StoreWriteGuard<'a> {
- guard: RwLockWriteGuard<'a, Store>,
+pub(crate) struct StoreGuard<'a> {
+ guard: MutexGuard<'a, Store>,
_not_send: PhantomData<Rc<()>>,
}
@@ -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<S, T, U>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
@@ -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::<Vec<_>>();
@@ -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<Server>,
request: TypedEnvelope<proto::RegisterProjectActivity>,
) -> 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<Server>,
request: TypedEnvelope<proto::StartLanguageServer>,
) -> 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<Self>, request: TypedEnvelope<proto::Unfollow>) -> 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<proto::UpdateFollowers>,
) -> 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<Self>) -> StoreReadGuard<'a> {
- #[cfg(test)]
- tokio::task::yield_now().await;
- let guard = self.store.read().await;
- #[cfg(test)]
- tokio::task::yield_now().await;
- StoreReadGuard {
- guard,
- _not_send: PhantomData,
- }
- }
-
- async fn store_mut<'a>(self: &'a Arc<Self>) -> StoreWriteGuard<'a> {
+ pub(crate) async fn store<'a>(&'a self) -> StoreGuard<'a> {
#[cfg(test)]
tokio::task::yield_now().await;
- let guard = self.store.write().await;
+ let guard = self.store.lock().await;
#[cfg(test)]
tokio::task::yield_now().await;
- StoreWriteGuard {
+ StoreGuard {
guard,
_not_send: PhantomData,
}
@@ -1709,21 +1724,13 @@ impl Server {
pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
ServerSnapshot {
- store: self.store.read().await,
+ store: self.store().await,
peer: &self.peer,
}
}
}
-impl<'a> Deref for StoreReadGuard<'a> {
- type Target = Store;
-
- fn deref(&self) -> &Self::Target {
- &*self.guard
- }
-}
-
-impl<'a> Deref for StoreWriteGuard<'a> {
+impl<'a> Deref for StoreGuard<'a> {
type Target = Store;
fn deref(&self) -> &Self::Target {
@@ -1731,13 +1738,13 @@ impl<'a> Deref for StoreWriteGuard<'a> {
}
}
-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();
@@ -56,6 +56,7 @@ pub struct Worktree {
#[serde(skip)]
pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
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<ConnectionId>, 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))
}
@@ -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]
@@ -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<Line>,
+ ) -> 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);
}
}
}
@@ -1634,14 +1634,10 @@ impl MutableAppContext {
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
let type_id = TypeId::of::<T>();
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()
}
@@ -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<dyn Action>) {
self.dispatched_actions.push(DispatchDirective {
dispatcher_view_id: self.view_stack.last().copied(),
@@ -164,7 +164,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
}
}
-#[derive(Default, Debug)]
+#[derive(Default, Debug, Clone)]
pub struct Line {
layout: Arc<LineLayout>,
style_runs: SmallVec<[(u32, Color, Underline); 32]>,
@@ -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<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
root_path: PathBuf,
+ _server: Option<Child>,
}
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<Child>,
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<T: request::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| {
@@ -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,
)
})
@@ -70,14 +70,26 @@ pub struct ProjectStore {
projects: Vec<WeakModelHandle<Project>>,
}
+// 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<WorktreeHandle>,
active_entry: Option<ProjectEntryId>,
languages: Arc<LanguageRegistry>,
- language_servers:
- HashMap<(WorktreeId, LanguageServerName), (Arc<dyn LspAdapter>, Arc<LanguageServer>)>,
- started_language_servers:
- HashMap<(WorktreeId, LanguageServerName), Task<Option<Arc<LanguageServer>>>>,
+ language_servers: HashMap<usize, LanguageServerState>,
+ language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
language_server_settings: Arc<Mutex<serde_json::Value>>,
last_workspace_edits_by_language_server: HashMap<usize, ProjectTransaction>,
@@ -175,6 +187,14 @@ pub enum Event {
ContactCancelledJoinRequest(Arc<User>),
}
+pub enum LanguageServerState {
+ Starting(Task<Option<Arc<LanguageServer>>>),
+ Running {
+ adapter: Arc<dyn LspAdapter>,
+ server: Arc<LanguageServer>,
+ },
+}
+
#[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<dyn LspAdapter>,
- lsp_server: Arc<LanguageServer>,
+ language_server_id: usize,
+ language_server_name: LanguageServerName,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
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::notification::DidOpenTextDocument>(
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<Item = &(Arc<dyn LspAdapter>, Arc<LanguageServer>)> {
- self.language_servers.iter().filter_map(
- move |((language_server_worktree_id, _), server)| {
+ ) -> impl Iterator<Item = (&Arc<dyn LspAdapter>, &Arc<LanguageServer>)> {
+ 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::<lsp::notification::PublishDiagnostics, _>({
- 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,
- );
- });
+ language_server
+ .on_notification::<lsp::notification::PublishDiagnostics, _>({
+ 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();
+ })
+ .detach();
- language_server
- .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
- 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())
+ language_server
+ .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
+ 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();
+ })
+ .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::<lsp::request::WorkDoneProgressCreate, _, _>({
- 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
+ // 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::<lsp::request::WorkDoneProgressCreate, _, _>({
+ 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)
{
- status.progress_tokens.insert(token);
+ if let lsp::NumberOrString::String(token) =
+ params.token
+ {
+ status.progress_tokens.insert(token);
+ }
}
- }
- });
+ });
+ }
+ Ok(())
}
+ })
+ .detach();
+ language_server
+ .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
Ok(())
- }
- })
- .detach();
- language_server
- .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
- Ok(())
- })
- .detach();
+ })
+ .detach();
- language_server
- .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
- 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_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+ 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::<lsp::notification::Progress, _>({
- 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,
- );
- });
+ language_server
+ .on_notification::<lsp::notification::Progress, _>({
+ 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;
}
- })
- .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::notification::DidChangeConfiguration>(
- lsp::DidChangeConfigurationParams {
- settings: this.language_server_settings.lock().clone(),
+ // 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(),
},
- )
- .ok();
+ );
+ 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::notification::DidChangeConfiguration>(
+ 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();
+ }
- 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;
+ }
- // 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::notification::DidOpenTextDocument>(
+ 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,
+ )
+ });
}
-
- 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::notification::DidOpenTextDocument>(
- 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(())
- });
+ cx.notify();
+ Some(language_server)
+ })
+ })),
+ );
- 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<Self>,
- ) -> Task<()> {
+ ) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
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());
+ 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::<Vec<_>>();
+ 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);
+ }
+ }
+
+ self.language_server_statuses.remove(&server_id);
cx.notify();
- }
- if let Some(started_language_server) = self.started_language_servers.remove(&key) {
+ 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() {
- shutdown.await;
+ 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(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.language_server_statuses
- .remove(&language_server.server_id());
- cx.notify();
- });
+ 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(&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<Path>,
+ fallback_path: Arc<Path>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
@@ -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::notification::DidChangeConfiguration>(
- lsp::DidChangeConfigurationParams {
- settings: settings.clone(),
- },
- )
- .ok();
+ for server_state in self.language_servers.values() {
+ if let LanguageServerState::Running { server, .. } = server_state {
+ server
+ .notify::<lsp::notification::DidChangeConfiguration>(
+ 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<Self>) -> Task<Result<Vec<Symbol>>> {
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::request::WorkspaceSymbol>(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::request::WorkspaceSymbol>(
+ 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<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
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(())
})
@@ -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<Client>,
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
- last_scan_id_rx: watch::Receiver<usize>,
+ snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
replica_id: ReplicaId,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
visible: bool,
@@ -96,6 +96,7 @@ pub struct Snapshot {
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
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<anyhow::Error>),
}
struct ShareState {
project_id: u64,
- snapshots_tx: Sender<LocalSnapshot>,
+ snapshots_tx: watch::Sender<LocalSnapshot>,
_maintain_remote_snapshot: Option<Task<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<Client>,
cx: &mut MutableAppContext,
- ) -> (ModelHandle<Self>, Task<()>) {
+ ) -> ModelHandle<Self> {
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>| {
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);
- }
- 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;
+ 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;
+ }
}
- }
+ });
+ } 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<Self>) {
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<Worktree>| {
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<Worktree>) {
+ 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<Output = ()> {
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, _| {
+ this.update(&mut cx, |this, cx| {
let this = this.as_local_mut().unwrap();
- let mut snapshot = this.background_snapshot.lock();
- snapshot.delete_entry(entry_id);
+ {
+ let mut snapshot = this.background_snapshot.lock();
+ snapshot.delete_entry(entry_id);
+ }
+ this.poll_snapshot(true, cx);
});
- this.update(&mut cx, |this, cx| {
- this.poll_snapshot(cx);
- this.as_local().unwrap().broadcast_snapshot()
- })
- .await;
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<Worktree>) -> Task<Result<()>> {
let (share_tx, share_rx) = oneshot::channel();
- let (snapshots_to_send_tx, snapshots_to_send_rx) =
- smol::channel::unbounded::<LocalSnapshot>();
+
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<Output = ()> {
- 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<Worktree>) {
+ 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<proto::UpdateWorktree>,
- ) -> 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<Output = ()> {
- 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<Output = ()> {
+ 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<Worktree>,
@@ -1128,7 +1023,7 @@ impl RemoteWorktree {
}
pub(crate) fn delete_entry(
- &self,
+ &mut self,
id: ProjectEntryId,
scan_id: usize,
cx: &mut ModelContext<Worktree>,
@@ -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<PathKey, DiagnosticSummary>,
- 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<Item = Vec<fsevent::Event>>) {
- 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<Client>, 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(),
};
@@ -108,7 +108,8 @@ actions!(
Cut,
Paste,
Delete,
- Rename
+ Rename,
+ Toggle
]
);
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
@@ -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;
@@ -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<Nonce> for u128 {
}
}
+pub fn split_worktree_update(
+ mut message: UpdateWorktree,
+ max_chunk_size: usize,
+) -> impl Iterator<Item = UpdateWorktree> {
+ 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::*;
@@ -58,7 +58,7 @@ fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableApp
}
pub struct BufferSearchBar {
- query_editor: ViewHandle<Editor>,
+ pub query_editor: ViewHandle<Editor>,
active_editor: Option<ViewHandle<Editor>>,
active_match_index: Option<usize>,
active_editor_subscription: Option<Subscription>,
@@ -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"] }
@@ -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
@@ -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<AlacTermEvent>);
+
+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<FairMutex<Term<ZedListener>>>,
+ 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<Self>, working_directory: Option<PathBuf>) -> 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<Self>,
+ ) {
+ 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::<Settings>().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>) {
+ 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<Workspace>) {
+ 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<Self>) {
+ cx.emit(Event::CloseTerminal);
+ }
+
+ ///Attempt to paste the clipboard into the terminal
+ fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ //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>) {
+ self.write_to_pty(&Input(UP_SEQ.to_string()), cx);
+ }
+
+ ///Send the `down` key
+ fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
+ self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx);
+ }
+
+ ///Send the `tab` key
+ fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+ self.write_to_pty(&Input(TAB_CHAR.to_string()), cx);
+ }
+
+ ///Send `SIGINT` (`ctrl-c`)
+ fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
+ self.write_to_pty(&Input(ETX_CHAR.to_string()), cx);
+ }
+
+ ///Send the `escape` key
+ fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
+ 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>) {
+ 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>) {
+ self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx);
+ }
+
+ //Send the `left` key
+ fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
+ self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx);
+ }
+
+ //Send the `right` key
+ fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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::<Settings>();
+ 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<ProjectPath> {
+ 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<Self>) {}
+
+ fn can_save(&self, _cx: &gpui::AppContext) -> bool {
+ false
+ }
+
+ fn save(
+ &mut self,
+ _project: gpui::ModelHandle<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<gpui::anyhow::Result<()>> {
+ unreachable!("save should not have been called");
+ }
+
+ fn save_as(
+ &mut self,
+ _project: gpui::ModelHandle<Project>,
+ _abs_path: std::path::PathBuf,
+ _cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<gpui::anyhow::Result<()>> {
+ unreachable!("save_as should not have been called");
+ }
+
+ fn reload(
+ &mut self,
+ _project: gpui::ModelHandle<Project>,
+ _cx: &mut ViewContext<Self>,
+ ) -> gpui::Task<gpui::anyhow::Result<()>> {
+ 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::<String>();
+ content.contains("7")
+ })
+ .await;
+ }
+}
@@ -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<Terminal>,
+}
+
+///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>,
+ line_height: LineHeight,
+ em_width: CellWidth,
+ cursor: Option<Cursor>,
+ 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<Terminal>) -> 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::<Settings>());
+ 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::<Settings>()).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<HighlightStyle>, 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<Cell>,
+ 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::<Vec<(String, Option<HighlightStyle>, RectSpan)>>()
+ })
+ .flatten()
+ //We have a Vec<Vec<>> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks)
+ .collect::<Vec<(String, Option<HighlightStyle>, 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<RectSpan>,
+ shaped_lines: &Vec<Line>,
+ 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::<Vec<(RectF, Color)>>()
+}
+
+// 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<Line>,
+ 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);
+ }
+ }
+}
@@ -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; colnum<total_cols; colnum++) {
+ r = 255-(colnum*255/total_cols);
+ g = (colnum*510/total_cols);
+ b = (colnum*255/total_cols);
+ if (g>255) 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";
+}'
@@ -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,
+}
@@ -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" }
@@ -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::<usize>(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);
})
}
@@ -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(
@@ -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);
+ }
}
@@ -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();
@@ -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::<Settings, _>(|cx| {
Vim::update(cx, |state, cx| {
state.set_enabled(cx.global::<Settings>().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<Operator> {
@@ -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::<CommandPaletteFilter, _, _>(|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::<CommandPaletteFilter, _, _>(|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::<Self>(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::<BufferSearchBar>()
+ .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");
+ })
+ }
}
@@ -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<Workspace>,
}
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<F, T>(&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, _| {
@@ -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<Pane>,
}
-#[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<WeakViewHandle<Pane>>,
}
-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);
});
@@ -55,7 +55,8 @@ impl Into<AnyViewHandle> for &dyn SidebarItemHandle {
pub struct Sidebar {
side: Side,
items: Vec<Item>,
- active_item_ix: Option<usize>,
+ is_open: bool,
+ active_item_ix: usize,
actual_width: Rc<RefCell<f32>>,
custom_width: Rc<RefCell<f32>>,
}
@@ -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<Self>) {
+ if open != self.is_open {
+ self.is_open = open;
+ cx.notify();
+ }
+ }
+
+ pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
+ if self.is_open {}
+ self.is_open = !self.is_open;
+ cx.notify();
+ }
+
pub fn add_item<T: SidebarItem>(
&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>) {
- 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<Self>) {
- 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<dyn SidebarItemHandle>> {
- 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<Self>) -> 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::<Self, _, _>(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())
@@ -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,
@@ -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<ModelHandle<Project>>,
}
+#[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<AppState>, cx: &mut MutableAppContext) {
pane::init(cx);
@@ -185,7 +190,6 @@ pub fn init(app_state: Arc<AppState>, 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<AppState>, 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<Self>) {
+ 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<Self>) {
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<Self>,
) {
- 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<Self>) {
+ 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<Self>) {
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
})
@@ -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" }
@@ -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();
@@ -187,11 +187,42 @@ pub fn menus() -> Vec<Menu<'static>> {
},
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",
@@ -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<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_action({
let app_state = app_state.clone();
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
+ println!("open settings");
open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
}
});
@@ -128,8 +129,18 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
}
},
);
+ cx.add_action(
+ |workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext<Workspace>| {
+ workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
+ },
+ );
+ cx.add_action(
+ |workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext<Workspace>| {
+ 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::<Workspace>(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));
});
@@ -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),
};
}
@@ -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(),
+ };
+}
@@ -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);
@@ -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,
};
}
@@ -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 };
}