From 0cceb3fdf1bcc8823688345334ba178d9111cc5c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 20 Sep 2023 06:55:24 -0700 Subject: [PATCH 01/89] Get nextLS running --- Cargo.lock | 1 + assets/settings/default.json | 21 +++++++++++++++ crates/semantic_index/examples/eval.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 36 +++++++++++++++++++++----- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 7 files changed, 56 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b42e11d326a8691933a516edb827696bf7c2e274..8a87cb99861712fb0b2f188e0087ec5155dcc2d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9861,6 +9861,7 @@ dependencies = [ "rpc", "rsa", "rust-embed", + "schemars", "search", "semantic_index", "serde", diff --git a/assets/settings/default.json b/assets/settings/default.json index 86def54d323aebc0225fb5c89ee8a7a104c50a40..126407a32dec59cc902d508ba997e371ad001340 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -373,6 +373,27 @@ "enabled": false, "reindexing_delay_seconds": 600 }, + // Settings specific to our elixir integration + "elixir": { + // Set Zed to use the experimental Next LS LSP server. + // Note that changing this setting requires a restart of Zed + // to take effect. + // + // May take 3 values: + // 1. Use the standard elixir-ls LSP server + // "next": "off" + // 2. Use a bundled version of the next Next LS LSP server + // "next": "on", + // 3. Use a locally running version of the next Next LS LSP server, + // on a specific port: + // "next": { + // "local": { + // "port": 4000 + // } + // }, + // + "next": "off" + }, // Different settings for specific languages. "languages": { "Plain Text": { diff --git a/crates/semantic_index/examples/eval.rs b/crates/semantic_index/examples/eval.rs index 15406cf63e3bb99271c5444933a3a01fffd041d1..2994cb29b61bd11ae2d726e0dac35fd3c0f6416e 100644 --- a/crates/semantic_index/examples/eval.rs +++ b/crates/semantic_index/examples/eval.rs @@ -456,7 +456,7 @@ fn main() { let languages = Arc::new(languages); let node_runtime = RealNodeRuntime::new(http.clone()); - languages::init(languages.clone(), node_runtime.clone()); + languages::init(languages.clone(), node_runtime.clone(), cx); language::init(cx); project::Project::init(&client, cx); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b2339f998f292161eedeaf4a5ef5f2365d9bee3f..fb41f7a3498d27b7e60ffd6b6a04a90f461604a3 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -99,6 +99,7 @@ rust-embed.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true +schemars.workspace = true simplelog = "0.9" smallvec.workspace = true smol.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 0b1fa750c084dd44a006cad258f4e7d11fc153f9..75674e78e0b8fa721da7b6c8edee3ce7eaeb006e 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,13 +1,17 @@ use anyhow::Context; +use gpui::AppContext; pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; use util::asset_str; +use self::elixir_next::ElixirSettings; + mod c; mod css; mod elixir; +mod elixir_next; mod go; mod html; mod json; @@ -37,7 +41,13 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init(languages: Arc, node_runtime: Arc) { +pub fn init( + languages: Arc, + node_runtime: Arc, + cx: &mut AppContext, +) { + settings::register::(cx); + let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) }; @@ -61,11 +71,25 @@ pub fn init(languages: Arc, node_runtime: Arc Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); - language( - "elixir", - tree_sitter_elixir::language(), - vec![Arc::new(elixir::ElixirLspAdapter)], - ); + + match settings::get::(cx).next { + elixir_next::ElixirNextSetting::Off => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir::ElixirLspAdapter)], + ), + elixir_next::ElixirNextSetting::On => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir_next::BundledNextLspAdapter)], + ), + elixir_next::ElixirNextSetting::Local { port } => unimplemented!(), /*language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir_next::LocalNextLspAdapter { port })], + )*/ + } + language( "go", tree_sitter_go::language(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c800e4e11097b02101c730b6e47b2263294a38cd..7c69bc2db11da61b12afb728d2b9ab6613f58cb8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -134,7 +134,7 @@ fn main() { let languages = Arc::new(languages); let node_runtime = RealNodeRuntime::new(http.clone()); - languages::init(languages.clone(), node_runtime.clone()); + languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d968a92646c1d3854193e28f196d7f5597268bc8..8a74522df1a40574efb3df2fc47fcc4c87b3c7db 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2392,7 +2392,7 @@ mod tests { languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let node_runtime = node_runtime::FakeNodeRuntime::new(); - languages::init(languages.clone(), node_runtime); + languages::init(languages.clone(), node_runtime, cx); for name in languages.language_names() { languages.language_for_name(&name); } From ea3a1745f56ed819aca7c3b235e123a5bedff0b9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 7 Sep 2023 22:48:01 -0600 Subject: [PATCH 02/89] Add vim-specific interactions to command This mostly adds the commonly requested set (:wq and friends) and a few that I use frequently : to go to a line number :vsp / :sp to create a split :cn / :cp to go to diagnostics --- assets/keymaps/vim.json | 1 + crates/command_palette/src/command_palette.rs | 47 +++- crates/vim/src/command.rs | 219 ++++++++++++++++++ crates/vim/src/vim.rs | 9 + crates/workspace/src/pane.rs | 32 ++- crates/workspace/src/workspace.rs | 103 ++++++-- crates/zed/src/menus.rs | 21 +- 7 files changed, 406 insertions(+), 26 deletions(-) create mode 100644 crates/vim/src/command.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 9e69240d27200c591e110fd5578a85b13fe96668..1a6a752e23f931658f352f86b4d8a201cbedd067 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -18,6 +18,7 @@ } } ], + ":": "command_palette::Toggle", "h": "vim::Left", "left": "vim::Left", "backspace": "vim::Backspace", diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 4f9bb231ce1aaa68c4fa994bf4f85a09db05fb95..06f76bbc5c68a848de11823d6746e693e600c1e5 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]); pub type CommandPalette = Picker; +pub type CommandPaletteInterceptor = + Box Option>; + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} + pub struct CommandPaletteDelegate { actions: Vec, matches: Vec, @@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate { char_bag: command.name.chars().collect(), }) .collect::>(); - let matches = if query.is_empty() { + let mut matches = if query.is_empty() { candidates .into_iter() .enumerate() @@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate { ) .await }; + let intercept_result = cx.read(|cx| { + if cx.has_global::() { + cx.global::()(&query, cx) + } else { + None + } + }); + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| actions[m.candidate_id].action.id() == action.id()) + { + matches.remove(idx); + } + actions.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: actions.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } picker .update(&mut cx, |picker, _| { let delegate = picker.delegate_mut(); @@ -254,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate { } } -fn humanize_action_name(name: &str) -> String { +pub fn humanize_action_name(name: &str) -> String { let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); let mut result = String::with_capacity(capacity); for char in name.chars() { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1f4777ec6b8d8fcf588eec4834eb08b0da76939 --- /dev/null +++ b/crates/vim/src/command.rs @@ -0,0 +1,219 @@ +use command_palette::{humanize_action_name, CommandInterceptResult}; +use gpui::{actions, impl_actions, Action, AppContext, AsyncAppContext, ViewContext}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use workspace::{SaveBehavior, Workspace}; + +use crate::{ + motion::{motion, Motion}, + normal::JoinLines, + Vim, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GoToLine { + pub line: u32, +} + +impl_actions!(vim, [GoToLine]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| { + Vim::update(cx, |vim, cx| { + vim.push_operator(crate::state::Operator::Number(action.line as usize), cx) + }); + motion(Motion::StartOfDocument, cx) + }); +} + +pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option { + while query.starts_with(":") { + query = &query[1..]; + } + + let (name, action) = match query { + // :w + "w" | "wr" | "wri" | "writ" | "write" => ( + "write", + workspace::Save { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "w!" | "wr!" | "wri!" | "writ!" | "write!" => ( + "write", + workspace::Save { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :q + "q" | "qu" | "qui" | "quit" => ( + "quit", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnWrite), + } + .boxed_clone(), + ), + "q!" | "qu!" | "qui!" | "quit!" => ( + "quit!", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::DontSave), + } + .boxed_clone(), + ), + + // :wq + "wq" => ( + "wq", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "wq!" => ( + "wq!", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + // :x + "x" | "xi" | "xit" | "exi" | "exit" => ( + "exit", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "x!" | "xi!" | "xit!" | "exi!" | "exit!" => ( + "xit", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :wa + "wa" | "wal" | "wall" => ( + "wall", + workspace::SaveAll { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "wa!" | "wal!" | "wall!" => ( + "wall!", + workspace::SaveAll { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :qa + "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => ( + "quitall", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::PromptOnWrite), + } + .boxed_clone(), + ), + "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => ( + "quitall!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::DontSave), + } + .boxed_clone(), + ), + + // :cq + "cq" | "cqu" | "cqui" | "cquit" | "cq!" | "cqu!" | "cqui!" | "cquit!" => ( + "cquit!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::DontSave), + } + .boxed_clone(), + ), + + // :xa + "xa" | "xal" | "xall" => ( + "xall", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "xa!" | "xal!" | "xall!" => ( + "zall!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + // :wqa + "wqa" | "wqal" | "wqall" => ( + "wqall", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::PromptOnConflict), + } + .boxed_clone(), + ), + "wqa!" | "wqal!" | "wqall!" => ( + "wqall!", + workspace::CloseAllItemsAndPanes { + save_behavior: Some(SaveBehavior::SilentlyOverwrite), + } + .boxed_clone(), + ), + + "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()), + + "sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()), + "vs" | "vsp" | "vspl" | "vspli" | "vsplit" => { + ("vsplit", workspace::SplitLeft.boxed_clone()) + } + "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()), + "cp" | "cpr" | "cpre" | "cprev" => ("cprev", editor::GoToPrevDiagnostic.boxed_clone()), + + _ => { + if let Ok(line) = query.parse::() { + (query, GoToLine { line }.boxed_clone()) + } else { + return None; + } + } + }; + + let string = ":".to_owned() + name; + let positions = generate_positions(&string, query); + + Some(CommandInterceptResult { + action, + string, + positions, + }) +} + +fn generate_positions(string: &str, query: &str) -> Vec { + let mut positions = Vec::new(); + let mut chars = query.chars().into_iter(); + + let Some(mut current) = chars.next() else { + return positions; + }; + + for (i, c) in string.chars().enumerate() { + if c == current { + positions.push(i); + if let Some(c) = chars.next() { + current = c; + } else { + break; + } + } + } + + positions +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e2fa6e989ab8ca674e322cb1e5fcaebef7a0471b..6ff997a16163234a5c66bd4568ccb803901542a4 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test; +mod command; mod editor_events; mod insert; mod mode_indicator; @@ -13,6 +14,7 @@ mod visual; use anyhow::Result; use collections::{CommandPaletteFilter, HashMap}; +use command_palette::CommandPaletteInterceptor; use editor::{movement, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action, @@ -63,6 +65,7 @@ pub fn init(cx: &mut AppContext) { insert::init(cx); object::init(cx); motion::init(cx); + command::init(cx); // Vim Actions cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| { @@ -469,6 +472,12 @@ impl Vim { } }); + if self.enabled { + cx.set_global::(Box::new(command::command_interceptor)); + } else if cx.has_global::() { + let _ = cx.remove_global::(); + } + cx.update_active_window(|cx| { if self.enabled { let active_editor = cx diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a3e6a547ddfd73c1fd67c50c4b6c4df2d6ff51d1..5275a2664ad2b2b6b9b15cf2b7a8ed988b9e9390 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -78,10 +78,17 @@ pub struct CloseItemsToTheRightById { } #[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] pub struct CloseActiveItem { pub save_behavior: Option, } +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItems { + pub save_behavior: Option, +} + actions!( pane, [ @@ -92,7 +99,6 @@ actions!( CloseCleanItems, CloseItemsToTheLeft, CloseItemsToTheRight, - CloseAllItems, GoBack, GoForward, ReopenClosedItem, @@ -103,7 +109,7 @@ actions!( ] ); -impl_actions!(pane, [ActivateItem, CloseActiveItem]); +impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -829,14 +835,18 @@ impl Pane { pub fn close_all_items( &mut self, - _: &CloseAllItems, + action: &CloseAllItems, cx: &mut ViewContext, ) -> Option>> { if self.items.is_empty() { return None; } - Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true)) + Some(self.close_items( + cx, + action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + |_| true, + )) } pub fn close_items( @@ -1175,7 +1185,12 @@ impl Pane { ContextMenuItem::action("Close Clean Items", CloseCleanItems), ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft), ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight), - ContextMenuItem::action("Close All Items", CloseAllItems), + ContextMenuItem::action( + "Close All Items", + CloseAllItems { + save_behavior: None, + }, + ), ] } else { // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command. @@ -1219,7 +1234,12 @@ impl Pane { } } }), - ContextMenuItem::action("Close All Items", CloseAllItems), + ContextMenuItem::action( + "Close All Items", + CloseAllItems { + save_behavior: None, + }, + ), ] }, cx, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index feab53d0941d1823fc79930d3697c39e54822207..c297962684b803f8f2edc6a9abbdc0312455b332 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -122,13 +122,11 @@ actions!( Open, NewFile, NewWindow, - CloseWindow, CloseInactiveTabsAndPanes, AddFolderToProject, Unfollow, - Save, SaveAs, - SaveAll, + ReloadActiveItem, ActivatePreviousPane, ActivateNextPane, FollowNextCollaborator, @@ -158,6 +156,30 @@ pub struct ActivatePane(pub usize); #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePaneInDirection(pub SplitDirection); +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SaveAll { + pub save_behavior: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Save { + pub save_behavior: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseWindow { + pub save_behavior: Option, +} + +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItemsAndPanes { + pub save_behavior: Option, +} + #[derive(Deserialize)] pub struct Toast { id: usize, @@ -210,7 +232,16 @@ pub struct OpenTerminal { impl_actions!( workspace, - [ActivatePane, ActivatePaneInDirection, Toast, OpenTerminal] + [ + ActivatePane, + ActivatePaneInDirection, + Toast, + OpenTerminal, + SaveAll, + Save, + CloseWindow, + CloseAllItemsAndPanes, + ] ); pub type WorkspaceId = i64; @@ -251,6 +282,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_async_action(Workspace::follow_next_collaborator); cx.add_async_action(Workspace::close); cx.add_async_action(Workspace::close_inactive_items_and_panes); + cx.add_async_action(Workspace::close_all_items_and_panes); cx.add_global_action(Workspace::close_global); cx.add_global_action(restart); cx.add_async_action(Workspace::save_all); @@ -1262,11 +1294,15 @@ impl Workspace { pub fn close( &mut self, - _: &CloseWindow, + action: &CloseWindow, cx: &mut ViewContext, ) -> Option>> { let window = cx.window(); - let prepare = self.prepare_to_close(false, cx); + let prepare = self.prepare_to_close( + false, + action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + cx, + ); Some(cx.spawn(|_, mut cx| async move { if prepare.await? { window.remove(&mut cx); @@ -1323,8 +1359,17 @@ impl Workspace { }) } - fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext) -> Option>> { - let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx); + fn save_all( + &mut self, + action: &SaveAll, + cx: &mut ViewContext, + ) -> Option>> { + let save_all = self.save_all_internal( + action + .save_behavior + .unwrap_or(SaveBehavior::PromptOnConflict), + cx, + ); Some(cx.foreground().spawn(async move { save_all.await?; Ok(()) @@ -1691,24 +1736,52 @@ impl Workspace { &mut self, _: &CloseInactiveTabsAndPanes, cx: &mut ViewContext, + ) -> Option>> { + self.close_all_internal(true, SaveBehavior::PromptOnWrite, cx) + } + + pub fn close_all_items_and_panes( + &mut self, + action: &CloseAllItemsAndPanes, + cx: &mut ViewContext, + ) -> Option>> { + self.close_all_internal( + false, + action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + cx, + ) + } + + fn close_all_internal( + &mut self, + retain_active_pane: bool, + save_behavior: SaveBehavior, + cx: &mut ViewContext, ) -> Option>> { let current_pane = self.active_pane(); let mut tasks = Vec::new(); - if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) - }) { - tasks.push(current_pane_close); - }; + if retain_active_pane { + if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) { + tasks.push(current_pane_close); + }; + } for pane in self.panes() { - if pane.id() == current_pane.id() { + if retain_active_pane && pane.id() == current_pane.id() { continue; } if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { - pane.close_all_items(&CloseAllItems, cx) + pane.close_all_items( + &CloseAllItems { + save_behavior: Some(save_behavior), + }, + cx, + ) }) { tasks.push(close_pane_items) } diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 6b5f7b3a35868ffc20e9a26bc6b694bbe2a9ecba..da3f7e4c32e1fbc535dcb071e9a2fa55df1b4ae6 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -38,16 +38,31 @@ pub fn menus() -> Vec> { MenuItem::action("Open Recent...", recent_projects::OpenRecent), MenuItem::separator(), MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), - MenuItem::action("Save", workspace::Save), + MenuItem::action( + "Save", + workspace::Save { + save_behavior: None, + }, + ), MenuItem::action("Save As…", workspace::SaveAs), - MenuItem::action("Save All", workspace::SaveAll), + MenuItem::action( + "Save All", + workspace::SaveAll { + save_behavior: None, + }, + ), MenuItem::action( "Close Editor", workspace::CloseActiveItem { save_behavior: None, }, ), - MenuItem::action("Close Window", workspace::CloseWindow), + MenuItem::action( + "Close Window", + workspace::CloseWindow { + save_behavior: None, + }, + ), ], }, Menu { From ba5d84f7e82433b427e53d566a28173a391c8a23 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Sep 2023 21:27:22 -0600 Subject: [PATCH 03/89] Fix vim tests on my machine In a rare case of "it broke on my machine" I haven't been able to run the vim tests locally for a few days; turns out I ran out of swap file names... --- crates/vim/src/test/neovim_connection.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index e44e8d0e4cb67dd13a7906767e62a72379b5aeec..38af2d1555e3af892a732e8d9b5ced60638313da 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -65,7 +65,13 @@ impl NeovimConnection { // Ensure we don't create neovim connections in parallel let _lock = NEOVIM_LOCK.lock(); let (nvim, join_handle, child) = new_child_cmd( - &mut Command::new("nvim").arg("--embed").arg("--clean"), + &mut Command::new("nvim") + .arg("--embed") + .arg("--clean") + // disable swap (otherwise after about 1000 test runs you run out of swap file names) + .arg("-n") + // disable writing files (just in case) + .arg("-m"), handler, ) .await From e27b7d78120b8dcdc2cc0982b4c7e60baa0acb85 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Sep 2023 22:20:20 -0600 Subject: [PATCH 04/89] Ensure the picker waits for pending updates Particularly in development builds (and in tests), when typing in the command palette, I tend to hit enter before the suggestions have settled. --- crates/picker/src/picker.rs | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 700b69117ae07767a449db4b2d856974a6f0b5de..89bfaa4b707c677f269558944bab6b8f008326c7 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -25,7 +25,8 @@ pub struct Picker { max_size: Vector2F, theme: Arc theme::Picker>>>, confirmed: bool, - pending_update_matches: Task>, + pending_update_matches: Option>>, + confirm_on_update: Option, has_focus: bool, } @@ -208,7 +209,8 @@ impl Picker { max_size: vec2f(540., 420.), theme, confirmed: false, - pending_update_matches: Task::ready(None), + pending_update_matches: None, + confirm_on_update: None, has_focus: false, }; this.update_matches(String::new(), cx); @@ -263,11 +265,13 @@ impl Picker { pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { let update = self.delegate.update_matches(query, cx); self.matches_updated(cx); - self.pending_update_matches = cx.spawn(|this, mut cx| async move { + self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move { update.await; - this.update(&mut cx, |this, cx| this.matches_updated(cx)) - .log_err() - }); + this.update(&mut cx, |this, cx| { + this.matches_updated(cx); + }) + .log_err() + })); } fn matches_updated(&mut self, cx: &mut ViewContext) { @@ -278,6 +282,11 @@ impl Picker { ScrollTarget::Show(index) }; self.list_state.scroll_to(target); + self.pending_update_matches = None; + if let Some(secondary) = self.confirm_on_update.take() { + self.confirmed = true; + self.delegate.confirm(secondary, cx) + } cx.notify(); } @@ -331,13 +340,21 @@ impl Picker { } pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - self.confirmed = true; - self.delegate.confirm(false, cx); + if self.pending_update_matches.is_some() { + self.confirm_on_update = Some(false) + } else { + self.confirmed = true; + self.delegate.confirm(false, cx); + } } pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext) { - self.confirmed = true; - self.delegate.confirm(true, cx); + if self.pending_update_matches.is_some() { + self.confirm_on_update = Some(true) + } else { + self.confirmed = true; + self.delegate.confirm(true, cx); + } } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { From a4f96e64528ecc88840a864fbd45ccee2bcefc85 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Sep 2023 22:36:13 -0600 Subject: [PATCH 05/89] tests: wait deterministically after simulating_keystrokes --- crates/editor/src/test/editor_test_context.rs | 15 +++++++++++++-- crates/vim/src/test.rs | 3 --- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 0bae32f1f7087b05b67f166554671ca6359a6104..0ef54dc3d557843d79d1e457d1651f7bf84e1af1 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -3,8 +3,8 @@ use crate::{ }; use futures::Future; use gpui::{ - keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext, - ViewContext, ViewHandle, + executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, + ModelContext, ViewContext, ViewHandle, }; use indoc::indoc; use language::{Buffer, BufferSnapshot}; @@ -114,6 +114,7 @@ impl<'a> EditorTestContext<'a> { let keystroke = Keystroke::parse(keystroke_text).unwrap(); self.cx.dispatch_keystroke(self.window, keystroke, false); + keystroke_under_test_handle } @@ -126,6 +127,16 @@ impl<'a> EditorTestContext<'a> { for keystroke_text in keystroke_texts.into_iter() { self.simulate_keystroke(keystroke_text); } + // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete + // before returning. + // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too + // quickly races with async actions. + if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() { + executor.run_until_parked(); + } else { + unreachable!(); + } + keystrokes_under_test_handle } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index e43b0ab22bc237146b1edecb1afbd4b45b4a4c95..82e4cc68630b5f4d5fb197f937192439801ece35 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -186,9 +186,6 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { assert_eq!(bar.query(cx), "cc"); }); - // wait for the query editor change event to fire. - search_bar.next_notification(&cx).await; - cx.update_editor(|editor, cx| { let highlights = editor.all_text_background_highlights(cx); assert_eq!(3, highlights.len()); From 88a32ae48d81dad90c5b30515ce2e8b595c29cb4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Sep 2023 23:48:21 -0600 Subject: [PATCH 06/89] Merge Workspace::save_item into Pane::save_item These methods were slightly different which caused (for example) there to be no "Discard" option in the conflict case at the workspace level. To make this work, a new SaveBehavior (::PromptForNewPath) was added to support SaveAs. --- crates/workspace/src/pane.rs | 31 ++++++++--- crates/workspace/src/workspace.rs | 91 ++++++++++++------------------- 2 files changed, 58 insertions(+), 64 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5275a2664ad2b2b6b9b15cf2b7a8ed988b9e9390..285cf1da603c64977c6942131105c0fe2462b263 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -46,13 +46,15 @@ use theme::{Theme, ThemeSettings}; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub enum SaveBehavior { - /// ask before overwriting conflicting files (used by default with %s) + /// ask before overwriting conflicting files (used by default with cmd-s) PromptOnConflict, - /// ask before writing any file that wouldn't be auto-saved (used by default with %w) + /// ask for a new path before writing (used with cmd-shift-s) + PromptForNewPath, + /// ask before writing any file that wouldn't be auto-saved (used by default with cmd-w) PromptOnWrite, /// never prompt, write on conflict (used with vim's :w!) SilentlyOverwrite, - /// skip all save-related behaviour (used with vim's :cq) + /// skip all save-related behaviour (used with vim's :q!) DontSave, } @@ -1019,7 +1021,7 @@ impl Pane { return Ok(true); } - let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| { + let (mut has_conflict, mut is_dirty, mut can_save, is_singleton) = cx.read(|cx| { ( item.has_conflict(cx), item.is_dirty(cx), @@ -1028,6 +1030,12 @@ impl Pane { ) }); + if save_behavior == SaveBehavior::PromptForNewPath { + has_conflict = false; + is_dirty = true; + can_save = false; + } + if has_conflict && can_save { if save_behavior == SaveBehavior::SilentlyOverwrite { pane.update(cx, |_, cx| item.save(project, cx))?.await?; @@ -2589,10 +2597,17 @@ mod tests { add_labeled_item(&pane, "C", false, cx); assert_item_labels(&pane, ["A", "B", "C*"], cx); - pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) - .unwrap() - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_all_items( + &CloseAllItems { + save_behavior: None, + }, + cx, + ) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, [], cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c297962684b803f8f2edc6a9abbdc0312455b332..a8e7f12b3a1d3d84c2a50f22ab823fa2341cb947 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -122,6 +122,7 @@ actions!( Open, NewFile, NewWindow, + CloseWindow, CloseInactiveTabsAndPanes, AddFolderToProject, Unfollow, @@ -168,12 +169,6 @@ pub struct Save { pub save_behavior: Option, } -#[derive(Clone, PartialEq, Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct CloseWindow { - pub save_behavior: Option, -} - #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseAllItemsAndPanes { @@ -236,10 +231,9 @@ impl_actions!( ActivatePane, ActivatePaneInDirection, Toast, - OpenTerminal, + OpenTerminal, SaveAll, Save, - CloseWindow, CloseAllItemsAndPanes, ] ); @@ -294,13 +288,22 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { }, ); cx.add_action( - |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { - workspace.save_active_item(false, cx).detach_and_log_err(cx); + |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { + workspace + .save_active_item( + action + .save_behavior + .unwrap_or(SaveBehavior::PromptOnConflict), + cx, + ) + .detach_and_log_err(cx); }, ); cx.add_action( |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { - workspace.save_active_item(true, cx).detach_and_log_err(cx); + workspace + .save_active_item(SaveBehavior::PromptForNewPath, cx) + .detach_and_log_err(cx); }, ); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { @@ -1294,15 +1297,11 @@ impl Workspace { pub fn close( &mut self, - action: &CloseWindow, + _: &CloseWindow, cx: &mut ViewContext, ) -> Option>> { let window = cx.window(); - let prepare = self.prepare_to_close( - false, - action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), - cx, - ); + let prepare = self.prepare_to_close(false, cx); Some(cx.spawn(|_, mut cx| async move { if prepare.await? { window.remove(&mut cx); @@ -1685,51 +1684,31 @@ impl Workspace { pub fn save_active_item( &mut self, - force_name_change: bool, + save_behavior: SaveBehavior, cx: &mut ViewContext, ) -> Task> { let project = self.project.clone(); - if let Some(item) = self.active_item(cx) { - if !force_name_change && item.can_save(cx) { - if item.has_conflict(cx) { - const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + let pane = self.active_pane(); + let item_ix = pane.read(cx).active_item_index(); + let item = pane.read(cx).active_item(); + let pane = pane.downgrade(); - let mut answer = cx.prompt( - PromptLevel::Warning, - CONFLICT_MESSAGE, - &["Overwrite", "Cancel"], - ); - cx.spawn(|this, mut cx| async move { - let answer = answer.recv().await; - if answer == Some(0) { - this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))? - .await?; - } - Ok(()) - }) - } else { - item.save(self.project.clone(), cx) - } - } else if item.is_singleton(cx) { - let worktree = self.worktrees(cx).next(); - let start_abs_path = worktree - .and_then(|w| w.read(cx).as_local()) - .map_or(Path::new(""), |w| w.abs_path()) - .to_path_buf(); - let mut abs_path = cx.prompt_for_new_path(&start_abs_path); - cx.spawn(|this, mut cx| async move { - if let Some(abs_path) = abs_path.recv().await.flatten() { - this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))? - .await?; - } - Ok(()) - }) + cx.spawn(|_, mut cx| async move { + if let Some(item) = item { + Pane::save_item( + project, + &pane, + item_ix, + item.as_ref(), + save_behavior, + &mut cx, + ) + .await + .map(|_| ()) } else { - Task::ready(Ok(())) + Ok(()) } - } else { - Task::ready(Ok(())) - } + }) } pub fn close_inactive_items_and_panes( From 6ad1f19a214f212b391c12c9ae6b1ba5291fbb5d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 15:00:41 -0600 Subject: [PATCH 07/89] Add NewFileInDirection --- assets/keymaps/vim.json | 12 +++++++++++- crates/editor/src/editor.rs | 25 +++++++++++++++++++++++-- crates/workspace/src/workspace.rs | 15 ++++++++++++--- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1a6a752e23f931658f352f86b4d8a201cbedd067..5db0fe748ef6885f697ec71c321814c44e1a9fcb 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -319,7 +319,17 @@ "ctrl-w c": "pane::CloseAllItems", "ctrl-w ctrl-c": "pane::CloseAllItems", "ctrl-w q": "pane::CloseAllItems", - "ctrl-w ctrl-q": "pane::CloseAllItems" + "ctrl-w ctrl-q": "pane::CloseAllItems", + "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w n": [ + "workspace::NewFileInDirection", + "Up" + ], + "ctrl-w ctrl-n": [ + "workspace::NewFileInDirection", + "Up" + ] } }, { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0827e1326402e39bfeeab389ad7a9df6d8eb5587..446ddfeab009d51055b5e4aa7e588211b3640f8d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -103,7 +103,7 @@ use sum_tree::TreeMap; use text::Rope; use theme::{DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; -use workspace::{ItemNavHistory, ViewId, Workspace}; +use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace}; use crate::git::diff_hunk_to_display; @@ -363,6 +363,7 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) { init_settings(cx); cx.add_action(Editor::new_file); + cx.add_action(Editor::new_file_in_direction); cx.add_action(Editor::cancel); cx.add_action(Editor::newline); cx.add_action(Editor::newline_above); @@ -1627,6 +1628,26 @@ impl Editor { } } + pub fn new_file_in_direction( + workspace: &mut Workspace, + action: &workspace::NewFileInDirection, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + if project.read(cx).is_remote() { + cx.propagate_action(); + } else if let Some(buffer) = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .log_err() + { + workspace.split_item( + action.0, + Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), + cx, + ); + } + } + pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { self.buffer.read(cx).replica_id() } @@ -7130,7 +7151,7 @@ impl Editor { ); }); if split { - workspace.split_item(Box::new(editor), cx); + workspace.split_item(SplitDirection::Right, Box::new(editor), cx); } else { workspace.add_item(Box::new(editor), cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a8e7f12b3a1d3d84c2a50f22ab823fa2341cb947..6043b946214257642082194043395a75d7417138 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -157,6 +157,9 @@ pub struct ActivatePane(pub usize); #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePaneInDirection(pub SplitDirection); +#[derive(Clone, Deserialize, PartialEq)] +pub struct NewFileInDirection(pub SplitDirection); + #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveAll { @@ -230,6 +233,7 @@ impl_actions!( [ ActivatePane, ActivatePaneInDirection, + NewFileInDirection, Toast, OpenTerminal, SaveAll, @@ -1991,8 +1995,13 @@ impl Workspace { .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); } - pub fn split_item(&mut self, item: Box, cx: &mut ViewContext) { - let new_pane = self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx); + pub fn split_item( + &mut self, + split_direction: SplitDirection, + item: Box, + cx: &mut ViewContext, + ) { + let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx); new_pane.update(cx, move |new_pane, cx| { new_pane.add_item(item, true, true, None, cx) }) @@ -2170,7 +2179,7 @@ impl Workspace { } let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); - self.split_item(Box::new(item.clone()), cx); + self.split_item(SplitDirection::Right, Box::new(item.clone()), cx); item } From 2d9db0fed15449a3390aabf70ffc0fd3f98ed8a6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 15:24:31 -0600 Subject: [PATCH 08/89] Flesh out v1.0 of vim : --- Cargo.lock | 2 + crates/command_palette/src/command_palette.rs | 2 +- crates/fs/src/fs.rs | 2 +- crates/gpui/src/app/test_app_context.rs | 2 +- crates/search/src/buffer_search.rs | 32 ++- crates/vim/Cargo.toml | 2 + crates/vim/src/command.rs | 258 +++++++++++++++--- crates/vim/src/normal.rs | 9 +- crates/vim/src/normal/search.rs | 196 ++++++++++++- .../neovim_backed_binding_test_context.rs | 17 +- .../src/test/neovim_backed_test_context.rs | 16 +- crates/vim/test_data/test_command_basics.json | 6 + crates/vim/test_data/test_command_goto.json | 5 + .../vim/test_data/test_command_replace.json | 22 ++ crates/zed/src/menus.rs | 7 +- crates/zed/src/zed.rs | 20 +- 16 files changed, 516 insertions(+), 82 deletions(-) create mode 100644 crates/vim/test_data/test_command_basics.json create mode 100644 crates/vim/test_data/test_command_goto.json create mode 100644 crates/vim/test_data/test_command_replace.json diff --git a/Cargo.lock b/Cargo.lock index 3cced78c4272e5fc9f571d8023719a1ea56d06d2..bfae8cae0b08a8a5cc446f21fe54dd639ae75d32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8860,6 +8860,7 @@ dependencies = [ "async-trait", "collections", "command_palette", + "diagnostics", "editor", "futures 0.3.28", "gpui", @@ -8881,6 +8882,7 @@ dependencies = [ "tokio", "util", "workspace", + "zed-actions", ] [[package]] diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 06f76bbc5c68a848de11823d6746e693e600c1e5..90c155cdf8caba22d945d3b982cd4ec88b339828 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -126,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate { } }) .collect::>(); - let actions = cx.read(move |cx| { + let mut actions = cx.read(move |cx| { let hit_counts = cx.optional_global::(); actions.sort_by_key(|action| { ( diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index ecaee4534e4b4d4ae11391c688d44caa1bdd0fa0..97175cb55e7f1c8edb494857d1e28ad16d4ee6d1 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -507,7 +507,7 @@ impl FakeFs { state.emit_event(&[path]); } - fn write_file_internal(&self, path: impl AsRef, content: String) -> Result<()> { + pub fn write_file_internal(&self, path: impl AsRef, content: String) -> Result<()> { let mut state = self.state.lock(); let path = path.as_ref(); let inode = state.next_inode; diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 0dc1d1eba437fb67beddbde47bd10bc497ed928a..5b15b5274c60bb835c22e58882f6d7eb95910151 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -33,7 +33,7 @@ use super::{ #[derive(Clone)] pub struct TestAppContext { - cx: Rc>, + pub cx: Rc>, foreground_platform: Rc, condition_duration: Option, pub function_name: String, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 142b548a9341cf459e12eb3072ac57c75183e9ba..97f9d7a7c8f44f2d78e3b362d5f2878c3002a492 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -539,6 +539,23 @@ impl BufferSearchBar { .map(|searchable_item| searchable_item.query_suggestion(cx)) } + pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { + if replacement.is_none() { + self.replace_is_active = false; + return; + } + self.replace_is_active = true; + self.replacement_editor + .update(cx, |replacement_editor, cx| { + replacement_editor + .buffer() + .update(cx, |replacement_buffer, cx| { + let len = replacement_buffer.len(cx); + replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx); + }); + }); + } + pub fn search( &mut self, query: &str, @@ -679,6 +696,19 @@ impl BufferSearchBar { } } + pub fn select_last_match(&mut self, cx: &mut ViewContext) { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let new_match_index = matches.len() - 1; + searchable_item.update_matches(matches, cx); + searchable_item.activate_match(new_match_index, matches, cx); + } + } + } + fn select_next_match_on_pane( pane: &mut Pane, action: &SelectNextMatch, @@ -934,7 +964,7 @@ impl BufferSearchBar { } } } - fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { if !self.dismissed && self.active_search.is_some() { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(query) = self.active_search.as_ref() { diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 5d40032024b5f78758e25d6ba0a6e865c827cf5b..509efc58257ce8499d5b27d8e63408ea189d5da6 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -34,6 +34,8 @@ settings = { path = "../settings" } workspace = { path = "../workspace" } theme = { path = "../theme" } language_selector = { path = "../language_selector"} +diagnostics = { path = "../diagnostics" } +zed-actions = { path = "../zed-actions" } [dev-dependencies] indoc.workspace = true diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index a1f4777ec6b8d8fcf588eec4834eb08b0da76939..10c2599dd9af76d98792898e33a97af1695bf4ac 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1,16 +1,21 @@ -use command_palette::{humanize_action_name, CommandInterceptResult}; -use gpui::{actions, impl_actions, Action, AppContext, AsyncAppContext, ViewContext}; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; +use command_palette::CommandInterceptResult; +use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive}; +use gpui::{impl_actions, Action, AppContext}; +use serde_derive::Deserialize; use workspace::{SaveBehavior, Workspace}; use crate::{ - motion::{motion, Motion}, - normal::JoinLines, + motion::{EndOfDocument, Motion}, + normal::{ + move_cursor, + search::{FindCommand, ReplaceCommand}, + JoinLines, + }, + state::Mode, Vim, }; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct GoToLine { pub line: u32, } @@ -20,19 +25,28 @@ impl_actions!(vim, [GoToLine]); pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| { Vim::update(cx, |vim, cx| { - vim.push_operator(crate::state::Operator::Number(action.line as usize), cx) + vim.switch_mode(Mode::Normal, false, cx); + move_cursor(vim, Motion::StartOfDocument, Some(action.line as usize), cx); }); - motion(Motion::StartOfDocument, cx) }); } pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option { + // Note: this is a very poor simulation of vim's command palette. + // In the future we should adjust it to handle parsing range syntax, + // and then calling the appropriate commands with/without ranges. + // + // We also need to support passing arguments to commands like :w + // (ideally with filename autocompletion). + // + // For now, you can only do a replace on the % range, and you can + // only use a specific line number range to "go to line" while query.starts_with(":") { query = &query[1..]; } let (name, action) = match query { - // :w + // save and quit "w" | "wr" | "wri" | "writ" | "write" => ( "write", workspace::Save { @@ -41,14 +55,12 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( - "write", + "write!", workspace::Save { save_behavior: Some(SaveBehavior::SilentlyOverwrite), } .boxed_clone(), ), - - // :q "q" | "qu" | "qui" | "quit" => ( "quit", workspace::CloseActiveItem { @@ -63,8 +75,6 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "wq", workspace::CloseActiveItem { @@ -79,7 +89,6 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "exit", workspace::CloseActiveItem { @@ -88,14 +97,12 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( - "xit", + "exit!", workspace::CloseActiveItem { save_behavior: Some(SaveBehavior::SilentlyOverwrite), } .boxed_clone(), ), - - // :wa "wa" | "wal" | "wall" => ( "wall", workspace::SaveAll { @@ -110,8 +117,6 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "quitall", workspace::CloseAllItemsAndPanes { @@ -126,17 +131,6 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( - "cquit!", - workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::DontSave), - } - .boxed_clone(), - ), - - // :xa "xa" | "xal" | "xall" => ( "xall", workspace::CloseAllItemsAndPanes { @@ -145,14 +139,12 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( - "zall!", + "xall!", workspace::CloseAllItemsAndPanes { save_behavior: Some(SaveBehavior::SilentlyOverwrite), } .boxed_clone(), ), - - // :wqa "wqa" | "wqal" | "wqall" => ( "wqall", workspace::CloseAllItemsAndPanes { @@ -167,18 +159,89 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option { + ("cquit!", zed_actions::Quit.boxed_clone()) + } - "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()), - + // pane management "sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()), "vs" | "vsp" | "vspl" | "vspli" | "vsplit" => { ("vsplit", workspace::SplitLeft.boxed_clone()) } + "new" => ( + "new", + workspace::NewFileInDirection(workspace::SplitDirection::Up).boxed_clone(), + ), + "vne" | "vnew" => ( + "vnew", + workspace::NewFileInDirection(workspace::SplitDirection::Left).boxed_clone(), + ), + "tabe" | "tabed" | "tabedi" | "tabedit" => ("tabedit", workspace::NewFile.boxed_clone()), + "tabnew" => ("tabnew", workspace::NewFile.boxed_clone()), + + "tabn" | "tabne" | "tabnex" | "tabnext" => { + ("tabnext", workspace::ActivateNextItem.boxed_clone()) + } + "tabp" | "tabpr" | "tabpre" | "tabprev" | "tabprevi" | "tabprevio" | "tabpreviou" + | "tabprevious" => ("tabprevious", workspace::ActivatePrevItem.boxed_clone()), + "tabN" | "tabNe" | "tabNex" | "tabNext" => { + ("tabNext", workspace::ActivatePrevItem.boxed_clone()) + } + "tabc" | "tabcl" | "tabclo" | "tabclos" | "tabclose" => ( + "tabclose", + workspace::CloseActiveItem { + save_behavior: Some(SaveBehavior::PromptOnWrite), + } + .boxed_clone(), + ), + + // quickfix / loclist (merged together for now) + "cl" | "cli" | "clis" | "clist" => ("clist", diagnostics::Deploy.boxed_clone()), + "cc" => ("cc", editor::Hover.boxed_clone()), + "ll" => ("ll", editor::Hover.boxed_clone()), "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()), - "cp" | "cpr" | "cpre" | "cprev" => ("cprev", editor::GoToPrevDiagnostic.boxed_clone()), + "lne" | "lnex" | "lnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()), + + "cpr" | "cpre" | "cprev" | "cprevi" | "cprevio" | "cpreviou" | "cprevious" => { + ("cprevious", editor::GoToPrevDiagnostic.boxed_clone()) + } + "cN" | "cNe" | "cNex" | "cNext" => ("cNext", editor::GoToPrevDiagnostic.boxed_clone()), + "lp" | "lpr" | "lpre" | "lprev" | "lprevi" | "lprevio" | "lpreviou" | "lprevious" => { + ("lprevious", editor::GoToPrevDiagnostic.boxed_clone()) + } + "lN" | "lNe" | "lNex" | "lNext" => ("lNext", editor::GoToPrevDiagnostic.boxed_clone()), + + // modify the buffer (should accept [range]) + "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()), + "d" | "de" | "del" | "dele" | "delet" | "delete" | "dl" | "dell" | "delel" | "deletl" + | "deletel" | "dp" | "dep" | "delp" | "delep" | "deletp" | "deletep" => { + ("delete", editor::DeleteLine.boxed_clone()) + } + "sor" | "sor " | "sort" | "sort " => ("sort", SortLinesCaseSensitive.boxed_clone()), + "sor i" | "sort i" => ("sort i", SortLinesCaseInsensitive.boxed_clone()), + + // goto (other ranges handled under _ => ) + "$" => ("$", EndOfDocument.boxed_clone()), _ => { - if let Ok(line) = query.parse::() { + if query.starts_with("/") || query.starts_with("?") { + ( + query, + FindCommand { + query: query[1..].to_string(), + backwards: query.starts_with("?"), + } + .boxed_clone(), + ) + } else if query.starts_with("%") { + ( + query, + ReplaceCommand { + query: query.to_string(), + } + .boxed_clone(), + ) + } else if let Ok(line) = query.parse::() { (query, GoToLine { line }.boxed_clone()) } else { return None; @@ -217,3 +280,120 @@ fn generate_positions(string: &str, query: &str) -> Vec { positions } + +#[cfg(test)] +mod test { + use std::path::Path; + + use crate::test::{NeovimBackedTestContext, VimTestContext}; + use gpui::{executor::Foreground, TestAppContext}; + use indoc::indoc; + + #[gpui::test] + async fn test_command_basics(cx: &mut TestAppContext) { + if let Foreground::Deterministic { cx_id: _, executor } = cx.foreground().as_ref() { + executor.run_until_parked(); + } + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇa + b + c"}) + .await; + + cx.simulate_shared_keystrokes([":", "j", "enter"]).await; + + // hack: our cursor positionining after a join command is wrong + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { + "ˇa b + c" + }) + .await; + } + + #[gpui::test] + async fn test_command_goto(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇa + b + c"}) + .await; + cx.simulate_shared_keystrokes([":", "3", "enter"]).await; + cx.assert_shared_state(indoc! {" + a + b + ˇc"}) + .await; + } + + #[gpui::test] + async fn test_command_replace(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇa + b + c"}) + .await; + cx.simulate_shared_keystrokes([":", "%", "s", "/", "b", "/", "d", "enter"]) + .await; + cx.assert_shared_state(indoc! {" + a + ˇd + c"}) + .await; + cx.simulate_shared_keystrokes([ + ":", "%", "s", ":", ".", ":", "\\", "0", "\\", "0", "enter", + ]) + .await; + cx.assert_shared_state(indoc! {" + aa + dd + ˇcc"}) + .await; + } + + #[gpui::test] + async fn test_command_write(cx: &mut TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + let path = Path::new("/root/dir/file.rs"); + let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone()); + + cx.simulate_keystrokes(["i", "@", "escape"]); + cx.simulate_keystrokes([":", "w", "enter"]); + + assert_eq!(fs.load(&path).await.unwrap(), "@\n"); + + fs.as_fake() + .write_file_internal(path, "oops\n".to_string()) + .unwrap(); + + // conflict! + cx.simulate_keystrokes(["i", "@", "escape"]); + cx.simulate_keystrokes([":", "w", "enter"]); + let window = cx.window; + assert!(window.has_pending_prompt(cx.cx)); + // "Cancel" + window.simulate_prompt_answer(0, cx.cx); + assert_eq!(fs.load(&path).await.unwrap(), "oops\n"); + assert!(!window.has_pending_prompt(cx.cx)); + // force overwrite + cx.simulate_keystrokes([":", "w", "!", "enter"]); + assert!(!window.has_pending_prompt(cx.cx)); + assert_eq!(fs.load(&path).await.unwrap(), "@@\n"); + } + + #[gpui::test] + async fn test_command_quit(cx: &mut TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.simulate_keystrokes([":", "n", "e", "w", "enter"]); + cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2)); + cx.simulate_keystrokes([":", "q", "enter"]); + cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1)); + } +} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index c8d12f8ee33047c2148710b79360735d8ae9c114..a23091c7a7a6433a49d715d498118a47d9e0ec99 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -4,7 +4,7 @@ mod delete; mod paste; pub(crate) mod repeat; mod scroll; -mod search; +pub(crate) mod search; pub mod substitute; mod yank; @@ -168,7 +168,12 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) { }) } -fn move_cursor(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { +pub(crate) fn move_cursor( + vim: &mut Vim, + motion: Motion, + times: Option, + cx: &mut WindowContext, +) { vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index c9c04007d1a4c0237047bb24f686668d40002290..a9e5e66b9e1bef7d31a764067bf9e37e13823756 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,9 +1,9 @@ use gpui::{actions, impl_actions, AppContext, ViewContext}; use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions}; use serde_derive::Deserialize; -use workspace::{searchable::Direction, Pane, Workspace}; +use workspace::{searchable::Direction, Pane, Toast, Workspace}; -use crate::{state::SearchState, Vim}; +use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim}; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -25,7 +25,29 @@ pub(crate) struct Search { backwards: bool, } -impl_actions!(vim, [MoveToNext, MoveToPrev, Search]); +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct FindCommand { + pub query: String, + pub backwards: bool, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct ReplaceCommand { + pub query: String, +} + +#[derive(Debug)] +struct Replacement { + search: String, + replacement: String, + should_replace_all: bool, + is_case_sensitive: bool, +} + +impl_actions!( + vim, + [MoveToNext, MoveToPrev, Search, FindCommand, ReplaceCommand] +); actions!(vim, [SearchSubmit]); pub(crate) fn init(cx: &mut AppContext) { @@ -34,6 +56,9 @@ pub(crate) fn init(cx: &mut AppContext) { cx.add_action(search); cx.add_action(search_submit); cx.add_action(search_deploy); + + cx.add_action(find_command); + cx.add_action(replace_command); } fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext) { @@ -65,6 +90,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + let search = search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return None; + } + let mut query = action.query.clone(); + if query == "" { + query = search_bar.query(cx); + }; + + search_bar.activate_search_mode(SearchMode::Regex, cx); + Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx)) + }); + let Some(search) = search else { return }; + let search_bar = search_bar.downgrade(); + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(Direction::Next, 1, cx) + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + }) +} + +fn replace_command( + workspace: &mut Workspace, + action: &ReplaceCommand, + cx: &mut ViewContext, +) { + let replacement = match parse_replace_all(&action.query) { + Ok(replacement) => replacement, + Err(message) => { + cx.handle().update(cx, |workspace, cx| { + workspace.show_toast(Toast::new(1544, message), cx) + }); + return; + } + }; + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + let search = search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return None; + } + + let mut options = SearchOptions::default(); + if replacement.is_case_sensitive { + options.set(SearchOptions::CASE_SENSITIVE, true) + } + let search = if replacement.search == "" { + search_bar.query(cx) + } else { + replacement.search + }; + + search_bar.set_replacement(Some(&replacement.replacement), cx); + search_bar.activate_search_mode(SearchMode::Regex, cx); + Some(search_bar.search(&search, Some(options), cx)) + }); + let Some(search) = search else { return }; + let search_bar = search_bar.downgrade(); + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + if replacement.should_replace_all { + search_bar.select_last_match(cx); + search_bar.replace_all(&Default::default(), cx); + Vim::update(cx, |vim, cx| { + move_cursor( + vim, + Motion::StartOfLine { + display_lines: false, + }, + None, + cx, + ) + }) + } + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + }) +} + +fn parse_replace_all(query: &str) -> Result { + let mut chars = query.chars(); + if Some('%') != chars.next() || Some('s') != chars.next() { + return Err("unsupported pattern".to_string()); + } + + let Some(delimeter) = chars.next() else { + return Err("unsupported pattern".to_string()); + }; + if delimeter == '\\' || !delimeter.is_ascii_punctuation() { + return Err(format!("cannot use {:?} as a search delimeter", delimeter)); + } + + let mut search = String::new(); + let mut replacement = String::new(); + let mut flags = String::new(); + + let mut buffer = &mut search; + + let mut escaped = false; + let mut phase = 0; + + for c in chars { + if escaped { + escaped = false; + if phase == 1 && c.is_digit(10) { + // help vim users discover zed regex syntax + // (though we don't try and fix arbitrary patterns for them) + buffer.push('$') + } else if phase == 0 && c == '(' || c == ')' { + // un-escape parens + } else if c != delimeter { + buffer.push('\\') + } + buffer.push(c) + } else if c == '\\' { + escaped = true; + } else if c == delimeter { + if phase == 0 { + buffer = &mut replacement; + phase = 1; + } else if phase == 1 { + buffer = &mut flags; + phase = 2; + } else { + return Err("trailing characters".to_string()); + } + } else { + buffer.push(c) + } + } + + let mut replacement = Replacement { + search, + replacement, + should_replace_all: true, + is_case_sensitive: true, + }; + + for c in flags.chars() { + match c { + 'g' | 'I' => {} // defaults, + 'c' | 'n' => replacement.should_replace_all = false, + 'i' => replacement.is_case_sensitive = false, + _ => return Err(format!("unsupported flag {:?}", c)), + } + } + + Ok(replacement) +} + #[cfg(test)] mod test { use std::sync::Arc; diff --git a/crates/vim/src/test/neovim_backed_binding_test_context.rs b/crates/vim/src/test/neovim_backed_binding_test_context.rs index 18de029fdc90ea0f70f1f71b19548c51e4ad6e56..15fce99aad3f4ea0e03129342a4bca48fba4166f 100644 --- a/crates/vim/src/test/neovim_backed_binding_test_context.rs +++ b/crates/vim/src/test/neovim_backed_binding_test_context.rs @@ -1,7 +1,5 @@ use std::ops::{Deref, DerefMut}; -use gpui::ContextHandle; - use crate::state::Mode; use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES}; @@ -33,26 +31,17 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> { self.consume().binding(keystrokes) } - pub async fn assert( - &mut self, - marked_positions: &str, - ) -> Option<(ContextHandle, ContextHandle)> { + pub async fn assert(&mut self, marked_positions: &str) { self.cx .assert_binding_matches(self.keystrokes_under_test, marked_positions) - .await + .await; } - pub async fn assert_exempted( - &mut self, - marked_positions: &str, - feature: ExemptionFeatures, - ) -> Option<(ContextHandle, ContextHandle)> { + pub async fn assert_exempted(&mut self, marked_positions: &str, feature: ExemptionFeatures) { if SUPPORTED_FEATURES.contains(&feature) { self.cx .assert_binding_matches(self.keystrokes_under_test, marked_positions) .await - } else { - None } } diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index e58f805a026f32473ab18b4dccc9eb662ae6e541..227d39bb6354c4c2dd3a8c5ce38dc75c567e513e 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -106,26 +106,25 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn simulate_shared_keystrokes( &mut self, keystroke_texts: [&str; COUNT], - ) -> ContextHandle { + ) { for keystroke_text in keystroke_texts.into_iter() { self.recent_keystrokes.push(keystroke_text.to_string()); self.neovim.send_keystroke(keystroke_text).await; } - self.simulate_keystrokes(keystroke_texts) + self.simulate_keystrokes(keystroke_texts); } - pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle { + pub async fn set_shared_state(&mut self, marked_text: &str) { let mode = if marked_text.contains("»") { Mode::Visual } else { Mode::Normal }; - let context_handle = self.set_state(marked_text, mode); + self.set_state(marked_text, mode); self.last_set_state = Some(marked_text.to_string()); self.recent_keystrokes = Vec::new(); self.neovim.set_state(marked_text).await; self.is_dirty = true; - context_handle } pub async fn set_shared_wrap(&mut self, columns: u32) { @@ -288,18 +287,18 @@ impl<'a> NeovimBackedTestContext<'a> { &mut self, keystrokes: [&str; COUNT], initial_state: &str, - ) -> Option<(ContextHandle, ContextHandle)> { + ) { if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) { match possible_exempted_keystrokes { Some(exempted_keystrokes) => { if exempted_keystrokes.contains(&format!("{keystrokes:?}")) { // This keystroke was exempted for this insertion text - return None; + return; } } None => { // All keystrokes for this insertion text are exempted - return None; + return; } } } @@ -307,7 +306,6 @@ impl<'a> NeovimBackedTestContext<'a> { let _state_context = self.set_shared_state(initial_state).await; let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await; self.assert_state_matches().await; - Some((_state_context, _keystroke_context)) } pub async fn assert_binding_matches_all( diff --git a/crates/vim/test_data/test_command_basics.json b/crates/vim/test_data/test_command_basics.json new file mode 100644 index 0000000000000000000000000000000000000000..669d34409f218f6205f69a0d13c4dc9bf75bd5b8 --- /dev/null +++ b/crates/vim/test_data/test_command_basics.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇa\nb\nc"}} +{"Key":":"} +{"Key":"j"} +{"Key":"enter"} +{"Key":"^"} +{"Get":{"state":"ˇa b\nc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_goto.json b/crates/vim/test_data/test_command_goto.json new file mode 100644 index 0000000000000000000000000000000000000000..2f7ed10eeb27017ca3fbdaa94f6a3f8f2d2ce316 --- /dev/null +++ b/crates/vim/test_data/test_command_goto.json @@ -0,0 +1,5 @@ +{"Put":{"state":"ˇa\nb\nc"}} +{"Key":":"} +{"Key":"3"} +{"Key":"enter"} +{"Get":{"state":"a\nb\nˇc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_replace.json b/crates/vim/test_data/test_command_replace.json new file mode 100644 index 0000000000000000000000000000000000000000..13928d5c7e138af7628e613f612a199a760d0798 --- /dev/null +++ b/crates/vim/test_data/test_command_replace.json @@ -0,0 +1,22 @@ +{"Put":{"state":"ˇa\nb\nc"}} +{"Key":":"} +{"Key":"%"} +{"Key":"s"} +{"Key":"/"} +{"Key":"b"} +{"Key":"/"} +{"Key":"d"} +{"Key":"enter"} +{"Get":{"state":"a\nˇd\nc","mode":"Normal"}} +{"Key":":"} +{"Key":"%"} +{"Key":"s"} +{"Key":":"} +{"Key":"."} +{"Key":":"} +{"Key":"\\"} +{"Key":"0"} +{"Key":"\\"} +{"Key":"0"} +{"Key":"enter"} +{"Get":{"state":"aa\ndd\nˇcc","mode":"Normal"}} diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index da3f7e4c32e1fbc535dcb071e9a2fa55df1b4ae6..acffbc29abbe1bf4486548e859a397bc7fb3a62d 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -57,12 +57,7 @@ pub fn menus() -> Vec> { save_behavior: None, }, ), - MenuItem::action( - "Close Window", - workspace::CloseWindow { - save_behavior: None, - }, - ), + MenuItem::action("Close Window", workspace::CloseWindow), ], }, Menu { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d968a92646c1d3854193e28f196d7f5597268bc8..72804cb523ddb8d335339dda7c07622b4d3cf32e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -947,7 +947,9 @@ mod tests { assert!(editor.text(cx).is_empty()); }); - let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); + let save_task = workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + }); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); save_task.await.unwrap(); @@ -1311,7 +1313,9 @@ mod tests { .await; cx.read(|cx| assert!(editor.is_dirty(cx))); - let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); + let save_task = workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + }); window.simulate_prompt_answer(0, cx); save_task.await.unwrap(); editor.read_with(cx, |editor, cx| { @@ -1353,7 +1357,9 @@ mod tests { }); // Save the buffer. This prompts for a filename. - let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); + let save_task = workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + }); cx.simulate_new_path_selection(|parent_dir| { assert_eq!(parent_dir, Path::new("/root")); Some(parent_dir.join("the-new-name.rs")) @@ -1377,7 +1383,9 @@ mod tests { editor.handle_input(" there", cx); assert!(editor.is_dirty(cx)); }); - let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); + let save_task = workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + }); save_task.await.unwrap(); assert!(!cx.did_prompt_for_new_path()); editor.read_with(cx, |editor, cx| { @@ -1444,7 +1452,9 @@ mod tests { }); // Save the buffer. This prompts for a filename. - let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); + let save_task = workspace.update(cx, |workspace, cx| { + workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + }); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); save_task.await.unwrap(); // The buffer is not dirty anymore and the language is assigned based on the path. From a25fcfdfa7116ad21d3fdc38f0be0297aed5119d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 16:30:45 -0600 Subject: [PATCH 09/89] Iron out some edge-cases --- Cargo.toml | 1 + crates/search/src/buffer_search.rs | 3 ++ crates/vim/src/command.rs | 3 ++ crates/vim/src/normal/search.rs | 31 ++++++------------- .../vim/test_data/test_command_replace.json | 7 +++++ 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1876434ad46fc7f8c5eddf59d46255fcde4136e..3e4c5911ed644014bc70aef383d357b5552c67d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,7 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07 [profile.dev] split-debuginfo = "unpacked" +panic = "abort" [profile.release] debug = true diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 97f9d7a7c8f44f2d78e3b362d5f2878c3002a492..11c35a6d75cd50cada606dbc52617d7441c56d0e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -702,6 +702,9 @@ impl BufferSearchBar { .searchable_items_with_matches .get(&searchable_item.downgrade()) { + if matches.len() == 0 { + return; + } let new_match_index = matches.len() - 1; searchable_item.update_matches(matches, cx); searchable_item.activate_match(new_match_index, matches, cx); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 10c2599dd9af76d98792898e33a97af1695bf4ac..7aaf3d8024acea6e5826fabaf08ae9a955105fca 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -355,6 +355,9 @@ mod test { dd ˇcc"}) .await; + + cx.simulate_shared_keystrokes([":", "%", "s", "/", "/", "/", "enter"]) + .await; } #[gpui::test] diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index a9e5e66b9e1bef7d31a764067bf9e37e13823756..8ef0718c326dc660fb064e492ca35d1faadaa062 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,7 +1,7 @@ use gpui::{actions, impl_actions, AppContext, ViewContext}; use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions}; use serde_derive::Deserialize; -use workspace::{searchable::Direction, Pane, Toast, Workspace}; +use workspace::{searchable::Direction, Pane, Workspace}; use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim}; @@ -36,7 +36,7 @@ pub struct ReplaceCommand { pub query: String, } -#[derive(Debug)] +#[derive(Debug, Default)] struct Replacement { search: String, replacement: String, @@ -212,15 +212,7 @@ fn replace_command( action: &ReplaceCommand, cx: &mut ViewContext, ) { - let replacement = match parse_replace_all(&action.query) { - Ok(replacement) => replacement, - Err(message) => { - cx.handle().update(cx, |workspace, cx| { - workspace.show_toast(Toast::new(1544, message), cx) - }); - return; - } - }; + let replacement = parse_replace_all(&action.query); let pane = workspace.active_pane().clone(); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { @@ -270,18 +262,15 @@ fn replace_command( }) } -fn parse_replace_all(query: &str) -> Result { +fn parse_replace_all(query: &str) -> Replacement { let mut chars = query.chars(); if Some('%') != chars.next() || Some('s') != chars.next() { - return Err("unsupported pattern".to_string()); + return Replacement::default(); } let Some(delimeter) = chars.next() else { - return Err("unsupported pattern".to_string()); + return Replacement::default(); }; - if delimeter == '\\' || !delimeter.is_ascii_punctuation() { - return Err(format!("cannot use {:?} as a search delimeter", delimeter)); - } let mut search = String::new(); let mut replacement = String::new(); @@ -315,7 +304,7 @@ fn parse_replace_all(query: &str) -> Result { buffer = &mut flags; phase = 2; } else { - return Err("trailing characters".to_string()); + break; } } else { buffer.push(c) @@ -331,14 +320,14 @@ fn parse_replace_all(query: &str) -> Result { for c in flags.chars() { match c { - 'g' | 'I' => {} // defaults, + 'g' | 'I' => {} 'c' | 'n' => replacement.should_replace_all = false, 'i' => replacement.is_case_sensitive = false, - _ => return Err(format!("unsupported flag {:?}", c)), + _ => {} } } - Ok(replacement) + replacement } #[cfg(test)] diff --git a/crates/vim/test_data/test_command_replace.json b/crates/vim/test_data/test_command_replace.json index 13928d5c7e138af7628e613f612a199a760d0798..91827c0285b3b74d3d7366047d408cba36879228 100644 --- a/crates/vim/test_data/test_command_replace.json +++ b/crates/vim/test_data/test_command_replace.json @@ -20,3 +20,10 @@ {"Key":"0"} {"Key":"enter"} {"Get":{"state":"aa\ndd\nˇcc","mode":"Normal"}} +{"Key":":"} +{"Key":"%"} +{"Key":"s"} +{"Key":"/"} +{"Key":"/"} +{"Key":"/"} +{"Key":"enter"} From a59da3634bb3b84eb4a1d5a9ebeb1afe0f0a89ae Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 16:41:40 -0600 Subject: [PATCH 10/89] Fix backward search from command --- crates/vim/src/command.rs | 27 ++++++++++++++++++- crates/vim/src/normal/search.rs | 7 ++++- crates/vim/test_data/test_command_search.json | 11 ++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 crates/vim/test_data/test_command_search.json diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 7aaf3d8024acea6e5826fabaf08ae9a955105fca..2ad8f0522eb84ac1a94348d4397f99259828e4d6 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -355,8 +355,33 @@ mod test { dd ˇcc"}) .await; + } + + #[gpui::test] + async fn test_command_search(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_shared_keystrokes([":", "%", "s", "/", "/", "/", "enter"]) + cx.set_shared_state(indoc! {" + ˇa + b + a + c"}) + .await; + cx.simulate_shared_keystrokes([":", "/", "b", "enter"]) + .await; + cx.assert_shared_state(indoc! {" + a + ˇb + a + c"}) + .await; + cx.simulate_shared_keystrokes([":", "?", "a", "enter"]) + .await; + cx.assert_shared_state(indoc! {" + ˇa + b + a + c"}) .await; } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 8ef0718c326dc660fb064e492ca35d1faadaa062..e76da1dfc5dd7dc1d7803c6fe0127a85fb089238 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -195,10 +195,15 @@ fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewCo }); let Some(search) = search else { return }; let search_bar = search_bar.downgrade(); + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; cx.spawn(|_, mut cx| async move { search.await?; search_bar.update(&mut cx, |search_bar, cx| { - search_bar.select_match(Direction::Next, 1, cx) + search_bar.select_match(direction, 1, cx) })?; anyhow::Ok(()) }) diff --git a/crates/vim/test_data/test_command_search.json b/crates/vim/test_data/test_command_search.json new file mode 100644 index 0000000000000000000000000000000000000000..705ce51fb75f19dfa831bb97c6df22b6c1e20e94 --- /dev/null +++ b/crates/vim/test_data/test_command_search.json @@ -0,0 +1,11 @@ +{"Put":{"state":"ˇa\nb\na\nc"}} +{"Key":":"} +{"Key":"/"} +{"Key":"b"} +{"Key":"enter"} +{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}} +{"Key":":"} +{"Key":"?"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"ˇa\nb\na\nc","mode":"Normal"}} From 7a7ff4bb960faa05b5fadbd2cd9baf494685a949 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 20:13:52 -0600 Subject: [PATCH 11/89] Fix save related tests, and refactor saves again --- crates/vim/src/command.rs | 43 ++++--- crates/workspace/src/item.rs | 6 +- crates/workspace/src/pane.rs | 195 +++++++++++++++--------------- crates/workspace/src/workspace.rs | 47 +++---- crates/zed/src/zed.rs | 28 +++-- 5 files changed, 156 insertions(+), 163 deletions(-) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 2ad8f0522eb84ac1a94348d4397f99259828e4d6..6bed7f5bb4b995df69bfb055b9b7a6acc64847ca 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -2,7 +2,7 @@ use command_palette::CommandInterceptResult; use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive}; use gpui::{impl_actions, Action, AppContext}; use serde_derive::Deserialize; -use workspace::{SaveBehavior, Workspace}; +use workspace::{SaveIntent, Workspace}; use crate::{ motion::{EndOfDocument, Motion}, @@ -50,112 +50,119 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "write", workspace::Save { - save_behavior: Some(SaveBehavior::PromptOnConflict), + save_behavior: Some(SaveIntent::Save), } .boxed_clone(), ), "w!" | "wr!" | "wri!" | "writ!" | "write!" => ( "write!", workspace::Save { - save_behavior: Some(SaveBehavior::SilentlyOverwrite), + save_behavior: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "q" | "qu" | "qui" | "quit" => ( "quit", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::PromptOnWrite), + save_behavior: Some(SaveIntent::Close), } .boxed_clone(), ), "q!" | "qu!" | "qui!" | "quit!" => ( "quit!", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::DontSave), + save_behavior: Some(SaveIntent::Skip), } .boxed_clone(), ), "wq" => ( "wq", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::PromptOnConflict), + save_behavior: Some(SaveIntent::Save), } .boxed_clone(), ), "wq!" => ( "wq!", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::SilentlyOverwrite), + save_behavior: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "x" | "xi" | "xit" | "exi" | "exit" => ( "exit", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::PromptOnConflict), + save_behavior: Some(SaveIntent::Save), } .boxed_clone(), ), "x!" | "xi!" | "xit!" | "exi!" | "exit!" => ( "exit!", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::SilentlyOverwrite), + save_behavior: Some(SaveIntent::Overwrite), + } + .boxed_clone(), + ), + "up" | "upd" | "upda" | "updat" | "update" => ( + "update", + workspace::Save { + save_behavior: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "wa" | "wal" | "wall" => ( "wall", workspace::SaveAll { - save_behavior: Some(SaveBehavior::PromptOnConflict), + save_behavior: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "wa!" | "wal!" | "wall!" => ( "wall!", workspace::SaveAll { - save_behavior: Some(SaveBehavior::SilentlyOverwrite), + save_behavior: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => ( "quitall", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::PromptOnWrite), + save_behavior: Some(SaveIntent::Close), } .boxed_clone(), ), "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => ( "quitall!", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::DontSave), + save_behavior: Some(SaveIntent::Skip), } .boxed_clone(), ), "xa" | "xal" | "xall" => ( "xall", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::PromptOnConflict), + save_behavior: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "xa!" | "xal!" | "xall!" => ( "xall!", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::SilentlyOverwrite), + save_behavior: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "wqa" | "wqal" | "wqall" => ( "wqall", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::PromptOnConflict), + save_behavior: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "wqa!" | "wqal!" | "wqall!" => ( "wqall!", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveBehavior::SilentlyOverwrite), + save_behavior: Some(SaveIntent::Overwrite), } .boxed_clone(), ), @@ -190,7 +197,7 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "tabclose", workspace::CloseActiveItem { - save_behavior: Some(SaveBehavior::PromptOnWrite), + save_behavior: Some(SaveIntent::Close), } .boxed_clone(), ), diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index ea747b3a364720c653f91ce7b8bc609750509fe3..24bed4c8d1427f0497ca0151a16b14133abbb514 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -475,11 +475,7 @@ impl ItemHandle for ViewHandle { match item_event { ItemEvent::CloseItem => { pane.update(cx, |pane, cx| { - pane.close_item_by_id( - item.id(), - crate::SaveBehavior::PromptOnWrite, - cx, - ) + pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) }) .detach_and_log_err(cx); return; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 285cf1da603c64977c6942131105c0fe2462b263..40e6d36f3bea4419d69be1b6e7dd31b6435cabcd 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -45,17 +45,21 @@ use theme::{Theme, ThemeSettings}; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub enum SaveBehavior { - /// ask before overwriting conflicting files (used by default with cmd-s) - PromptOnConflict, - /// ask for a new path before writing (used with cmd-shift-s) - PromptForNewPath, - /// ask before writing any file that wouldn't be auto-saved (used by default with cmd-w) - PromptOnWrite, - /// never prompt, write on conflict (used with vim's :w!) - SilentlyOverwrite, - /// skip all save-related behaviour (used with vim's :q!) - DontSave, +pub enum SaveIntent { + /// write all files (even if unchanged) + /// prompt before overwriting on-disk changes + Save, + /// write any files that have local changes + /// prompt before overwriting on-disk changes + SaveAll, + /// always prompt for a new path + SaveAs, + /// prompt "you have unsaved changes" before writing + Close, + /// write all dirty files, don't prompt on conflict + Overwrite, + /// skip all save-related behavior + Skip, } #[derive(Clone, Deserialize, PartialEq)] @@ -82,13 +86,13 @@ pub struct CloseItemsToTheRightById { #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseActiveItem { - pub save_behavior: Option, + pub save_behavior: Option, } #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CloseAllItems { - pub save_behavior: Option, + pub save_behavior: Option, } actions!( @@ -730,7 +734,7 @@ impl Pane { let active_item_id = self.items[self.active_item_index].id(); Some(self.close_item_by_id( active_item_id, - action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + action.save_behavior.unwrap_or(SaveIntent::Close), cx, )) } @@ -738,7 +742,7 @@ impl Pane { pub fn close_item_by_id( &mut self, item_id_to_close: usize, - save_behavior: SaveBehavior, + save_behavior: SaveIntent, cx: &mut ViewContext, ) -> Task> { self.close_items(cx, save_behavior, move |view_id| { @@ -756,11 +760,9 @@ impl Pane { } let active_item_id = self.items[self.active_item_index].id(); - Some( - self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| { - item_id != active_item_id - }), - ) + Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + item_id != active_item_id + })) } pub fn close_clean_items( @@ -773,11 +775,9 @@ impl Pane { .filter(|item| !item.is_dirty(cx)) .map(|item| item.id()) .collect(); - Some( - self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| { - item_ids.contains(&item_id) - }), - ) + Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + })) } pub fn close_items_to_the_left( @@ -802,7 +802,7 @@ impl Pane { .take_while(|item| item.id() != item_id) .map(|item| item.id()) .collect(); - self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| { + self.close_items(cx, SaveIntent::Close, move |item_id| { item_ids.contains(&item_id) }) } @@ -830,7 +830,7 @@ impl Pane { .take_while(|item| item.id() != item_id) .map(|item| item.id()) .collect(); - self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| { + self.close_items(cx, SaveIntent::Close, move |item_id| { item_ids.contains(&item_id) }) } @@ -846,7 +846,7 @@ impl Pane { Some(self.close_items( cx, - action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), + action.save_behavior.unwrap_or(SaveIntent::Close), |_| true, )) } @@ -854,7 +854,7 @@ impl Pane { pub fn close_items( &mut self, cx: &mut ViewContext, - save_behavior: SaveBehavior, + save_behavior: SaveIntent, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { // Find the items to close. @@ -1010,18 +1010,18 @@ impl Pane { pane: &WeakViewHandle, item_ix: usize, item: &dyn ItemHandle, - save_behavior: SaveBehavior, + save_behavior: SaveIntent, cx: &mut AsyncAppContext, ) -> Result { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?"; - if save_behavior == SaveBehavior::DontSave { + if save_behavior == SaveIntent::Skip { return Ok(true); } - let (mut has_conflict, mut is_dirty, mut can_save, is_singleton) = cx.read(|cx| { + let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.read(|cx| { ( item.has_conflict(cx), item.is_dirty(cx), @@ -1030,73 +1030,76 @@ impl Pane { ) }); - if save_behavior == SaveBehavior::PromptForNewPath { - has_conflict = false; + // when saving a single buffer, we ignore whether or not it's dirty. + if save_behavior == SaveIntent::Save { is_dirty = true; + } + + if save_behavior == SaveIntent::SaveAs { + is_dirty = true; + has_conflict = false; can_save = false; } + if save_behavior == SaveIntent::Overwrite { + has_conflict = false; + } + if has_conflict && can_save { - if save_behavior == SaveBehavior::SilentlyOverwrite { - pane.update(cx, |_, cx| item.save(project, cx))?.await?; - } else { - let mut answer = pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, true, true, cx); - cx.prompt( - PromptLevel::Warning, - CONFLICT_MESSAGE, - &["Overwrite", "Discard", "Cancel"], - ) - })?; - match answer.next().await { - Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, - Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, - _ => return Ok(false), - } + let mut answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + &["Overwrite", "Discard", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?, + Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?, + _ => return Ok(false), } - } else if is_dirty && (can_save || is_singleton) { - let will_autosave = cx.read(|cx| { - matches!( - settings::get::(cx).autosave, - AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange - ) && Self::can_autosave_item(&*item, cx) - }); - let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave { - let mut answer = pane.update(cx, |pane, cx| { - pane.activate_item(item_ix, true, true, cx); - cx.prompt( - PromptLevel::Warning, - DIRTY_MESSAGE, - &["Save", "Don't Save", "Cancel"], - ) - })?; - match answer.next().await { - Some(0) => true, - Some(1) => false, - _ => return Ok(false), + } else if is_dirty && (can_save || can_save_as) { + if save_behavior == SaveIntent::Close { + let will_autosave = cx.read(|cx| { + matches!( + settings::get::(cx).autosave, + AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange + ) && Self::can_autosave_item(&*item, cx) + }); + if !will_autosave { + let mut answer = pane.update(cx, |pane, cx| { + pane.activate_item(item_ix, true, true, cx); + cx.prompt( + PromptLevel::Warning, + DIRTY_MESSAGE, + &["Save", "Don't Save", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => {} + Some(1) => return Ok(true), // Don't save this file + _ => return Ok(false), // Cancel + } } - } else { - true - }; + } - if should_save { - if can_save { - pane.update(cx, |_, cx| item.save(project, cx))?.await?; - } else if is_singleton { - let start_abs_path = project - .read_with(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next()?; - Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) - }) - .unwrap_or_else(|| Path::new("").into()); + if can_save { + pane.update(cx, |_, cx| item.save(project, cx))?.await?; + } else if can_save_as { + let start_abs_path = project + .read_with(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next()?; + Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) + }) + .unwrap_or_else(|| Path::new("").into()); - let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); - if let Some(abs_path) = abs_path.next().await.flatten() { - pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? - .await?; - } else { - return Ok(false); - } + let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path)); + if let Some(abs_path) = abs_path.next().await.flatten() { + pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? + .await?; + } else { + return Ok(false); } } } @@ -1210,7 +1213,7 @@ impl Pane { pane.update(cx, |pane, cx| { pane.close_item_by_id( target_item_id, - SaveBehavior::PromptOnWrite, + SaveIntent::Close, cx, ) .detach_and_log_err(cx); @@ -1367,12 +1370,8 @@ impl Pane { .on_click(MouseButton::Middle, { let item_id = item.id(); move |_, pane, cx| { - pane.close_item_by_id( - item_id, - SaveBehavior::PromptOnWrite, - cx, - ) - .detach_and_log_err(cx); + pane.close_item_by_id(item_id, SaveIntent::Close, cx) + .detach_and_log_err(cx); } }) .on_down( @@ -1580,7 +1579,7 @@ impl Pane { cx.window_context().defer(move |cx| { if let Some(pane) = pane.upgrade(cx) { pane.update(cx, |pane, cx| { - pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(item_id, SaveIntent::Close, cx) .detach_and_log_err(cx); }); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6043b946214257642082194043395a75d7417138..a853691b7644028e570e7b7b78176d71d2f1aca2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -163,19 +163,19 @@ pub struct NewFileInDirection(pub SplitDirection); #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveAll { - pub save_behavior: Option, + pub save_behavior: Option, } #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Save { - pub save_behavior: Option, + pub save_behavior: Option, } #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseAllItemsAndPanes { - pub save_behavior: Option, + pub save_behavior: Option, } #[derive(Deserialize)] @@ -294,19 +294,14 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_action( |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { workspace - .save_active_item( - action - .save_behavior - .unwrap_or(SaveBehavior::PromptOnConflict), - cx, - ) + .save_active_item(action.save_behavior.unwrap_or(SaveIntent::Save), cx) .detach_and_log_err(cx); }, ); cx.add_action( |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { workspace - .save_active_item(SaveBehavior::PromptForNewPath, cx) + .save_active_item(SaveIntent::SaveAs, cx) .detach_and_log_err(cx); }, ); @@ -1356,7 +1351,7 @@ impl Workspace { Ok(this .update(&mut cx, |this, cx| { - this.save_all_internal(SaveBehavior::PromptOnWrite, cx) + this.save_all_internal(SaveIntent::Close, cx) })? .await?) }) @@ -1367,12 +1362,8 @@ impl Workspace { action: &SaveAll, cx: &mut ViewContext, ) -> Option>> { - let save_all = self.save_all_internal( - action - .save_behavior - .unwrap_or(SaveBehavior::PromptOnConflict), - cx, - ); + let save_all = + self.save_all_internal(action.save_behavior.unwrap_or(SaveIntent::SaveAll), cx); Some(cx.foreground().spawn(async move { save_all.await?; Ok(()) @@ -1381,7 +1372,7 @@ impl Workspace { fn save_all_internal( &mut self, - save_behaviour: SaveBehavior, + save_behaviour: SaveIntent, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { @@ -1688,7 +1679,7 @@ impl Workspace { pub fn save_active_item( &mut self, - save_behavior: SaveBehavior, + save_behavior: SaveIntent, cx: &mut ViewContext, ) -> Task> { let project = self.project.clone(); @@ -1720,7 +1711,7 @@ impl Workspace { _: &CloseInactiveTabsAndPanes, cx: &mut ViewContext, ) -> Option>> { - self.close_all_internal(true, SaveBehavior::PromptOnWrite, cx) + self.close_all_internal(true, SaveIntent::Close, cx) } pub fn close_all_items_and_panes( @@ -1728,17 +1719,13 @@ impl Workspace { action: &CloseAllItemsAndPanes, cx: &mut ViewContext, ) -> Option>> { - self.close_all_internal( - false, - action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite), - cx, - ) + self.close_all_internal(false, action.save_behavior.unwrap_or(SaveIntent::Close), cx) } fn close_all_internal( &mut self, retain_active_pane: bool, - save_behavior: SaveBehavior, + save_behavior: SaveIntent, cx: &mut ViewContext, ) -> Option>> { let current_pane = self.active_pane(); @@ -4433,7 +4420,7 @@ mod tests { let item1_id = item1.id(); let item3_id = item3.id(); let item4_id = item4.id(); - pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| { + pane.close_items(cx, SaveIntent::Close, move |id| { [item1_id, item3_id, item4_id].contains(&id) }) }); @@ -4571,7 +4558,7 @@ mod tests { // prompts, the task should complete. let close = left_pane.update(cx, |pane, cx| { - pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true) + pane.close_items(cx, SaveIntent::Close, move |_| true) }); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { @@ -4689,7 +4676,7 @@ mod tests { }); pane.update(cx, |pane, cx| { - pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id) + pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) }) .await .unwrap(); @@ -4712,7 +4699,7 @@ mod tests { // Ensure autosave is prevented for deleted files also when closing the buffer. let _close_items = pane.update(cx, |pane, cx| { - pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id) + pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) }); deterministic.run_until_parked(); assert!(window.has_pending_prompt(cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 72804cb523ddb8d335339dda7c07622b4d3cf32e..5363262e3f610d5a5bd073132ee26df29feb5dae 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -744,7 +744,7 @@ mod tests { use theme::{ThemeRegistry, ThemeSettings}; use workspace::{ item::{Item, ItemHandle}, - open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle, + open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle, }; #[gpui::test] @@ -945,12 +945,14 @@ mod tests { editor.update(cx, |editor, cx| { assert!(editor.text(cx).is_empty()); + assert!(!editor.is_dirty(cx)); }); let save_task = workspace.update(cx, |workspace, cx| { - workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + workspace.save_active_item(SaveIntent::Save, cx) }); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); + cx.foreground().run_until_parked(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); save_task.await.unwrap(); editor.read_with(cx, |editor, cx| { @@ -1314,7 +1316,7 @@ mod tests { cx.read(|cx| assert!(editor.is_dirty(cx))); let save_task = workspace.update(cx, |workspace, cx| { - workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + workspace.save_active_item(SaveIntent::Save, cx) }); window.simulate_prompt_answer(0, cx); save_task.await.unwrap(); @@ -1358,8 +1360,9 @@ mod tests { // Save the buffer. This prompts for a filename. let save_task = workspace.update(cx, |workspace, cx| { - workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + workspace.save_active_item(SaveIntent::Save, cx) }); + cx.foreground().run_until_parked(); cx.simulate_new_path_selection(|parent_dir| { assert_eq!(parent_dir, Path::new("/root")); Some(parent_dir.join("the-new-name.rs")) @@ -1384,7 +1387,7 @@ mod tests { assert!(editor.is_dirty(cx)); }); let save_task = workspace.update(cx, |workspace, cx| { - workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + workspace.save_active_item(SaveIntent::Save, cx) }); save_task.await.unwrap(); assert!(!cx.did_prompt_for_new_path()); @@ -1453,8 +1456,9 @@ mod tests { // Save the buffer. This prompts for a filename. let save_task = workspace.update(cx, |workspace, cx| { - workspace.save_active_item(SaveBehavior::PromptOnConflict, cx) + workspace.save_active_item(SaveIntent::Save, cx) }); + cx.foreground().run_until_parked(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); save_task.await.unwrap(); // The buffer is not dirty anymore and the language is assigned based on the path. @@ -1692,7 +1696,7 @@ mod tests { pane.update(cx, |pane, cx| { let editor3_id = editor3.id(); drop(editor3); - pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(editor3_id, SaveIntent::Close, cx) }) .await .unwrap(); @@ -1727,7 +1731,7 @@ mod tests { pane.update(cx, |pane, cx| { let editor2_id = editor2.id(); drop(editor2); - pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(editor2_id, SaveIntent::Close, cx) }) .await .unwrap(); @@ -1884,28 +1888,28 @@ mod tests { // Close all the pane items in some arbitrary order. pane.update(cx, |pane, cx| { - pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx) }) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); pane.update(cx, |pane, cx| { - pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx) }) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); pane.update(cx, |pane, cx| { - pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx) }) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); pane.update(cx, |pane, cx| { - pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx) + pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx) }) .await .unwrap(); From 4bf4c780be8d75705d269a9ee7c75399ac5f4c3c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 20:50:22 -0600 Subject: [PATCH 12/89] Revert accidental Cargo change --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3e4c5911ed644014bc70aef383d357b5552c67d6..c1876434ad46fc7f8c5eddf59d46255fcde4136e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,7 +158,6 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07 [profile.dev] split-debuginfo = "unpacked" -panic = "abort" [profile.release] debug = true From 32f87333138617cd7525285d8bc5441dc7fcb728 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Sep 2023 20:54:30 -0600 Subject: [PATCH 13/89] Code review changes --- assets/keymaps/vim.json | 4 +- crates/command_palette/src/command_palette.rs | 2 +- crates/file_finder/src/file_finder.rs | 9 +- crates/gpui/src/app/test_app_context.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 7 +- crates/vim/src/command.rs | 36 +++---- crates/vim/src/normal/search.rs | 100 ++++++++++-------- crates/workspace/src/pane.rs | 92 +++++----------- crates/workspace/src/workspace.rs | 35 +++--- crates/zed/src/menus.rs | 18 +--- crates/zed/src/zed.rs | 9 +- 11 files changed, 127 insertions(+), 187 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 5db0fe748ef6885f697ec71c321814c44e1a9fcb..9453607ce997e5d822f12df8abc529f2f91a3ce7 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -206,13 +206,13 @@ "shift-z shift-q": [ "pane::CloseActiveItem", { - "saveBehavior": "dontSave" + "saveIntent": "skip" } ], "shift-z shift-z": [ "pane::CloseActiveItem", { - "saveBehavior": "promptOnConflict" + "saveIntent": "saveAll" } ], // Count support diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 90c155cdf8caba22d945d3b982cd4ec88b339828..90c448137453fa0be95744958972c6dc63d85405 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate { } } -pub fn humanize_action_name(name: &str) -> String { +fn humanize_action_name(name: &str) -> String { let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); let mut result = String::with_capacity(capacity); for char in name.chars() { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index c2d8cc52b26f7483a40982cac04a96c4c8d92bb0..64ef31cd307dc8e5fbf82d01c98f3890d761c1c2 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1528,13 +1528,8 @@ mod tests { let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); active_pane .update(cx, |pane, cx| { - pane.close_active_item( - &workspace::CloseActiveItem { - save_behavior: None, - }, - cx, - ) - .unwrap() + pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) + .unwrap() }) .await .unwrap(); diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 5b15b5274c60bb835c22e58882f6d7eb95910151..0dc1d1eba437fb67beddbde47bd10bc497ed928a 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -33,7 +33,7 @@ use super::{ #[derive(Clone)] pub struct TestAppContext { - pub cx: Rc>, + cx: Rc>, foreground_platform: Rc, condition_duration: Option, pub function_name: String, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index b79f655f815a71b3985eb26450b2b5edf9837c26..cd939b5604716a1b6c0f523db53acce735cc9ac1 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -284,12 +284,7 @@ impl TerminalView { pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext) { let menu_entries = vec![ ContextMenuItem::action("Clear", Clear), - ContextMenuItem::action( - "Close", - pane::CloseActiveItem { - save_behavior: None, - }, - ), + ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }), ]; self.context_menu.update(cx, |menu, cx| { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 6bed7f5bb4b995df69bfb055b9b7a6acc64847ca..092d72c2fcd19a4f3dcacf78c0f6487015a0a379 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -50,119 +50,119 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "write", workspace::Save { - save_behavior: Some(SaveIntent::Save), + save_intent: Some(SaveIntent::Save), } .boxed_clone(), ), "w!" | "wr!" | "wri!" | "writ!" | "write!" => ( "write!", workspace::Save { - save_behavior: Some(SaveIntent::Overwrite), + save_intent: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "q" | "qu" | "qui" | "quit" => ( "quit", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Close), + save_intent: Some(SaveIntent::Close), } .boxed_clone(), ), "q!" | "qu!" | "qui!" | "quit!" => ( "quit!", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Skip), + save_intent: Some(SaveIntent::Skip), } .boxed_clone(), ), "wq" => ( "wq", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Save), + save_intent: Some(SaveIntent::Save), } .boxed_clone(), ), "wq!" => ( "wq!", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Overwrite), + save_intent: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "x" | "xi" | "xit" | "exi" | "exit" => ( "exit", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Save), + save_intent: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "x!" | "xi!" | "xit!" | "exi!" | "exit!" => ( "exit!", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Overwrite), + save_intent: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "up" | "upd" | "upda" | "updat" | "update" => ( "update", workspace::Save { - save_behavior: Some(SaveIntent::SaveAll), + save_intent: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "wa" | "wal" | "wall" => ( "wall", workspace::SaveAll { - save_behavior: Some(SaveIntent::SaveAll), + save_intent: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "wa!" | "wal!" | "wall!" => ( "wall!", workspace::SaveAll { - save_behavior: Some(SaveIntent::Overwrite), + save_intent: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => ( "quitall", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveIntent::Close), + save_intent: Some(SaveIntent::Close), } .boxed_clone(), ), "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => ( "quitall!", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveIntent::Skip), + save_intent: Some(SaveIntent::Skip), } .boxed_clone(), ), "xa" | "xal" | "xall" => ( "xall", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveIntent::SaveAll), + save_intent: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "xa!" | "xal!" | "xall!" => ( "xall!", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveIntent::Overwrite), + save_intent: Some(SaveIntent::Overwrite), } .boxed_clone(), ), "wqa" | "wqal" | "wqall" => ( "wqall", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveIntent::SaveAll), + save_intent: Some(SaveIntent::SaveAll), } .boxed_clone(), ), "wqa!" | "wqal!" | "wqall!" => ( "wqall!", workspace::CloseAllItemsAndPanes { - save_behavior: Some(SaveIntent::Overwrite), + save_intent: Some(SaveIntent::Overwrite), } .boxed_clone(), ), @@ -197,7 +197,7 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ( "tabclose", workspace::CloseActiveItem { - save_behavior: Some(SaveIntent::Close), + save_intent: Some(SaveIntent::Close), } .boxed_clone(), ), diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index e76da1dfc5dd7dc1d7803c6fe0127a85fb089238..f74625c8b30586ae6a05a637134f9e4c3f1f4191 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -220,53 +220,58 @@ fn replace_command( let replacement = parse_replace_all(&action.query); let pane = workspace.active_pane().clone(); pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(cx) { - return None; - } + let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { + return; + }; + let search = search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return None; + } - let mut options = SearchOptions::default(); - if replacement.is_case_sensitive { - options.set(SearchOptions::CASE_SENSITIVE, true) - } - let search = if replacement.search == "" { - search_bar.query(cx) - } else { - replacement.search - }; + let mut options = SearchOptions::default(); + if replacement.is_case_sensitive { + options.set(SearchOptions::CASE_SENSITIVE, true) + } + let search = if replacement.search == "" { + search_bar.query(cx) + } else { + replacement.search + }; - search_bar.set_replacement(Some(&replacement.replacement), cx); - search_bar.activate_search_mode(SearchMode::Regex, cx); - Some(search_bar.search(&search, Some(options), cx)) - }); - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - cx.spawn(|_, mut cx| async move { - search.await?; - search_bar.update(&mut cx, |search_bar, cx| { - if replacement.should_replace_all { - search_bar.select_last_match(cx); - search_bar.replace_all(&Default::default(), cx); - Vim::update(cx, |vim, cx| { - move_cursor( - vim, - Motion::StartOfLine { - display_lines: false, - }, - None, - cx, - ) - }) - } - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + search_bar.set_replacement(Some(&replacement.replacement), cx); + search_bar.activate_search_mode(SearchMode::Regex, cx); + Some(search_bar.search(&search, Some(options), cx)) + }); + let Some(search) = search else { return }; + let search_bar = search_bar.downgrade(); + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + if replacement.should_replace_all { + search_bar.select_last_match(cx); + search_bar.replace_all(&Default::default(), cx); + Vim::update(cx, |vim, cx| { + move_cursor( + vim, + Motion::StartOfLine { + display_lines: false, + }, + None, + cx, + ) + }) + } + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); }) } +// convert a vim query into something more usable by zed. +// we don't attempt to fully convert between the two regex syntaxes, +// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern, +// and convert \0..\9 to $0..$9 in the replacement so that common idioms work. fn parse_replace_all(query: &str) -> Replacement { let mut chars = query.chars(); if Some('%') != chars.next() || Some('s') != chars.next() { @@ -284,17 +289,18 @@ fn parse_replace_all(query: &str) -> Replacement { let mut buffer = &mut search; let mut escaped = false; + // 0 - parsing search + // 1 - parsing replacement + // 2 - parsing flags let mut phase = 0; for c in chars { if escaped { escaped = false; if phase == 1 && c.is_digit(10) { - // help vim users discover zed regex syntax - // (though we don't try and fix arbitrary patterns for them) buffer.push('$') + // unescape escaped parens } else if phase == 0 && c == '(' || c == ')' { - // un-escape parens } else if c != delimeter { buffer.push('\\') } @@ -312,6 +318,10 @@ fn parse_replace_all(query: &str) -> Replacement { break; } } else { + // escape unescaped parens + if phase == 0 && c == '(' || c == ')' { + buffer.push('\\') + } buffer.push(c) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 40e6d36f3bea4419d69be1b6e7dd31b6435cabcd..fbe018409b4008d3675146c44c3cc9f3c81b3784 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -86,13 +86,13 @@ pub struct CloseItemsToTheRightById { #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseActiveItem { - pub save_behavior: Option, + pub save_intent: Option, } #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CloseAllItems { - pub save_behavior: Option, + pub save_intent: Option, } actions!( @@ -734,7 +734,7 @@ impl Pane { let active_item_id = self.items[self.active_item_index].id(); Some(self.close_item_by_id( active_item_id, - action.save_behavior.unwrap_or(SaveIntent::Close), + action.save_intent.unwrap_or(SaveIntent::Close), cx, )) } @@ -742,12 +742,10 @@ impl Pane { pub fn close_item_by_id( &mut self, item_id_to_close: usize, - save_behavior: SaveIntent, + save_intent: SaveIntent, cx: &mut ViewContext, ) -> Task> { - self.close_items(cx, save_behavior, move |view_id| { - view_id == item_id_to_close - }) + self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) } pub fn close_inactive_items( @@ -844,17 +842,17 @@ impl Pane { return None; } - Some(self.close_items( - cx, - action.save_behavior.unwrap_or(SaveIntent::Close), - |_| true, - )) + Some( + self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { + true + }), + ) } pub fn close_items( &mut self, cx: &mut ViewContext, - save_behavior: SaveIntent, + save_intent: SaveIntent, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { // Find the items to close. @@ -912,7 +910,7 @@ impl Pane { &pane, item_ix, &*item, - save_behavior, + save_intent, &mut cx, ) .await? @@ -1010,14 +1008,14 @@ impl Pane { pane: &WeakViewHandle, item_ix: usize, item: &dyn ItemHandle, - save_behavior: SaveIntent, + save_intent: SaveIntent, cx: &mut AsyncAppContext, ) -> Result { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?"; - if save_behavior == SaveIntent::Skip { + if save_intent == SaveIntent::Skip { return Ok(true); } @@ -1031,17 +1029,17 @@ impl Pane { }); // when saving a single buffer, we ignore whether or not it's dirty. - if save_behavior == SaveIntent::Save { + if save_intent == SaveIntent::Save { is_dirty = true; } - if save_behavior == SaveIntent::SaveAs { + if save_intent == SaveIntent::SaveAs { is_dirty = true; has_conflict = false; can_save = false; } - if save_behavior == SaveIntent::Overwrite { + if save_intent == SaveIntent::Overwrite { has_conflict = false; } @@ -1060,7 +1058,7 @@ impl Pane { _ => return Ok(false), } } else if is_dirty && (can_save || can_save_as) { - if save_behavior == SaveIntent::Close { + if save_intent == SaveIntent::Close { let will_autosave = cx.read(|cx| { matches!( settings::get::(cx).autosave, @@ -1188,9 +1186,7 @@ impl Pane { vec![ ContextMenuItem::action( "Close Active Item", - CloseActiveItem { - save_behavior: None, - }, + CloseActiveItem { save_intent: None }, ), ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), ContextMenuItem::action("Close Clean Items", CloseCleanItems), @@ -1198,9 +1194,7 @@ impl Pane { ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight), ContextMenuItem::action( "Close All Items", - CloseAllItems { - save_behavior: None, - }, + CloseAllItems { save_intent: None }, ), ] } else { @@ -1247,9 +1241,7 @@ impl Pane { }), ContextMenuItem::action( "Close All Items", - CloseAllItems { - save_behavior: None, - }, + CloseAllItems { save_intent: None }, ), ] }, @@ -2182,12 +2174,7 @@ mod tests { pane.update(cx, |pane, cx| { assert!(pane - .close_active_item( - &CloseActiveItem { - save_behavior: None - }, - cx - ) + .close_active_item(&CloseActiveItem { save_intent: None }, cx) .is_none()) }); } @@ -2439,12 +2426,7 @@ mod tests { assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); pane.update(cx, |pane, cx| { - pane.close_active_item( - &CloseActiveItem { - save_behavior: None, - }, - cx, - ) + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) }) .unwrap() .await @@ -2455,12 +2437,7 @@ mod tests { assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); pane.update(cx, |pane, cx| { - pane.close_active_item( - &CloseActiveItem { - save_behavior: None, - }, - cx, - ) + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) }) .unwrap() .await @@ -2468,12 +2445,7 @@ mod tests { assert_item_labels(&pane, ["A", "B*", "C"], cx); pane.update(cx, |pane, cx| { - pane.close_active_item( - &CloseActiveItem { - save_behavior: None, - }, - cx, - ) + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) }) .unwrap() .await @@ -2481,12 +2453,7 @@ mod tests { assert_item_labels(&pane, ["A", "C*"], cx); pane.update(cx, |pane, cx| { - pane.close_active_item( - &CloseActiveItem { - save_behavior: None, - }, - cx, - ) + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) }) .unwrap() .await @@ -2597,12 +2564,7 @@ mod tests { assert_item_labels(&pane, ["A", "B", "C*"], cx); pane.update(cx, |pane, cx| { - pane.close_all_items( - &CloseAllItems { - save_behavior: None, - }, - cx, - ) + pane.close_all_items(&CloseAllItems { save_intent: None }, cx) }) .unwrap() .await diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a853691b7644028e570e7b7b78176d71d2f1aca2..092286973e867007cedbb0b37c94030a6e906fe6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -163,19 +163,19 @@ pub struct NewFileInDirection(pub SplitDirection); #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveAll { - pub save_behavior: Option, + pub save_intent: Option, } #[derive(Clone, PartialEq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Save { - pub save_behavior: Option, + pub save_intent: Option, } #[derive(Clone, PartialEq, Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CloseAllItemsAndPanes { - pub save_behavior: Option, + pub save_intent: Option, } #[derive(Deserialize)] @@ -294,7 +294,7 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_action( |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { workspace - .save_active_item(action.save_behavior.unwrap_or(SaveIntent::Save), cx) + .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) .detach_and_log_err(cx); }, ); @@ -1363,7 +1363,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Option>> { let save_all = - self.save_all_internal(action.save_behavior.unwrap_or(SaveIntent::SaveAll), cx); + self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx); Some(cx.foreground().spawn(async move { save_all.await?; Ok(()) @@ -1372,7 +1372,7 @@ impl Workspace { fn save_all_internal( &mut self, - save_behaviour: SaveIntent, + save_intent: SaveIntent, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { @@ -1407,7 +1407,7 @@ impl Workspace { &pane, ix, &*item, - save_behaviour, + save_intent, &mut cx, ) .await? @@ -1679,7 +1679,7 @@ impl Workspace { pub fn save_active_item( &mut self, - save_behavior: SaveIntent, + save_intent: SaveIntent, cx: &mut ViewContext, ) -> Task> { let project = self.project.clone(); @@ -1690,16 +1690,9 @@ impl Workspace { cx.spawn(|_, mut cx| async move { if let Some(item) = item { - Pane::save_item( - project, - &pane, - item_ix, - item.as_ref(), - save_behavior, - &mut cx, - ) - .await - .map(|_| ()) + Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx) + .await + .map(|_| ()) } else { Ok(()) } @@ -1719,13 +1712,13 @@ impl Workspace { action: &CloseAllItemsAndPanes, cx: &mut ViewContext, ) -> Option>> { - self.close_all_internal(false, action.save_behavior.unwrap_or(SaveIntent::Close), cx) + self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx) } fn close_all_internal( &mut self, retain_active_pane: bool, - save_behavior: SaveIntent, + save_intent: SaveIntent, cx: &mut ViewContext, ) -> Option>> { let current_pane = self.active_pane(); @@ -1748,7 +1741,7 @@ impl Workspace { if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { pane.close_all_items( &CloseAllItems { - save_behavior: Some(save_behavior), + save_intent: Some(save_intent), }, cx, ) diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index acffbc29abbe1bf4486548e859a397bc7fb3a62d..4e01693dbf6980c10d99c2fc727eeb1ad642b31b 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -38,24 +38,12 @@ pub fn menus() -> Vec> { MenuItem::action("Open Recent...", recent_projects::OpenRecent), MenuItem::separator(), MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), - MenuItem::action( - "Save", - workspace::Save { - save_behavior: None, - }, - ), + MenuItem::action("Save", workspace::Save { save_intent: None }), MenuItem::action("Save As…", workspace::SaveAs), - MenuItem::action( - "Save All", - workspace::SaveAll { - save_behavior: None, - }, - ), + MenuItem::action("Save All", workspace::SaveAll { save_intent: None }), MenuItem::action( "Close Editor", - workspace::CloseActiveItem { - save_behavior: None, - }, + workspace::CloseActiveItem { save_intent: None }, ), MenuItem::action("Close Window", workspace::CloseWindow), ], diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5363262e3f610d5a5bd073132ee26df29feb5dae..11e80ffb4a5917f1c1b4f814a48140987ee90633 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1318,6 +1318,7 @@ mod tests { let save_task = workspace.update(cx, |workspace, cx| { workspace.save_active_item(SaveIntent::Save, cx) }); + cx.foreground().run_until_parked(); window.simulate_prompt_answer(0, cx); save_task.await.unwrap(); editor.read_with(cx, |editor, cx| { @@ -1522,9 +1523,7 @@ mod tests { }); cx.dispatch_action( window.into(), - workspace::CloseActiveItem { - save_behavior: None, - }, + workspace::CloseActiveItem { save_intent: None }, ); cx.foreground().run_until_parked(); @@ -1535,9 +1534,7 @@ mod tests { cx.dispatch_action( window.into(), - workspace::CloseActiveItem { - save_behavior: None, - }, + workspace::CloseActiveItem { save_intent: None }, ); cx.foreground().run_until_parked(); window.simulate_prompt_answer(1, cx); From 997f362cc2617cd7afc509e64f37333f97459b94 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 21 Sep 2023 13:40:01 -0400 Subject: [PATCH 14/89] add semantic index status, for non authenticated users --- crates/search/src/project_search.rs | 22 ++++++++++++++++----- crates/semantic_index/src/semantic_index.rs | 21 +++++++++++++++++++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 19442191172b5713f56746a5c449df11bae923dd..c1127a5389ebdcd7446eb466aeda184c047e41e9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -303,7 +303,7 @@ impl View for ProjectSearchView { // If Text -> Major: "Text search all files and folders", Minor: {...} let current_mode = self.current_mode; - let major_text = if model.pending_search.is_some() { + let mut major_text = if model.pending_search.is_some() { Cow::Borrowed("Searching...") } else if model.no_results.is_some_and(|v| v) { Cow::Borrowed("No Results") @@ -317,9 +317,18 @@ impl View for ProjectSearchView { } }; + let mut show_minor_text = true; let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { let status = semantic.index_status; match status { + SemanticIndexStatus::NotAuthenticated => { + major_text = Cow::Borrowed("Not Authenticated"); + show_minor_text = false; + Some( + "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables" + .to_string(), + ) + } SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()), SemanticIndexStatus::Indexing { remaining_files, @@ -361,10 +370,13 @@ impl View for ProjectSearchView { let mut minor_text = Vec::new(); minor_text.push("".into()); minor_text.extend(semantic_status); - minor_text.push("Simply explain the code you are looking to find.".into()); - minor_text.push( - "ex. 'prompt user for permissions to index their project'".into(), - ); + if show_minor_text { + minor_text + .push("Simply explain the code you are looking to find.".into()); + minor_text.push( + "ex. 'prompt user for permissions to index their project'".into(), + ); + } minor_text } _ => vec![ diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index c808d33f23a6e08858aae9de0aa4b893c40921df..b6ad75a34ea9db1c2fea0a2a55bcfd86c6902a29 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -16,6 +16,7 @@ use embedding_queue::{EmbeddingQueue, FileToEmbed}; use futures::{future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use language::{Anchor, Bias, Buffer, Language, LanguageRegistry}; +use lazy_static::lazy_static; use ordered_float::OrderedFloat; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES}; @@ -24,6 +25,7 @@ use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Work use smol::channel; use std::{ cmp::Reverse, + env, future::Future, mem, ops::Range, @@ -38,6 +40,10 @@ const SEMANTIC_INDEX_VERSION: usize = 11; const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60); const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250); +lazy_static! { + static ref OPENAI_API_KEY: Option = env::var("OPENAI_API_KEY").ok(); +} + pub fn init( fs: Arc, http_client: Arc, @@ -100,6 +106,7 @@ pub fn init( #[derive(Copy, Clone, Debug)] pub enum SemanticIndexStatus { + NotAuthenticated, NotIndexed, Indexed, Indexing { @@ -274,7 +281,15 @@ impl SemanticIndex { settings::get::(cx).enabled } + pub fn has_api_key(&self) -> bool { + OPENAI_API_KEY.as_ref().is_some() + } + pub fn status(&self, project: &ModelHandle) -> SemanticIndexStatus { + if !self.has_api_key() { + return SemanticIndexStatus::NotAuthenticated; + } + if let Some(project_state) = self.projects.get(&project.downgrade()) { if project_state .worktrees @@ -694,12 +709,12 @@ impl SemanticIndex { let embedding_provider = self.embedding_provider.clone(); cx.spawn(|this, mut cx| async move { + index.await?; let query = embedding_provider .embed_batch(vec![query]) .await? .pop() .ok_or_else(|| anyhow!("could not embed query"))?; - index.await?; let search_start = Instant::now(); let modified_buffer_results = this.update(&mut cx, |this, cx| { @@ -965,6 +980,10 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { + if !self.has_api_key() { + return Task::ready(Err(anyhow!("no open ai key present"))); + } + if !self.projects.contains_key(&project.downgrade()) { let subscription = cx.subscribe(&project, |this, project, event, cx| match event { project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => { From 7b63369df2ae5a48fc04bec94fbf5d1f0db3c2b6 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 21 Sep 2023 14:00:00 -0400 Subject: [PATCH 15/89] move api authentication to embedding provider --- crates/semantic_index/src/embedding.rs | 7 +++++++ crates/semantic_index/src/semantic_index.rs | 10 +++------- crates/semantic_index/src/semantic_index_tests.rs | 3 +++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index b0124bf7df2664f1b3f237edd601a8e59b196fbd..2b6e94854e2c0e8e21f9df11874f45d35dca8a59 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -117,6 +117,7 @@ struct OpenAIEmbeddingUsage { #[async_trait] pub trait EmbeddingProvider: Sync + Send { + fn is_authenticated(&self) -> bool; async fn embed_batch(&self, spans: Vec) -> Result>; fn max_tokens_per_batch(&self) -> usize; fn truncate(&self, span: &str) -> (String, usize); @@ -127,6 +128,9 @@ pub struct DummyEmbeddings {} #[async_trait] impl EmbeddingProvider for DummyEmbeddings { + fn is_authenticated(&self) -> bool { + true + } fn rate_limit_expiration(&self) -> Option { None } @@ -229,6 +233,9 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { + fn is_authenticated(&self) -> bool { + OPENAI_API_KEY.as_ref().is_some() + } fn max_tokens_per_batch(&self) -> usize { 50000 } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index b6ad75a34ea9db1c2fea0a2a55bcfd86c6902a29..1ba0001cfda9c6be7128e1378bc75750cd1842da 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -281,12 +281,8 @@ impl SemanticIndex { settings::get::(cx).enabled } - pub fn has_api_key(&self) -> bool { - OPENAI_API_KEY.as_ref().is_some() - } - pub fn status(&self, project: &ModelHandle) -> SemanticIndexStatus { - if !self.has_api_key() { + if !self.embedding_provider.is_authenticated() { return SemanticIndexStatus::NotAuthenticated; } @@ -980,8 +976,8 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { - if !self.has_api_key() { - return Task::ready(Err(anyhow!("no open ai key present"))); + if !self.embedding_provider.is_authenticated() { + return Task::ready(Err(anyhow!("user is not authenticated"))); } if !self.projects.contains_key(&project.downgrade()) { diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 9035327b2e8d910312ebb2f7c117a51d300f5d6a..f386665915b1d9b9314c7246da93daec6624900e 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1267,6 +1267,9 @@ impl FakeEmbeddingProvider { #[async_trait] impl EmbeddingProvider for FakeEmbeddingProvider { + fn is_authenticated(&self) -> bool { + true + } fn truncate(&self, span: &str) -> (String, usize) { (span.to_string(), 1) } From 6bbf614a37a9cc8da6518fe36f1cdd42f4af3eb7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Sep 2023 16:56:04 -0400 Subject: [PATCH 16/89] Fix some typos in `README.md` --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c502ebc74fadd46df3546c85e0dc25fbb3ca806..b3d4987526a46be3304ca649d90166566c03029e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea sudo xcodebuild -license ``` -* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.) +* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.) ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install node rustup-init @@ -36,7 +36,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea brew install foreman ``` -* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies: +* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. From 92d3115f3d8337a7c04d69d93f86a8b174d164da Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Sep 2023 17:21:40 -0400 Subject: [PATCH 17/89] Fix some typos in `tools.md` --- docs/tools.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tools.md b/docs/tools.md index 6e424a6f8145bc26235cc4e6eeca2db1d52df234..22810e3e07909c6d0f42bdd4c2336d8cd438bf7a 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -32,7 +32,7 @@ Have a team member add you to the [Zed Industries](https://zed-industries.slack. ### Discord -We have a discord community. You can use [this link](https://discord.gg/SSD9eJrn6s) to join. **!Don't share this link, this is specifically for team memebers!** +We have a Discord community. You can use [this link](https://discord.gg/SSD9eJrn6s) to join. **!Don't share this link, this is specifically for team members!** Once you have joined the community, let a team member know and we can add your correct role. @@ -56,7 +56,7 @@ We use Vercel for all of our web deployments and some backend things. If you sig ### Environment Variables -You can get access to many of our shared enviroment variables through 1Password and Vercel. For one password search the value you are looking for, or sort by passwords or API credentials. +You can get access to many of our shared enviroment variables through 1Password and Vercel. For 1Password search the value you are looking for, or sort by passwords or API credentials. For Vercel, go to `settings` -> `Environment Variables` (either on the entire org, or on a specific project depending on where it is shared.) For a given Vercel project if you have their CLI installed you can use `vercel pull` or `vercel env` to pull values down directly. More on those in their [CLI docs](https://vercel.com/docs/cli/env). From c252eae32e395e668a6585cf2dc35643ba970614 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Sep 2023 17:46:37 -0400 Subject: [PATCH 18/89] Reorganize `ui` module exports (#3007) This PR reorganizes the exports for the `ui` module in the `storybook` crate. ### Motivation Currently we expose each of the various elements/components/modules in two places: - Through the module itself (e.g., `ui::element::Avatar`) - Through the `ui` module's re-exports (e.g., `ui::Avatar`) This means it's possible to import any given item from two spots, which can lead to inconsistencies in the consumers. Additionally, it also means we're shipping the exact module structure underneath `ui` as part of the public API. ### Explanation To avoid this, we can avoid exposing each of the individual modules underneath `ui::{element, component, module}` and instead export just the module contents themselves. This makes the `ui` module namespace flat. Release Notes: - N/A --- crates/storybook/src/ui.rs | 26 +++++--------------------- crates/storybook/src/ui/component.rs | 13 +++++++++---- crates/storybook/src/ui/element.rs | 28 +++++++++++++++++++--------- crates/storybook/src/ui/module.rs | 16 +++++++++++----- 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/crates/storybook/src/ui.rs b/crates/storybook/src/ui.rs index 056ad56a2b81a4ef15ad134391e4bca55c5aef35..f8f0e7a65f45bbfc70c4b96fea2d90f41fc0707f 100644 --- a/crates/storybook/src/ui.rs +++ b/crates/storybook/src/ui.rs @@ -1,23 +1,7 @@ -mod element; -pub use element::avatar::*; -pub use element::details::*; -pub use element::icon::*; -pub use element::icon_button::*; -pub use element::indicator::*; -pub use element::input::*; -pub use element::label::*; -pub use element::text_button::*; -pub use element::tool_divider::*; - mod component; -pub use component::facepile::*; -pub use component::follow_group::*; -pub use component::list_item::*; -pub use component::tab::*; - +mod element; mod module; -pub use module::chat_panel::*; -pub use module::project_panel::*; -pub use module::status_bar::*; -pub use module::tab_bar::*; -pub use module::title_bar::*; + +pub use component::*; +pub use element::*; +pub use module::*; diff --git a/crates/storybook/src/ui/component.rs b/crates/storybook/src/ui/component.rs index 49a126886301a01a67b65ce9b27d6a75b2514625..26fa01584761da227b4c6be6ea185ceb2b5a141a 100644 --- a/crates/storybook/src/ui/component.rs +++ b/crates/storybook/src/ui/component.rs @@ -1,4 +1,9 @@ -pub(crate) mod facepile; -pub(crate) mod follow_group; -pub(crate) mod list_item; -pub(crate) mod tab; +mod facepile; +mod follow_group; +mod list_item; +mod tab; + +pub use facepile::*; +pub use follow_group::*; +pub use list_item::*; +pub use tab::*; diff --git a/crates/storybook/src/ui/element.rs b/crates/storybook/src/ui/element.rs index e79a9c5986ad2c0e63e82acc19ad851587492cad..3f76af0d150e5aa2bd39a3c9ad5eedefbaf57019 100644 --- a/crates/storybook/src/ui/element.rs +++ b/crates/storybook/src/ui/element.rs @@ -1,9 +1,19 @@ -pub(crate) mod avatar; -pub(crate) mod details; -pub(crate) mod icon; -pub(crate) mod icon_button; -pub(crate) mod indicator; -pub(crate) mod input; -pub(crate) mod label; -pub(crate) mod text_button; -pub(crate) mod tool_divider; +mod avatar; +mod details; +mod icon; +mod icon_button; +mod indicator; +mod input; +mod label; +mod text_button; +mod tool_divider; + +pub use avatar::*; +pub use details::*; +pub use icon::*; +pub use icon_button::*; +pub use indicator::*; +pub use input::*; +pub use label::*; +pub use text_button::*; +pub use tool_divider::*; diff --git a/crates/storybook/src/ui/module.rs b/crates/storybook/src/ui/module.rs index a261fffcd61a7129ba102ddb70bbf1dcf38e51fd..a1cead7df9df397602906cdfca7750508532522a 100644 --- a/crates/storybook/src/ui/module.rs +++ b/crates/storybook/src/ui/module.rs @@ -1,5 +1,11 @@ -pub(crate) mod chat_panel; -pub(crate) mod project_panel; -pub(crate) mod status_bar; -pub(crate) mod tab_bar; -pub(crate) mod title_bar; +mod chat_panel; +mod project_panel; +mod status_bar; +mod tab_bar; +mod title_bar; + +pub use chat_panel::*; +pub use project_panel::*; +pub use status_bar::*; +pub use tab_bar::*; +pub use title_bar::*; From baa07e935ea300adcbeb36044aef278fbc5dc383 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Sep 2023 19:25:35 -0400 Subject: [PATCH 19/89] Extract UI elements from `storybook` into new `ui` crate (#3008) This PR extracts the various UI elements from the `storybook` crate into a new `ui` library crate. Release Notes: - N/A --- Cargo.lock | 12 ++++++++++ Cargo.toml | 1 + crates/storybook/Cargo.toml | 1 + crates/storybook/src/collab_panel.rs | 2 +- crates/storybook/src/storybook.rs | 8 +------ crates/storybook/src/ui.rs | 7 ------ crates/storybook/src/ui/component.rs | 9 -------- crates/storybook/src/workspace.rs | 5 +--- crates/ui/Cargo.toml | 12 ++++++++++ crates/{storybook => ui}/src/components.rs | 23 +++++++++++++++---- .../src/components}/facepile.rs | 7 +++--- .../src/components}/follow_group.rs | 8 +++---- .../src/components}/list_item.rs | 10 ++++---- .../ui/component => ui/src/components}/tab.rs | 7 +++--- crates/{storybook => ui}/src/element_ext.rs | 6 +++-- .../src/ui/element.rs => ui/src/elements.rs} | 0 .../ui/element => ui/src/elements}/avatar.rs | 8 +++---- .../ui/element => ui/src/elements}/details.rs | 6 ++--- .../ui/element => ui/src/elements}/icon.rs | 6 ++--- .../src/elements}/icon_button.rs | 10 ++++---- .../element => ui/src/elements}/indicator.rs | 7 +++--- .../ui/element => ui/src/elements}/input.rs | 9 ++++---- .../ui/element => ui/src/elements}/label.rs | 6 ++--- .../src/elements}/text_button.rs | 9 ++++---- .../src/elements}/tool_divider.rs | 7 +++--- crates/ui/src/lib.rs | 14 +++++++++++ .../src/ui/module.rs => ui/src/modules.rs} | 0 .../module => ui/src/modules}/chat_panel.rs | 8 +++---- .../src/modules}/project_panel.rs | 19 +++++++-------- .../module => ui/src/modules}/status_bar.rs | 9 ++++---- .../ui/module => ui/src/modules}/tab_bar.rs | 10 ++++---- .../ui/module => ui/src/modules}/title_bar.rs | 10 ++++---- crates/{storybook => ui}/src/prelude.rs | 0 crates/{storybook => ui}/src/theme.rs | 15 +++++++----- crates/{storybook/src => }/ui/tracker.md | 0 35 files changed, 154 insertions(+), 117 deletions(-) delete mode 100644 crates/storybook/src/ui.rs delete mode 100644 crates/storybook/src/ui/component.rs create mode 100644 crates/ui/Cargo.toml rename crates/{storybook => ui}/src/components.rs (85%) rename crates/{storybook/src/ui/component => ui/src/components}/facepile.rs (84%) rename crates/{storybook/src/ui/component => ui/src/components}/follow_group.rs (88%) rename crates/{storybook/src/ui/component => ui/src/components}/list_item.rs (92%) rename crates/{storybook/src/ui/component => ui/src/components}/tab.rs (92%) rename crates/{storybook => ui}/src/element_ext.rs (99%) rename crates/{storybook/src/ui/element.rs => ui/src/elements.rs} (100%) rename crates/{storybook/src/ui/element => ui/src/elements}/avatar.rs (87%) rename crates/{storybook/src/ui/element => ui/src/elements}/details.rs (88%) rename crates/{storybook/src/ui/element => ui/src/elements}/icon.rs (95%) rename crates/{storybook/src/ui/element => ui/src/elements}/icon_button.rs (88%) rename crates/{storybook/src/ui/element => ui/src/elements}/indicator.rs (86%) rename crates/{storybook/src/ui/element => ui/src/elements}/input.rs (94%) rename crates/{storybook/src/ui/element => ui/src/elements}/label.rs (92%) rename crates/{storybook/src/ui/element => ui/src/elements}/text_button.rs (93%) rename crates/{storybook/src/ui/element => ui/src/elements}/tool_divider.rs (78%) create mode 100644 crates/ui/src/lib.rs rename crates/{storybook/src/ui/module.rs => ui/src/modules.rs} (100%) rename crates/{storybook/src/ui/module => ui/src/modules}/chat_panel.rs (91%) rename crates/{storybook/src/ui/module => ui/src/modules}/project_panel.rs (93%) rename crates/{storybook/src/ui/module => ui/src/modules}/status_bar.rs (96%) rename crates/{storybook/src/ui/module => ui/src/modules}/tab_bar.rs (95%) rename crates/{storybook/src/ui/module => ui/src/modules}/title_bar.rs (95%) rename crates/{storybook => ui}/src/prelude.rs (100%) rename crates/{storybook => ui}/src/theme.rs (94%) rename crates/{storybook/src => }/ui/tracker.md (100%) diff --git a/Cargo.lock b/Cargo.lock index 3cced78c4272e5fc9f571d8023719a1ea56d06d2..05b29aabf400e9843dc45041c956044c3bb7658c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7397,6 +7397,7 @@ dependencies = [ "settings", "simplelog", "theme", + "ui", "util", ] @@ -8599,6 +8600,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "serde", + "settings", + "theme", +] + [[package]] name = "unicase" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index c1876434ad46fc7f8c5eddf59d46255fcde4136e..b9622a1c3e3b1970cdae4bd1cb24bdcf96430301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ members = [ "crates/text", "crates/theme", "crates/theme_selector", + "crates/ui", "crates/util", "crates/semantic_index", "crates/vim", diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index f634ea4eca1cb58056f9a14769dfadfcba83a118..c1096a87babed86dd08e513ffbd2f629d6e44c90 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -17,6 +17,7 @@ serde.workspace = true settings = { path = "../settings" } simplelog = "0.9" theme = { path = "../theme" } +ui = { path = "../ui" } util = { path = "../util" } [dev-dependencies] diff --git a/crates/storybook/src/collab_panel.rs b/crates/storybook/src/collab_panel.rs index 87fd536391609c91a1e277015780114d53ef94fc..30d15a5b0400c653dc41a9b65d546b05bb1eed72 100644 --- a/crates/storybook/src/collab_panel.rs +++ b/crates/storybook/src/collab_panel.rs @@ -1,10 +1,10 @@ -use crate::theme::{theme, Theme}; use gpui2::{ elements::{div, div::ScrollState, img, svg}, style::{StyleHelpers, Styleable}, ArcCow, Element, IntoElement, ParentElement, ViewContext, }; use std::marker::PhantomData; +use ui::{theme, Theme}; #[derive(Element)] pub struct CollabPanelElement { diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index df1db7b8c21d0b6e9083ec2c974b56e7bf99cbeb..92d178fad2728cb1e5e3a5d44caaaf3a5b2347be 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -1,20 +1,14 @@ #![allow(dead_code, unused_variables)] -use crate::theme::Theme; use ::theme as legacy_theme; -use element_ext::ElementExt; use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds}; use legacy_theme::ThemeSettings; use log::LevelFilter; use settings::{default_settings, SettingsStore}; use simplelog::SimpleLogger; +use ui::{ElementExt, Theme}; mod collab_panel; -mod components; -mod element_ext; -mod prelude; -mod theme; -mod ui; mod workspace; gpui2::actions! { diff --git a/crates/storybook/src/ui.rs b/crates/storybook/src/ui.rs deleted file mode 100644 index f8f0e7a65f45bbfc70c4b96fea2d90f41fc0707f..0000000000000000000000000000000000000000 --- a/crates/storybook/src/ui.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod component; -mod element; -mod module; - -pub use component::*; -pub use element::*; -pub use module::*; diff --git a/crates/storybook/src/ui/component.rs b/crates/storybook/src/ui/component.rs deleted file mode 100644 index 26fa01584761da227b4c6be6ea185ceb2b5a141a..0000000000000000000000000000000000000000 --- a/crates/storybook/src/ui/component.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod facepile; -mod follow_group; -mod list_item; -mod tab; - -pub use facepile::*; -pub use follow_group::*; -pub use list_item::*; -pub use tab::*; diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs index 3127bcf837cc24694e78759fbeec8b6788634b78..95152ec8328f26e0749215a7fe55694f7247da2f 100644 --- a/crates/storybook/src/workspace.rs +++ b/crates/storybook/src/workspace.rs @@ -1,12 +1,9 @@ -use crate::{ - theme::theme, - ui::{chat_panel, project_panel, status_bar, tab_bar, title_bar}, -}; use gpui2::{ elements::{div, div::ScrollState}, style::StyleHelpers, Element, IntoElement, ParentElement, ViewContext, }; +use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar}; #[derive(Element, Default)] struct WorkspaceElement { diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..5018a9173988203d61e26efc2ccbbac2db1e796f --- /dev/null +++ b/crates/ui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ui" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +anyhow.workspace = true +gpui2 = { path = "../gpui2" } +serde.workspace = true +settings = { path = "../settings" } +theme = { path = "../theme" } diff --git a/crates/storybook/src/components.rs b/crates/ui/src/components.rs similarity index 85% rename from crates/storybook/src/components.rs rename to crates/ui/src/components.rs index 1aafefc1a6a0a89728f64b6c6299e8c68ef1cc20..a82d28eb8cb2539cef985c12a59ba712b97699ae 100644 --- a/crates/storybook/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,8 +1,21 @@ -use gpui2::{ - elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow, - Element, EventContext, IntoElement, ParentElement, ViewContext, -}; -use std::{marker::PhantomData, rc::Rc}; +mod facepile; +mod follow_group; +mod list_item; +mod tab; + +pub use facepile::*; +pub use follow_group::*; +pub use list_item::*; +pub use tab::*; + +use std::marker::PhantomData; +use std::rc::Rc; + +use gpui2::elements::div; +use gpui2::interactive::Interactive; +use gpui2::platform::MouseButton; +use gpui2::style::StyleHelpers; +use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext}; struct ButtonHandlers { click: Option)>>, diff --git a/crates/storybook/src/ui/component/facepile.rs b/crates/ui/src/components/facepile.rs similarity index 84% rename from crates/storybook/src/ui/component/facepile.rs rename to crates/ui/src/components/facepile.rs index 73ab231c07fe6b28742831fd5120d5ae9d257020..d9566c216cfbf82fc0dce336b3da0dc2bf67e6c6 100644 --- a/crates/storybook/src/ui/component/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -1,7 +1,8 @@ -use crate::{theme::theme, ui::Avatar}; +use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::{theme, Avatar}; #[derive(Element)] pub struct Facepile { diff --git a/crates/storybook/src/ui/component/follow_group.rs b/crates/ui/src/components/follow_group.rs similarity index 88% rename from crates/storybook/src/ui/component/follow_group.rs rename to crates/ui/src/components/follow_group.rs index 5d79b8fc06fb3bdab9ddaf83832aa00d685ae676..75e24e9135a93576b63dd65f90dd5e617c26fc37 100644 --- a/crates/storybook/src/ui/component/follow_group.rs +++ b/crates/ui/src/components/follow_group.rs @@ -1,8 +1,8 @@ -use crate::theme::theme; -use crate::ui::{facepile, indicator, Avatar}; +use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::{facepile, indicator, theme, Avatar}; #[derive(Element)] pub struct FollowGroup { diff --git a/crates/storybook/src/ui/component/list_item.rs b/crates/ui/src/components/list_item.rs similarity index 92% rename from crates/storybook/src/ui/component/list_item.rs rename to crates/ui/src/components/list_item.rs index 46df86bfa1e5985a6e28aba6d5aea79e2269a093..868b58e449f778d8356a1823c91f3312328ab4aa 100644 --- a/crates/storybook/src/ui/component/list_item.rs +++ b/crates/ui/src/components/list_item.rs @@ -1,10 +1,10 @@ -use crate::prelude::{InteractionState, ToggleState}; -use crate::theme::theme; -use crate::ui::{icon, IconAsset, Label}; +use gpui2::elements::div; use gpui2::geometry::rems; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::*; +use crate::{icon, theme, IconAsset, Label}; #[derive(Element)] pub struct ListItem { diff --git a/crates/storybook/src/ui/component/tab.rs b/crates/ui/src/components/tab.rs similarity index 92% rename from crates/storybook/src/ui/component/tab.rs rename to crates/ui/src/components/tab.rs index 8237aac0041254c1754d7ba4fd7ea0037597244f..e812a26cd51a024d27036ea8fecf2f83fa796923 100644 --- a/crates/storybook/src/ui/component/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -1,7 +1,8 @@ -use crate::theme::theme; +use gpui2::elements::div; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::theme; #[derive(Element)] pub struct Tab { diff --git a/crates/storybook/src/element_ext.rs b/crates/ui/src/element_ext.rs similarity index 99% rename from crates/storybook/src/element_ext.rs rename to crates/ui/src/element_ext.rs index 72ec5b328af00279427795a13e12cd969c34688c..67352f0779774527af4067b37bda662b1fcfdb2c 100644 --- a/crates/storybook/src/element_ext.rs +++ b/crates/ui/src/element_ext.rs @@ -1,7 +1,9 @@ -use crate::theme::{Theme, Themed}; -use gpui2::Element; use std::marker::PhantomData; +use gpui2::Element; + +use crate::theme::{Theme, Themed}; + pub trait ElementExt: Element { fn themed(self, theme: Theme) -> Themed where diff --git a/crates/storybook/src/ui/element.rs b/crates/ui/src/elements.rs similarity index 100% rename from crates/storybook/src/ui/element.rs rename to crates/ui/src/elements.rs diff --git a/crates/storybook/src/ui/element/avatar.rs b/crates/ui/src/elements/avatar.rs similarity index 87% rename from crates/storybook/src/ui/element/avatar.rs rename to crates/ui/src/elements/avatar.rs index 83edc73deba7d6d08539c3dec3b66ad9697860c6..068ec7a28a99472775f887a71d79e83de0459114 100644 --- a/crates/storybook/src/ui/element/avatar.rs +++ b/crates/ui/src/elements/avatar.rs @@ -1,9 +1,9 @@ -use crate::prelude::Shape; -use crate::theme::theme; use gpui2::elements::img; use gpui2::style::StyleHelpers; -use gpui2::{ArcCow, IntoElement}; -use gpui2::{Element, ViewContext}; +use gpui2::{ArcCow, Element, IntoElement, ViewContext}; + +use crate::prelude::*; +use crate::theme; #[derive(Element, Clone)] pub struct Avatar { diff --git a/crates/storybook/src/ui/element/details.rs b/crates/ui/src/elements/details.rs similarity index 88% rename from crates/storybook/src/ui/element/details.rs rename to crates/ui/src/elements/details.rs index 5d06862439cd4b8c4ba177f9302b8f4d4f81e65d..f156199f9e8556a1212f8439f6c71cd20725fff9 100644 --- a/crates/storybook/src/ui/element/details.rs +++ b/crates/ui/src/elements/details.rs @@ -1,8 +1,8 @@ -use crate::theme::theme; use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{Element, ViewContext}; -use gpui2::{IntoElement, ParentElement}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::theme; #[derive(Element, Clone)] pub struct Details { diff --git a/crates/storybook/src/ui/element/icon.rs b/crates/ui/src/elements/icon.rs similarity index 95% rename from crates/storybook/src/ui/element/icon.rs rename to crates/ui/src/elements/icon.rs index 5a9e8fb6cb7769f1e951d77a216d6d15646f8dad..dbe30cb4f941c9b4cfb2bf1d3386f768c35c43da 100644 --- a/crates/storybook/src/ui/element/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -1,8 +1,8 @@ -use crate::theme::theme; use gpui2::elements::svg; use gpui2::style::StyleHelpers; -use gpui2::IntoElement; -use gpui2::{Element, ViewContext}; +use gpui2::{Element, IntoElement, ViewContext}; + +use crate::theme; // Icon::Hash // icon(IconAsset::Hash).color(IconColor::Warning) diff --git a/crates/storybook/src/ui/element/icon_button.rs b/crates/ui/src/elements/icon_button.rs similarity index 88% rename from crates/storybook/src/ui/element/icon_button.rs rename to crates/ui/src/elements/icon_button.rs index f82979ab7c6fb871e56ccbf67f23fcde06c1ebbf..4270e0ca5daa3e1dd3655dd54a9e3f48028c7d1c 100644 --- a/crates/storybook/src/ui/element/icon_button.rs +++ b/crates/ui/src/elements/icon_button.rs @@ -1,9 +1,9 @@ -use crate::prelude::{ButtonVariant, InteractionState}; -use crate::theme::theme; -use gpui2::elements::svg; +use gpui2::elements::{div, svg}; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::*; +use crate::theme; #[derive(Element)] pub struct IconButton { diff --git a/crates/storybook/src/ui/element/indicator.rs b/crates/ui/src/elements/indicator.rs similarity index 86% rename from crates/storybook/src/ui/element/indicator.rs rename to crates/ui/src/elements/indicator.rs index b905892f1a592cf460a8ab16e408f7aa76e69c1f..2ee40a57ac67de5740dc3db47be9c23949f941bd 100644 --- a/crates/storybook/src/ui/element/indicator.rs +++ b/crates/ui/src/elements/indicator.rs @@ -1,7 +1,8 @@ -use crate::theme::theme; +use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ViewContext}; +use gpui2::{Element, IntoElement, ViewContext}; + +use crate::theme; #[derive(Element)] pub struct Indicator { diff --git a/crates/storybook/src/ui/element/input.rs b/crates/ui/src/elements/input.rs similarity index 94% rename from crates/storybook/src/ui/element/input.rs rename to crates/ui/src/elements/input.rs index 310287797cfff956650080115059405372c69667..5a3da16fdd5caf24b1a83a8e1f973c29754fa111 100644 --- a/crates/storybook/src/ui/element/input.rs +++ b/crates/ui/src/elements/input.rs @@ -1,8 +1,9 @@ -use crate::prelude::{InputVariant, InteractionState}; -use crate::theme::theme; +use gpui2::elements::div; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::*; +use crate::theme; #[derive(Element)] pub struct Input { diff --git a/crates/storybook/src/ui/element/label.rs b/crates/ui/src/elements/label.rs similarity index 92% rename from crates/storybook/src/ui/element/label.rs rename to crates/ui/src/elements/label.rs index bbbedd0b10879a2183f67e0b0f433695870d09d0..d3e94297ad891f2670a35c76aaf28ab69f0e5de1 100644 --- a/crates/storybook/src/ui/element/label.rs +++ b/crates/ui/src/elements/label.rs @@ -1,8 +1,8 @@ -use crate::theme::theme; use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{Element, ViewContext}; -use gpui2::{IntoElement, ParentElement}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::theme; #[derive(Default, PartialEq, Copy, Clone)] pub enum LabelColor { diff --git a/crates/storybook/src/ui/element/text_button.rs b/crates/ui/src/elements/text_button.rs similarity index 93% rename from crates/storybook/src/ui/element/text_button.rs rename to crates/ui/src/elements/text_button.rs index 24fd1eb5d35898511694a90dde9f101e3f88195b..851efdd4f691db8dbd5f836cf89cb487d9d71920 100644 --- a/crates/storybook/src/ui/element/text_button.rs +++ b/crates/ui/src/elements/text_button.rs @@ -1,8 +1,9 @@ -use crate::prelude::{ButtonVariant, InteractionState}; -use crate::theme::theme; +use gpui2::elements::div; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::*; +use crate::theme; #[derive(Element)] pub struct TextButton { diff --git a/crates/storybook/src/ui/element/tool_divider.rs b/crates/ui/src/elements/tool_divider.rs similarity index 78% rename from crates/storybook/src/ui/element/tool_divider.rs rename to crates/ui/src/elements/tool_divider.rs index a923628defbd3d6622e61b17b660cc8bd986a6d6..2ef29b225f235031974a6fde0728ef95360282f4 100644 --- a/crates/storybook/src/ui/element/tool_divider.rs +++ b/crates/ui/src/elements/tool_divider.rs @@ -1,7 +1,8 @@ -use crate::theme::theme; +use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ViewContext}; +use gpui2::{Element, IntoElement, ViewContext}; + +use crate::theme; #[derive(Element)] pub struct ToolDivider {} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..415cdcbaf383c95b4986c7f54378571b1494e239 --- /dev/null +++ b/crates/ui/src/lib.rs @@ -0,0 +1,14 @@ +#![allow(dead_code, unused_variables)] + +mod components; +mod element_ext; +mod elements; +mod modules; +pub mod prelude; +mod theme; + +pub use components::*; +pub use element_ext::*; +pub use elements::*; +pub use modules::*; +pub use theme::*; diff --git a/crates/storybook/src/ui/module.rs b/crates/ui/src/modules.rs similarity index 100% rename from crates/storybook/src/ui/module.rs rename to crates/ui/src/modules.rs diff --git a/crates/storybook/src/ui/module/chat_panel.rs b/crates/ui/src/modules/chat_panel.rs similarity index 91% rename from crates/storybook/src/ui/module/chat_panel.rs rename to crates/ui/src/modules/chat_panel.rs index de4428826b9074f643326fbc0e643194338f6be0..77c5b2ef20ba8a2de7cc47fead7d715b1b055706 100644 --- a/crates/storybook/src/ui/module/chat_panel.rs +++ b/crates/ui/src/modules/chat_panel.rs @@ -1,11 +1,11 @@ use std::marker::PhantomData; -use crate::theme::theme; -use crate::ui::icon_button; +use gpui2::elements::div; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::{icon_button, theme}; #[derive(Element)] pub struct ChatPanel { diff --git a/crates/storybook/src/ui/module/project_panel.rs b/crates/ui/src/modules/project_panel.rs similarity index 93% rename from crates/storybook/src/ui/module/project_panel.rs rename to crates/ui/src/modules/project_panel.rs index 5a9608d3e3db7bdfa923b1a35e12f2127e390a3e..e17ff4c8edf425801731a37f60d1e05d65f61186 100644 --- a/crates/storybook/src/ui/module/project_panel.rs +++ b/crates/ui/src/modules/project_panel.rs @@ -1,16 +1,13 @@ -use crate::{ - prelude::{InteractionState, ToggleState}, - theme::theme, - ui::{details, input, label, list_item, IconAsset, LabelColor}, -}; -use gpui2::{ - elements::{div, div::ScrollState}, - style::StyleHelpers, - ParentElement, ViewContext, -}; -use gpui2::{Element, IntoElement}; use std::marker::PhantomData; +use gpui2::elements::div; +use gpui2::elements::div::ScrollState; +use gpui2::style::StyleHelpers; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::*; +use crate::{details, input, label, list_item, theme, IconAsset, LabelColor}; + #[derive(Element)] pub struct ProjectPanel { view_type: PhantomData, diff --git a/crates/storybook/src/ui/module/status_bar.rs b/crates/ui/src/modules/status_bar.rs similarity index 96% rename from crates/storybook/src/ui/module/status_bar.rs rename to crates/ui/src/modules/status_bar.rs index f34cfa88efd44f63692acd9c295714ddb6ed9e73..763a585176128fa1e832bfc35fa35e670aeb5e41 100644 --- a/crates/storybook/src/ui/module/status_bar.rs +++ b/crates/ui/src/modules/status_bar.rs @@ -1,10 +1,11 @@ use std::marker::PhantomData; -use crate::theme::{theme, Theme}; -use crate::ui::{icon_button, text_button, tool_divider}; +use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::theme::{theme, Theme}; +use crate::{icon_button, text_button, tool_divider}; #[derive(Default, PartialEq)] pub enum Tool { diff --git a/crates/storybook/src/ui/module/tab_bar.rs b/crates/ui/src/modules/tab_bar.rs similarity index 95% rename from crates/storybook/src/ui/module/tab_bar.rs rename to crates/ui/src/modules/tab_bar.rs index 9b96171f2f3591997aa31c408e50849382ce08b8..08caad310729f66b1f723366994a73c660b447a3 100644 --- a/crates/storybook/src/ui/module/tab_bar.rs +++ b/crates/ui/src/modules/tab_bar.rs @@ -1,12 +1,12 @@ use std::marker::PhantomData; -use crate::prelude::InteractionState; -use crate::theme::theme; -use crate::ui::{icon_button, tab}; +use gpui2::elements::div; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::InteractionState; +use crate::{icon_button, tab, theme}; #[derive(Element)] pub struct TabBar { diff --git a/crates/storybook/src/ui/module/title_bar.rs b/crates/ui/src/modules/title_bar.rs similarity index 95% rename from crates/storybook/src/ui/module/title_bar.rs rename to crates/ui/src/modules/title_bar.rs index a41d56476f1dae379fc93176c3d8ebcfe8377f03..705d0d8866dc757f488cf52e5d162121a2abd0be 100644 --- a/crates/storybook/src/ui/module/title_bar.rs +++ b/crates/ui/src/modules/title_bar.rs @@ -1,11 +1,11 @@ use std::marker::PhantomData; -use crate::prelude::Shape; -use crate::theme::theme; -use crate::ui::{avatar, follow_group, icon_button, text_button, tool_divider}; +use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{elements::div, IntoElement}; -use gpui2::{Element, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::prelude::Shape; +use crate::{avatar, follow_group, icon_button, text_button, theme, tool_divider}; #[derive(Element)] pub struct TitleBar { diff --git a/crates/storybook/src/prelude.rs b/crates/ui/src/prelude.rs similarity index 100% rename from crates/storybook/src/prelude.rs rename to crates/ui/src/prelude.rs diff --git a/crates/storybook/src/theme.rs b/crates/ui/src/theme.rs similarity index 94% rename from crates/storybook/src/theme.rs rename to crates/ui/src/theme.rs index 0a86a61499f6d1d642d56b25cab3caaa0bb509ed..71235625d6dfc3cc7faa9c1f143688b5cb68fa20 100644 --- a/crates/storybook/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -1,9 +1,12 @@ -use gpui2::{ - color::Hsla, element::Element, serde_json, AppContext, IntoElement, Vector2F, ViewContext, - WindowContext, -}; -use serde::{de::Visitor, Deserialize, Deserializer}; -use std::{collections::HashMap, fmt, marker::PhantomData}; +use std::collections::HashMap; +use std::fmt; +use std::marker::PhantomData; + +use gpui2::color::Hsla; +use gpui2::element::Element; +use gpui2::{serde_json, AppContext, IntoElement, Vector2F, ViewContext, WindowContext}; +use serde::de::Visitor; +use serde::{Deserialize, Deserializer}; use theme::ThemeSettings; #[derive(Deserialize, Clone, Default, Debug)] diff --git a/crates/storybook/src/ui/tracker.md b/crates/ui/tracker.md similarity index 100% rename from crates/storybook/src/ui/tracker.md rename to crates/ui/tracker.md From 1e6ac8caf2f312ea71a64cbc01c23a5d8344334f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 21 Sep 2023 20:21:56 -0400 Subject: [PATCH 20/89] `theme::*` -> `crate::theme::*;` --- crates/ui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 415cdcbaf383c95b4986c7f54378571b1494e239..eac16aa776cebda31ea81e578a8d63fc6946ec90 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -11,4 +11,4 @@ pub use components::*; pub use element_ext::*; pub use elements::*; pub use modules::*; -pub use theme::*; +pub use crate::theme::*; From 8440ac3a54ce9743cc797f832474d92ff4b1fe48 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 21 Sep 2023 20:25:25 -0400 Subject: [PATCH 21/89] Fix fmt complaining about order --- crates/ui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index eac16aa776cebda31ea81e578a8d63fc6946ec90..c7bab1e0b0786a9390dcf3612cc87dd80276675f 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -7,8 +7,8 @@ mod modules; pub mod prelude; mod theme; +pub use crate::theme::*; pub use components::*; pub use element_ext::*; pub use elements::*; pub use modules::*; -pub use crate::theme::*; From 02a85b12524ad47fce792c8b17027d6755ec3bbe Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 21 Sep 2023 18:06:00 -0700 Subject: [PATCH 22/89] Add local next LSP adapter --- crates/lsp/src/lsp.rs | 4 +- crates/project/src/project.rs | 20 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/zed/src/languages.rs | 14 +- crates/zed/src/languages/elixir_next.rs | 178 ++++++++++++++++++ 5 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 crates/zed/src/languages/elixir_next.rs diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index dcfce4f1fbafaba606eaf11aacffb1ec967eef76..4aa0e5cd1055a6784ed10b52900bd46a1d936c2d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -712,11 +712,11 @@ impl LanguageServer { } } - pub fn name<'a>(self: &'a Arc) -> &'a str { + pub fn name(&self) -> &str { &self.name } - pub fn capabilities<'a>(self: &'a Arc) -> &'a ServerCapabilities { + pub fn capabilities(&self) -> &ServerCapabilities { &self.capabilities } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b4e698e08a3ba427b0c48582f774434ad1012d1c..98508e1bee6ac776548f1aae2ba3f0af57262d92 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2267,11 +2267,13 @@ impl Project { }; for (_, _, server) in self.language_servers_for_worktree(worktree_id) { + let text = include_text(server.as_ref()).then(|| buffer.read(cx).text()); + server .notify::( lsp::DidSaveTextDocumentParams { text_document: text_document.clone(), - text: None, + text, }, ) .log_err(); @@ -8274,3 +8276,19 @@ async fn wait_for_loading_buffer( receiver.next().await; } } + +fn include_text(server: &lsp::LanguageServer) -> bool { + server + .capabilities() + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + lsp::TextDocumentSyncCapability::Kind(_) => None, + lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(), + }) + .and_then(|save_options| match save_options { + lsp::TextDocumentSyncSaveOptions::Supported(_) => None, + lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text, + }) + .unwrap_or(false) +} diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index e88aee5dcfb6b9b5862ba92e6e8fc32fbc0da8cb..3273d5c6e664fb8ec84af4270222dbcf7b0d673f 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -69,7 +69,7 @@ impl ProjectSymbolsDelegate { &self.external_match_candidates, query, false, - MAX_MATCHES - visible_matches.len(), + MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), &Default::default(), cx.background().clone(), )); diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 75674e78e0b8fa721da7b6c8edee3ce7eaeb006e..0d1c2a9d3687fc9296fd65bd39d4adab80ce2efa 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -72,22 +72,20 @@ pub fn init( ], ); - match settings::get::(cx).next { + match &settings::get::(cx).next { elixir_next::ElixirNextSetting::Off => language( "elixir", tree_sitter_elixir::language(), vec![Arc::new(elixir::ElixirLspAdapter)], ), - elixir_next::ElixirNextSetting::On => language( + elixir_next::ElixirNextSetting::On => todo!(), + elixir_next::ElixirNextSetting::Local { path } => language( "elixir", tree_sitter_elixir::language(), - vec![Arc::new(elixir_next::BundledNextLspAdapter)], + vec![Arc::new(elixir_next::LocalNextLspAdapter { + path: path.clone(), + })], ), - elixir_next::ElixirNextSetting::Local { port } => unimplemented!(), /*language( - "elixir", - tree_sitter_elixir::language(), - vec![Arc::new(elixir_next::LocalNextLspAdapter { port })], - )*/ } language( diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs new file mode 100644 index 0000000000000000000000000000000000000000..a25ada92b8182b9656190597c7fa9bf0170caf83 --- /dev/null +++ b/crates/zed/src/languages/elixir_next.rs @@ -0,0 +1,178 @@ +use anyhow::Result; +use async_trait::async_trait; +pub use language::*; +use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Setting; +use std::{any::Any, path::PathBuf, sync::Arc}; + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct ElixirSettings { + pub next: ElixirNextSetting, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ElixirNextSetting { + Off, + On, + Local { path: String }, +} + +#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] +pub struct ElixirSettingsContent { + next: Option, +} + +impl Setting for ElixirSettings { + const KEY: Option<&'static str> = Some("elixir"); + + type FileContent = ElixirSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} + +pub struct LocalNextLspAdapter { + pub path: String, +} + +#[async_trait] +impl LspAdapter for LocalNextLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("elixir-next-ls".into()) + } + + fn short_name(&self) -> &'static str { + "next-ls" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + Ok(LanguageServerBinary { + path: self.path.clone().into(), + arguments: vec!["--stdio".into()], + }) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + Some(LanguageServerBinary { + path: self.path.clone().into(), + arguments: vec!["--stdio".into()], + }) + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + Some(LanguageServerBinary { + path: self.path.clone().into(), + arguments: vec!["--stdio".into()], + }) + } + + async fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + match completion.kind.zip(completion.detail.as_ref()) { + Some((_, detail)) if detail.starts_with("(function)") => { + let text = detail.strip_prefix("(function) ")?; + let filter_range = 0..text.find('(').unwrap_or(text.len()); + let source = Rope::from(format!("def {text}").as_str()); + let runs = language.highlight_text(&source, 4..4 + text.len()); + return Some(CodeLabel { + text: text.to_string(), + runs, + filter_range, + }); + } + Some((_, detail)) if detail.starts_with("(macro)") => { + let text = detail.strip_prefix("(macro) ")?; + let filter_range = 0..text.find('(').unwrap_or(text.len()); + let source = Rope::from(format!("defmacro {text}").as_str()); + let runs = language.highlight_text(&source, 9..9 + text.len()); + return Some(CodeLabel { + text: text.to_string(), + runs, + filter_range, + }); + } + Some(( + CompletionItemKind::CLASS + | CompletionItemKind::MODULE + | CompletionItemKind::INTERFACE + | CompletionItemKind::STRUCT, + _, + )) => { + let filter_range = 0..completion + .label + .find(" (") + .unwrap_or(completion.label.len()); + let text = &completion.label[filter_range.clone()]; + let source = Rope::from(format!("defmodule {text}").as_str()); + let runs = language.highlight_text(&source, 10..10 + text.len()); + return Some(CodeLabel { + text: completion.label.clone(), + runs, + filter_range, + }); + } + _ => {} + } + + None + } + + async fn label_for_symbol( + &self, + name: &str, + kind: SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + SymbolKind::METHOD | SymbolKind::FUNCTION => { + let text = format!("def {}", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => { + let text = format!("defmodule {}", name); + let filter_range = 10..10 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } +} From 5f6334696a27299760f2a1e3724569a6f59d3d05 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 21 Sep 2023 21:54:59 -0400 Subject: [PATCH 23/89] rename ai crate to assistant crate --- Cargo.lock | 224 +++++++----------- Cargo.toml | 2 +- crates/{ai => assistant}/Cargo.toml | 4 +- crates/{ai => assistant}/README.zmd | 0 crates/{ai => assistant}/features.zmd | 0 .../src/ai.rs => assistant/src/assistant.rs} | 6 +- .../src/assistant_panel.rs} | 0 .../src/assistant_settings.rs | 0 crates/{ai => assistant}/src/codegen.rs | 0 .../{ai => assistant}/src/streaming_diff.rs | 0 crates/quick_action_bar/Cargo.toml | 2 +- .../quick_action_bar/src/quick_action_bar.rs | 2 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 15 files changed, 100 insertions(+), 146 deletions(-) rename crates/{ai => assistant}/Cargo.toml (96%) rename crates/{ai => assistant}/README.zmd (100%) rename crates/{ai => assistant}/features.zmd (100%) rename crates/{ai/src/ai.rs => assistant/src/assistant.rs} (98%) rename crates/{ai/src/assistant.rs => assistant/src/assistant_panel.rs} (100%) rename crates/{ai => assistant}/src/assistant_settings.rs (100%) rename crates/{ai => assistant}/src/codegen.rs (100%) rename crates/{ai => assistant}/src/streaming_diff.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 3cced78c4272e5fc9f571d8023719a1ea56d06d2..58095343f137fb43ae0483330dc6be367ae68a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,50 +79,13 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] -[[package]] -name = "ai" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "client", - "collections", - "ctor", - "editor", - "env_logger 0.9.3", - "fs", - "futures 0.3.28", - "gpui", - "indoc", - "isahc", - "language", - "log", - "menu", - "ordered-float", - "parking_lot 0.11.2", - "project", - "rand 0.8.5", - "regex", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "smol", - "theme", - "tiktoken-rs 0.4.5", - "util", - "uuid 1.4.1", - "workspace", -] - [[package]] name = "alacritty_config" version = "0.1.2-dev" @@ -305,6 +268,43 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assistant" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "client", + "collections", + "ctor", + "editor", + "env_logger 0.9.3", + "fs", + "futures 0.3.28", + "gpui", + "indoc", + "isahc", + "language", + "log", + "menu", + "ordered-float", + "parking_lot 0.11.2", + "project", + "rand 0.8.5", + "regex", + "schemars", + "search", + "serde", + "serde_json", + "settings", + "smol", + "theme", + "tiktoken-rs 0.4.5", + "util", + "uuid 1.4.1", + "workspace", +] + [[package]] name = "async-broadcast" version = "0.4.1" @@ -2141,7 +2141,7 @@ dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn 1.0.109", ] @@ -3234,7 +3234,7 @@ dependencies = [ "indexmap 1.9.3", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", "tracing", ] @@ -3355,9 +3355,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -3651,7 +3651,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", "windows-sys", ] @@ -3708,8 +3708,8 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", - "rustix 0.38.13", + "hermit-abi 0.3.3", + "rustix 0.38.14", "windows-sys", ] @@ -4277,9 +4277,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" [[package]] name = "matrixmultiply" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" dependencies = [ "autocfg", "rawpointer", @@ -4806,7 +4806,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", ] @@ -4843,7 +4843,7 @@ dependencies = [ "rmp", "rmpv", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", ] [[package]] @@ -5144,11 +5144,11 @@ dependencies = [ [[package]] name = "pathfinder_simd" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" +checksum = "0444332826c70dc47be74a7c6a5fc44e23a7905ad6858d4162b658320455ef93" dependencies = [ - "rustc_version 0.3.3", + "rustc_version", ] [[package]] @@ -5183,17 +5183,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "pest" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - [[package]] name = "petgraph" version = "0.6.4" @@ -5728,7 +5717,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ - "ai", + "assistant", "editor", "gpui", "search", @@ -5864,9 +5853,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -5874,14 +5863,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -6331,22 +6318,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.18", + "semver", ] [[package]] @@ -6381,9 +6359,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno 0.3.3", @@ -6785,30 +6763,12 @@ dependencies = [ "zed", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "seq-macro" version = "0.2.2" @@ -6978,9 +6938,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -7153,9 +7113,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smol" @@ -7436,15 +7396,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" +checksum = "05d11eec9fbe2bc8bc71e7349f0e7534db9a96d961fb9f302574275b7880ad06" [[package]] name = "sval_buffer" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +checksum = "6b7451f69a93c5baf2653d5aa8bb4178934337f16c22830a50b06b386f72d761" dependencies = [ "sval", "sval_ref", @@ -7452,18 +7412,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +checksum = "c34f5a2cc12b4da2adfb59d5eedfd9b174a23cc3fae84cec71dcbcd9302068f5" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +checksum = "2f578b2301341e246d00b35957f2952c4ec554ad9c7cfaee10bc86bc92896578" dependencies = [ "itoa", "ryu", @@ -7472,9 +7432,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +checksum = "8346c00f5dc6efe18bea8d13c1f7ca4f112b20803434bf3657ac17c0f74cbc4b" dependencies = [ "itoa", "ryu", @@ -7483,18 +7443,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +checksum = "6617cc89952f792aebc0f4a1a76bc51e80c70b18c491bd52215c7989c4c3dd06" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +checksum = "fe3d1e59f023341d9af75d86f3bc148a6704f3f831eef0dd90bbe9cb445fa024" dependencies = [ "serde", "sval", @@ -7645,7 +7605,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.13", + "rustix 0.38.14", "windows-sys", ] @@ -8047,9 +8007,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes 1.5.0", "futures-core", @@ -8148,7 +8108,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.8", + "tokio-util 0.7.9", "tower-layer", "tower-service", "tracing", @@ -8593,12 +8553,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "unicase" version = "2.7.0" @@ -8667,9 +8621,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode_categories" @@ -9382,7 +9336,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.13", + "rustix 0.38.14", ] [[package]] @@ -9467,9 +9421,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi 0.3.9", ] @@ -9795,8 +9749,8 @@ name = "zed" version = "0.106.0" dependencies = [ "activity_indicator", - "ai", "anyhow", + "assistant", "async-compression", "async-recursion 0.3.2", "async-tar", diff --git a/Cargo.toml b/Cargo.toml index c1876434ad46fc7f8c5eddf59d46255fcde4136e..1c05c810f6372e5651cd85234255ef25cfdd3a02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = [ "crates/activity_indicator", - "crates/ai", + "crates/assistant", "crates/audio", "crates/auto_update", "crates/breadcrumbs", diff --git a/crates/ai/Cargo.toml b/crates/assistant/Cargo.toml similarity index 96% rename from crates/ai/Cargo.toml rename to crates/assistant/Cargo.toml index 8002b0d35d92c8da2d244d6bbd1229a0de20bf5b..a3ee4125489549b8f733d8caee73d5e0042a0cb8 100644 --- a/crates/ai/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "ai" +name = "assistant" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/ai.rs" +path = "src/assistant.rs" doctest = false [dependencies] diff --git a/crates/ai/README.zmd b/crates/assistant/README.zmd similarity index 100% rename from crates/ai/README.zmd rename to crates/assistant/README.zmd diff --git a/crates/ai/features.zmd b/crates/assistant/features.zmd similarity index 100% rename from crates/ai/features.zmd rename to crates/assistant/features.zmd diff --git a/crates/ai/src/ai.rs b/crates/assistant/src/assistant.rs similarity index 98% rename from crates/ai/src/ai.rs rename to crates/assistant/src/assistant.rs index dfd9a523b443b79076bbd0ef699a3ff154a0f4bc..48e31bc55a5076442d40b52c614e30161f9d1bf6 100644 --- a/crates/ai/src/ai.rs +++ b/crates/assistant/src/assistant.rs @@ -1,10 +1,10 @@ -pub mod assistant; +pub mod assistant_panel; mod assistant_settings; mod codegen; mod streaming_diff; use anyhow::{anyhow, Result}; -pub use assistant::AssistantPanel; +pub use assistant_panel::AssistantPanel; use assistant_settings::OpenAIModel; use chrono::{DateTime, Local}; use collections::HashMap; @@ -196,7 +196,7 @@ struct OpenAIChoice { } pub fn init(cx: &mut AppContext) { - assistant::init(cx); + assistant_panel::init(cx); } pub async fn stream_completion( diff --git a/crates/ai/src/assistant.rs b/crates/assistant/src/assistant_panel.rs similarity index 100% rename from crates/ai/src/assistant.rs rename to crates/assistant/src/assistant_panel.rs diff --git a/crates/ai/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs similarity index 100% rename from crates/ai/src/assistant_settings.rs rename to crates/assistant/src/assistant_settings.rs diff --git a/crates/ai/src/codegen.rs b/crates/assistant/src/codegen.rs similarity index 100% rename from crates/ai/src/codegen.rs rename to crates/assistant/src/codegen.rs diff --git a/crates/ai/src/streaming_diff.rs b/crates/assistant/src/streaming_diff.rs similarity index 100% rename from crates/ai/src/streaming_diff.rs rename to crates/assistant/src/streaming_diff.rs diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 1f8ec4e92b973e0b94c99fe8259aad2811bf7c8f..c2f54e10786d5cece21cdbf80153f3f28138a628 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs" doctest = false [dependencies] -ai = { path = "../ai" } +assistant = { path = "../assistant" } editor = { path = "../editor" } gpui = { path = "../gpui" } search = { path = "../search" } diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 8a40325203cef1dbb7a0961d0eaad5c1be6f2666..7e0be4d097d3d41cb87930f73d387ca6a86cb7bd 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -1,4 +1,4 @@ -use ai::{assistant::InlineAssist, AssistantPanel}; +use assistant::{assistant_panel::InlineAssist, AssistantPanel}; use editor::Editor; use gpui::{ elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg}, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7478f5cca1a85416e844139a11decc50c74949e3..fc06e8865a1d6b97d22981d960c7cce72d3195eb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -50,7 +50,7 @@ language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } -ai = { path = "../ai" } +assistant = { path = "../assistant" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime",optional = true } project = { path = "../project" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d22e26c1f56b7cb0940dce2280b8fd4c5ead4011..0032c24cbbdf7bf250719540f2488178933f3f6d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -161,7 +161,7 @@ fn main() { vim::init(cx); terminal_view::init(cx); copilot::init(copilot_language_server_id, http.clone(), node_runtime, cx); - ai::init(cx); + assistant::init(cx); component_test::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d968a92646c1d3854193e28f196d7f5597268bc8..dcdbd004b7c757657790ca41574d6d529bfc98fa 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5,7 +5,7 @@ pub mod only_instance; #[cfg(any(test, feature = "test-support"))] pub mod test; -use ai::AssistantPanel; +use assistant::AssistantPanel; use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; From 66358f2900cf3c2ec2d42b169d81b012c8642e7e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Sep 2023 22:41:53 -0400 Subject: [PATCH 24/89] Update storybook to support stories for individual components (#3010) This PR updates the `storybook` with support for adding stories for individual components. ### Motivation Right now we just have one story in the storybook that renders an entire `WorkspaceElement`. While iterating on the various UI components, it will be helpful to be able to create stories of those components just by themselves. This is especially true for components that have a number of different states, as we can render the components in all of the various states in a single layout. ### Explanation We achieve this by adding a simple CLI to the storybook. The `storybook` binary now accepts an optional `[STORY]` parameter that can be used to indicate which story should be loaded. If this parameter is not provided, it will load the workspace story as it currently does. Passing a story name will load the corresponding story, if it exists. For example: ``` cargo run -- elements/avatar ``` Screenshot 2023-09-21 at 10 29 52 PM ``` cargo run -- components/facepile ``` Screenshot 2023-09-21 at 10 30 07 PM Release Notes: - N/A --- Cargo.lock | 1 + crates/storybook/Cargo.toml | 3 +- crates/storybook/src/stories.rs | 2 + crates/storybook/src/stories/components.rs | 1 + .../src/stories/components/facepile.rs | 69 +++++++++++++++++ crates/storybook/src/stories/elements.rs | 1 + .../storybook/src/stories/elements/avatar.rs | 41 ++++++++++ crates/storybook/src/storybook.rs | 75 +++++++++++++++---- crates/storybook/src/workspace.rs | 6 +- 9 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 crates/storybook/src/stories.rs create mode 100644 crates/storybook/src/stories/components.rs create mode 100644 crates/storybook/src/stories/components/facepile.rs create mode 100644 crates/storybook/src/stories/elements.rs create mode 100644 crates/storybook/src/stories/elements/avatar.rs diff --git a/Cargo.lock b/Cargo.lock index 05b29aabf400e9843dc45041c956044c3bb7658c..9d6358edb496a7b01d6b00c951aaed6dba87bf0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7390,6 +7390,7 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", + "clap 3.2.25", "gpui2", "log", "rust-embed", diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index c1096a87babed86dd08e513ffbd2f629d6e44c90..882a1a1bdb14b98907728543bd0552fb50667441 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -9,8 +9,9 @@ name = "storybook" path = "src/storybook.rs" [dependencies] -gpui2 = { path = "../gpui2" } anyhow.workspace = true +clap = { version = "3.1", features = ["derive"] } +gpui2 = { path = "../gpui2" } log.workspace = true rust-embed.workspace = true serde.workspace = true diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs new file mode 100644 index 0000000000000000000000000000000000000000..57674c3c70b8aaeeef62f5717faecb040f87174b --- /dev/null +++ b/crates/storybook/src/stories.rs @@ -0,0 +1,2 @@ +pub mod components; +pub mod elements; diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs new file mode 100644 index 0000000000000000000000000000000000000000..5e3d309419beecf44de6e396e04cebcd80cba716 --- /dev/null +++ b/crates/storybook/src/stories/components.rs @@ -0,0 +1 @@ +pub mod facepile; diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs new file mode 100644 index 0000000000000000000000000000000000000000..6bde4539ed5aa4f1187d32b0879f025e89a37b69 --- /dev/null +++ b/crates/storybook/src/stories/components/facepile.rs @@ -0,0 +1,69 @@ +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext}; +use ui::{avatar, theme}; +use ui::{facepile, prelude::*}; + +#[derive(Element, Default)] +pub struct FacepileStory {} + +impl FacepileStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .size_full() + .flex() + .flex_col() + .pt_2() + .px_4() + .font("Zed Mono Extended") + .fill(rgb::(0x282c34)) + .child( + div() + .text_2xl() + .text_color(rgb::(0xffffff)) + .child(std::any::type_name::()), + ) + .child( + div() + .flex() + .gap_3() + .child(facepile(vec![avatar( + "https://avatars.githubusercontent.com/u/1714999?v=4", + )])) + .child(facepile(vec![ + avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), + ])) + .child(facepile(vec![ + avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), + ])), + ) + .child( + div() + .flex() + .gap_3() + .child(facepile(vec![avatar( + "https://avatars.githubusercontent.com/u/1714999?v=4", + ) + .shape(Shape::RoundedRectangle)])) + .child(facepile(vec![ + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), + ])) + .child(facepile(vec![ + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), + ])), + ) + } +} diff --git a/crates/storybook/src/stories/elements.rs b/crates/storybook/src/stories/elements.rs new file mode 100644 index 0000000000000000000000000000000000000000..124369cf9d0b26354048b6b4515d05e9fb533a07 --- /dev/null +++ b/crates/storybook/src/stories/elements.rs @@ -0,0 +1 @@ +pub mod avatar; diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs new file mode 100644 index 0000000000000000000000000000000000000000..3239dbec9c0a8141ccc2cefa13151bf85f9a22e6 --- /dev/null +++ b/crates/storybook/src/stories/elements/avatar.rs @@ -0,0 +1,41 @@ +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext}; +use ui::prelude::*; +use ui::{avatar, theme}; + +#[derive(Element, Default)] +pub struct AvatarStory {} + +impl AvatarStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .size_full() + .flex() + .flex_col() + .pt_2() + .px_4() + .font("Zed Mono Extended") + .fill(rgb::(0x282c34)) + .child( + div() + .text_2xl() + .text_color(rgb::(0xffffff)) + .child(std::any::type_name::()), + ) + .child( + div() + .flex() + .gap_3() + .child(avatar( + "https://avatars.githubusercontent.com/u/1714999?v=4", + )) + .child( + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), + ), + ) + } +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 92d178fad2728cb1e5e3a5d44caaaf3a5b2347be..089ff33049e3acccc91a1127e778935c86523802 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -1,25 +1,66 @@ #![allow(dead_code, unused_variables)] +mod collab_panel; +mod stories; +mod workspace; + +use std::str::FromStr; + use ::theme as legacy_theme; -use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds}; +use clap::Parser; +use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds}; use legacy_theme::ThemeSettings; use log::LevelFilter; use settings::{default_settings, SettingsStore}; use simplelog::SimpleLogger; +use stories::components::facepile::FacepileStory; +use stories::elements::avatar::AvatarStory; use ui::{ElementExt, Theme}; -mod collab_panel; -mod workspace; - gpui2::actions! { storybook, [ToggleInspector] } +#[derive(Debug, Clone, Copy)] +enum Story { + Element(ElementStory), + Component(ComponentStory), +} + +impl FromStr for Story { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_ascii_lowercase().as_str() { + "elements/avatar" => Ok(Self::Element(ElementStory::Avatar)), + "components/facepile" => Ok(Self::Component(ComponentStory::Facepile)), + _ => Err(anyhow!("story not found for '{s}'")), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum ElementStory { + Avatar, +} + +#[derive(Debug, Clone, Copy)] +enum ComponentStory { + Facepile, +} + +#[derive(Parser)] +struct Args { + story: Option, +} + fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); - gpui2::App::new(Assets).unwrap().run(|cx| { + let args = Args::parse(); + + gpui2::App::new(Assets).unwrap().run(move |cx| { let mut store = SettingsStore::default(); store .set_default_settings(default_settings().as_ref(), cx) @@ -34,19 +75,27 @@ fn main() { center: true, ..Default::default() }, - |cx| { - view(|cx| { - // cx.enable_inspector(); - storybook(&mut ViewContext::new(cx)) - }) + |cx| match args.story { + Some(Story::Element(ElementStory::Avatar)) => { + view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default())) + } + Some(Story::Component(ComponentStory::Facepile)) => { + view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default())) + } + None => { + view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default())) + } }, ); cx.platform().activate(true); }); } -fn storybook(cx: &mut ViewContext) -> impl Element { - workspace().themed(current_theme(cx)) +fn render_story>( + cx: &mut ViewContext, + story: S, +) -> impl Element { + story.into_element().themed(current_theme(cx)) } // Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct. @@ -69,7 +118,7 @@ fn current_theme(cx: &mut ViewContext) -> Theme { use anyhow::{anyhow, Result}; use gpui2::AssetSource; use rust_embed::RustEmbed; -use workspace::workspace; +use workspace::WorkspaceElement; #[derive(RustEmbed)] #[folder = "../../assets"] diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs index 95152ec8328f26e0749215a7fe55694f7247da2f..58c5fed62dad748f98f225d0acb6d1d77b3090cd 100644 --- a/crates/storybook/src/workspace.rs +++ b/crates/storybook/src/workspace.rs @@ -6,16 +6,12 @@ use gpui2::{ use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar}; #[derive(Element, Default)] -struct WorkspaceElement { +pub struct WorkspaceElement { left_scroll_state: ScrollState, right_scroll_state: ScrollState, tab_bar_scroll_state: ScrollState, } -pub fn workspace() -> impl Element { - WorkspaceElement::default() -} - impl WorkspaceElement { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); From 48e151495fd84888494636b3d9690ccbe3250751 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 21 Sep 2023 22:44:56 -0400 Subject: [PATCH 25/89] introduce ai crate with completion providers --- Cargo.lock | 15 ++ Cargo.toml | 1 + crates/ai/Cargo.toml | 21 +++ crates/ai/src/ai.rs | 1 + crates/ai/src/completion.rs | 212 ++++++++++++++++++++++++ crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant.rs | 192 +-------------------- crates/assistant/src/assistant_panel.rs | 9 +- crates/assistant/src/codegen.rs | 59 +------ crates/zed/src/zed.rs | 4 +- 10 files changed, 273 insertions(+), 242 deletions(-) create mode 100644 crates/ai/Cargo.toml create mode 100644 crates/ai/src/ai.rs create mode 100644 crates/ai/src/completion.rs diff --git a/Cargo.lock b/Cargo.lock index 58095343f137fb43ae0483330dc6be367ae68a0f..103b9c2c2a730b23f774b67b1d859ba55debafa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "ai" +version = "0.1.0" +dependencies = [ + "anyhow", + "ctor", + "futures 0.3.28", + "gpui", + "isahc", + "regex", + "serde", + "serde_json", +] + [[package]] name = "alacritty_config" version = "0.1.2-dev" @@ -272,6 +286,7 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" name = "assistant" version = "0.1.0" dependencies = [ + "ai", "anyhow", "chrono", "client", diff --git a/Cargo.toml b/Cargo.toml index 1c05c810f6372e5651cd85234255ef25cfdd3a02..e86960daf43de1de3358248cae191ec8bb076700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/activity_indicator", + "crates/ai", "crates/assistant", "crates/audio", "crates/auto_update", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c4e129c1f5eb497c6e25bbaed60a3e30bebdcc51 --- /dev/null +++ b/crates/ai/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ai" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/ai.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +anyhow.workspace = true +futures.workspace = true +isahc.workspace = true +regex.workspace = true +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +ctor.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs new file mode 100644 index 0000000000000000000000000000000000000000..c893d109abe3168d0aa5d2afecc4bcf15ad69ec9 --- /dev/null +++ b/crates/ai/src/ai.rs @@ -0,0 +1 @@ +pub mod completion; diff --git a/crates/ai/src/completion.rs b/crates/ai/src/completion.rs new file mode 100644 index 0000000000000000000000000000000000000000..170b2268f9ed1132fad1bfe69194d8cc7a2e91bf --- /dev/null +++ b/crates/ai/src/completion.rs @@ -0,0 +1,212 @@ +use anyhow::{anyhow, Result}; +use futures::{ + future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt, + Stream, StreamExt, +}; +use gpui::executor::Background; +use isahc::{http::StatusCode, Request, RequestExt}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + io, + sync::Arc, +}; + +pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; + +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Role { + User, + Assistant, + System, +} + +impl Role { + pub fn cycle(&mut self) { + *self = match self { + Role::User => Role::Assistant, + Role::Assistant => Role::System, + Role::System => Role::User, + } + } +} + +impl Display for Role { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Role::User => write!(f, "User"), + Role::Assistant => write!(f, "Assistant"), + Role::System => write!(f, "System"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct RequestMessage { + pub role: Role, + pub content: String, +} + +#[derive(Debug, Default, Serialize)] +pub struct OpenAIRequest { + pub model: String, + pub messages: Vec, + pub stream: bool, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ResponseMessage { + pub role: Option, + pub content: Option, +} + +#[derive(Deserialize, Debug)] +pub struct OpenAIUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} + +#[derive(Deserialize, Debug)] +pub struct ChatChoiceDelta { + pub index: u32, + pub delta: ResponseMessage, + pub finish_reason: Option, +} + +#[derive(Deserialize, Debug)] +pub struct OpenAIResponseStreamEvent { + pub id: Option, + pub object: String, + pub created: u32, + pub model: String, + pub choices: Vec, + pub usage: Option, +} + +pub async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + let json_data = serde_json::to_string(&request)?; + let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_data)? + .send_async() + .await?; + + let status = response.status(); + if status == StatusCode::OK { + executor + .spawn(async move { + let mut lines = BufReader::new(response.body_mut()).lines(); + + fn parse_line( + line: Result, + ) -> Result> { + if let Some(data) = line?.strip_prefix("data: ") { + let event = serde_json::from_str(&data)?; + Ok(Some(event)) + } else { + Ok(None) + } + } + + while let Some(line) = lines.next().await { + if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); + if tx.unbounded_send(event).is_err() { + break; + } + + if done { + break; + } + } + } + + anyhow::Ok(()) + }) + .detach(); + + Ok(rx) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + #[derive(Deserialize)] + struct OpenAIResponse { + error: OpenAIError, + } + + #[derive(Deserialize)] + struct OpenAIError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } + } +} + +pub trait CompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>>; +} + +pub struct OpenAICompletionProvider { + api_key: String, + executor: Arc, +} + +impl OpenAICompletionProvider { + pub fn new(api_key: String, executor: Arc) -> Self { + Self { api_key, executor } + } +} + +impl CompletionProvider for OpenAICompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>> { + let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt); + async move { + let response = request.await?; + let stream = response + .filter_map(|response| async move { + match response { + Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }) + .boxed(); + Ok(stream) + } + .boxed() + } +} diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index a3ee4125489549b8f733d8caee73d5e0042a0cb8..5d141b32d5a448c43a9db96f0879461709963697 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -9,6 +9,7 @@ path = "src/assistant.rs" doctest = false [dependencies] +ai = { path = "../ai" } client = { path = "../client" } collections = { path = "../collections"} editor = { path = "../editor" } diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 48e31bc55a5076442d40b52c614e30161f9d1bf6..258684db47096bac2d3df33d0289462dbc841214 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -3,37 +3,20 @@ mod assistant_settings; mod codegen; mod streaming_diff; -use anyhow::{anyhow, Result}; +use ai::completion::Role; +use anyhow::Result; pub use assistant_panel::AssistantPanel; use assistant_settings::OpenAIModel; use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; -use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; -use gpui::{executor::Background, AppContext}; -use isahc::{http::StatusCode, Request, RequestExt}; +use futures::StreamExt; +use gpui::AppContext; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::{ - cmp::Reverse, - ffi::OsStr, - fmt::{self, Display}, - io, - path::PathBuf, - sync::Arc, -}; +use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; use util::paths::CONVERSATIONS_DIR; -const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; - -// Data types for chat completion requests -#[derive(Debug, Default, Serialize)] -pub struct OpenAIRequest { - model: String, - messages: Vec, - stream: bool, -} - #[derive( Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] @@ -116,175 +99,10 @@ impl SavedConversationMetadata { } } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -struct RequestMessage { - role: Role, - content: String, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessage { - role: Option, - content: Option, -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -enum Role { - User, - Assistant, - System, -} - -impl Role { - pub fn cycle(&mut self) { - *self = match self { - Role::User => Role::Assistant, - Role::Assistant => Role::System, - Role::System => Role::User, - } - } -} - -impl Display for Role { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Role::User => write!(f, "User"), - Role::Assistant => write!(f, "Assistant"), - Role::System => write!(f, "System"), - } - } -} - -#[derive(Deserialize, Debug)] -pub struct OpenAIResponseStreamEvent { - pub id: Option, - pub object: String, - pub created: u32, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u32, - pub completion_tokens: u32, - pub total_tokens: u32, -} - -#[derive(Deserialize, Debug)] -pub struct ChatChoiceDelta { - pub index: u32, - pub delta: ResponseMessage, - pub finish_reason: Option, -} - -#[derive(Deserialize, Debug)] -struct OpenAIUsage { - prompt_tokens: u64, - completion_tokens: u64, - total_tokens: u64, -} - -#[derive(Deserialize, Debug)] -struct OpenAIChoice { - text: String, - index: u32, - logprobs: Option, - finish_reason: Option, -} - pub fn init(cx: &mut AppContext) { assistant_panel::init(cx); } -pub async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - let json_data = serde_json::to_string(&request)?; - let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body(json_data)? - .send_async() - .await?; - - let status = response.status(); - if status == StatusCode::OK { - executor - .spawn(async move { - let mut lines = BufReader::new(response.body_mut()).lines(); - - fn parse_line( - line: Result, - ) -> Result> { - if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; - Ok(Some(event)) - } else { - Ok(None) - } - } - - while let Some(line) = lines.next().await { - if let Some(event) = parse_line(line).transpose() { - let done = event.as_ref().map_or(false, |event| { - event - .choices - .last() - .map_or(false, |choice| choice.finish_reason.is_some()) - }); - if tx.unbounded_send(event).is_err() { - break; - } - - if done { - break; - } - } - } - - anyhow::Ok(()) - }) - .detach(); - - Ok(rx) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - #[derive(Deserialize)] - struct OpenAIResponse { - error: OpenAIError, - } - - #[derive(Deserialize)] - struct OpenAIError { - message: String, - } - - match serde_json::from_str::(&body) { - Ok(response) if !response.error.message.is_empty() => Err(anyhow!( - "Failed to connect to OpenAI API: {}", - response.error.message, - )), - - _ => Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )), - } - } -} - #[cfg(test)] #[ctor::ctor] fn init_logger() { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 263382c03e0fc5e680c1c011e2c7fea0d7c555ae..42e5fb78979a6b8136c5c60d29e38e064df3435d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,8 +1,11 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider}, - stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, - Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, + codegen::{self, Codegen, CodegenKind}, + MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, +}; +use ai::completion::{ + stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index e7da46cdf95f50927f0f9350f45dfb361bdfbd2e..e956d722606f6db27c73385d7cf54d58bc82958b 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -1,59 +1,14 @@ -use crate::{ - stream_completion, - streaming_diff::{Hunk, StreamingDiff}, - OpenAIRequest, -}; +use crate::streaming_diff::{Hunk, StreamingDiff}; +use ai::completion::{CompletionProvider, OpenAIRequest}; use anyhow::Result; use editor::{ multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; -use futures::{ - channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt, -}; -use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use gpui::{Entity, ModelContext, ModelHandle, Task}; use language::{Rope, TransactionId}; use std::{cmp, future, ops::Range, sync::Arc}; -pub trait CompletionProvider { - fn complete( - &self, - prompt: OpenAIRequest, - ) -> BoxFuture<'static, Result>>>; -} - -pub struct OpenAICompletionProvider { - api_key: String, - executor: Arc, -} - -impl OpenAICompletionProvider { - pub fn new(api_key: String, executor: Arc) -> Self { - Self { api_key, executor } - } -} - -impl CompletionProvider for OpenAICompletionProvider { - fn complete( - &self, - prompt: OpenAIRequest, - ) -> BoxFuture<'static, Result>>> { - let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt); - async move { - let response = request.await?; - let stream = response - .filter_map(|response| async move { - match response { - Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)), - Err(error) => Some(Err(error)), - } - }) - .boxed(); - Ok(stream) - } - .boxed() - } -} - pub enum Event { Finished, Undone, @@ -397,13 +352,17 @@ fn strip_markdown_codeblock( #[cfg(test)] mod tests { use super::*; - use futures::stream; + use futures::{ + future::BoxFuture, + stream::{self, BoxStream}, + }; use gpui::{executor::Deterministic, TestAppContext}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use parking_lot::Mutex; use rand::prelude::*; use settings::SettingsStore; + use smol::future::FutureExt; #[gpui::test(iterations = 10)] async fn test_transform_autoindent( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index dcdbd004b7c757657790ca41574d6d529bfc98fa..bdf060205a7895a3e78736edf43bf1df03714fbc 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5,9 +5,9 @@ pub mod only_instance; #[cfg(any(test, feature = "test-support"))] pub mod test; -use assistant::AssistantPanel; use anyhow::Context; use assets::Assets; +use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; pub use client; use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut @@ -2418,7 +2418,7 @@ mod tests { pane::init(cx); project_panel::init((), cx); terminal_view::init(cx); - ai::init(cx); + assistant::init(cx); app_state }) } From 5083ab7694d7bc99ddd9c41f76c6c2313d9d6cac Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Sep 2023 23:42:18 -0400 Subject: [PATCH 26/89] Add `TrafficLights` component (#3011) This PR adds a `TrafficLights` component for GPUI2. Screenshot 2023-09-21 at 11 32 10 PM Release Notes: - N/A --- crates/storybook/src/stories/components.rs | 1 + .../src/stories/components/traffic_lights.rs | 29 +++++++++++++++++ crates/storybook/src/storybook.rs | 6 ++++ crates/ui/src/components.rs | 2 ++ crates/ui/src/components/traffic_lights.rs | 30 ++++++++++++++++++ crates/ui/src/modules/title_bar.rs | 31 ++----------------- 6 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 crates/storybook/src/stories/components/traffic_lights.rs create mode 100644 crates/ui/src/components/traffic_lights.rs diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs index 5e3d309419beecf44de6e396e04cebcd80cba716..89ee5a92eae848dbd31721f378472cd2615f942a 100644 --- a/crates/storybook/src/stories/components.rs +++ b/crates/storybook/src/stories/components.rs @@ -1 +1,2 @@ pub mod facepile; +pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/traffic_lights.rs b/crates/storybook/src/stories/components/traffic_lights.rs new file mode 100644 index 0000000000000000000000000000000000000000..252caf99f64b5433ac33f9c2b45c551cf1473409 --- /dev/null +++ b/crates/storybook/src/stories/components/traffic_lights.rs @@ -0,0 +1,29 @@ +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext}; +use ui::{theme, traffic_lights}; + +#[derive(Element, Default)] +pub struct TrafficLightsStory {} + +impl TrafficLightsStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .size_full() + .flex() + .flex_col() + .pt_2() + .px_4() + .font("Zed Mono Extended") + .fill(rgb::(0x282c34)) + .child( + div() + .text_2xl() + .text_color(rgb::(0xffffff)) + .child(std::any::type_name::()), + ) + .child(traffic_lights()) + } +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 089ff33049e3acccc91a1127e778935c86523802..b72c4a5681e99b48e326673464f9510e24bbe531 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -14,6 +14,7 @@ use log::LevelFilter; use settings::{default_settings, SettingsStore}; use simplelog::SimpleLogger; use stories::components::facepile::FacepileStory; +use stories::components::traffic_lights::TrafficLightsStory; use stories::elements::avatar::AvatarStory; use ui::{ElementExt, Theme}; @@ -35,6 +36,7 @@ impl FromStr for Story { match s.to_ascii_lowercase().as_str() { "elements/avatar" => Ok(Self::Element(ElementStory::Avatar)), "components/facepile" => Ok(Self::Component(ComponentStory::Facepile)), + "components/traffic_lights" => Ok(Self::Component(ComponentStory::TrafficLights)), _ => Err(anyhow!("story not found for '{s}'")), } } @@ -48,6 +50,7 @@ enum ElementStory { #[derive(Debug, Clone, Copy)] enum ComponentStory { Facepile, + TrafficLights, } #[derive(Parser)] @@ -82,6 +85,9 @@ fn main() { Some(Story::Component(ComponentStory::Facepile)) => { view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default())) } + Some(Story::Component(ComponentStory::TrafficLights)) => view(|cx| { + render_story(&mut ViewContext::new(cx), TrafficLightsStory::default()) + }), None => { view(|cx| render_story(&mut ViewContext::new(cx), WorkspaceElement::default())) } diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index a82d28eb8cb2539cef985c12a59ba712b97699ae..b400a491e4434883ab9d2e65c8a4dc309c9cdca3 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -2,11 +2,13 @@ mod facepile; mod follow_group; mod list_item; mod tab; +mod traffic_lights; pub use facepile::*; pub use follow_group::*; pub use list_item::*; pub use tab::*; +pub use traffic_lights::*; use std::marker::PhantomData; use std::rc::Rc; diff --git a/crates/ui/src/components/traffic_lights.rs b/crates/ui/src/components/traffic_lights.rs new file mode 100644 index 0000000000000000000000000000000000000000..128af9976f71c47684731838e9965b3d727ed1ee --- /dev/null +++ b/crates/ui/src/components/traffic_lights.rs @@ -0,0 +1,30 @@ +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{Element, Hsla, IntoElement, ParentElement, ViewContext}; + +use crate::theme; + +#[derive(Element)] +pub struct TrafficLights {} + +pub fn traffic_lights() -> TrafficLights { + TrafficLights {} +} + +impl TrafficLights { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .flex() + .items_center() + .gap_2() + .child(traffic_light(theme.lowest.negative.default.foreground)) + .child(traffic_light(theme.lowest.warning.default.foreground)) + .child(traffic_light(theme.lowest.positive.default.foreground)) + } +} + +fn traffic_light>(fill: C) -> div::Div { + div().w_3().h_3().rounded_full().fill(fill.into()) +} diff --git a/crates/ui/src/modules/title_bar.rs b/crates/ui/src/modules/title_bar.rs index 705d0d8866dc757f488cf52e5d162121a2abd0be..82c5c0234b8f3f449bed331a84f88de0fa16fcc0 100644 --- a/crates/ui/src/modules/title_bar.rs +++ b/crates/ui/src/modules/title_bar.rs @@ -5,7 +5,7 @@ use gpui2::style::StyleHelpers; use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use crate::prelude::Shape; -use crate::{avatar, follow_group, icon_button, text_button, theme, tool_divider}; +use crate::{avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights}; #[derive(Element)] pub struct TitleBar { @@ -40,34 +40,7 @@ impl TitleBar { .h_full() .gap_4() .px_2() - // === Traffic Lights === // - .child( - div() - .flex() - .items_center() - .gap_2() - .child( - div() - .w_3() - .h_3() - .rounded_full() - .fill(theme.lowest.positive.default.foreground), - ) - .child( - div() - .w_3() - .h_3() - .rounded_full() - .fill(theme.lowest.warning.default.foreground), - ) - .child( - div() - .w_3() - .h_3() - .rounded_full() - .fill(theme.lowest.negative.default.foreground), - ), - ) + .child(traffic_lights()) // === Project Info === // .child( div() From f54634aeb234c0881f34682c04db1054b1dcb38b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 21 Sep 2023 23:46:06 -0400 Subject: [PATCH 27/89] Bring UI crate up to date --- crates/ui/doc/elevation.md | 57 ++++++ crates/ui/src/components.rs | 4 + crates/ui/src/components/list_item.rs | 72 ++++--- .../ui/src/components/list_section_header.rs | 88 +++++++++ crates/ui/src/components/palette_item.rs | 63 +++++++ crates/ui/src/elements/icon.rs | 45 ++--- crates/ui/src/elements/label.rs | 34 +++- crates/ui/src/lib.rs | 7 + crates/ui/src/modules.rs | 14 +- crates/ui/src/modules/list.rs | 64 +++++++ crates/ui/src/modules/palette.rs | 124 ++++++++++++ crates/ui/src/modules/project_panel.rs | 94 ---------- crates/ui/src/prelude.rs | 15 ++ crates/ui/src/static_data.rs | 166 ++++++++++++++++ crates/ui/src/templates.rs | 17 ++ .../src/{modules => templates}/chat_panel.rs | 8 +- crates/ui/src/templates/collab_panel.rs | 177 ++++++++++++++++++ crates/ui/src/templates/command_palette.rs | 31 +++ crates/ui/src/templates/project_panel.rs | 62 ++++++ .../src/{modules => templates}/status_bar.rs | 7 +- .../ui/src/{modules => templates}/tab_bar.rs | 10 +- .../src/{modules => templates}/title_bar.rs | 0 crates/ui/src/templates/workspace.rs | 80 ++++++++ crates/ui/src/tokens.rs | 18 ++ 24 files changed, 1090 insertions(+), 167 deletions(-) create mode 100644 crates/ui/doc/elevation.md create mode 100644 crates/ui/src/components/list_section_header.rs create mode 100644 crates/ui/src/components/palette_item.rs create mode 100644 crates/ui/src/modules/list.rs create mode 100644 crates/ui/src/modules/palette.rs delete mode 100644 crates/ui/src/modules/project_panel.rs create mode 100644 crates/ui/src/static_data.rs create mode 100644 crates/ui/src/templates.rs rename crates/ui/src/{modules => templates}/chat_panel.rs (92%) create mode 100644 crates/ui/src/templates/collab_panel.rs create mode 100644 crates/ui/src/templates/command_palette.rs create mode 100644 crates/ui/src/templates/project_panel.rs rename crates/ui/src/{modules => templates}/status_bar.rs (97%) rename crates/ui/src/{modules => templates}/tab_bar.rs (95%) rename crates/ui/src/{modules => templates}/title_bar.rs (100%) create mode 100644 crates/ui/src/templates/workspace.rs create mode 100644 crates/ui/src/tokens.rs diff --git a/crates/ui/doc/elevation.md b/crates/ui/doc/elevation.md new file mode 100644 index 0000000000000000000000000000000000000000..bd34de3396dea1f1042bb267f8a23abe6a45441f --- /dev/null +++ b/crates/ui/doc/elevation.md @@ -0,0 +1,57 @@ +# Elevation + +Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels. + +Elevation accomplishes the following: +- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars. +- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards. +- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces. + +Elevations are the initial elevation values assigned to components by default. + +Components may transition to a higher elevation in some cases, like user interations. + +On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest. + +## Understanding Elevation + +Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations. + +Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview) + +## Elevation Levels + +Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets. + +The levels are detailed as follows: + +0. App Background +1. UI Surface +2. Elevated Elements +3. Wash +4. Focused Element +5. Dragged Element + +### 0. App Background + +The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. + +### 1. UI Surface + +The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet. + +### 2. Elevated Elements + +Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips. + +### 3. Wash + +Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design. + +### 4. Focused Element + +Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment. + +### 5. Dragged Element + +Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index a82d28eb8cb2539cef985c12a59ba712b97699ae..1d513db7d29e4415923b59acb997f735b5ea91f1 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,11 +1,15 @@ mod facepile; mod follow_group; mod list_item; +mod list_section_header; +mod palette_item; mod tab; pub use facepile::*; pub use follow_group::*; pub use list_item::*; +pub use list_section_header::*; +pub use palette_item::*; pub use tab::*; use std::marker::PhantomData; diff --git a/crates/ui/src/components/list_item.rs b/crates/ui/src/components/list_item.rs index 868b58e449f778d8356a1823c91f3312328ab4aa..19e5b26abe4c4d21103f771b1b2b26403c83238f 100644 --- a/crates/ui/src/components/list_item.rs +++ b/crates/ui/src/components/list_item.rs @@ -1,17 +1,18 @@ -use gpui2::elements::div; -use gpui2::geometry::rems; +use crate::prelude::{DisclosureControlVisibility, InteractionState, ToggleState}; +use crate::theme::theme; +use crate::tokens::token; +use crate::{icon, IconAsset, Label}; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; -use crate::prelude::*; -use crate::{icon, theme, IconAsset, Label}; - -#[derive(Element)] +#[derive(Element, Clone)] pub struct ListItem { label: Label, left_icon: Option, indent_level: u32, state: InteractionState, + disclosure_control_style: DisclosureControlVisibility, toggle: Option, } @@ -20,6 +21,7 @@ pub fn list_item(label: Label) -> ListItem { label, indent_level: 0, left_icon: None, + disclosure_control_style: DisclosureControlVisibility::default(), state: InteractionState::default(), toggle: None, } @@ -46,8 +48,30 @@ impl ListItem { self } + pub fn disclosure_control_style( + mut self, + disclosure_control_style: DisclosureControlVisibility, + ) -> Self { + self.disclosure_control_style = disclosure_control_style; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let token = token(); + let mut disclosure_control = match self.toggle { + Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))), + Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))), + None => Some(div()), + }; + + match self.disclosure_control_style { + DisclosureControlVisibility::OnHover => { + disclosure_control = + disclosure_control.map(|c| div().absolute().neg_left_5().child(c)); + } + DisclosureControlVisibility::Always => {} + } div() .fill(theme.middle.base.default.background) @@ -56,31 +80,31 @@ impl ListItem { .active() .fill(theme.middle.base.pressed.background) .relative() + .py_1() .child( div() - .h_7() + .h_6() .px_2() // .ml(rems(0.75 * self.indent_level as f32)) .children((0..self.indent_level).map(|_| { - div().w(rems(0.75)).h_full().flex().justify_center().child( - div() - .w_px() - .h_full() - .fill(theme.middle.base.default.border) - .hover() - .fill(theme.middle.warning.default.border) - .active() - .fill(theme.middle.negative.default.border), - ) + div() + .w(token.list_indent_depth) + .h_full() + .flex() + .justify_center() + .child( + div() + .ml_px() + .w_px() + .h_full() + .fill(theme.middle.base.default.border), + ) })) .flex() - .gap_2() + .gap_1() .items_center() - .children(match self.toggle { - Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)), - Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)), - None => None, - }) + .relative() + .children(disclosure_control) .children(self.left_icon.map(|i| icon(i))) .child(self.label.clone()), ) diff --git a/crates/ui/src/components/list_section_header.rs b/crates/ui/src/components/list_section_header.rs new file mode 100644 index 0000000000000000000000000000000000000000..76a0d4cee7612fb291b325f08dd6a8600d13fbbe --- /dev/null +++ b/crates/ui/src/components/list_section_header.rs @@ -0,0 +1,88 @@ +use crate::prelude::{InteractionState, ToggleState}; +use crate::theme::theme; +use crate::tokens::token; +use crate::{icon, label, IconAsset, LabelColor, LabelSize}; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element, Clone, Copy)] +pub struct ListSectionHeader { + label: &'static str, + left_icon: Option, + state: InteractionState, + toggle: Option, +} + +pub fn list_section_header(label: &'static str) -> ListSectionHeader { + ListSectionHeader { + label, + left_icon: None, + state: InteractionState::default(), + toggle: None, + } +} + +impl ListSectionHeader { + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggle = Some(toggle); + self + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + let token = token(); + + let disclosure_control = match self.toggle { + Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))), + Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))), + None => Some(div()), + }; + + div() + .flex() + .flex_1() + .w_full() + .fill(theme.middle.base.default.background) + .hover() + .fill(theme.middle.base.hovered.background) + .active() + .fill(theme.middle.base.pressed.background) + .relative() + .py_1() + .child( + div() + .h_6() + .px_2() + .flex() + .flex_1() + .w_full() + .gap_1() + .items_center() + .justify_between() + .child( + div() + .flex() + .gap_1() + .items_center() + .children(self.left_icon.map(|i| icon(i))) + .child( + label(self.label.clone()) + .color(LabelColor::Muted) + .size(LabelSize::Small), + ), + ) + .children(disclosure_control), + ) + } +} diff --git a/crates/ui/src/components/palette_item.rs b/crates/ui/src/components/palette_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..9e7883d700252e621eabf569f91261ffc58d6824 --- /dev/null +++ b/crates/ui/src/components/palette_item.rs @@ -0,0 +1,63 @@ +use crate::theme::theme; +use crate::{label, LabelColor, LabelSize}; +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{Element, IntoElement}; +use gpui2::{ParentElement, ViewContext}; + +#[derive(Element)] +pub struct PaletteItem { + pub label: &'static str, + pub keybinding: Option<&'static str>, +} + +pub fn palette_item(label: &'static str, keybinding: Option<&'static str>) -> PaletteItem { + PaletteItem { label, keybinding } +} + +impl PaletteItem { + pub fn label(mut self, label: &'static str) -> Self { + self.label = label; + self + } + + pub fn keybinding(mut self, keybinding: Option<&'static str>) -> Self { + self.keybinding = keybinding; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + let keybinding_label = match self.keybinding { + Some(keybind) => label(keybind) + .color(LabelColor::Muted) + .size(LabelSize::Small), + None => label(""), + }; + + div() + .flex() + .flex_row() + .grow() + .justify_between() + .child(label(self.label)) + .child( + self.keybinding + .map(|_| { + div() + .flex() + .items_center() + .justify_center() + .px_1() + .py_0() + .my_0p5() + .rounded_md() + .text_sm() + .fill(theme.lowest.on.default.background) + .child(keybinding_label) + }) + .unwrap_or_else(|| div()), + ) + } +} diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index dbe30cb4f941c9b4cfb2bf1d3386f768c35c43da..08b8e3e7c5733589537f46623c257d0801de9852 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -1,29 +1,30 @@ +use crate::theme::theme; use gpui2::elements::svg; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ViewContext}; - -use crate::theme; - -// Icon::Hash -// icon(IconAsset::Hash).color(IconColor::Warning) -// Icon::new(IconAsset::Hash).color(IconColor::Warning) +use gpui2::IntoElement; +use gpui2::{Element, ViewContext}; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconAsset { Ai, ArrowLeft, ArrowRight, - #[default] ArrowUpRight, Bolt, - Hash, - File, - Folder, - FolderOpen, ChevronDown, - ChevronUp, ChevronLeft, ChevronRight, + ChevronUp, + #[default] + File, + FileDoc, + FileGit, + FileLock, + FileRust, + FileToml, + Folder, + FolderOpen, + Hash, } impl IconAsset { @@ -34,14 +35,19 @@ impl IconAsset { IconAsset::ArrowRight => "icons/arrow_right.svg", IconAsset::ArrowUpRight => "icons/arrow_up_right.svg", IconAsset::Bolt => "icons/bolt.svg", - IconAsset::Hash => "icons/hash.svg", IconAsset::ChevronDown => "icons/chevron_down.svg", - IconAsset::ChevronUp => "icons/chevron_up.svg", IconAsset::ChevronLeft => "icons/chevron_left.svg", IconAsset::ChevronRight => "icons/chevron_right.svg", + IconAsset::ChevronUp => "icons/chevron_up.svg", IconAsset::File => "icons/file_icons/file.svg", + IconAsset::FileDoc => "icons/file_icons/book.svg", + IconAsset::FileGit => "icons/file_icons/git.svg", + IconAsset::FileLock => "icons/file_icons/lock.svg", + IconAsset::FileRust => "icons/file_icons/rust.svg", + IconAsset::FileToml => "icons/file_icons/toml.svg", IconAsset::Folder => "icons/file_icons/folder.svg", IconAsset::FolderOpen => "icons/file_icons/folder_open.svg", + IconAsset::Hash => "icons/hash.svg", } } } @@ -55,19 +61,14 @@ pub fn icon(asset: IconAsset) -> Icon { Icon { asset } } -// impl Icon { -// pub fn new(asset: IconAsset) -> Icon { -// Icon { asset } -// } -// } - impl Icon { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); svg() + .flex_none() .path(self.asset.path()) .size_4() - .fill(theme.lowest.base.default.foreground) + .fill(theme.lowest.variant.default.foreground) } } diff --git a/crates/ui/src/elements/label.rs b/crates/ui/src/elements/label.rs index d3e94297ad891f2670a35c76aaf28ab69f0e5de1..90e6b525ebb2fc1c675a07bd73325fb6504efa08 100644 --- a/crates/ui/src/elements/label.rs +++ b/crates/ui/src/elements/label.rs @@ -1,29 +1,40 @@ +use crate::theme::theme; use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::theme; +use gpui2::{Element, ViewContext}; +use gpui2::{IntoElement, ParentElement}; #[derive(Default, PartialEq, Copy, Clone)] pub enum LabelColor { #[default] Default, + Muted, Created, Modified, Deleted, Hidden, + Placeholder, +} + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum LabelSize { + #[default] + Default, + Small, } #[derive(Element, Clone)] pub struct Label { label: &'static str, color: LabelColor, + size: LabelSize, } pub fn label(label: &'static str) -> Label { Label { label, color: LabelColor::Default, + size: LabelSize::Default, } } @@ -33,17 +44,32 @@ impl Label { self } + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let color = match self.color { LabelColor::Default => theme.lowest.base.default.foreground, + LabelColor::Muted => theme.lowest.variant.default.foreground, LabelColor::Created => theme.lowest.positive.default.foreground, LabelColor::Modified => theme.lowest.warning.default.foreground, LabelColor::Deleted => theme.lowest.negative.default.foreground, LabelColor::Hidden => theme.lowest.variant.default.foreground, + LabelColor::Placeholder => theme.lowest.base.disabled.foreground, }; - div().text_sm().text_color(color).child(self.label.clone()) + let mut div = div(); + + if self.size == LabelSize::Small { + div = div.text_xs(); + } else { + div = div.text_sm(); + } + + div.text_color(color).child(self.label.clone()) } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index c7bab1e0b0786a9390dcf3612cc87dd80276675f..0bcd96164393716fe60cdba409a012988c630e03 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -5,10 +5,17 @@ mod element_ext; mod elements; mod modules; pub mod prelude; +mod static_data; +mod templates; mod theme; +mod tokens; pub use crate::theme::*; pub use components::*; pub use element_ext::*; pub use elements::*; pub use modules::*; +pub use prelude::*; +pub use static_data::*; +pub use templates::*; +pub use tokens::*; diff --git a/crates/ui/src/modules.rs b/crates/ui/src/modules.rs index a1cead7df9df397602906cdfca7750508532522a..d29e31072b5662ad574fb31d58f9b9bb224a4b2a 100644 --- a/crates/ui/src/modules.rs +++ b/crates/ui/src/modules.rs @@ -1,11 +1,5 @@ -mod chat_panel; -mod project_panel; -mod status_bar; -mod tab_bar; -mod title_bar; +mod list; +mod palette; -pub use chat_panel::*; -pub use project_panel::*; -pub use status_bar::*; -pub use tab_bar::*; -pub use title_bar::*; +pub use list::*; +pub use palette::*; diff --git a/crates/ui/src/modules/list.rs b/crates/ui/src/modules/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7fb06132faf861931b4cba3c18a3ddd7d36d573 --- /dev/null +++ b/crates/ui/src/modules/list.rs @@ -0,0 +1,64 @@ +use crate::theme::theme; +use crate::tokens::token; +use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader}; +use gpui2::style::StyleHelpers; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct List { + header: Option, + items: Vec, + empty_message: &'static str, + toggle: Option, + // footer: Option, +} + +pub fn list(items: Vec) -> List { + List { + header: None, + items, + empty_message: "No items", + toggle: None, + } +} + +impl List { + pub fn header(mut self, header: ListSectionHeader) -> Self { + self.header = Some(header); + self + } + + pub fn empty_message(mut self, empty_message: &'static str) -> Self { + self.empty_message = empty_message; + self + } + + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggle = Some(toggle); + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + let token = token(); + + let disclosure_control = match self.toggle { + Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)), + Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)), + None => None, + }; + + div() + .py_1() + .flex() + .flex_col() + .children(self.header.map(|h| h)) + .children( + self.items + .is_empty() + .then(|| label(self.empty_message).color(LabelColor::Muted)), + ) + .children(self.items.iter().cloned()) + } +} diff --git a/crates/ui/src/modules/palette.rs b/crates/ui/src/modules/palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..a540d29169b0fa65d8fe7d9e7fba2f048efb2ee3 --- /dev/null +++ b/crates/ui/src/modules/palette.rs @@ -0,0 +1,124 @@ +use std::marker::PhantomData; + +use crate::prelude::OrderMethod; +use crate::theme::theme; +use crate::{label, palette_item, LabelColor, PaletteItem}; +use gpui2::elements::div::ScrollState; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct Palette { + view_type: PhantomData, + scroll_state: ScrollState, + input_placeholder: &'static str, + empty_string: &'static str, + items: Vec, + default_order: OrderMethod, +} + +pub fn palette(scroll_state: ScrollState) -> Palette { + Palette { + view_type: PhantomData, + scroll_state, + input_placeholder: "Find something...", + empty_string: "No items found.", + items: vec![], + default_order: OrderMethod::default(), + } +} + +impl Palette { + pub fn items(mut self, mut items: Vec) -> Self { + items.sort_by_key(|item| item.label); + self.items = items; + self + } + + pub fn placeholder(mut self, input_placeholder: &'static str) -> Self { + self.input_placeholder = input_placeholder; + self + } + + pub fn empty_string(mut self, empty_string: &'static str) -> Self { + self.empty_string = empty_string; + self + } + + // TODO: Hook up sort order + pub fn default_order(mut self, default_order: OrderMethod) -> Self { + self.default_order = default_order; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .w_96() + .rounded_lg() + .fill(theme.lowest.base.default.background) + .border() + .border_color(theme.lowest.base.default.border) + .flex() + .flex_col() + .child( + div() + .flex() + .flex_col() + .gap_px() + .child( + div().py_0p5().px_1().flex().flex_col().child( + div().px_2().py_0p5().child( + label(self.input_placeholder).color(LabelColor::Placeholder), + ), + ), + ) + .child(div().h_px().w_full().fill(theme.lowest.base.default.border)) + .child( + div() + .py_0p5() + .px_1() + .flex() + .flex_col() + .grow() + .max_h_96() + .overflow_y_scroll(self.scroll_state.clone()) + .children( + vec![if self.items.is_empty() { + Some( + div() + .flex() + .flex_row() + .justify_between() + .px_2() + .py_1() + .child( + label(self.empty_string).color(LabelColor::Muted), + ), + ) + } else { + None + }] + .into_iter() + .flatten(), + ) + .children(self.items.iter().map(|item| { + div() + .flex() + .flex_row() + .justify_between() + .px_2() + .py_0p5() + .rounded_lg() + .hover() + .fill(theme.lowest.base.hovered.background) + .active() + .fill(theme.lowest.base.pressed.background) + .child(palette_item(item.label, item.keybinding)) + })), + ), + ) + } +} diff --git a/crates/ui/src/modules/project_panel.rs b/crates/ui/src/modules/project_panel.rs deleted file mode 100644 index e17ff4c8edf425801731a37f60d1e05d65f61186..0000000000000000000000000000000000000000 --- a/crates/ui/src/modules/project_panel.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::elements::div; -use gpui2::elements::div::ScrollState; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::prelude::*; -use crate::{details, input, label, list_item, theme, IconAsset, LabelColor}; - -#[derive(Element)] -pub struct ProjectPanel { - view_type: PhantomData, - scroll_state: ScrollState, -} - -pub fn project_panel(scroll_state: ScrollState) -> ProjectPanel { - ProjectPanel { - view_type: PhantomData, - scroll_state, - } -} - -impl ProjectPanel { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - let theme = theme(cx); - - div() - .w_56() - .h_full() - .flex() - .flex_col() - .fill(theme.middle.base.default.background) - .child( - div() - .w_56() - .flex() - .flex_col() - .overflow_y_scroll(self.scroll_state.clone()) - .child(details("This is a long string that should wrap when it keeps going for a long time.").meta_text("6 h ago)")) - .child( - div().flex().flex_col().children( - std::iter::repeat_with(|| { - vec![ - list_item(label("sqlez").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) - .indent_level(0) - .set_toggle(ToggleState::NotToggled), - list_item(label("storybook").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) - .indent_level(0) - .set_toggle(ToggleState::Toggled), - list_item(label("docs").color(LabelColor::Default)) - .left_icon(IconAsset::Folder.into()) - .indent_level(1) - .set_toggle(ToggleState::Toggled), - list_item(label("src").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) - .indent_level(2) - .set_toggle(ToggleState::Toggled), - list_item(label("ui").color(LabelColor::Modified)) - .left_icon(IconAsset::FolderOpen.into()) - .indent_level(3) - .set_toggle(ToggleState::Toggled), - list_item(label("component").color(LabelColor::Created)) - .left_icon(IconAsset::FolderOpen.into()) - .indent_level(4) - .set_toggle(ToggleState::Toggled), - list_item(label("facepile.rs").color(LabelColor::Default)) - .left_icon(IconAsset::File.into()) - .indent_level(5), - list_item(label("follow_group.rs").color(LabelColor::Default)) - .left_icon(IconAsset::File.into()) - .indent_level(5), - list_item(label("list_item.rs").color(LabelColor::Created)) - .left_icon(IconAsset::File.into()) - .indent_level(5), - list_item(label("tab.rs").color(LabelColor::Default)) - .left_icon(IconAsset::File.into()) - .indent_level(5), - ] - }) - .take(10) - .flatten(), - ), - ), - ) - .child( - input("Find something...") - .value("buffe".to_string()) - .state(InteractionState::Focused), - ) - } -} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 61dbb676e2d7d01a4843f2c5a84db7f32064c30b..70b9ab4a5ed0e0dbbc6f0c13670c50fd452cf225 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -1,3 +1,11 @@ +#[derive(Default, PartialEq)] +pub enum OrderMethod { + #[default] + Ascending, + Descending, + MostRecent, +} + #[derive(Default, PartialEq)] pub enum ButtonVariant { #[default] @@ -19,6 +27,13 @@ pub enum Shape { RoundedRectangle, } +#[derive(Default, PartialEq, Clone, Copy)] +pub enum DisclosureControlVisibility { + #[default] + OnHover, + Always, +} + #[derive(Default, PartialEq, Clone, Copy)] pub enum InteractionState { #[default] diff --git a/crates/ui/src/static_data.rs b/crates/ui/src/static_data.rs new file mode 100644 index 0000000000000000000000000000000000000000..de946dab28d9cbabd5ba95a28ab162386d8e101c --- /dev/null +++ b/crates/ui/src/static_data.rs @@ -0,0 +1,166 @@ +use crate::{ + label, list_item, palette_item, IconAsset, LabelColor, ListItem, PaletteItem, ToggleState, +}; + +pub fn static_project_panel_project_items() -> Vec { + vec![ + list_item(label("zed")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(0) + .set_toggle(ToggleState::Toggled), + list_item(label(".cargo")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".config")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".git").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".cargo")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".idea").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label("assets")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1) + .set_toggle(ToggleState::Toggled), + list_item(label("cargo-target").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label("crates")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(1) + .set_toggle(ToggleState::Toggled), + list_item(label("activity_indicator")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("ai")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("audio")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("auto_update")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("breadcrumbs")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("call")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("sqlez").color(LabelColor::Modified)) + .left_icon(IconAsset::Folder.into()) + .indent_level(2) + .set_toggle(ToggleState::NotToggled), + list_item(label("gpui2")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(2) + .set_toggle(ToggleState::Toggled), + list_item(label("src")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(3) + .set_toggle(ToggleState::Toggled), + list_item(label("derrive_element.rs")) + .left_icon(IconAsset::FileRust.into()) + .indent_level(4), + list_item(label("storybook").color(LabelColor::Modified)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(1) + .set_toggle(ToggleState::Toggled), + list_item(label("docs").color(LabelColor::Default)) + .left_icon(IconAsset::Folder.into()) + .indent_level(2) + .set_toggle(ToggleState::Toggled), + list_item(label("src").color(LabelColor::Modified)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(3) + .set_toggle(ToggleState::Toggled), + list_item(label("ui").color(LabelColor::Modified)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(4) + .set_toggle(ToggleState::Toggled), + list_item(label("component").color(LabelColor::Created)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(5) + .set_toggle(ToggleState::Toggled), + list_item(label("facepile.rs").color(LabelColor::Default)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("follow_group.rs").color(LabelColor::Default)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("list_item.rs").color(LabelColor::Created)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("tab.rs").color(LabelColor::Default)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("target").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".dockerignore")) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label(".DS_Store").color(LabelColor::Hidden)) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label("Cargo.lock")) + .left_icon(IconAsset::FileLock.into()) + .indent_level(1), + list_item(label("Cargo.toml")) + .left_icon(IconAsset::FileToml.into()) + .indent_level(1), + list_item(label("Dockerfile")) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label("Procfile")) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label("README.md")) + .left_icon(IconAsset::FileDoc.into()) + .indent_level(1), + ] +} + +pub fn static_project_panel_single_items() -> Vec { + vec![ + list_item(label("todo.md")) + .left_icon(IconAsset::FileDoc.into()) + .indent_level(0), + list_item(label("README.md")) + .left_icon(IconAsset::FileDoc.into()) + .indent_level(0), + list_item(label("config.json")) + .left_icon(IconAsset::File.into()) + .indent_level(0), + ] +} + +pub fn example_editor_actions() -> Vec { + vec![ + palette_item("New File", Some("Ctrl+N")), + palette_item("Open File", Some("Ctrl+O")), + palette_item("Save File", Some("Ctrl+S")), + palette_item("Cut", Some("Ctrl+X")), + palette_item("Copy", Some("Ctrl+C")), + palette_item("Paste", Some("Ctrl+V")), + palette_item("Undo", Some("Ctrl+Z")), + palette_item("Redo", Some("Ctrl+Shift+Z")), + palette_item("Find", Some("Ctrl+F")), + palette_item("Replace", Some("Ctrl+R")), + palette_item("Jump to Line", None), + palette_item("Select All", None), + palette_item("Deselect All", None), + palette_item("Switch Document", None), + palette_item("Insert Line Below", None), + palette_item("Insert Line Above", None), + palette_item("Move Line Up", None), + palette_item("Move Line Down", None), + palette_item("Toggle Comment", None), + palette_item("Delete Line", None), + ] +} diff --git a/crates/ui/src/templates.rs b/crates/ui/src/templates.rs new file mode 100644 index 0000000000000000000000000000000000000000..f09a8a7ea4e93795d4a5b669b2f2816476c6c26c --- /dev/null +++ b/crates/ui/src/templates.rs @@ -0,0 +1,17 @@ +mod chat_panel; +mod collab_panel; +mod command_palette; +mod project_panel; +mod status_bar; +mod tab_bar; +mod title_bar; +mod workspace; + +pub use chat_panel::*; +pub use collab_panel::*; +pub use command_palette::*; +pub use project_panel::*; +pub use status_bar::*; +pub use tab_bar::*; +pub use title_bar::*; +pub use workspace::*; diff --git a/crates/ui/src/modules/chat_panel.rs b/crates/ui/src/templates/chat_panel.rs similarity index 92% rename from crates/ui/src/modules/chat_panel.rs rename to crates/ui/src/templates/chat_panel.rs index 77c5b2ef20ba8a2de7cc47fead7d715b1b055706..7c3462b3fe46c31cb1e158548435f572915ff15c 100644 --- a/crates/ui/src/modules/chat_panel.rs +++ b/crates/ui/src/templates/chat_panel.rs @@ -1,11 +1,11 @@ use std::marker::PhantomData; -use gpui2::elements::div; +use crate::icon_button; +use crate::theme::theme; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::{icon_button, theme}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; #[derive(Element)] pub struct ChatPanel { diff --git a/crates/ui/src/templates/collab_panel.rs b/crates/ui/src/templates/collab_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..7e76cb68355b3c09bf45e02a40c62376d4934612 --- /dev/null +++ b/crates/ui/src/templates/collab_panel.rs @@ -0,0 +1,177 @@ +use crate::theme::{theme, Theme}; +use gpui2::{ + elements::{div, div::ScrollState, img, svg}, + style::{StyleHelpers, Styleable}, + ArcCow, Element, IntoElement, ParentElement, ViewContext, +}; +use std::marker::PhantomData; + +#[derive(Element)] +pub struct CollabPanelElement { + view_type: PhantomData, + scroll_state: ScrollState, +} + +// When I improve child view rendering, I'd like to have V implement a trait that +// provides the scroll state, among other things. +pub fn collab_panel(scroll_state: ScrollState) -> CollabPanelElement { + CollabPanelElement { + view_type: PhantomData, + scroll_state, + } +} + +impl CollabPanelElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + // Panel + div() + .w_64() + .h_full() + .flex() + .flex_col() + .font("Zed Sans Extended") + .text_color(theme.middle.base.default.foreground) + .border_color(theme.middle.base.default.border) + .border() + .fill(theme.middle.base.default.background) + .child( + div() + .w_full() + .flex() + .flex_col() + .overflow_y_scroll(self.scroll_state.clone()) + // List Container + .child( + div() + .fill(theme.lowest.base.default.background) + .pb_1() + .border_color(theme.lowest.base.default.border) + .border_b() + //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state + // .group() + // List Section Header + .child(self.list_section_header("#CRDB", true, theme)) + // List Item Large + .child(self.list_item( + "http://github.com/maxbrunsfeld.png?s=50", + "maxbrunsfeld", + theme, + )), + ) + .child( + div() + .py_2() + .flex() + .flex_col() + .child(self.list_section_header("CHANNELS", true, theme)), + ) + .child( + div() + .py_2() + .flex() + .flex_col() + .child(self.list_section_header("CONTACTS", true, theme)) + .children( + std::iter::repeat_with(|| { + vec![ + self.list_item( + "http://github.com/as-cii.png?s=50", + "as-cii", + theme, + ), + self.list_item( + "http://github.com/nathansobo.png?s=50", + "nathansobo", + theme, + ), + self.list_item( + "http://github.com/maxbrunsfeld.png?s=50", + "maxbrunsfeld", + theme, + ), + ] + }) + .take(3) + .flatten(), + ), + ), + ) + .child( + div() + .h_7() + .px_2() + .border_t() + .border_color(theme.middle.variant.default.border) + .flex() + .items_center() + .child( + div() + .text_sm() + .text_color(theme.middle.variant.default.foreground) + .child("Find..."), + ), + ) + } + + fn list_section_header( + &self, + label: impl Into>, + expanded: bool, + theme: &Theme, + ) -> impl Element { + div() + .h_7() + .px_2() + .flex() + .justify_between() + .items_center() + .child(div().flex().gap_1().text_sm().child(label)) + .child( + div().flex().h_full().gap_1().items_center().child( + svg() + .path(if expanded { + "icons/caret_down.svg" + } else { + "icons/caret_up.svg" + }) + .w_3p5() + .h_3p5() + .fill(theme.middle.variant.default.foreground), + ), + ) + } + + fn list_item( + &self, + avatar_uri: impl Into>, + label: impl Into>, + theme: &Theme, + ) -> impl Element { + div() + .h_7() + .px_2() + .flex() + .items_center() + .hover() + .fill(theme.lowest.variant.hovered.background) + .active() + .fill(theme.lowest.variant.pressed.background) + .child( + div() + .flex() + .items_center() + .gap_1() + .text_sm() + .child( + img() + .uri(avatar_uri) + .size_3p5() + .rounded_full() + .fill(theme.middle.positive.default.foreground), + ) + .child(label), + ) + } +} diff --git a/crates/ui/src/templates/command_palette.rs b/crates/ui/src/templates/command_palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..303e2d6de9f69b0dc75a53f705878ff25319f1ae --- /dev/null +++ b/crates/ui/src/templates/command_palette.rs @@ -0,0 +1,31 @@ +use gpui2::elements::div; +use gpui2::{elements::div::ScrollState, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement}; +use std::marker::PhantomData; + +use crate::{example_editor_actions, palette, OrderMethod}; + +#[derive(Element)] +pub struct CommandPalette { + view_type: PhantomData, + scroll_state: ScrollState, +} + +pub fn command_palette(scroll_state: ScrollState) -> CommandPalette { + CommandPalette { + view_type: PhantomData, + scroll_state, + } +} + +impl CommandPalette { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + palette(self.scroll_state.clone()) + .items(example_editor_actions()) + .placeholder("Execute a command...") + .empty_string("No items found.") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/templates/project_panel.rs b/crates/ui/src/templates/project_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..8204ad26c077c2aa03e5a8a245925cd97987c1c6 --- /dev/null +++ b/crates/ui/src/templates/project_panel.rs @@ -0,0 +1,62 @@ +use crate::{ + input, list, list_section_header, prelude::*, static_project_panel_project_items, + static_project_panel_single_items, theme, +}; + +use gpui2::{ + elements::{div, div::ScrollState}, + style::StyleHelpers, + ParentElement, ViewContext, +}; +use gpui2::{Element, IntoElement}; +use std::marker::PhantomData; + +#[derive(Element)] +pub struct ProjectPanel { + view_type: PhantomData, + scroll_state: ScrollState, +} + +pub fn project_panel(scroll_state: ScrollState) -> ProjectPanel { + ProjectPanel { + view_type: PhantomData, + scroll_state, + } +} + +impl ProjectPanel { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .w_56() + .h_full() + .flex() + .flex_col() + .fill(theme.middle.base.default.background) + .child( + div() + .w_56() + .flex() + .flex_col() + .overflow_y_scroll(self.scroll_state.clone()) + .child( + list(static_project_panel_single_items()) + .header(list_section_header("FILES").set_toggle(ToggleState::Toggled)) + .empty_message("No files in directory") + .set_toggle(ToggleState::Toggled), + ) + .child( + list(static_project_panel_project_items()) + .header(list_section_header("PROJECT").set_toggle(ToggleState::Toggled)) + .empty_message("No folders in directory") + .set_toggle(ToggleState::Toggled), + ), + ) + .child( + input("Find something...") + .value("buffe".to_string()) + .state(InteractionState::Focused), + ) + } +} diff --git a/crates/ui/src/modules/status_bar.rs b/crates/ui/src/templates/status_bar.rs similarity index 97% rename from crates/ui/src/modules/status_bar.rs rename to crates/ui/src/templates/status_bar.rs index 763a585176128fa1e832bfc35fa35e670aeb5e41..79265a757296394c3603abbca02750d2a855cb23 100644 --- a/crates/ui/src/modules/status_bar.rs +++ b/crates/ui/src/templates/status_bar.rs @@ -1,11 +1,10 @@ use std::marker::PhantomData; -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - use crate::theme::{theme, Theme}; use crate::{icon_button, text_button, tool_divider}; +use gpui2::style::StyleHelpers; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; #[derive(Default, PartialEq)] pub enum Tool { diff --git a/crates/ui/src/modules/tab_bar.rs b/crates/ui/src/templates/tab_bar.rs similarity index 95% rename from crates/ui/src/modules/tab_bar.rs rename to crates/ui/src/templates/tab_bar.rs index 08caad310729f66b1f723366994a73c660b447a3..e1ca6b1dc793ed87bcc7d278ef1e2a3c6a9cc0b3 100644 --- a/crates/ui/src/modules/tab_bar.rs +++ b/crates/ui/src/templates/tab_bar.rs @@ -1,12 +1,12 @@ use std::marker::PhantomData; -use gpui2::elements::div; +use crate::prelude::InteractionState; +use crate::theme::theme; +use crate::{icon_button, tab}; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::prelude::InteractionState; -use crate::{icon_button, tab, theme}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; #[derive(Element)] pub struct TabBar { diff --git a/crates/ui/src/modules/title_bar.rs b/crates/ui/src/templates/title_bar.rs similarity index 100% rename from crates/ui/src/modules/title_bar.rs rename to crates/ui/src/templates/title_bar.rs diff --git a/crates/ui/src/templates/workspace.rs b/crates/ui/src/templates/workspace.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb785a317f71be0b476f294b2c95141c06b56566 --- /dev/null +++ b/crates/ui/src/templates/workspace.rs @@ -0,0 +1,80 @@ +use crate::{chat_panel, collab_panel, project_panel, status_bar, tab_bar, theme, title_bar}; + +use gpui2::{ + elements::{div, div::ScrollState}, + style::StyleHelpers, + Element, IntoElement, ParentElement, ViewContext, +}; + +#[derive(Element, Default)] +struct WorkspaceElement { + project_panel_scroll_state: ScrollState, + collab_panel_scroll_state: ScrollState, + right_scroll_state: ScrollState, + tab_bar_scroll_state: ScrollState, + palette_scroll_state: ScrollState, +} + +pub fn workspace() -> impl Element { + WorkspaceElement::default() +} + +impl WorkspaceElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + // Elevation Level 0 + .size_full() + .flex() + .flex_col() + .font("Zed Sans Extended") + .gap_0() + .justify_start() + .items_start() + .text_color(theme.lowest.base.default.foreground) + .fill(theme.lowest.base.default.background) + .relative() + // Elevation Level 1 + .child(title_bar()) + .child( + div() + .flex_1() + .w_full() + .flex() + .flex_row() + .overflow_hidden() + .child(project_panel(self.project_panel_scroll_state.clone())) + .child(collab_panel(self.collab_panel_scroll_state.clone())) + .child( + div() + .h_full() + .flex_1() + .fill(theme.highest.base.default.background) + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(tab_bar(self.tab_bar_scroll_state.clone())), + ), + ) + .child(chat_panel(self.right_scroll_state.clone())), + ) + .child(status_bar()) + // Elevation Level 3 + // .child( + // div() + // .absolute() + // .top_0() + // .left_0() + // .size_full() + // .flex() + // .justify_center() + // .items_center() + // // .fill(theme.lowest.base.default.background) + // // Elevation Level 4 + // .child(command_palette(self.palette_scroll_state.clone())), + // ) + } +} diff --git a/crates/ui/src/tokens.rs b/crates/ui/src/tokens.rs new file mode 100644 index 0000000000000000000000000000000000000000..79128205330c4e1424024f9af4823749f47f3a06 --- /dev/null +++ b/crates/ui/src/tokens.rs @@ -0,0 +1,18 @@ +use gpui2::geometry::AbsoluteLength; + +#[derive(Clone, Copy)] +pub struct Token { + pub list_indent_depth: AbsoluteLength, +} + +impl Default for Token { + fn default() -> Self { + Self { + list_indent_depth: AbsoluteLength::Rems(0.5), + } + } +} + +pub fn token() -> Token { + Token::default() +} From 30b105afd55a58d33246a0ffc1356823ae61a3d1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 21 Sep 2023 23:51:03 -0400 Subject: [PATCH 28/89] Remove leftover state doc --- docs/ui/states.md | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 docs/ui/states.md diff --git a/docs/ui/states.md b/docs/ui/states.md deleted file mode 100644 index 7dc3110ceda07386fbede131e155aeb7f8a87c01..0000000000000000000000000000000000000000 --- a/docs/ui/states.md +++ /dev/null @@ -1,43 +0,0 @@ -## Interaction State - -**Enabled** - -An enabled state communicates an interactive component or element. - -**Disabled** - -A disabled state communicates a inoperable component or element. - -**Hover** - -A hover state communicates when a user has placed a cursor above an interactive element. - -**Focused** - -A focused state communicates when a user has highlighted an element, using an input method such as a keyboard or voice. - -**Activated** - -An activated state communicates a highlighted destination, whether initiated by the user or by default. - -**Pressed** - -A pressed state communicates a user tap. - -**Dragged** - -A dragged state communicates when a user presses and moves an element. - -## Selected State - -**Unselected** - -dfa - -**Partially Selected** - -daf - -**Selected** - -dfa From d61565d227460742b6c2b8d45b8d079506a0bb35 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Sep 2023 13:40:20 +0300 Subject: [PATCH 29/89] Do not resubscribe for Copilot logs events Copilot sends multiple events about its LSP server readiness, not necessarily recreating the server from scratch (e.g. due to re-sign in action). Avoid re-adding same log subscriptions on the same LSP server, which causes panics. --- crates/language_tools/src/lsp_log.rs | 7 +++++++ crates/lsp/src/lsp.rs | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d0e9e9383941cfe39af543a98fb3188fcb917c1a..3f29d6f79b87b1c168b8fda47d82f73511c370bd 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -181,6 +181,13 @@ impl LogStore { }); let server = project.read(cx).language_server_for_id(id); + if let Some(server) = server.as_deref() { + if server.has_notification_handler::() { + // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. + return Some(server_state.log_buffer.clone()); + } + } + let weak_project = project.downgrade(); let io_tx = self.io_tx.clone(); server_state._io_logs_subscription = server.as_ref().map(|server| { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index dcfce4f1fbafaba606eaf11aacffb1ec967eef76..9b0d6c98b0ec48fd7207daa25f6a602abe8fd16b 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -605,6 +605,10 @@ impl LanguageServer { self.notification_handlers.lock().remove(T::METHOD); } + pub fn has_notification_handler(&self) -> bool { + self.notification_handlers.lock().contains_key(T::METHOD) + } + #[must_use] pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where From 68c37ca2a4896666f845f2dad485fa7010da614b Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 22 Sep 2023 09:33:59 -0400 Subject: [PATCH 30/89] move embedding provider to ai crate --- Cargo.lock | 19 ++++-- crates/ai/Cargo.toml | 15 ++++- crates/ai/src/ai.rs | 1 + .../{semantic_index => ai}/src/embedding.rs | 60 +++++++++++++------ crates/semantic_index/Cargo.toml | 5 +- crates/semantic_index/examples/eval.rs | 2 +- crates/semantic_index/src/db.rs | 2 +- crates/semantic_index/src/embedding_queue.rs | 3 +- crates/semantic_index/src/parsing.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 4 +- .../src/semantic_index_tests.rs | 2 +- 11 files changed, 79 insertions(+), 36 deletions(-) rename crates/{semantic_index => ai}/src/embedding.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index 103b9c2c2a730b23f774b67b1d859ba55debafa0..0216bea496572e636764689aceadc123622a8329 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,13 +91,25 @@ name = "ai" version = "0.1.0" dependencies = [ "anyhow", - "ctor", + "async-trait", + "bincode", "futures 0.3.28", "gpui", "isahc", + "lazy_static", + "log", + "matrixmultiply", + "ordered-float", + "parking_lot 0.11.2", + "parse_duration", + "postage", + "rand 0.8.5", "regex", + "rusqlite", "serde", "serde_json", + "tiktoken-rs 0.5.4", + "util", ] [[package]] @@ -6725,9 +6737,9 @@ dependencies = [ name = "semantic_index" version = "0.1.0" dependencies = [ + "ai", "anyhow", "async-trait", - "bincode", "client", "collections", "ctor", @@ -6736,15 +6748,12 @@ dependencies = [ "futures 0.3.28", "globset", "gpui", - "isahc", "language", "lazy_static", "log", - "matrixmultiply", "node_runtime", "ordered-float", "parking_lot 0.11.2", - "parse_duration", "picker", "postage", "pretty_assertions", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index c4e129c1f5eb497c6e25bbaed60a3e30bebdcc51..a2c70ce8c6f99b70d4992c7bb26fa3c79c7f2ad5 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -10,12 +10,25 @@ doctest = false [dependencies] gpui = { path = "../gpui" } +util = { path = "../util" } +async-trait.workspace = true anyhow.workspace = true futures.workspace = true +lazy_static.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true isahc.workspace = true regex.workspace = true serde.workspace = true serde_json.workspace = true +postage.workspace = true +rand.workspace = true +log.workspace = true +parse_duration = "2.1.1" +tiktoken-rs = "0.5.0" +matrixmultiply = "0.3.7" +rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } +bincode = "1.3.3" [dev-dependencies] -ctor.workspace = true +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index c893d109abe3168d0aa5d2afecc4bcf15ad69ec9..5256a6a6432907dd22c30d6a03e492a46fef77df 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1 +1,2 @@ pub mod completion; +pub mod embedding; diff --git a/crates/semantic_index/src/embedding.rs b/crates/ai/src/embedding.rs similarity index 92% rename from crates/semantic_index/src/embedding.rs rename to crates/ai/src/embedding.rs index 2b6e94854e2c0e8e21f9df11874f45d35dca8a59..332470aa546832fc083ca9064d842dc5b66dafd4 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/ai/src/embedding.rs @@ -27,8 +27,30 @@ lazy_static! { } #[derive(Debug, PartialEq, Clone)] -pub struct Embedding(Vec); +pub struct Embedding(pub Vec); +// This is needed for semantic index functionality +// Unfortunately it has to live wherever the "Embedding" struct is created. +// Keeping this in here though, introduces a 'rusqlite' dependency into AI +// which is less than ideal +impl FromSql for Embedding { + fn column_result(value: ValueRef) -> FromSqlResult { + let bytes = value.as_blob()?; + let embedding: Result, Box> = bincode::deserialize(bytes); + if embedding.is_err() { + return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); + } + Ok(Embedding(embedding.unwrap())) + } +} + +impl ToSql for Embedding { + fn to_sql(&self) -> rusqlite::Result { + let bytes = bincode::serialize(&self.0) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; + Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) + } +} impl From> for Embedding { fn from(value: Vec) -> Self { Embedding(value) @@ -63,24 +85,24 @@ impl Embedding { } } -impl FromSql for Embedding { - fn column_result(value: ValueRef) -> FromSqlResult { - let bytes = value.as_blob()?; - let embedding: Result, Box> = bincode::deserialize(bytes); - if embedding.is_err() { - return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); - } - Ok(Embedding(embedding.unwrap())) - } -} - -impl ToSql for Embedding { - fn to_sql(&self) -> rusqlite::Result { - let bytes = bincode::serialize(&self.0) - .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; - Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) - } -} +// impl FromSql for Embedding { +// fn column_result(value: ValueRef) -> FromSqlResult { +// let bytes = value.as_blob()?; +// let embedding: Result, Box> = bincode::deserialize(bytes); +// if embedding.is_err() { +// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err())); +// } +// Ok(Embedding(embedding.unwrap())) +// } +// } + +// impl ToSql for Embedding { +// fn to_sql(&self) -> rusqlite::Result { +// let bytes = bincode::serialize(&self.0) +// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; +// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes))) +// } +// } #[derive(Clone)] pub struct OpenAIEmbeddings { diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 44afecb0c2a64076945bf35559955d93980e79e9..e38ae1f06db35ed96f7ac367c0529cd27e8cbd4f 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -9,6 +9,7 @@ path = "src/semantic_index.rs" doctest = false [dependencies] +ai = { path = "../ai" } collections = { path = "../collections" } gpui = { path = "../gpui" } language = { path = "../language" } @@ -26,22 +27,18 @@ futures.workspace = true ordered-float.workspace = true smol.workspace = true rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } -isahc.workspace = true log.workspace = true tree-sitter.workspace = true lazy_static.workspace = true serde.workspace = true serde_json.workspace = true async-trait.workspace = true -bincode = "1.3.3" -matrixmultiply = "0.3.7" tiktoken-rs = "0.5.0" parking_lot.workspace = true rand.workspace = true schemars.workspace = true globset.workspace = true sha1 = "0.10.5" -parse_duration = "2.1.1" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/semantic_index/examples/eval.rs b/crates/semantic_index/examples/eval.rs index 15406cf63e3bb99271c5444933a3a01fffd041d1..a0cdbeea0505e378230b9208b9c08402fbe00c4e 100644 --- a/crates/semantic_index/examples/eval.rs +++ b/crates/semantic_index/examples/eval.rs @@ -1,10 +1,10 @@ +use ai::embedding::OpenAIEmbeddings; use anyhow::{anyhow, Result}; use client::{self, UserStore}; use gpui::{AsyncAppContext, ModelHandle, Task}; use language::LanguageRegistry; use node_runtime::RealNodeRuntime; use project::{Project, RealFs}; -use semantic_index::embedding::OpenAIEmbeddings; use semantic_index::semantic_index_settings::SemanticIndexSettings; use semantic_index::{SearchResult, SemanticIndex}; use serde::{Deserialize, Serialize}; diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index 3e35284027d8cdb230106d9f22d1043a1cc2fd2c..8280dc7d65c44bdbfd656c40db1e4b9e91cb9b7e 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -1,8 +1,8 @@ use crate::{ - embedding::Embedding, parsing::{Span, SpanDigest}, SEMANTIC_INDEX_VERSION, }; +use ai::embedding::Embedding; use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::channel::oneshot; diff --git a/crates/semantic_index/src/embedding_queue.rs b/crates/semantic_index/src/embedding_queue.rs index 3026eef9ae1cd21fcbb5cfb3ba941ed2a83f1884..6ae8faa4cdb0998acad8ab361c83183cff909578 100644 --- a/crates/semantic_index/src/embedding_queue.rs +++ b/crates/semantic_index/src/embedding_queue.rs @@ -1,4 +1,5 @@ -use crate::{embedding::EmbeddingProvider, parsing::Span, JobHandle}; +use crate::{parsing::Span, JobHandle}; +use ai::embedding::EmbeddingProvider; use gpui::executor::Background; use parking_lot::Mutex; use smol::channel; diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index 9f5a339b23567bbd87e9acd2b9ad2980e7227ab4..498ad6187e49abdbab57e22218db97161136e0a9 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -1,4 +1,4 @@ -use crate::embedding::{Embedding, EmbeddingProvider}; +use ai::embedding::{Embedding, EmbeddingProvider}; use anyhow::{anyhow, Result}; use language::{Grammar, Language}; use rusqlite::{ diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 1ba0001cfda9c6be7128e1378bc75750cd1842da..3bb6e17fb5538ff76cba2ba1640677dfdb0d397b 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -1,5 +1,5 @@ mod db; -pub mod embedding; +// pub mod embedding; mod embedding_queue; mod parsing; pub mod semantic_index_settings; @@ -11,7 +11,7 @@ use crate::semantic_index_settings::SemanticIndexSettings; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; use db::VectorDatabase; -use embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings}; +use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings}; use embedding_queue::{EmbeddingQueue, FileToEmbed}; use futures::{future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index f386665915b1d9b9314c7246da93daec6624900e..f2cae8a55701e910379d672225c19ac18a489897 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1,10 +1,10 @@ use crate::{ - embedding::{DummyEmbeddings, Embedding, EmbeddingProvider}, embedding_queue::EmbeddingQueue, parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest}, semantic_index_settings::SemanticIndexSettings, FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT, }; +use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider}; use anyhow::Result; use async_trait::async_trait; use gpui::{executor::Deterministic, Task, TestAppContext}; From fbd6b5b4344ca6231c84165a65e2df26678b0e59 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 22 Sep 2023 09:46:06 -0400 Subject: [PATCH 31/89] cargo fmt --- crates/semantic_index/src/semantic_index.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 3bb6e17fb5538ff76cba2ba1640677dfdb0d397b..fd41cb150024fd0ed5c2882f5835d76f8c152f41 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -1,5 +1,4 @@ mod db; -// pub mod embedding; mod embedding_queue; mod parsing; pub mod semantic_index_settings; @@ -8,10 +7,10 @@ pub mod semantic_index_settings; mod semantic_index_tests; use crate::semantic_index_settings::SemanticIndexSettings; +use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings}; use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, HashSet}; use db::VectorDatabase; -use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings}; use embedding_queue::{EmbeddingQueue, FileToEmbed}; use futures::{future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; From afa704584733149639ea78034da17664bc0bc954 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 22 Sep 2023 17:04:11 +0300 Subject: [PATCH 32/89] Tone down inlay hint update logs --- crates/editor/src/inlay_hint_cache.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 8aa7a1e40edb872917ce44132a24ae832da4d3b2..dd75d2bab6664591c1cfaf18225269a8c1f40f3f 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -941,7 +941,7 @@ async fn fetch_and_update_hints( }) .await; if let Some(new_update) = new_update { - log::info!( + log::debug!( "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", new_update.remove_from_visible.len(), new_update.remove_from_cache.len(), From d8c6adf338b746bc360c4990309bd63f905088d1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 11:38:01 -0400 Subject: [PATCH 33/89] Factor story boilerplate out into separate components (#3016) This PR factors out the bulk of the boilerplate required to setup a story in the storybook out into separate components. The pattern we're using here is adapted from the "[associated component](https://maxdeviant.com/posts/2021/react-associated-components/)" pattern in React. Release Notes: - N/A --- .../src/stories/components/facepile.rs | 24 ++++++------------ .../src/stories/components/traffic_lights.rs | 22 ++++------------ .../storybook/src/stories/elements/avatar.rs | 20 ++++----------- crates/storybook/src/story.rs | 25 +++++++++++++++++++ crates/storybook/src/storybook.rs | 13 +++++----- 5 files changed, 49 insertions(+), 55 deletions(-) create mode 100644 crates/storybook/src/story.rs diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs index 6bde4539ed5aa4f1187d32b0879f025e89a37b69..96c3aff2afbe5dff1971667eb18428d5d601f845 100644 --- a/crates/storybook/src/stories/components/facepile.rs +++ b/crates/storybook/src/stories/components/facepile.rs @@ -1,8 +1,10 @@ use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext}; -use ui::{avatar, theme}; -use ui::{facepile, prelude::*}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; +use ui::prelude::*; +use ui::{avatar, facepile, theme}; + +use crate::story::Story; #[derive(Element, Default)] pub struct FacepileStory {} @@ -11,20 +13,8 @@ impl FacepileStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - div() - .size_full() - .flex() - .flex_col() - .pt_2() - .px_4() - .font("Zed Mono Extended") - .fill(rgb::(0x282c34)) - .child( - div() - .text_2xl() - .text_color(rgb::(0xffffff)) - .child(std::any::type_name::()), - ) + Story::container() + .child(Story::title(std::any::type_name::())) .child( div() .flex() diff --git a/crates/storybook/src/stories/components/traffic_lights.rs b/crates/storybook/src/stories/components/traffic_lights.rs index 252caf99f64b5433ac33f9c2b45c551cf1473409..9f84446eed383ee8fbd637b5b00b7212137acdc2 100644 --- a/crates/storybook/src/stories/components/traffic_lights.rs +++ b/crates/storybook/src/stories/components/traffic_lights.rs @@ -1,8 +1,8 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use ui::{theme, traffic_lights}; +use crate::story::Story; + #[derive(Element, Default)] pub struct TrafficLightsStory {} @@ -10,20 +10,8 @@ impl TrafficLightsStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - div() - .size_full() - .flex() - .flex_col() - .pt_2() - .px_4() - .font("Zed Mono Extended") - .fill(rgb::(0x282c34)) - .child( - div() - .text_2xl() - .text_color(rgb::(0xffffff)) - .child(std::any::type_name::()), - ) + Story::container() + .child(Story::title(std::any::type_name::())) .child(traffic_lights()) } } diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs index 3239dbec9c0a8141ccc2cefa13151bf85f9a22e6..a21ee0a0bc334c743798519bcfd3658b689597b7 100644 --- a/crates/storybook/src/stories/elements/avatar.rs +++ b/crates/storybook/src/stories/elements/avatar.rs @@ -1,9 +1,11 @@ use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{rgb, Element, Hsla, IntoElement, ParentElement, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use ui::prelude::*; use ui::{avatar, theme}; +use crate::story::Story; + #[derive(Element, Default)] pub struct AvatarStory {} @@ -11,20 +13,8 @@ impl AvatarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - div() - .size_full() - .flex() - .flex_col() - .pt_2() - .px_4() - .font("Zed Mono Extended") - .fill(rgb::(0x282c34)) - .child( - div() - .text_2xl() - .text_color(rgb::(0xffffff)) - .child(std::any::type_name::()), - ) + Story::container() + .child(Story::title(std::any::type_name::())) .child( div() .flex() diff --git a/crates/storybook/src/story.rs b/crates/storybook/src/story.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a3145cf080a63ab2ea8284d6a7ae3da93a782f9 --- /dev/null +++ b/crates/storybook/src/story.rs @@ -0,0 +1,25 @@ +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{rgb, Element, Hsla, ParentElement}; + +pub struct Story {} + +impl Story { + pub fn container() -> div::Div { + div() + .size_full() + .flex() + .flex_col() + .pt_2() + .px_4() + .font("Zed Mono Extended") + .fill(rgb::(0x282c34)) + } + + pub fn title(title: &str) -> impl Element { + div() + .text_2xl() + .text_color(rgb::(0xffffff)) + .child(title.to_owned()) + } +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index b72c4a5681e99b48e326673464f9510e24bbe531..3974bbce401940fb8d4779451811fbf8ed3a8170 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -2,6 +2,7 @@ mod collab_panel; mod stories; +mod story; mod workspace; use std::str::FromStr; @@ -24,12 +25,12 @@ gpui2::actions! { } #[derive(Debug, Clone, Copy)] -enum Story { +enum StorySelector { Element(ElementStory), Component(ComponentStory), } -impl FromStr for Story { +impl FromStr for StorySelector { type Err = anyhow::Error; fn from_str(s: &str) -> std::result::Result { @@ -55,7 +56,7 @@ enum ComponentStory { #[derive(Parser)] struct Args { - story: Option, + story: Option, } fn main() { @@ -79,13 +80,13 @@ fn main() { ..Default::default() }, |cx| match args.story { - Some(Story::Element(ElementStory::Avatar)) => { + Some(StorySelector::Element(ElementStory::Avatar)) => { view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default())) } - Some(Story::Component(ComponentStory::Facepile)) => { + Some(StorySelector::Component(ComponentStory::Facepile)) => { view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default())) } - Some(Story::Component(ComponentStory::TrafficLights)) => view(|cx| { + Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| { render_story(&mut ViewContext::new(cx), TrafficLightsStory::default()) }), None => { From 71c1e36d1e436631af212b593048a184d2122885 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 13:35:30 -0400 Subject: [PATCH 34/89] Put `Theme` behind an `Arc` (#3017) This PR puts the `Theme` returned from the `theme` function behind an `Arc`. ### Motivation While working on wiring up window focus events for the `TitleBar` component we ran into issues where `theme` was holding an immutable borrow to the `ViewContext` for the entirety of the `render` scope, which prevented having mutable borrows in the same scope. ### Explanation To avoid this, we can make `theme` return an `Arc` to allow for cheap clones and avoiding the issues with the borrow checker. Release Notes: - N/A Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/gpui/src/app/window.rs | 14 +++++++++----- crates/storybook/src/collab_panel.rs | 14 +++++++------- crates/ui/src/templates/collab_panel.rs | 14 +++++++------- crates/ui/src/templates/status_bar.rs | 4 ++-- crates/ui/src/theme.rs | 3 ++- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 626a969bd8f35795295fa3f70aa8182d0cad93d5..4eca6f3a301f3a817393cdd787161f86b770f630 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -33,6 +33,7 @@ use std::{ any::{type_name, Any, TypeId}, mem, ops::{Deref, DerefMut, Range, Sub}, + sync::Arc, }; use taffy::{ tree::{Measurable, MeasureFunc}, @@ -56,7 +57,7 @@ pub struct Window { pub(crate) rendered_views: HashMap>, scene: SceneBuilder, pub(crate) text_style_stack: Vec, - pub(crate) theme_stack: Vec>, + pub(crate) theme_stack: Vec>, pub(crate) new_parents: HashMap, pub(crate) views_to_notify_if_ancestors_change: HashMap>, titlebar_height: f32, @@ -1336,18 +1337,21 @@ impl<'a> WindowContext<'a> { self.window.text_style_stack.pop(); } - pub fn theme(&self) -> &T { + pub fn theme(&self) -> Arc { self.window .theme_stack .iter() .rev() - .find_map(|theme| theme.downcast_ref()) + .find_map(|theme| { + let entry = Arc::clone(theme); + entry.downcast::().ok() + }) .ok_or_else(|| anyhow!("no theme provided of type {}", type_name::())) .unwrap() } - pub fn push_theme(&mut self, theme: T) { - self.window.theme_stack.push(Box::new(theme)); + pub fn push_theme(&mut self, theme: T) { + self.window.theme_stack.push(Arc::new(theme)); } pub fn pop_theme(&mut self) { diff --git a/crates/storybook/src/collab_panel.rs b/crates/storybook/src/collab_panel.rs index 30d15a5b0400c653dc41a9b65d546b05bb1eed72..a6be248d6af066bf3e688d14073e2bd6ef44bccb 100644 --- a/crates/storybook/src/collab_panel.rs +++ b/crates/storybook/src/collab_panel.rs @@ -52,12 +52,12 @@ impl CollabPanelElement { //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state // .group() // List Section Header - .child(self.list_section_header("#CRDB", true, theme)) + .child(self.list_section_header("#CRDB", true, &theme)) // List Item Large .child(self.list_item( "http://github.com/maxbrunsfeld.png?s=50", "maxbrunsfeld", - theme, + &theme, )), ) .child( @@ -65,31 +65,31 @@ impl CollabPanelElement { .py_2() .flex() .flex_col() - .child(self.list_section_header("CHANNELS", true, theme)), + .child(self.list_section_header("CHANNELS", true, &theme)), ) .child( div() .py_2() .flex() .flex_col() - .child(self.list_section_header("CONTACTS", true, theme)) + .child(self.list_section_header("CONTACTS", true, &theme)) .children( std::iter::repeat_with(|| { vec![ self.list_item( "http://github.com/as-cii.png?s=50", "as-cii", - theme, + &theme, ), self.list_item( "http://github.com/nathansobo.png?s=50", "nathansobo", - theme, + &theme, ), self.list_item( "http://github.com/maxbrunsfeld.png?s=50", "maxbrunsfeld", - theme, + &theme, ), ] }) diff --git a/crates/ui/src/templates/collab_panel.rs b/crates/ui/src/templates/collab_panel.rs index 7e76cb68355b3c09bf45e02a40c62376d4934612..3d6efe54f43ad78d74a021d632a6ac56f1dde4cb 100644 --- a/crates/ui/src/templates/collab_panel.rs +++ b/crates/ui/src/templates/collab_panel.rs @@ -52,12 +52,12 @@ impl CollabPanelElement { //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state // .group() // List Section Header - .child(self.list_section_header("#CRDB", true, theme)) + .child(self.list_section_header("#CRDB", true, &theme)) // List Item Large .child(self.list_item( "http://github.com/maxbrunsfeld.png?s=50", "maxbrunsfeld", - theme, + &theme, )), ) .child( @@ -65,31 +65,31 @@ impl CollabPanelElement { .py_2() .flex() .flex_col() - .child(self.list_section_header("CHANNELS", true, theme)), + .child(self.list_section_header("CHANNELS", true, &theme)), ) .child( div() .py_2() .flex() .flex_col() - .child(self.list_section_header("CONTACTS", true, theme)) + .child(self.list_section_header("CONTACTS", true, &theme)) .children( std::iter::repeat_with(|| { vec![ self.list_item( "http://github.com/as-cii.png?s=50", "as-cii", - theme, + &theme, ), self.list_item( "http://github.com/nathansobo.png?s=50", "nathansobo", - theme, + &theme, ), self.list_item( "http://github.com/maxbrunsfeld.png?s=50", "maxbrunsfeld", - theme, + &theme, ), ] }) diff --git a/crates/ui/src/templates/status_bar.rs b/crates/ui/src/templates/status_bar.rs index 79265a757296394c3603abbca02750d2a855cb23..22a99fdbf8f4a7099cb44b14180225fbc91b6e10 100644 --- a/crates/ui/src/templates/status_bar.rs +++ b/crates/ui/src/templates/status_bar.rs @@ -96,8 +96,8 @@ impl StatusBar { .justify_between() .w_full() .fill(theme.lowest.base.default.background) - .child(self.left_tools(theme)) - .child(self.right_tools(theme)) + .child(self.left_tools(&theme)) + .child(self.right_tools(&theme)) } fn left_tools(&self, theme: &Theme) -> impl Element { diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 71235625d6dfc3cc7faa9c1f143688b5cb68fa20..d4c8c262739d102c2515c097d88f8da22e70a515 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fmt; use std::marker::PhantomData; +use std::sync::Arc; use gpui2::color::Hsla; use gpui2::element::Element; @@ -190,6 +191,6 @@ fn preferred_theme(cx: &AppContext) -> Theme { .clone() } -pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme { +pub fn theme(cx: &WindowContext) -> Arc { cx.theme::() } From 8b6e982495aabf8396cae6fd93ff312d6c525fab Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 14:06:09 -0400 Subject: [PATCH 35/89] Remove manual mapping in `FromStr` implementation for `StorySelector` (#3018) This PR removes the need for writing manual mappings in the `FromStr` implementation for the `StorySelector` enum used in the storybook CLI. We are now using the [`EnumString`](https://docs.rs/strum/0.25.0/strum/derive.EnumString.html) trait from `strum` to automatically derive snake_cased names for the enums. This will cut down on some of the manual work needed to wire up more stories to the storybook. Release Notes: - N/A --- Cargo.lock | 23 ++++++++++++++++++++++ crates/storybook/Cargo.toml | 1 + crates/storybook/src/storybook.rs | 32 ++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c01bbc4a9cd0dce0a89eb858e016c817657c204..186427e863b97903833c66c4506a0b4263b9a954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7381,6 +7381,7 @@ dependencies = [ "serde", "settings", "simplelog", + "strum", "theme", "ui", "util", @@ -7403,6 +7404,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] + [[package]] name = "subtle" version = "2.4.1" diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 882a1a1bdb14b98907728543bd0552fb50667441..49dd05ba30b515d81e59d24e91dc74192579bbeb 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -17,6 +17,7 @@ rust-embed.workspace = true serde.workspace = true settings = { path = "../settings" } simplelog = "0.9" +strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } ui = { path = "../ui" } util = { path = "../util" } diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 3974bbce401940fb8d4779451811fbf8ed3a8170..a57be4fd89e98a4b6686180010c2d1a90a488631 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -17,6 +17,7 @@ use simplelog::SimpleLogger; use stories::components::facepile::FacepileStory; use stories::components::traffic_lights::TrafficLightsStory; use stories::elements::avatar::AvatarStory; +use strum::EnumString; use ui::{ElementExt, Theme}; gpui2::actions! { @@ -33,22 +34,35 @@ enum StorySelector { impl FromStr for StorySelector { type Err = anyhow::Error; - fn from_str(s: &str) -> std::result::Result { - match s.to_ascii_lowercase().as_str() { - "elements/avatar" => Ok(Self::Element(ElementStory::Avatar)), - "components/facepile" => Ok(Self::Component(ComponentStory::Facepile)), - "components/traffic_lights" => Ok(Self::Component(ComponentStory::TrafficLights)), - _ => Err(anyhow!("story not found for '{s}'")), + fn from_str(raw_story_name: &str) -> std::result::Result { + let story = raw_story_name.to_ascii_lowercase(); + + if let Some((_, story)) = story.split_once("elements/") { + let element_story = ElementStory::from_str(story) + .with_context(|| format!("story not found for element '{story}'"))?; + + return Ok(Self::Element(element_story)); + } + + if let Some((_, story)) = story.split_once("components/") { + let component_story = ComponentStory::from_str(story) + .with_context(|| format!("story not found for component '{story}'"))?; + + return Ok(Self::Component(component_story)); } + + Err(anyhow!("story not found for '{raw_story_name}'")) } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, EnumString)] +#[strum(serialize_all = "snake_case")] enum ElementStory { Avatar, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, EnumString)] +#[strum(serialize_all = "snake_case")] enum ComponentStory { Facepile, TrafficLights, @@ -122,7 +136,7 @@ fn current_theme(cx: &mut ViewContext) -> Theme { .clone() } -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use gpui2::AssetSource; use rust_embed::RustEmbed; use workspace::WorkspaceElement; From d0b15ed9408bf3b7d67eda63d4273b928368724b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 15:27:42 -0400 Subject: [PATCH 36/89] Report which requested font families are not present on the system (#3006) This PR improves the error message when `FontCache.load_family` attempts to load a font that is not present on the system. I ran into this while trying to run the `storybook` for the first time. The error message indicated that a font family was not found, but did not provide any information as to which font family was being loaded. ### Before ``` Compiling storybook v0.1.0 (/Users/maxdeviant/projects/zed/crates/storybook) Finished dev [unoptimized + debuginfo] target(s) in 8.52s Running `/Users/maxdeviant/projects/zed/target/debug/storybook` thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: could not find a non-empty font family matching one of the given names', crates/theme/src/theme_settings.rs:132:18 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace libc++abi: terminating due to uncaught foreign exception fish: Job 1, 'cargo run' terminated by signal SIGABRT (Abort) ``` ### After ``` Compiling storybook v0.1.0 (/Users/maxdeviant/projects/zed/crates/storybook) Finished dev [unoptimized + debuginfo] target(s) in 7.90s Running `/Users/maxdeviant/projects/zed/target/debug/storybook` thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: could not find a non-empty font family matching one of the given names: `Zed Mono`', crates/theme/src/theme_settings.rs:132:18 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace libc++abi: terminating due to uncaught foreign exception fish: Job 1, 'cargo run' terminated by signal SIGABRT (Abort) ``` Release Notes: - N/A --- crates/gpui/src/font_cache.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/font_cache.rs b/crates/gpui/src/font_cache.rs index 4f0d4fd46159aa259d94f404e78bd9d5a55c0365..b2dc79c87b1455386d0a1a5642a54d963f060019 100644 --- a/crates/gpui/src/font_cache.rs +++ b/crates/gpui/src/font_cache.rs @@ -98,7 +98,12 @@ impl FontCache { } Err(anyhow!( - "could not find a non-empty font family matching one of the given names" + "could not find a non-empty font family matching one of the given names: {}", + names + .iter() + .map(|name| format!("`{name}`")) + .collect::>() + .join(", ") )) } From 27e3e09bb9e4f08a9c836e62381c8b9f9f7ed08a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 15:48:32 -0400 Subject: [PATCH 37/89] Label component states in stories (#3019) This PR updates the UI component stories to label the various states that they are in. Release Notes: - N/A --- .../src/stories/components/facepile.rs | 57 ++++++++----------- .../src/stories/components/traffic_lights.rs | 3 +- .../storybook/src/stories/elements/avatar.rs | 21 +++---- crates/storybook/src/story.rs | 15 ++++- crates/ui/src/components/facepile.rs | 6 +- crates/ui/src/components/follow_group.rs | 2 +- 6 files changed, 53 insertions(+), 51 deletions(-) diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs index 96c3aff2afbe5dff1971667eb18428d5d601f845..37893a9a0a4c1400493df87b9eebd76bbc568fae 100644 --- a/crates/storybook/src/stories/components/facepile.rs +++ b/crates/storybook/src/stories/components/facepile.rs @@ -13,47 +13,38 @@ impl FacepileStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let avatars = vec![ + avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), + avatar("https://avatars.githubusercontent.com/u/482957?v=4"), + avatar("https://avatars.githubusercontent.com/u/1789?v=4"), + ]; + Story::container() - .child(Story::title(std::any::type_name::())) + .child(Story::title_for::<_, ui::Facepile>()) + .child(Story::label("Default")) .child( div() .flex() .gap_3() - .child(facepile(vec![avatar( - "https://avatars.githubusercontent.com/u/1714999?v=4", - )])) - .child(facepile(vec![ - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - ])) - .child(facepile(vec![ - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4"), - ])), + .child(facepile(avatars.clone().into_iter().take(1))) + .child(facepile(avatars.clone().into_iter().take(2))) + .child(facepile(avatars.clone().into_iter().take(3))), ) - .child( + .child(Story::label("Rounded rectangle avatars")) + .child({ + let shape = Shape::RoundedRectangle; + + let avatars = avatars + .clone() + .into_iter() + .map(|avatar| avatar.shape(Shape::RoundedRectangle)); + div() .flex() .gap_3() - .child(facepile(vec![avatar( - "https://avatars.githubusercontent.com/u/1714999?v=4", - ) - .shape(Shape::RoundedRectangle)])) - .child(facepile(vec![ - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - ])) - .child(facepile(vec![ - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - ])), - ) + .child(facepile(avatars.clone().take(1))) + .child(facepile(avatars.clone().take(2))) + .child(facepile(avatars.clone().take(3))) + }) } } diff --git a/crates/storybook/src/stories/components/traffic_lights.rs b/crates/storybook/src/stories/components/traffic_lights.rs index 9f84446eed383ee8fbd637b5b00b7212137acdc2..f3c7b93005b602b1f58a4c3f762f60217f11ebb6 100644 --- a/crates/storybook/src/stories/components/traffic_lights.rs +++ b/crates/storybook/src/stories/components/traffic_lights.rs @@ -11,7 +11,8 @@ impl TrafficLightsStory { let theme = theme(cx); Story::container() - .child(Story::title(std::any::type_name::())) + .child(Story::title_for::<_, ui::TrafficLights>()) + .child(Story::label("Default")) .child(traffic_lights()) } } diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs index a21ee0a0bc334c743798519bcfd3658b689597b7..a5caf717e7168c2bf32efd5eff2276b1341b5637 100644 --- a/crates/storybook/src/stories/elements/avatar.rs +++ b/crates/storybook/src/stories/elements/avatar.rs @@ -1,5 +1,3 @@ -use gpui2::elements::div; -use gpui2::style::StyleHelpers; use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use ui::prelude::*; use ui::{avatar, theme}; @@ -14,18 +12,15 @@ impl AvatarStory { let theme = theme(cx); Story::container() - .child(Story::title(std::any::type_name::())) + .child(Story::title_for::<_, ui::Avatar>()) + .child(Story::label("Default")) + .child(avatar( + "https://avatars.githubusercontent.com/u/1714999?v=4", + )) + .child(Story::label("Rounded rectangle")) .child( - div() - .flex() - .gap_3() - .child(avatar( - "https://avatars.githubusercontent.com/u/1714999?v=4", - )) - .child( - avatar("https://avatars.githubusercontent.com/u/1714999?v=4") - .shape(Shape::RoundedRectangle), - ), + avatar("https://avatars.githubusercontent.com/u/1714999?v=4") + .shape(Shape::RoundedRectangle), ) } } diff --git a/crates/storybook/src/story.rs b/crates/storybook/src/story.rs index 7a3145cf080a63ab2ea8284d6a7ae3da93a782f9..c758518762b4ba124d9e74237940d05547dac04d 100644 --- a/crates/storybook/src/story.rs +++ b/crates/storybook/src/story.rs @@ -18,8 +18,21 @@ impl Story { pub fn title(title: &str) -> impl Element { div() - .text_2xl() + .text_xl() .text_color(rgb::(0xffffff)) .child(title.to_owned()) } + + pub fn title_for() -> impl Element { + Self::title(std::any::type_name::()) + } + + pub fn label(label: &str) -> impl Element { + div() + .mt_4() + .mb_2() + .text_xs() + .text_color(rgb::(0xffffff)) + .child(label.to_owned()) + } } diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index d9566c216cfbf82fc0dce336b3da0dc2bf67e6c6..39167498a95c447d69f2c59ab74410e1fb2777be 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -9,8 +9,10 @@ pub struct Facepile { players: Vec, } -pub fn facepile(players: Vec) -> Facepile { - Facepile { players } +pub fn facepile>(players: P) -> Facepile { + Facepile { + players: players.collect(), + } } impl Facepile { diff --git a/crates/ui/src/components/follow_group.rs b/crates/ui/src/components/follow_group.rs index 75e24e9135a93576b63dd65f90dd5e617c26fc37..a52285986812145a1fb0b7eb226b856f2f0fa4cf 100644 --- a/crates/ui/src/components/follow_group.rs +++ b/crates/ui/src/components/follow_group.rs @@ -46,7 +46,7 @@ impl FollowGroup { .px_1() .rounded_lg() .fill(player_bg) - .child(facepile(self.players.clone())), + .child(facepile(self.players.clone().into_iter())), ) } } From fe4248cf342ea00942e9048dbd8ed2c544dd9cdd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 16:57:33 -0400 Subject: [PATCH 38/89] Scaffold `Toolbar` and `Breadcrumb` components (#3020) This PR scaffolds the `Toolbar` and `Breadcrumb` components. Right now they both just consist of hardcoded data. Screenshot 2023-09-22 at 4 54 00 PM Screenshot 2023-09-22 at 4 46 04 PM Release Notes: - N/A --- crates/storybook/src/stories/components.rs | 2 ++ .../src/stories/components/breadcrumb.rs | 16 +++++++++ .../src/stories/components/toolbar.rs | 16 +++++++++ crates/storybook/src/storybook.rs | 10 ++++++ crates/storybook/src/workspace.rs | 5 +-- crates/ui/src/components.rs | 4 +++ crates/ui/src/components/breadcrumb.rs | 36 +++++++++++++++++++ crates/ui/src/components/toolbar.rs | 35 ++++++++++++++++++ 8 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 crates/storybook/src/stories/components/breadcrumb.rs create mode 100644 crates/storybook/src/stories/components/toolbar.rs create mode 100644 crates/ui/src/components/breadcrumb.rs create mode 100644 crates/ui/src/components/toolbar.rs diff --git a/crates/storybook/src/stories/components.rs b/crates/storybook/src/stories/components.rs index 89ee5a92eae848dbd31721f378472cd2615f942a..35bed24c01d27f3542a5bdc04c9f111e28bdca54 100644 --- a/crates/storybook/src/stories/components.rs +++ b/crates/storybook/src/stories/components.rs @@ -1,2 +1,4 @@ +pub mod breadcrumb; pub mod facepile; +pub mod toolbar; pub mod traffic_lights; diff --git a/crates/storybook/src/stories/components/breadcrumb.rs b/crates/storybook/src/stories/components/breadcrumb.rs new file mode 100644 index 0000000000000000000000000000000000000000..c702eb3def7ca3558a722e05f219767d0e48e103 --- /dev/null +++ b/crates/storybook/src/stories/components/breadcrumb.rs @@ -0,0 +1,16 @@ +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; +use ui::breadcrumb; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct BreadcrumbStory {} + +impl BreadcrumbStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container() + .child(Story::title_for::<_, ui::Breadcrumb>()) + .child(Story::label("Default")) + .child(breadcrumb()) + } +} diff --git a/crates/storybook/src/stories/components/toolbar.rs b/crates/storybook/src/stories/components/toolbar.rs new file mode 100644 index 0000000000000000000000000000000000000000..7a7688a9d8f5577929d3d33da581985c6f31663e --- /dev/null +++ b/crates/storybook/src/stories/components/toolbar.rs @@ -0,0 +1,16 @@ +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; +use ui::toolbar; + +use crate::story::Story; + +#[derive(Element, Default)] +pub struct ToolbarStory {} + +impl ToolbarStory { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + Story::container() + .child(Story::title_for::<_, ui::Toolbar>()) + .child(Story::label("Default")) + .child(toolbar()) + } +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index a57be4fd89e98a4b6686180010c2d1a90a488631..59aca22be7bd7716befdeb86bfe7c9d818f4e1ef 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -14,7 +14,9 @@ use legacy_theme::ThemeSettings; use log::LevelFilter; use settings::{default_settings, SettingsStore}; use simplelog::SimpleLogger; +use stories::components::breadcrumb::BreadcrumbStory; use stories::components::facepile::FacepileStory; +use stories::components::toolbar::ToolbarStory; use stories::components::traffic_lights::TrafficLightsStory; use stories::elements::avatar::AvatarStory; use strum::EnumString; @@ -64,7 +66,9 @@ enum ElementStory { #[derive(Debug, Clone, Copy, EnumString)] #[strum(serialize_all = "snake_case")] enum ComponentStory { + Breadcrumb, Facepile, + Toolbar, TrafficLights, } @@ -97,9 +101,15 @@ fn main() { Some(StorySelector::Element(ElementStory::Avatar)) => { view(|cx| render_story(&mut ViewContext::new(cx), AvatarStory::default())) } + Some(StorySelector::Component(ComponentStory::Breadcrumb)) => { + view(|cx| render_story(&mut ViewContext::new(cx), BreadcrumbStory::default())) + } Some(StorySelector::Component(ComponentStory::Facepile)) => { view(|cx| render_story(&mut ViewContext::new(cx), FacepileStory::default())) } + Some(StorySelector::Component(ComponentStory::Toolbar)) => { + view(|cx| render_story(&mut ViewContext::new(cx), ToolbarStory::default())) + } Some(StorySelector::Component(ComponentStory::TrafficLights)) => view(|cx| { render_story(&mut ViewContext::new(cx), TrafficLightsStory::default()) }), diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs index 58c5fed62dad748f98f225d0acb6d1d77b3090cd..3ddaa5caa672b26a5271d2131c4a08abff4c7e0e 100644 --- a/crates/storybook/src/workspace.rs +++ b/crates/storybook/src/workspace.rs @@ -3,7 +3,7 @@ use gpui2::{ style::StyleHelpers, Element, IntoElement, ParentElement, ViewContext, }; -use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar}; +use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar, toolbar}; #[derive(Element, Default)] pub struct WorkspaceElement { @@ -45,7 +45,8 @@ impl WorkspaceElement { .flex() .flex_col() .flex_1() - .child(tab_bar(self.tab_bar_scroll_state.clone())), + .child(tab_bar(self.tab_bar_scroll_state.clone())) + .child(toolbar()), ), ) .child(chat_panel(self.right_scroll_state.clone())), diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index a8fbc02265b024fed8398dd941dbccebb0ed2b78..15790fe9f80000ef95401ea386acf0826c5cc4f5 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,17 +1,21 @@ +mod breadcrumb; mod facepile; mod follow_group; mod list_item; mod list_section_header; mod palette_item; mod tab; +mod toolbar; mod traffic_lights; +pub use breadcrumb::*; pub use facepile::*; pub use follow_group::*; pub use list_item::*; pub use list_section_header::*; pub use palette_item::*; pub use tab::*; +pub use toolbar::*; pub use traffic_lights::*; use std::marker::PhantomData; diff --git a/crates/ui/src/components/breadcrumb.rs b/crates/ui/src/components/breadcrumb.rs new file mode 100644 index 0000000000000000000000000000000000000000..1883e35ae99d4cea585802d1f92504e19d80ef8e --- /dev/null +++ b/crates/ui/src/components/breadcrumb.rs @@ -0,0 +1,36 @@ +use gpui2::elements::div; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::theme; + +#[derive(Element)] +pub struct Breadcrumb {} + +pub fn breadcrumb() -> Breadcrumb { + Breadcrumb {} +} + +impl Breadcrumb { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .px_1() + .flex() + .flex_row() + // TODO: Read font from theme (or settings?). + .font("Zed Mono Extended") + .text_sm() + .text_color(theme.middle.base.default.foreground) + .rounded_md() + .hover() + .fill(theme.highest.base.hovered.background) + // TODO: Replace hardcoded breadcrumbs. + .child("crates/ui/src/components/toolbar.rs") + .child(" › ") + .child("impl Breadcrumb") + .child(" › ") + .child("fn render") + } +} diff --git a/crates/ui/src/components/toolbar.rs b/crates/ui/src/components/toolbar.rs new file mode 100644 index 0000000000000000000000000000000000000000..cc493c00f4ca615a836eb8e3b2b4420c10fb3cc6 --- /dev/null +++ b/crates/ui/src/components/toolbar.rs @@ -0,0 +1,35 @@ +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{Element, IntoElement, ParentElement, ViewContext}; + +use crate::{breadcrumb, icon_button, theme}; + +pub struct ToolbarItem {} + +#[derive(Element)] +pub struct Toolbar { + items: Vec, +} + +pub fn toolbar() -> Toolbar { + Toolbar { items: Vec::new() } +} + +impl Toolbar { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .p_2() + .flex() + .justify_between() + .child(breadcrumb()) + .child( + div() + .flex() + .child(icon_button("icons/inlay_hint.svg")) + .child(icon_button("icons/magnifying_glass.svg")) + .child(icon_button("icons/magic-wand.svg")), + ) + } +} From ad62a966a65dc506d90c5db80d231e869bb0077b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 18:16:16 -0400 Subject: [PATCH 39/89] Display available stories in storybook CLI (#3021) This PR updates the storybook CLI to support displaying all of the available stories. The `--help` flag will now show a list of all the available stories: Screenshot 2023-09-22 at 6 11 00 PM Inputting an invalid story name will also show the list of available stories: Screenshot 2023-09-22 at 6 10 43 PM Release Notes: - N/A --- Cargo.lock | 2 +- crates/storybook/Cargo.toml | 2 +- crates/storybook/src/story_selector.rs | 76 ++++++++++++++++++++++++++ crates/storybook/src/storybook.rs | 55 ++----------------- 4 files changed, 84 insertions(+), 51 deletions(-) create mode 100644 crates/storybook/src/story_selector.rs diff --git a/Cargo.lock b/Cargo.lock index 186427e863b97903833c66c4506a0b4263b9a954..878604f3609fab58f724f62e16312a150522fc35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7374,7 +7374,7 @@ name = "storybook" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap 4.4.4", "gpui2", "log", "rust-embed", diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 49dd05ba30b515d81e59d24e91dc74192579bbeb..5c73235d3e6a1b8bfe021c2bf44f0ea519b5fee2 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -10,7 +10,7 @@ path = "src/storybook.rs" [dependencies] anyhow.workspace = true -clap = { version = "3.1", features = ["derive"] } +clap = { version = "4.4", features = ["derive", "string"] } gpui2 = { path = "../gpui2" } log.workspace = true rust-embed.workspace = true diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..d12368706a6908e201063d2ec14609f198b28321 --- /dev/null +++ b/crates/storybook/src/story_selector.rs @@ -0,0 +1,76 @@ +use std::{str::FromStr, sync::OnceLock}; + +use anyhow::{anyhow, Context}; +use clap::builder::PossibleValue; +use clap::ValueEnum; +use strum::{EnumIter, EnumString, IntoEnumIterator}; + +#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)] +#[strum(serialize_all = "snake_case")] +pub enum ElementStory { + Avatar, +} + +#[derive(Debug, Clone, Copy, strum::Display, EnumString, EnumIter)] +#[strum(serialize_all = "snake_case")] +pub enum ComponentStory { + Breadcrumb, + Facepile, + Toolbar, + TrafficLights, +} + +#[derive(Debug, Clone, Copy)] +pub enum StorySelector { + Element(ElementStory), + Component(ComponentStory), +} + +impl FromStr for StorySelector { + type Err = anyhow::Error; + + fn from_str(raw_story_name: &str) -> std::result::Result { + let story = raw_story_name.to_ascii_lowercase(); + + if let Some((_, story)) = story.split_once("elements/") { + let element_story = ElementStory::from_str(story) + .with_context(|| format!("story not found for element '{story}'"))?; + + return Ok(Self::Element(element_story)); + } + + if let Some((_, story)) = story.split_once("components/") { + let component_story = ComponentStory::from_str(story) + .with_context(|| format!("story not found for component '{story}'"))?; + + return Ok(Self::Component(component_story)); + } + + Err(anyhow!("story not found for '{raw_story_name}'")) + } +} + +/// The list of all stories available in the storybook. +static ALL_STORIES: OnceLock> = OnceLock::new(); + +impl ValueEnum for StorySelector { + fn value_variants<'a>() -> &'a [Self] { + let stories = ALL_STORIES.get_or_init(|| { + let element_stories = ElementStory::iter().map(Self::Element); + let component_stories = ComponentStory::iter().map(Self::Component); + + element_stories.chain(component_stories).collect::>() + }); + + stories + } + + fn to_possible_value(&self) -> Option { + let value = match self { + Self::Element(story) => format!("elements/{story}"), + Self::Component(story) => format!("components/{story}"), + }; + + Some(PossibleValue::new(value)) + } +} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 59aca22be7bd7716befdeb86bfe7c9d818f4e1ef..d52219f61ef768f52cd103ba80dc22c17858bdf2 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -3,10 +3,9 @@ mod collab_panel; mod stories; mod story; +mod story_selector; mod workspace; -use std::str::FromStr; - use ::theme as legacy_theme; use clap::Parser; use gpui2::{serde_json, vec2f, view, Element, IntoElement, RectF, ViewContext, WindowBounds}; @@ -19,61 +18,19 @@ use stories::components::facepile::FacepileStory; use stories::components::toolbar::ToolbarStory; use stories::components::traffic_lights::TrafficLightsStory; use stories::elements::avatar::AvatarStory; -use strum::EnumString; use ui::{ElementExt, Theme}; +use crate::story_selector::{ComponentStory, ElementStory, StorySelector}; + gpui2::actions! { storybook, [ToggleInspector] } -#[derive(Debug, Clone, Copy)] -enum StorySelector { - Element(ElementStory), - Component(ComponentStory), -} - -impl FromStr for StorySelector { - type Err = anyhow::Error; - - fn from_str(raw_story_name: &str) -> std::result::Result { - let story = raw_story_name.to_ascii_lowercase(); - - if let Some((_, story)) = story.split_once("elements/") { - let element_story = ElementStory::from_str(story) - .with_context(|| format!("story not found for element '{story}'"))?; - - return Ok(Self::Element(element_story)); - } - - if let Some((_, story)) = story.split_once("components/") { - let component_story = ComponentStory::from_str(story) - .with_context(|| format!("story not found for component '{story}'"))?; - - return Ok(Self::Component(component_story)); - } - - Err(anyhow!("story not found for '{raw_story_name}'")) - } -} - -#[derive(Debug, Clone, Copy, EnumString)] -#[strum(serialize_all = "snake_case")] -enum ElementStory { - Avatar, -} - -#[derive(Debug, Clone, Copy, EnumString)] -#[strum(serialize_all = "snake_case")] -enum ComponentStory { - Breadcrumb, - Facepile, - Toolbar, - TrafficLights, -} - #[derive(Parser)] +#[command(author, version, about, long_about = None)] struct Args { + #[arg(value_enum)] story: Option, } @@ -146,7 +103,7 @@ fn current_theme(cx: &mut ViewContext) -> Theme { .clone() } -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use gpui2::AssetSource; use rust_embed::RustEmbed; use workspace::WorkspaceElement; From 895386cfaf33c775205d870f1b7ad620cdb01077 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 19:14:12 -0400 Subject: [PATCH 40/89] Mainline `Icon` and `IconButton` changes (#3022) This PR mainlines the `Icon` and `IconButton` changes from the `gpui2-ui` branch. Release Notes: - N/A Co-authored-by: Nate Butler --- crates/ui/src/components/toolbar.rs | 8 +-- crates/ui/src/elements/icon.rs | 89 +++++++++++++++++++++++++-- crates/ui/src/elements/icon_button.rs | 46 +++++++++----- crates/ui/src/templates/chat_panel.rs | 9 +-- crates/ui/src/templates/status_bar.rs | 21 ++++--- crates/ui/src/templates/tab_bar.rs | 16 ++--- crates/ui/src/templates/title_bar.rs | 19 ++++-- 7 files changed, 159 insertions(+), 49 deletions(-) diff --git a/crates/ui/src/components/toolbar.rs b/crates/ui/src/components/toolbar.rs index cc493c00f4ca615a836eb8e3b2b4420c10fb3cc6..08e671311f9aea7cb7a60a66a2bf610cfa6fc6fd 100644 --- a/crates/ui/src/components/toolbar.rs +++ b/crates/ui/src/components/toolbar.rs @@ -2,7 +2,7 @@ use gpui2::elements::div; use gpui2::style::StyleHelpers; use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -use crate::{breadcrumb, icon_button, theme}; +use crate::{breadcrumb, theme, IconAsset, IconButton}; pub struct ToolbarItem {} @@ -27,9 +27,9 @@ impl Toolbar { .child( div() .flex() - .child(icon_button("icons/inlay_hint.svg")) - .child(icon_button("icons/magnifying_glass.svg")) - .child(icon_button("icons/magic-wand.svg")), + .child(IconButton::new(IconAsset::InlayHint)) + .child(IconButton::new(IconAsset::MagnifyingGlass)) + .child(IconButton::new(IconAsset::MagicWand)), ) } } diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index 08b8e3e7c5733589537f46623c257d0801de9852..7d2bb45b494aa2ecfa8032936e17e7007a4303af 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -1,8 +1,41 @@ +use std::sync::Arc; + use crate::theme::theme; +use crate::Theme; use gpui2::elements::svg; use gpui2::style::StyleHelpers; -use gpui2::IntoElement; use gpui2::{Element, ViewContext}; +use gpui2::{Hsla, IntoElement}; + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum IconColor { + #[default] + Default, + Muted, + Disabled, + Placeholder, + Accent, + Error, + Warning, + Success, + Info, +} + +impl IconColor { + pub fn color(self, theme: Arc) -> Hsla { + match self { + IconColor::Default => theme.lowest.base.default.foreground, + IconColor::Muted => theme.lowest.variant.default.foreground, + IconColor::Disabled => theme.lowest.base.disabled.foreground, + IconColor::Placeholder => theme.lowest.base.disabled.foreground, + IconColor::Accent => theme.lowest.accent.default.foreground, + IconColor::Error => theme.lowest.negative.default.foreground, + IconColor::Warning => theme.lowest.warning.default.foreground, + IconColor::Success => theme.lowest.positive.default.foreground, + IconColor::Info => theme.lowest.accent.default.foreground, + } + } +} #[derive(Default, PartialEq, Copy, Clone)] pub enum IconAsset { @@ -10,21 +43,40 @@ pub enum IconAsset { ArrowLeft, ArrowRight, ArrowUpRight, + AudioOff, + AudioOn, Bolt, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, - #[default] + Close, + ExclamationTriangle, File, FileDoc, FileGit, FileLock, FileRust, FileToml, + FileTree, Folder, FolderOpen, + FolderX, + #[default] Hash, + InlayHint, + MagicWand, + MagnifyingGlass, + MessageBubbles, + Mic, + MicMute, + Plus, + Screen, + Split, + Terminal, + XCircle, + Copilot, + Envelope, } impl IconAsset { @@ -34,20 +86,39 @@ impl IconAsset { IconAsset::ArrowLeft => "icons/arrow_left.svg", IconAsset::ArrowRight => "icons/arrow_right.svg", IconAsset::ArrowUpRight => "icons/arrow_up_right.svg", + IconAsset::AudioOff => "icons/speaker-off.svg", + IconAsset::AudioOn => "icons/speaker-loud.svg", IconAsset::Bolt => "icons/bolt.svg", IconAsset::ChevronDown => "icons/chevron_down.svg", IconAsset::ChevronLeft => "icons/chevron_left.svg", IconAsset::ChevronRight => "icons/chevron_right.svg", IconAsset::ChevronUp => "icons/chevron_up.svg", + IconAsset::Close => "icons/x.svg", + IconAsset::ExclamationTriangle => "icons/warning.svg", IconAsset::File => "icons/file_icons/file.svg", IconAsset::FileDoc => "icons/file_icons/book.svg", IconAsset::FileGit => "icons/file_icons/git.svg", IconAsset::FileLock => "icons/file_icons/lock.svg", IconAsset::FileRust => "icons/file_icons/rust.svg", IconAsset::FileToml => "icons/file_icons/toml.svg", + IconAsset::FileTree => "icons/project.svg", IconAsset::Folder => "icons/file_icons/folder.svg", IconAsset::FolderOpen => "icons/file_icons/folder_open.svg", + IconAsset::FolderX => "icons/stop_sharing.svg", IconAsset::Hash => "icons/hash.svg", + IconAsset::InlayHint => "icons/inlay_hint.svg", + IconAsset::MagicWand => "icons/magic-wand.svg", + IconAsset::MagnifyingGlass => "icons/magnifying_glass.svg", + IconAsset::MessageBubbles => "icons/conversations.svg", + IconAsset::Mic => "icons/mic.svg", + IconAsset::MicMute => "icons/mic-mute.svg", + IconAsset::Plus => "icons/plus.svg", + IconAsset::Screen => "icons/desktop.svg", + IconAsset::Split => "icons/split.svg", + IconAsset::Terminal => "icons/terminal.svg", + IconAsset::XCircle => "icons/error.svg", + IconAsset::Copilot => "icons/copilot.svg", + IconAsset::Envelope => "icons/feedback.svg", } } } @@ -55,20 +126,30 @@ impl IconAsset { #[derive(Element, Clone)] pub struct Icon { asset: IconAsset, + color: IconColor, } pub fn icon(asset: IconAsset) -> Icon { - Icon { asset } + Icon { + asset, + color: IconColor::default(), + } } impl Icon { + pub fn color(mut self, color: IconColor) -> Self { + self.color = color; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let fill = self.color.color(theme); svg() .flex_none() .path(self.asset.path()) .size_4() - .fill(theme.lowest.variant.default.foreground) + .fill(fill) } } diff --git a/crates/ui/src/elements/icon_button.rs b/crates/ui/src/elements/icon_button.rs index 4270e0ca5daa3e1dd3655dd54a9e3f48028c7d1c..893d41871da02ce135e21aea468d4907e40f312c 100644 --- a/crates/ui/src/elements/icon_button.rs +++ b/crates/ui/src/elements/icon_button.rs @@ -1,26 +1,47 @@ -use gpui2::elements::{div, svg}; +use gpui2::elements::div; use gpui2::style::{StyleHelpers, Styleable}; use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -use crate::prelude::*; -use crate::theme; +use crate::{icon, theme, IconColor}; +use crate::{prelude::*, IconAsset}; #[derive(Element)] pub struct IconButton { - path: &'static str, + icon: IconAsset, + color: IconColor, variant: ButtonVariant, state: InteractionState, } -pub fn icon_button(path: &'static str) -> IconButton { +pub fn icon_button() -> IconButton { IconButton { - path, + icon: IconAsset::default(), + color: IconColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), } } impl IconButton { + pub fn new(icon: IconAsset) -> Self { + Self { + icon, + color: IconColor::default(), + variant: ButtonVariant::default(), + state: InteractionState::default(), + } + } + + pub fn icon(mut self, icon: IconAsset) -> Self { + self.icon = icon; + self + } + + pub fn color(mut self, color: IconColor) -> Self { + self.color = color; + self + } + pub fn variant(mut self, variant: ButtonVariant) -> Self { self.variant = variant; self @@ -34,13 +55,10 @@ impl IconButton { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - let icon_color; - - if self.state == InteractionState::Disabled { - icon_color = theme.highest.base.disabled.foreground; - } else { - icon_color = theme.highest.base.default.foreground; - } + let icon_color = match (self.state, self.color) { + (InteractionState::Disabled, _) => IconColor::Disabled, + _ => self.color, + }; let mut div = div(); if self.variant == ButtonVariant::Filled { @@ -57,6 +75,6 @@ impl IconButton { .fill(theme.highest.base.hovered.background) .active() .fill(theme.highest.base.pressed.background) - .child(svg().path(self.path).w_4().h_4().fill(icon_color)) + .child(icon(self.icon).color(icon_color)) } } diff --git a/crates/ui/src/templates/chat_panel.rs b/crates/ui/src/templates/chat_panel.rs index 7c3462b3fe46c31cb1e158548435f572915ff15c..2e3f73d30f093b758cbcae3fd09673e6b2b1a913 100644 --- a/crates/ui/src/templates/chat_panel.rs +++ b/crates/ui/src/templates/chat_panel.rs @@ -1,12 +1,13 @@ use std::marker::PhantomData; -use crate::icon_button; -use crate::theme::theme; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; +use crate::theme::theme; +use crate::{icon_button, IconAsset}; + #[derive(Element)] pub struct ChatPanel { view_type: PhantomData, @@ -57,8 +58,8 @@ impl ChatPanel { .flex() .items_center() .gap_px() - .child(icon_button("icons/plus.svg")) - .child(icon_button("icons/split.svg")), + .child(icon_button().icon(IconAsset::Plus)) + .child(icon_button().icon(IconAsset::Split)), ), ) } diff --git a/crates/ui/src/templates/status_bar.rs b/crates/ui/src/templates/status_bar.rs index 22a99fdbf8f4a7099cb44b14180225fbc91b6e10..21f792c35825314f7015a67768c665962c2b8410 100644 --- a/crates/ui/src/templates/status_bar.rs +++ b/crates/ui/src/templates/status_bar.rs @@ -1,11 +1,12 @@ use std::marker::PhantomData; -use crate::theme::{theme, Theme}; -use crate::{icon_button, text_button, tool_divider}; use gpui2::style::StyleHelpers; use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; +use crate::theme::{theme, Theme}; +use crate::{icon_button, text_button, tool_divider, IconAsset}; + #[derive(Default, PartialEq)] pub enum Tool { #[default] @@ -105,10 +106,10 @@ impl StatusBar { .flex() .items_center() .gap_1() - .child(icon_button("icons/project.svg")) - .child(icon_button("icons/hash.svg")) + .child(icon_button().icon(IconAsset::FileTree)) + .child(icon_button().icon(IconAsset::Hash)) .child(tool_divider()) - .child(icon_button("icons/error.svg")) + .child(icon_button().icon(IconAsset::XCircle)) } fn right_tools(&self, theme: &Theme) -> impl Element { div() @@ -129,8 +130,8 @@ impl StatusBar { .flex() .items_center() .gap_1() - .child(icon_button("icons/copilot.svg")) - .child(icon_button("icons/feedback.svg")), + .child(icon_button().icon(IconAsset::Copilot)) + .child(icon_button().icon(IconAsset::Envelope)), ) .child(tool_divider()) .child( @@ -138,9 +139,9 @@ impl StatusBar { .flex() .items_center() .gap_1() - .child(icon_button("icons/terminal.svg")) - .child(icon_button("icons/conversations.svg")) - .child(icon_button("icons/ai.svg")), + .child(icon_button().icon(IconAsset::Terminal)) + .child(icon_button().icon(IconAsset::MessageBubbles)) + .child(icon_button().icon(IconAsset::Ai)), ) } } diff --git a/crates/ui/src/templates/tab_bar.rs b/crates/ui/src/templates/tab_bar.rs index e1ca6b1dc793ed87bcc7d278ef1e2a3c6a9cc0b3..c8892b46973ca639e95f87f76815cd405cc0e6ed 100644 --- a/crates/ui/src/templates/tab_bar.rs +++ b/crates/ui/src/templates/tab_bar.rs @@ -1,13 +1,14 @@ use std::marker::PhantomData; -use crate::prelude::InteractionState; -use crate::theme::theme; -use crate::{icon_button, tab}; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; use gpui2::{elements::div, IntoElement}; use gpui2::{Element, ParentElement, ViewContext}; +use crate::prelude::InteractionState; +use crate::theme::theme; +use crate::{icon_button, tab, IconAsset}; + #[derive(Element)] pub struct TabBar { view_type: PhantomData, @@ -43,11 +44,12 @@ impl TabBar { .items_center() .gap_px() .child( - icon_button("icons/arrow_left.svg") + icon_button() + .icon(IconAsset::ArrowLeft) .state(InteractionState::Enabled.if_enabled(can_navigate_back)), ) .child( - icon_button("icons/arrow_right.svg").state( + icon_button().icon(IconAsset::ArrowRight).state( InteractionState::Enabled.if_enabled(can_navigate_forward), ), ), @@ -83,8 +85,8 @@ impl TabBar { .flex() .items_center() .gap_px() - .child(icon_button("icons/plus.svg")) - .child(icon_button("icons/split.svg")), + .child(icon_button().icon(IconAsset::Plus)) + .child(icon_button().icon(IconAsset::Split)), ), ) } diff --git a/crates/ui/src/templates/title_bar.rs b/crates/ui/src/templates/title_bar.rs index 82c5c0234b8f3f449bed331a84f88de0fa16fcc0..f926adbd26c13e8fb4315d20657b85adb4bed8f3 100644 --- a/crates/ui/src/templates/title_bar.rs +++ b/crates/ui/src/templates/title_bar.rs @@ -5,7 +5,10 @@ use gpui2::style::StyleHelpers; use gpui2::{Element, IntoElement, ParentElement, ViewContext}; use crate::prelude::Shape; -use crate::{avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights}; +use crate::{ + avatar, follow_group, icon_button, text_button, theme, tool_divider, traffic_lights, IconAsset, + IconColor, +}; #[derive(Element)] pub struct TitleBar { @@ -65,8 +68,8 @@ impl TitleBar { .flex() .items_center() .gap_1() - .child(icon_button("icons/stop_sharing.svg")) - .child(icon_button("icons/exit.svg")), + .child(icon_button().icon(IconAsset::FolderX)) + .child(icon_button().icon(IconAsset::Close)), ) .child(tool_divider()) .child( @@ -75,9 +78,13 @@ impl TitleBar { .flex() .items_center() .gap_1() - .child(icon_button("icons/mic.svg")) - .child(icon_button("icons/speaker-loud.svg")) - .child(icon_button("icons/desktop.svg")), + .child(icon_button().icon(IconAsset::Mic)) + .child(icon_button().icon(IconAsset::AudioOn)) + .child( + icon_button() + .icon(IconAsset::Screen) + .color(IconColor::Accent), + ), ) .child( div().px_2().flex().items_center().child( From 0697d08e54559513976182269b74f882982f5635 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Sep 2023 21:27:47 -0400 Subject: [PATCH 41/89] Restructure `ui` into just `elements` and `components` (#3023) This PR restructures the `ui` crate into just `elements` and `components`. This was already done on the `gpui2-ui` branch, just getting it onto `main`. Release Notes: - N/A --------- Co-authored-by: Nate Butler --- crates/ui/src/components.rs | 22 +++++++++++++++++++ .../{templates => components}/chat_panel.rs | 0 .../{templates => components}/collab_panel.rs | 0 .../command_palette.rs | 0 .../{elements => components}/icon_button.rs | 0 crates/ui/src/{modules => components}/list.rs | 0 .../ui/src/{modules => components}/palette.rs | 0 .../project_panel.rs | 0 .../{templates => components}/status_bar.rs | 0 .../src/{templates => components}/tab_bar.rs | 0 .../{templates => components}/title_bar.rs | 0 .../{templates => components}/workspace.rs | 0 crates/ui/src/elements.rs | 2 -- crates/ui/src/lib.rs | 4 ---- crates/ui/src/modules.rs | 5 ----- crates/ui/src/templates.rs | 17 -------------- 16 files changed, 22 insertions(+), 28 deletions(-) rename crates/ui/src/{templates => components}/chat_panel.rs (100%) rename crates/ui/src/{templates => components}/collab_panel.rs (100%) rename crates/ui/src/{templates => components}/command_palette.rs (100%) rename crates/ui/src/{elements => components}/icon_button.rs (100%) rename crates/ui/src/{modules => components}/list.rs (100%) rename crates/ui/src/{modules => components}/palette.rs (100%) rename crates/ui/src/{templates => components}/project_panel.rs (100%) rename crates/ui/src/{templates => components}/status_bar.rs (100%) rename crates/ui/src/{templates => components}/tab_bar.rs (100%) rename crates/ui/src/{templates => components}/title_bar.rs (100%) rename crates/ui/src/{templates => components}/workspace.rs (100%) delete mode 100644 crates/ui/src/modules.rs delete mode 100644 crates/ui/src/templates.rs diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 15790fe9f80000ef95401ea386acf0826c5cc4f5..50a680e0f71a4a9fc4a46ec7b38ceac007093355 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,22 +1,44 @@ mod breadcrumb; +mod chat_panel; +mod collab_panel; +mod command_palette; mod facepile; mod follow_group; +mod icon_button; +mod list; mod list_item; mod list_section_header; +mod palette; mod palette_item; +mod project_panel; +mod status_bar; mod tab; +mod tab_bar; +mod title_bar; mod toolbar; mod traffic_lights; +mod workspace; pub use breadcrumb::*; +pub use chat_panel::*; +pub use collab_panel::*; +pub use command_palette::*; pub use facepile::*; pub use follow_group::*; +pub use icon_button::*; +pub use list::*; pub use list_item::*; pub use list_section_header::*; +pub use palette::*; pub use palette_item::*; +pub use project_panel::*; +pub use status_bar::*; pub use tab::*; +pub use tab_bar::*; +pub use title_bar::*; pub use toolbar::*; pub use traffic_lights::*; +pub use workspace::*; use std::marker::PhantomData; use std::rc::Rc; diff --git a/crates/ui/src/templates/chat_panel.rs b/crates/ui/src/components/chat_panel.rs similarity index 100% rename from crates/ui/src/templates/chat_panel.rs rename to crates/ui/src/components/chat_panel.rs diff --git a/crates/ui/src/templates/collab_panel.rs b/crates/ui/src/components/collab_panel.rs similarity index 100% rename from crates/ui/src/templates/collab_panel.rs rename to crates/ui/src/components/collab_panel.rs diff --git a/crates/ui/src/templates/command_palette.rs b/crates/ui/src/components/command_palette.rs similarity index 100% rename from crates/ui/src/templates/command_palette.rs rename to crates/ui/src/components/command_palette.rs diff --git a/crates/ui/src/elements/icon_button.rs b/crates/ui/src/components/icon_button.rs similarity index 100% rename from crates/ui/src/elements/icon_button.rs rename to crates/ui/src/components/icon_button.rs diff --git a/crates/ui/src/modules/list.rs b/crates/ui/src/components/list.rs similarity index 100% rename from crates/ui/src/modules/list.rs rename to crates/ui/src/components/list.rs diff --git a/crates/ui/src/modules/palette.rs b/crates/ui/src/components/palette.rs similarity index 100% rename from crates/ui/src/modules/palette.rs rename to crates/ui/src/components/palette.rs diff --git a/crates/ui/src/templates/project_panel.rs b/crates/ui/src/components/project_panel.rs similarity index 100% rename from crates/ui/src/templates/project_panel.rs rename to crates/ui/src/components/project_panel.rs diff --git a/crates/ui/src/templates/status_bar.rs b/crates/ui/src/components/status_bar.rs similarity index 100% rename from crates/ui/src/templates/status_bar.rs rename to crates/ui/src/components/status_bar.rs diff --git a/crates/ui/src/templates/tab_bar.rs b/crates/ui/src/components/tab_bar.rs similarity index 100% rename from crates/ui/src/templates/tab_bar.rs rename to crates/ui/src/components/tab_bar.rs diff --git a/crates/ui/src/templates/title_bar.rs b/crates/ui/src/components/title_bar.rs similarity index 100% rename from crates/ui/src/templates/title_bar.rs rename to crates/ui/src/components/title_bar.rs diff --git a/crates/ui/src/templates/workspace.rs b/crates/ui/src/components/workspace.rs similarity index 100% rename from crates/ui/src/templates/workspace.rs rename to crates/ui/src/components/workspace.rs diff --git a/crates/ui/src/elements.rs b/crates/ui/src/elements.rs index 3f76af0d150e5aa2bd39a3c9ad5eedefbaf57019..0ed40e147e16b0b5bfb0c595e3354a31bf2c75a1 100644 --- a/crates/ui/src/elements.rs +++ b/crates/ui/src/elements.rs @@ -1,7 +1,6 @@ mod avatar; mod details; mod icon; -mod icon_button; mod indicator; mod input; mod label; @@ -11,7 +10,6 @@ mod tool_divider; pub use avatar::*; pub use details::*; pub use icon::*; -pub use icon_button::*; pub use indicator::*; pub use input::*; pub use label::*; diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 0bcd96164393716fe60cdba409a012988c630e03..ae06009238f625d77b894ad000410adffbf822dc 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -3,10 +3,8 @@ mod components; mod element_ext; mod elements; -mod modules; pub mod prelude; mod static_data; -mod templates; mod theme; mod tokens; @@ -14,8 +12,6 @@ pub use crate::theme::*; pub use components::*; pub use element_ext::*; pub use elements::*; -pub use modules::*; pub use prelude::*; pub use static_data::*; -pub use templates::*; pub use tokens::*; diff --git a/crates/ui/src/modules.rs b/crates/ui/src/modules.rs deleted file mode 100644 index d29e31072b5662ad574fb31d58f9b9bb224a4b2a..0000000000000000000000000000000000000000 --- a/crates/ui/src/modules.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod list; -mod palette; - -pub use list::*; -pub use palette::*; diff --git a/crates/ui/src/templates.rs b/crates/ui/src/templates.rs deleted file mode 100644 index f09a8a7ea4e93795d4a5b669b2f2816476c6c26c..0000000000000000000000000000000000000000 --- a/crates/ui/src/templates.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod chat_panel; -mod collab_panel; -mod command_palette; -mod project_panel; -mod status_bar; -mod tab_bar; -mod title_bar; -mod workspace; - -pub use chat_panel::*; -pub use collab_panel::*; -pub use command_palette::*; -pub use project_panel::*; -pub use status_bar::*; -pub use tab_bar::*; -pub use title_bar::*; -pub use workspace::*; From 052cb459a6d1666a5a1419b6c8a20a7e73e5463d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 24 Sep 2023 04:59:55 -0700 Subject: [PATCH 42/89] Improve lsp log viewer's behavior in the presence of LSP restarts Improve settings interface to local LSP --- Cargo.lock | 1 + assets/settings/default.json | 6 +-- crates/language_tools/src/lsp_log.rs | 50 +++++++++++++++++++++++-- crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 3 +- crates/zed/src/languages/elixir_next.rs | 25 +++++++++---- 6 files changed, 71 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a87cb99861712fb0b2f188e0087ec5155dcc2d3..4f43215bc79827643a2e81d531b23b1fcc3f8502 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9868,6 +9868,7 @@ dependencies = [ "serde_derive", "serde_json", "settings", + "shellexpand", "simplelog", "smallvec", "smol", diff --git a/assets/settings/default.json b/assets/settings/default.json index 126407a32dec59cc902d508ba997e371ad001340..fbc40b475680fad7fda9cfcc267e145e758eac1f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -384,11 +384,11 @@ // "next": "off" // 2. Use a bundled version of the next Next LS LSP server // "next": "on", - // 3. Use a locally running version of the next Next LS LSP server, - // on a specific port: + // 3. Use a local build of the next Next LS LSP server: // "next": { // "local": { - // "port": 4000 + // "path": "~/next-ls/bin/start", + // "arguments": ["--stdio"] // } // }, // diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 587e6ed25aba2c5603ca700cf8e61e8391926705..d2ad8fac90e14d26d42b80d68db2f4835f868498 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -8,8 +8,8 @@ use gpui::{ ParentElement, Stack, }, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext, - ViewHandle, WeakModelHandle, + AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View, + ViewContext, ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; use lsp::IoKind; @@ -52,10 +52,12 @@ pub struct LspLogView { current_server_id: Option, is_showing_rpc_trace: bool, project: ModelHandle, + _log_store_subscription: Subscription, } pub struct LspLogToolbarItemView { log_view: Option>, + _log_view_subscription: Option, menu_open: bool, } @@ -346,12 +348,49 @@ impl LspLogView { .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); + let _log_store_subscription = cx.observe(&log_store, |this, store, cx| { + (|| -> Option<()> { + let project_state = store.read(cx).projects.get(&this.project.downgrade())?; + if let Some(current_lsp) = this.current_server_id { + if !project_state.servers.contains_key(¤t_lsp) { + if let Some(server) = project_state.servers.iter().next() { + if this.is_showing_rpc_trace { + this.show_rpc_trace_for_server(*server.0, cx) + } else { + this.show_logs_for_server(*server.0, cx) + } + } else { + this.current_server_id = None; + this.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + cx.notify(); + } + } + } else { + if let Some(server) = project_state.servers.iter().next() { + if this.is_showing_rpc_trace { + this.show_rpc_trace_for_server(*server.0, cx) + } else { + this.show_logs_for_server(*server.0, cx) + } + } + } + + Some(()) + })(); + + cx.notify(); + }); let mut this = Self { editor: Self::editor_for_buffer(project.clone(), buffer, cx), project, log_store, current_server_id: None, is_showing_rpc_trace: false, + _log_store_subscription, }; if let Some(server_id) = server_id { this.show_logs_for_server(server_id, cx); @@ -556,18 +595,22 @@ impl ToolbarItemView for LspLogToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, - _: &mut ViewContext, + cx: &mut ViewContext, ) -> workspace::ToolbarItemLocation { self.menu_open = false; if let Some(item) = active_pane_item { if let Some(log_view) = item.downcast::() { self.log_view = Some(log_view.clone()); + self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { + cx.notify(); + })); return ToolbarItemLocation::PrimaryLeft { flex: Some((1., false)), }; } } self.log_view = None; + self._log_view_subscription = None; ToolbarItemLocation::Hidden } } @@ -697,6 +740,7 @@ impl LspLogToolbarItemView { Self { menu_open: false, log_view: None, + _log_view_subscription: None, } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fb41f7a3498d27b7e60ffd6b6a04a90f461604a3..ab6844d2dec47d8678eb81fdd560c6a22d894160 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -62,6 +62,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +shellexpand = "2.1.0" text = { path = "../text" } terminal_view = { path = "../terminal_view" } theme = { path = "../theme" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 0d1c2a9d3687fc9296fd65bd39d4adab80ce2efa..2a21e4035fa94d3af06a82b6cd32320d07302707 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -79,11 +79,12 @@ pub fn init( vec![Arc::new(elixir::ElixirLspAdapter)], ), elixir_next::ElixirNextSetting::On => todo!(), - elixir_next::ElixirNextSetting::Local { path } => language( + elixir_next::ElixirNextSetting::Local { path, arguments } => language( "elixir", tree_sitter_elixir::language(), vec![Arc::new(elixir_next::LocalNextLspAdapter { path: path.clone(), + arguments: arguments.clone(), })], ), } diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs index a25ada92b8182b9656190597c7fa9bf0170caf83..9b12572ec396562abf018b3f3669e2887d4d3a48 100644 --- a/crates/zed/src/languages/elixir_next.rs +++ b/crates/zed/src/languages/elixir_next.rs @@ -5,7 +5,7 @@ use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::Setting; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{any::Any, ops::Deref, path::PathBuf, sync::Arc}; #[derive(Clone, Serialize, Deserialize, JsonSchema)] pub struct ElixirSettings { @@ -17,7 +17,10 @@ pub struct ElixirSettings { pub enum ElixirNextSetting { Off, On, - Local { path: String }, + Local { + path: String, + arguments: Vec, + }, } #[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] @@ -44,6 +47,7 @@ impl Setting for ElixirSettings { pub struct LocalNextLspAdapter { pub path: String, + pub arguments: Vec, } #[async_trait] @@ -69,9 +73,10 @@ impl LspAdapter for LocalNextLspAdapter { _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { + let path = shellexpand::full(&self.path)?; Ok(LanguageServerBinary { - path: self.path.clone().into(), - arguments: vec!["--stdio".into()], + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } @@ -80,19 +85,22 @@ impl LspAdapter for LocalNextLspAdapter { _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { + let path = shellexpand::full(&self.path).ok()?; Some(LanguageServerBinary { - path: self.path.clone().into(), - arguments: vec!["--stdio".into()], + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } async fn installation_test_binary(&self, _: PathBuf) -> Option { + let path = shellexpand::full(&self.path).ok()?; Some(LanguageServerBinary { - path: self.path.clone().into(), - arguments: vec!["--stdio".into()], + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), }) } + // TODO: async fn label_for_completion( &self, completion: &lsp::CompletionItem, @@ -147,6 +155,7 @@ impl LspAdapter for LocalNextLspAdapter { None } + // TODO: async fn label_for_symbol( &self, name: &str, From 8b63e45f0baa9d994d29ca5cee203fa5d48d4830 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 24 Sep 2023 05:08:05 -0700 Subject: [PATCH 43/89] Implement LSP adapter methods for syntax highlighting --- crates/zed/src/languages/elixir_next.rs | 82 ++----------------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs index 9b12572ec396562abf018b3f3669e2887d4d3a48..cd0d13c71288917848733622aa7d4d4165efab6c 100644 --- a/crates/zed/src/languages/elixir_next.rs +++ b/crates/zed/src/languages/elixir_next.rs @@ -1,7 +1,7 @@ use anyhow::Result; use async_trait::async_trait; pub use language::*; -use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; +use lsp::{LanguageServerBinary, SymbolKind}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::Setting; @@ -100,88 +100,16 @@ impl LspAdapter for LocalNextLspAdapter { }) } - // TODO: - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - match completion.kind.zip(completion.detail.as_ref()) { - Some((_, detail)) if detail.starts_with("(function)") => { - let text = detail.strip_prefix("(function) ")?; - let filter_range = 0..text.find('(').unwrap_or(text.len()); - let source = Rope::from(format!("def {text}").as_str()); - let runs = language.highlight_text(&source, 4..4 + text.len()); - return Some(CodeLabel { - text: text.to_string(), - runs, - filter_range, - }); - } - Some((_, detail)) if detail.starts_with("(macro)") => { - let text = detail.strip_prefix("(macro) ")?; - let filter_range = 0..text.find('(').unwrap_or(text.len()); - let source = Rope::from(format!("defmacro {text}").as_str()); - let runs = language.highlight_text(&source, 9..9 + text.len()); - return Some(CodeLabel { - text: text.to_string(), - runs, - filter_range, - }); - } - Some(( - CompletionItemKind::CLASS - | CompletionItemKind::MODULE - | CompletionItemKind::INTERFACE - | CompletionItemKind::STRUCT, - _, - )) => { - let filter_range = 0..completion - .label - .find(" (") - .unwrap_or(completion.label.len()); - let text = &completion.label[filter_range.clone()]; - let source = Rope::from(format!("defmodule {text}").as_str()); - let runs = language.highlight_text(&source, 10..10 + text.len()); - return Some(CodeLabel { - text: completion.label.clone(), - runs, - filter_range, - }); - } - _ => {} - } - - None - } - - // TODO: async fn label_for_symbol( &self, name: &str, - kind: SymbolKind, + _: SymbolKind, language: &Arc, ) -> Option { - let (text, filter_range, display_range) = match kind { - SymbolKind::METHOD | SymbolKind::FUNCTION => { - let text = format!("def {}", name); - let filter_range = 4..4 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => { - let text = format!("defmodule {}", name); - let filter_range = 10..10 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - _ => return None, - }; - Some(CodeLabel { - runs: language.highlight_text(&text.as_str().into(), display_range.clone()), - text: text[display_range].to_string(), - filter_range, + runs: language.highlight_text(&name.into(), 0..name.len()), + text: name.to_string(), + filter_range: 0..name.len(), }) } } From 0a491e773b689a74f96b7555070cf5a3bf245543 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:15:29 +0200 Subject: [PATCH 44/89] workspace: Improve save prompt. (#3025) Add buffer path to the prompt. Z-2903 Release Notes: - Added a "Save all/Discard all" prompt when closing a pane with multiple edited buffers. --- crates/util/src/util.rs | 14 ++++ crates/workspace/src/pane.rs | 128 +++++++++++++++++++++++------- crates/workspace/src/workspace.rs | 39 +++++++-- 3 files changed, 148 insertions(+), 33 deletions(-) diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 3f83f8e37a5c493062d2291b7f56995ca527254b..629f9500147533a85154995fe2462f5b6d5f5297 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -41,6 +41,8 @@ pub fn truncate(s: &str, max_chars: usize) -> &str { } } +/// Removes characters from the end of the string if it's length is greater than `max_chars` and +/// appends "..." to the string. Returns string unchanged if it's length is smaller than max_chars. pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { debug_assert!(max_chars >= 5); @@ -51,6 +53,18 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { } } +/// Removes characters from the front of the string if it's length is greater than `max_chars` and +/// prepends the string with "...". Returns string unchanged if it's length is smaller than max_chars. +pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { + debug_assert!(max_chars >= 5); + + let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars); + match truncation_ix { + Some(length) => "…".to_string() + &s[length..], + None => s.to_string(), + } +} + pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { let prev = *value; *value += T::from(1); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a3e6a547ddfd73c1fd67c50c4b6c4df2d6ff51d1..a191adcc05e0798316d311b26b5368b711eebee5 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -42,6 +42,7 @@ use std::{ }, }; use theme::{Theme, ThemeSettings}; +use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -839,10 +840,45 @@ impl Pane { Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true)) } + pub(super) fn file_names_for_prompt( + items: &mut dyn Iterator>, + all_dirty_items: usize, + cx: &AppContext, + ) -> String { + /// Quantity of item paths displayed in prompt prior to cutoff.. + const FILE_NAMES_CUTOFF_POINT: usize = 10; + let mut file_names: Vec<_> = items + .filter_map(|item| { + item.project_path(cx).and_then(|project_path| { + project_path + .path + .file_name() + .and_then(|name| name.to_str().map(ToOwned::to_owned)) + }) + }) + .take(FILE_NAMES_CUTOFF_POINT) + .collect(); + let should_display_followup_text = + all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; + if should_display_followup_text { + let not_shown_files = all_dirty_items - file_names.len(); + if not_shown_files == 1 { + file_names.push(".. 1 file not shown".into()); + } else { + file_names.push(format!(".. {} files not shown", not_shown_files).into()); + } + } + let file_names = file_names.join("\n"); + format!( + "Do you want to save changes to the following {} files?\n{file_names}", + all_dirty_items + ) + } + pub fn close_items( &mut self, cx: &mut ViewContext, - save_behavior: SaveBehavior, + mut save_behavior: SaveBehavior, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { // Find the items to close. @@ -861,6 +897,25 @@ impl Pane { let workspace = self.workspace.clone(); cx.spawn(|pane, mut cx| async move { + if save_behavior == SaveBehavior::PromptOnWrite && items_to_close.len() > 1 { + let mut answer = pane.update(&mut cx, |_, cx| { + let prompt = Self::file_names_for_prompt( + &mut items_to_close.iter(), + items_to_close.len(), + cx, + ); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => save_behavior = SaveBehavior::PromptOnConflict, + Some(1) => save_behavior = SaveBehavior::DontSave, + _ => {} + } + } let mut saved_project_items_ids = HashSet::default(); for item in items_to_close.clone() { // Find the item's current index and its set of project item models. Avoid @@ -1003,7 +1058,6 @@ impl Pane { ) -> Result { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?"; if save_behavior == SaveBehavior::DontSave { return Ok(true); @@ -1046,9 +1100,10 @@ impl Pane { let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave { let mut answer = pane.update(cx, |pane, cx| { pane.activate_item(item_ix, true, true, cx); + let prompt = dirty_message_for(item.project_path(cx)); cx.prompt( PromptLevel::Warning, - DIRTY_MESSAGE, + &prompt, &["Save", "Don't Save", "Cancel"], ) })?; @@ -2135,6 +2190,15 @@ impl Element for PaneBackdrop { } } +fn dirty_message_for(buffer_path: Option) -> String { + let path = buffer_path + .as_ref() + .and_then(|p| p.path.to_str()) + .unwrap_or(&"Untitled buffer"); + let path = truncate_and_remove_front(path, 80); + format!("{path} contains unsaved edits. Do you want to save it?") +} + #[cfg(test)] mod tests { use super::*; @@ -2479,12 +2543,14 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - pane.update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) - }) - .unwrap() - .await - .unwrap(); + let task = pane + .update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["C*"], cx); } @@ -2505,10 +2571,12 @@ mod tests { add_labeled_item(&pane, "E", false, cx); assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) - .unwrap() - .await + let task = pane + .update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["A^", "C*^"], cx); } @@ -2524,12 +2592,14 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - pane.update(cx, |pane, cx| { - pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) - }) - .unwrap() - .await - .unwrap(); + let task = pane + .update(cx, |pane, cx| { + pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["C*", "D", "E"], cx); } @@ -2545,12 +2615,14 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - pane.update(cx, |pane, cx| { - pane.close_items_to_the_right(&CloseItemsToTheRight, cx) - }) - .unwrap() - .await - .unwrap(); + let task = pane + .update(cx, |pane, cx| { + pane.close_items_to_the_right(&CloseItemsToTheRight, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["A", "B", "C*"], cx); } @@ -2569,10 +2641,12 @@ mod tests { add_labeled_item(&pane, "C", false, cx); assert_item_labels(&pane, ["A", "B", "C*"], cx); - pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) - .unwrap() - .await + let t = pane + .update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + t.await.unwrap(); assert_item_labels(&pane, [], cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index feab53d0941d1823fc79930d3697c39e54822207..263652184a89c85810861831d6b1b660454875df 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1333,13 +1333,12 @@ impl Workspace { fn save_all_internal( &mut self, - save_behaviour: SaveBehavior, + mut save_behaviour: SaveBehavior, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { return Task::ready(Ok(true)); } - let dirty_items = self .panes .iter() @@ -1355,7 +1354,27 @@ impl Workspace { .collect::>(); let project = self.project.clone(); - cx.spawn(|_, mut cx| async move { + cx.spawn(|workspace, mut cx| async move { + // Override save mode and display "Save all files" prompt + if save_behaviour == SaveBehavior::PromptOnWrite && dirty_items.len() > 1 { + let mut answer = workspace.update(&mut cx, |_, cx| { + let prompt = Pane::file_names_for_prompt( + &mut dirty_items.iter().map(|(_, handle)| handle), + dirty_items.len(), + cx, + ); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => save_behaviour = SaveBehavior::PromptOnConflict, + Some(1) => save_behaviour = SaveBehavior::DontSave, + _ => {} + } + } for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); @@ -4320,7 +4339,9 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); // cancel + window.simulate_prompt_answer(2, cx); // cancel save all + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); // cancel save all cx.foreground().run_until_parked(); assert!(!window.has_pending_prompt(cx)); assert!(!task.await.unwrap()); @@ -4378,13 +4399,15 @@ mod tests { }); cx.foreground().run_until_parked(); + assert!(window.has_pending_prompt(cx)); + // Ignore "Save all" prompt + window.simulate_prompt_answer(2, cx); + cx.foreground().run_until_parked(); // There's a prompt to save item 1. pane.read_with(cx, |pane, _| { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); - assert!(window.has_pending_prompt(cx)); - // Confirm saving item 1. window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); @@ -4512,6 +4535,10 @@ mod tests { let close = left_pane.update(cx, |pane, cx| { pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true) }); + cx.foreground().run_until_parked(); + // Discard "Save all" prompt + window.simulate_prompt_answer(2, cx); + cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( From a278428bd58689f2c007f6349d509bd2bc35bbbd Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 25 Sep 2023 11:13:50 -0400 Subject: [PATCH 45/89] Trigger `scroll_to` on code action list when moving selection --- crates/editor/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0827e1326402e39bfeeab389ad7a9df6d8eb5587..0c9d8de9697269fe68f98d5127a92234fd6ca368 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1131,12 +1131,14 @@ struct CodeActionsMenu { impl CodeActionsMenu { fn select_first(&mut self, cx: &mut ViewContext) { self.selected_item = 0; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } fn select_prev(&mut self, cx: &mut ViewContext) { if self.selected_item > 0 { self.selected_item -= 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } } @@ -1144,12 +1146,14 @@ impl CodeActionsMenu { fn select_next(&mut self, cx: &mut ViewContext) { if self.selected_item + 1 < self.actions.len() { self.selected_item += 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } } fn select_last(&mut self, cx: &mut ViewContext) { self.selected_item = self.actions.len() - 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); cx.notify() } From 80eaabd360321b719cc937989ee40e2f35721dbf Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 25 Sep 2023 13:31:00 -0400 Subject: [PATCH 46/89] Activate correct item when clicking on a code action with the mouse --- crates/editor/src/editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0c9d8de9697269fe68f98d5127a92234fd6ca368..6070590d4f1215c15990f2b6264c70b0b1a343f4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1206,7 +1206,9 @@ impl CodeActionsMenu { workspace.update(cx, |workspace, cx| { if let Some(task) = Editor::confirm_code_action( workspace, - &Default::default(), + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, cx, ) { task.detach_and_log_err(cx); From 23767f734f9eb358a7064d44247b2f95c9716adb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Sep 2023 11:13:43 -0600 Subject: [PATCH 47/89] Add cmd-+ as an alias for cmd-= For github.com/zed-industries/community#1021 --- assets/keymaps/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 8fbe87de2bf76f6dbf09febc9145d8db3676c677..110502c259532b3b28b14103178b9f46d8f5075d 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -30,6 +30,7 @@ "cmd-s": "workspace::Save", "cmd-shift-s": "workspace::SaveAs", "cmd-=": "zed::IncreaseBufferFontSize", + "cmd-+": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", "cmd-0": "zed::ResetBufferFontSize", "cmd-,": "zed::OpenSettings", From ad7c1f3c81622184f2ac6e9678fe29871ea0bf2a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 25 Sep 2023 10:40:20 -0700 Subject: [PATCH 48/89] Download next-ls automatically from github --- crates/zed/src/languages.rs | 6 +- crates/zed/src/languages/elixir_next.rs | 170 ++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 10 deletions(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 2a21e4035fa94d3af06a82b6cd32320d07302707..be8d05256ae30646da0f759b13abf8786c3a301a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -78,7 +78,11 @@ pub fn init( tree_sitter_elixir::language(), vec![Arc::new(elixir::ElixirLspAdapter)], ), - elixir_next::ElixirNextSetting::On => todo!(), + elixir_next::ElixirNextSetting::On => language( + "elixir", + tree_sitter_elixir::language(), + vec![Arc::new(elixir_next::NextLspAdapter)], + ), elixir_next::ElixirNextSetting::Local { path, arguments } => language( "elixir", tree_sitter_elixir::language(), diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs index cd0d13c71288917848733622aa7d4d4165efab6c..6293d4bfb99d5c8dfa25ef91012e6738a9301e92 100644 --- a/crates/zed/src/languages/elixir_next.rs +++ b/crates/zed/src/languages/elixir_next.rs @@ -1,11 +1,19 @@ -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; use async_trait::async_trait; pub use language::*; use lsp::{LanguageServerBinary, SymbolKind}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::Setting; -use std::{any::Any, ops::Deref, path::PathBuf, sync::Arc}; +use smol::{fs, io::BufReader, stream::StreamExt}; +use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc}; +use util::{ + async_iife, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; #[derive(Clone, Serialize, Deserialize, JsonSchema)] pub struct ElixirSettings { @@ -45,6 +53,146 @@ impl Setting for ElixirSettings { } } +pub struct NextLspAdapter; + +#[async_trait] +impl LspAdapter for NextLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("next-ls".into()) + } + + fn short_name(&self) -> &'static str { + "next-ls" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?; + let version = release.name.clone(); + let platform = match consts::ARCH { + "x86_64" => "darwin_arm64", + "aarch64" => "darwin_amd64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("next_ls_{}", platform); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: version, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + + let binary_path = container_dir.join("next-ls"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + let mut file = smol::fs::File::create(&binary_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec!["--stdio".into()], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--stdio".into()]; + binary + }) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol_kind: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_next(name, symbol_kind, language) + } +} + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_iife!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "next-ls") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: Vec::new(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} + pub struct LocalNextLspAdapter { pub path: String, pub arguments: Vec, @@ -53,7 +201,7 @@ pub struct LocalNextLspAdapter { #[async_trait] impl LspAdapter for LocalNextLspAdapter { async fn name(&self) -> LanguageServerName { - LanguageServerName("elixir-next-ls".into()) + LanguageServerName("local-next-ls".into()) } fn short_name(&self) -> &'static str { @@ -103,13 +251,17 @@ impl LspAdapter for LocalNextLspAdapter { async fn label_for_symbol( &self, name: &str, - _: SymbolKind, + symbol: SymbolKind, language: &Arc, ) -> Option { - Some(CodeLabel { - runs: language.highlight_text(&name.into(), 0..name.len()), - text: name.to_string(), - filter_range: 0..name.len(), - }) + label_for_symbol_next(name, symbol, language) } } + +fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc) -> Option { + Some(CodeLabel { + runs: language.highlight_text(&name.into(), 0..name.len()), + text: name.to_string(), + filter_range: 0..name.len(), + }) +} From 5c75450a77b0579649bbf79365c9cef5a2c1110f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Sep 2023 11:41:09 -0600 Subject: [PATCH 49/89] Revert "workspace: Improve save prompt. (#3025)" This reverts commit 0a491e773b689a74f96b7555070cf5a3bf245543. --- crates/util/src/util.rs | 14 ---- crates/workspace/src/pane.rs | 128 +++++++----------------------- crates/workspace/src/workspace.rs | 39 ++------- 3 files changed, 33 insertions(+), 148 deletions(-) diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 629f9500147533a85154995fe2462f5b6d5f5297..3f83f8e37a5c493062d2291b7f56995ca527254b 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -41,8 +41,6 @@ pub fn truncate(s: &str, max_chars: usize) -> &str { } } -/// Removes characters from the end of the string if it's length is greater than `max_chars` and -/// appends "..." to the string. Returns string unchanged if it's length is smaller than max_chars. pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { debug_assert!(max_chars >= 5); @@ -53,18 +51,6 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { } } -/// Removes characters from the front of the string if it's length is greater than `max_chars` and -/// prepends the string with "...". Returns string unchanged if it's length is smaller than max_chars. -pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { - debug_assert!(max_chars >= 5); - - let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars); - match truncation_ix { - Some(length) => "…".to_string() + &s[length..], - None => s.to_string(), - } -} - pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { let prev = *value; *value += T::from(1); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a191adcc05e0798316d311b26b5368b711eebee5..a3e6a547ddfd73c1fd67c50c4b6c4df2d6ff51d1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -42,7 +42,6 @@ use std::{ }, }; use theme::{Theme, ThemeSettings}; -use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -840,45 +839,10 @@ impl Pane { Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true)) } - pub(super) fn file_names_for_prompt( - items: &mut dyn Iterator>, - all_dirty_items: usize, - cx: &AppContext, - ) -> String { - /// Quantity of item paths displayed in prompt prior to cutoff.. - const FILE_NAMES_CUTOFF_POINT: usize = 10; - let mut file_names: Vec<_> = items - .filter_map(|item| { - item.project_path(cx).and_then(|project_path| { - project_path - .path - .file_name() - .and_then(|name| name.to_str().map(ToOwned::to_owned)) - }) - }) - .take(FILE_NAMES_CUTOFF_POINT) - .collect(); - let should_display_followup_text = - all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; - if should_display_followup_text { - let not_shown_files = all_dirty_items - file_names.len(); - if not_shown_files == 1 { - file_names.push(".. 1 file not shown".into()); - } else { - file_names.push(format!(".. {} files not shown", not_shown_files).into()); - } - } - let file_names = file_names.join("\n"); - format!( - "Do you want to save changes to the following {} files?\n{file_names}", - all_dirty_items - ) - } - pub fn close_items( &mut self, cx: &mut ViewContext, - mut save_behavior: SaveBehavior, + save_behavior: SaveBehavior, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { // Find the items to close. @@ -897,25 +861,6 @@ impl Pane { let workspace = self.workspace.clone(); cx.spawn(|pane, mut cx| async move { - if save_behavior == SaveBehavior::PromptOnWrite && items_to_close.len() > 1 { - let mut answer = pane.update(&mut cx, |_, cx| { - let prompt = Self::file_names_for_prompt( - &mut items_to_close.iter(), - items_to_close.len(), - cx, - ); - cx.prompt( - PromptLevel::Warning, - &prompt, - &["Save all", "Discard all", "Cancel"], - ) - })?; - match answer.next().await { - Some(0) => save_behavior = SaveBehavior::PromptOnConflict, - Some(1) => save_behavior = SaveBehavior::DontSave, - _ => {} - } - } let mut saved_project_items_ids = HashSet::default(); for item in items_to_close.clone() { // Find the item's current index and its set of project item models. Avoid @@ -1058,6 +1003,7 @@ impl Pane { ) -> Result { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; + const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?"; if save_behavior == SaveBehavior::DontSave { return Ok(true); @@ -1100,10 +1046,9 @@ impl Pane { let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave { let mut answer = pane.update(cx, |pane, cx| { pane.activate_item(item_ix, true, true, cx); - let prompt = dirty_message_for(item.project_path(cx)); cx.prompt( PromptLevel::Warning, - &prompt, + DIRTY_MESSAGE, &["Save", "Don't Save", "Cancel"], ) })?; @@ -2190,15 +2135,6 @@ impl Element for PaneBackdrop { } } -fn dirty_message_for(buffer_path: Option) -> String { - let path = buffer_path - .as_ref() - .and_then(|p| p.path.to_str()) - .unwrap_or(&"Untitled buffer"); - let path = truncate_and_remove_front(path, 80); - format!("{path} contains unsaved edits. Do you want to save it?") -} - #[cfg(test)] mod tests { use super::*; @@ -2543,14 +2479,12 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - let task = pane - .update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) - }) - .unwrap(); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); - task.await.unwrap(); + pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["C*"], cx); } @@ -2571,12 +2505,10 @@ mod tests { add_labeled_item(&pane, "E", false, cx); assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - let task = pane - .update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) + pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) + .unwrap() + .await .unwrap(); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); - task.await.unwrap(); assert_item_labels(&pane, ["A^", "C*^"], cx); } @@ -2592,14 +2524,12 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - let task = pane - .update(cx, |pane, cx| { - pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) - }) - .unwrap(); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); - task.await.unwrap(); + pane.update(cx, |pane, cx| { + pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["C*", "D", "E"], cx); } @@ -2615,14 +2545,12 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - let task = pane - .update(cx, |pane, cx| { - pane.close_items_to_the_right(&CloseItemsToTheRight, cx) - }) - .unwrap(); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); - task.await.unwrap(); + pane.update(cx, |pane, cx| { + pane.close_items_to_the_right(&CloseItemsToTheRight, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B", "C*"], cx); } @@ -2641,12 +2569,10 @@ mod tests { add_labeled_item(&pane, "C", false, cx); assert_item_labels(&pane, ["A", "B", "C*"], cx); - let t = pane - .update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) + pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) + .unwrap() + .await .unwrap(); - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); - t.await.unwrap(); assert_item_labels(&pane, [], cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 263652184a89c85810861831d6b1b660454875df..feab53d0941d1823fc79930d3697c39e54822207 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1333,12 +1333,13 @@ impl Workspace { fn save_all_internal( &mut self, - mut save_behaviour: SaveBehavior, + save_behaviour: SaveBehavior, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { return Task::ready(Ok(true)); } + let dirty_items = self .panes .iter() @@ -1354,27 +1355,7 @@ impl Workspace { .collect::>(); let project = self.project.clone(); - cx.spawn(|workspace, mut cx| async move { - // Override save mode and display "Save all files" prompt - if save_behaviour == SaveBehavior::PromptOnWrite && dirty_items.len() > 1 { - let mut answer = workspace.update(&mut cx, |_, cx| { - let prompt = Pane::file_names_for_prompt( - &mut dirty_items.iter().map(|(_, handle)| handle), - dirty_items.len(), - cx, - ); - cx.prompt( - PromptLevel::Warning, - &prompt, - &["Save all", "Discard all", "Cancel"], - ) - })?; - match answer.next().await { - Some(0) => save_behaviour = SaveBehavior::PromptOnConflict, - Some(1) => save_behaviour = SaveBehavior::DontSave, - _ => {} - } - } + cx.spawn(|_, mut cx| async move { for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); @@ -4339,9 +4320,7 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); // cancel save all - cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); // cancel save all + window.simulate_prompt_answer(2, cx); // cancel cx.foreground().run_until_parked(); assert!(!window.has_pending_prompt(cx)); assert!(!task.await.unwrap()); @@ -4399,15 +4378,13 @@ mod tests { }); cx.foreground().run_until_parked(); - assert!(window.has_pending_prompt(cx)); - // Ignore "Save all" prompt - window.simulate_prompt_answer(2, cx); - cx.foreground().run_until_parked(); // There's a prompt to save item 1. pane.read_with(cx, |pane, _| { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); + assert!(window.has_pending_prompt(cx)); + // Confirm saving item 1. window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); @@ -4535,10 +4512,6 @@ mod tests { let close = left_pane.update(cx, |pane, cx| { pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true) }); - cx.foreground().run_until_parked(); - // Discard "Save all" prompt - window.simulate_prompt_answer(2, cx); - cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( From 86ec0b1d9f92dd22cbabc6e45d65e14ceff99229 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 25 Sep 2023 13:44:19 -0400 Subject: [PATCH 50/89] implement new search strategy --- Cargo.lock | 121 ++++++++++++++++++- crates/semantic_index/Cargo.toml | 2 + crates/semantic_index/src/db.rs | 123 +++++++++++++------- crates/semantic_index/src/semantic_index.rs | 29 +++-- script/evaluate_semantic_index | 2 +- 5 files changed, 227 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 878604f3609fab58f724f62e16312a150522fc35..96cac48f7a610acddb8e7524ed2914ef130278a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,7 +570,7 @@ dependencies = [ "libc", "pin-project", "redox_syscall 0.2.16", - "xattr", + "xattr 0.2.3", ] [[package]] @@ -903,6 +903,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blas-src" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb48fbaa7a0cb9d6d96c46bac6cedb16f13a10aebcef1c4e73515aaae8c9909d" +dependencies = [ + "openblas-src", +] + [[package]] name = "block" version = "0.1.6" @@ -1181,6 +1190,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" +[[package]] +name = "cblas-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6feecd82cce51b0204cf063f0041d69f24ce83f680d87514b004248e7b0fa65" +dependencies = [ + "libc", +] + [[package]] name = "cc" version = "1.0.83" @@ -4580,6 +4598,21 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "cblas-sys", + "libc", + "matrixmultiply", + "num-complex 0.4.4", + "num-integer", + "num-traits", + "rawpointer", +] + [[package]] name = "ndk" version = "0.7.0" @@ -4706,7 +4739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ "num-bigint 0.2.6", - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", "num-rational 0.2.4", @@ -4762,6 +4795,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -4948,6 +4990,32 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openblas-build" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba42c395477605f400a8d79ee0b756cfb82abe3eb5618e35fa70d3a36010a7f" +dependencies = [ + "anyhow", + "flate2", + "native-tls", + "tar", + "thiserror", + "ureq", + "walkdir", +] + +[[package]] +name = "openblas-src" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e5d8af0b707ac2fe1574daa88b4157da73b0de3dc7c39fe3e2c0bb64070501" +dependencies = [ + "dirs 3.0.2", + "openblas-build", + "vcpkg", +] + [[package]] name = "openssl" version = "0.10.57" @@ -6422,6 +6490,18 @@ dependencies = [ "webpki 0.22.1", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -6740,6 +6820,7 @@ dependencies = [ "ai", "anyhow", "async-trait", + "blas-src", "client", "collections", "ctor", @@ -6751,6 +6832,7 @@ dependencies = [ "language", "lazy_static", "log", + "ndarray", "node_runtime", "ordered-float", "parking_lot 0.11.2", @@ -7629,6 +7711,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr 1.0.1", +] + [[package]] name = "target-lexicon" version = "0.12.11" @@ -8703,6 +8796,21 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "ureq" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" +dependencies = [ + "base64 0.21.4", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls-native-certs", + "url", +] + [[package]] name = "url" version = "2.4.1" @@ -9754,6 +9862,15 @@ dependencies = [ "libc", ] +[[package]] +name = "xattr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +dependencies = [ + "libc", +] + [[package]] name = "xmlparser" version = "0.13.5" diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index e38ae1f06db35ed96f7ac367c0529cd27e8cbd4f..7e68399b10c6747df075302856ab3c5e4d9c8380 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -39,6 +39,8 @@ rand.workspace = true schemars.workspace = true globset.workspace = true sha1 = "0.10.5" +ndarray = { version = "0.15.0", features = ["blas"] } +blas-src = { version = "0.8", features = ["openblas"] } [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index 8280dc7d65c44bdbfd656c40db1e4b9e91cb9b7e..3558bf6b0a0335bf1d4bca0114d08edb9d835d7d 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -1,3 +1,5 @@ +extern crate blas_src; + use crate::{ parsing::{Span, SpanDigest}, SEMANTIC_INDEX_VERSION, @@ -7,6 +9,7 @@ use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::channel::oneshot; use gpui::executor; +use ndarray::{Array1, Array2}; use ordered_float::OrderedFloat; use project::{search::PathMatcher, Fs}; use rpc::proto::Timestamp; @@ -19,10 +22,16 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::Arc, - time::SystemTime, + time::{Instant, SystemTime}, }; use util::TryFutureExt; +pub fn argsort(data: &[T]) -> Vec { + let mut indices = (0..data.len()).collect::>(); + indices.sort_by_key(|&i| &data[i]); + indices +} + #[derive(Debug)] pub struct FileRecord { pub id: usize, @@ -409,23 +418,82 @@ impl VectorDatabase { limit: usize, file_ids: &[i64], ) -> impl Future)>>> { - let query_embedding = query_embedding.clone(); let file_ids = file_ids.to_vec(); + let query = query_embedding.clone().0; + let query = Array1::from_vec(query); self.transact(move |db| { - let mut results = Vec::<(i64, OrderedFloat)>::with_capacity(limit + 1); - Self::for_each_span(db, &file_ids, |id, embedding| { - let similarity = embedding.similarity(&query_embedding); - let ix = match results - .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s)) - { - Ok(ix) => ix, - Err(ix) => ix, - }; - results.insert(ix, (id, similarity)); - results.truncate(limit); - })?; + let mut query_statement = db.prepare( + " + SELECT + id, embedding + FROM + spans + WHERE + file_id IN rarray(?) + ", + )?; + + let deserialized_rows = query_statement + .query_map(params![ids_to_sql(&file_ids)], |row| { + Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?)) + })? + .filter_map(|row| row.ok()) + .collect::>(); + + let batch_n = 250; + let mut batches = Vec::new(); + let mut batch_ids = Vec::new(); + let mut batch_embeddings: Vec = Vec::new(); + deserialized_rows.iter().for_each(|(id, embedding)| { + batch_ids.push(id); + batch_embeddings.extend(&embedding.0); + if batch_ids.len() == batch_n { + let array = + Array2::from_shape_vec((batch_ids.len(), 1536), batch_embeddings.clone()); + match array { + Ok(array) => { + batches.push((batch_ids.clone(), array)); + } + Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err), + } + + batch_ids = Vec::new(); + batch_embeddings = Vec::new(); + } + }); + + if batch_ids.len() > 0 { + let array = + Array2::from_shape_vec((batch_ids.len(), 1536), batch_embeddings.clone()); + match array { + Ok(array) => { + batches.push((batch_ids.clone(), array)); + } + Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err), + } + } + + let mut ids: Vec = Vec::new(); + let mut results = Vec::new(); + for (batch_ids, array) in batches { + let scores = array + .dot(&query.t()) + .to_vec() + .iter() + .map(|score| OrderedFloat(*score)) + .collect::>>(); + results.extend(scores); + ids.extend(batch_ids); + } - anyhow::Ok(results) + let sorted_idx = argsort(&results); + let mut sorted_results = Vec::new(); + let last_idx = limit.min(sorted_idx.len()); + for idx in &sorted_idx[0..last_idx] { + sorted_results.push((ids[*idx] as i64, results[*idx])) + } + + Ok(sorted_results) }) } @@ -468,31 +536,6 @@ impl VectorDatabase { }) } - fn for_each_span( - db: &rusqlite::Connection, - file_ids: &[i64], - mut f: impl FnMut(i64, Embedding), - ) -> Result<()> { - let mut query_statement = db.prepare( - " - SELECT - id, embedding - FROM - spans - WHERE - file_id IN rarray(?) - ", - )?; - - query_statement - .query_map(params![ids_to_sql(&file_ids)], |row| { - Ok((row.get(0)?, row.get::<_, Embedding>(1)?)) - })? - .filter_map(|row| row.ok()) - .for_each(|(id, embedding)| f(id, embedding)); - Ok(()) - } - pub fn spans_for_ids( &self, ids: &[i64], diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index fd41cb150024fd0ed5c2882f5835d76f8c152f41..59cf596e7f06eb136e2dbf5a2d1109a557883013 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -705,11 +705,13 @@ impl SemanticIndex { cx.spawn(|this, mut cx| async move { index.await?; + let t0 = Instant::now(); let query = embedding_provider .embed_batch(vec![query]) .await? .pop() .ok_or_else(|| anyhow!("could not embed query"))?; + log::trace!("Embedding Search Query: {:?}", t0.elapsed().as_millis()); let search_start = Instant::now(); let modified_buffer_results = this.update(&mut cx, |this, cx| { @@ -787,10 +789,15 @@ impl SemanticIndex { let batch_n = cx.background().num_cpus(); let ids_len = file_ids.clone().len(); - let batch_size = if ids_len <= batch_n { - ids_len - } else { - ids_len / batch_n + let minimum_batch_size = 50; + + let batch_size = { + let size = ids_len / batch_n; + if size < minimum_batch_size { + minimum_batch_size + } else { + size + } }; let mut batch_results = Vec::new(); @@ -813,17 +820,26 @@ impl SemanticIndex { let batch_results = futures::future::join_all(batch_results).await; let mut results = Vec::new(); + let mut min_similarity = None; for batch_result in batch_results { if batch_result.is_ok() { for (id, similarity) in batch_result.unwrap() { + if min_similarity.map_or_else(|| false, |min_sim| min_sim > similarity) { + continue; + } + let ix = match results .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s)) { Ok(ix) => ix, Err(ix) => ix, }; - results.insert(ix, (id, similarity)); - results.truncate(limit); + + if ix <= limit { + min_similarity = Some(similarity); + results.insert(ix, (id, similarity)); + results.truncate(limit); + } } } } @@ -856,7 +872,6 @@ impl SemanticIndex { })?; let buffers = futures::future::join_all(tasks).await; - Ok(buffers .into_iter() .zip(ranges) diff --git a/script/evaluate_semantic_index b/script/evaluate_semantic_index index e9a96a02b40d46c09dbf588361bfc98990321377..8dcb53c399dad42c7ceedb5d6181f78482843377 100755 --- a/script/evaluate_semantic_index +++ b/script/evaluate_semantic_index @@ -1,3 +1,3 @@ #!/bin/bash -cargo run -p semantic_index --example eval +RUST_LOG=semantic_index=trace cargo run -p semantic_index --example eval --release From c2fca054ae32c2ee0fd6646dc598f517ec2796d7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 25 Sep 2023 10:46:09 -0700 Subject: [PATCH 51/89] Fix compile and test errors --- crates/zed/src/languages/elixir_next.rs | 5 ++--- crates/zed/src/zed.rs | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs index 6293d4bfb99d5c8dfa25ef91012e6738a9301e92..f5a77c75681289c3ab50d79fc990f4e3b7d621dc 100644 --- a/crates/zed/src/languages/elixir_next.rs +++ b/crates/zed/src/languages/elixir_next.rs @@ -1,13 +1,12 @@ use anyhow::{anyhow, bail, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; + use async_trait::async_trait; pub use language::*; use lsp::{LanguageServerBinary, SymbolKind}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::Setting; -use smol::{fs, io::BufReader, stream::StreamExt}; +use smol::{fs, stream::StreamExt}; use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc}; use util::{ async_iife, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8a74522df1a40574efb3df2fc47fcc4c87b3c7db..58eb1e453e16bd0557ebb2d530a174eef507c3c2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2388,6 +2388,7 @@ mod tests { #[gpui::test] fn test_bundled_languages(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); From 359847d04758beb30822242ca6ccba19b5e3325d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Sep 2023 12:18:03 -0600 Subject: [PATCH 52/89] Revert "Revert "workspace: Improve save prompt. (#3025)"" This reverts commit 5c75450a77b0579649bbf79365c9cef5a2c1110f. --- crates/search/src/buffer_search.rs | 4 +- crates/util/src/util.rs | 14 +++ crates/workspace/src/pane.rs | 136 ++++++++++++++++++++++------- crates/workspace/src/workspace.rs | 39 +++++++-- 4 files changed, 154 insertions(+), 39 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 72893656d61a82327ecdd36e242fd1381e0b39ec..4f0deda6e0e95e2006240c88f19e3f71dc800c7e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -541,10 +541,10 @@ impl BufferSearchBar { pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { if replacement.is_none() { - self.replace_is_active = false; + self.replace_enabled = false; return; } - self.replace_is_active = true; + self.replace_enabled = true; self.replacement_editor .update(cx, |replacement_editor, cx| { replacement_editor diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 3f83f8e37a5c493062d2291b7f56995ca527254b..629f9500147533a85154995fe2462f5b6d5f5297 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -41,6 +41,8 @@ pub fn truncate(s: &str, max_chars: usize) -> &str { } } +/// Removes characters from the end of the string if it's length is greater than `max_chars` and +/// appends "..." to the string. Returns string unchanged if it's length is smaller than max_chars. pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { debug_assert!(max_chars >= 5); @@ -51,6 +53,18 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { } } +/// Removes characters from the front of the string if it's length is greater than `max_chars` and +/// prepends the string with "...". Returns string unchanged if it's length is smaller than max_chars. +pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { + debug_assert!(max_chars >= 5); + + let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars); + match truncation_ix { + Some(length) => "…".to_string() + &s[length..], + None => s.to_string(), + } +} + pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { let prev = *value; *value += T::from(1); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fbe018409b4008d3675146c44c3cc9f3c81b3784..e27100863732b68607e6e6dce0c14ea008bb3298 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -42,6 +42,7 @@ use std::{ }, }; use theme::{Theme, ThemeSettings}; +use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -849,10 +850,45 @@ impl Pane { ) } + pub(super) fn file_names_for_prompt( + items: &mut dyn Iterator>, + all_dirty_items: usize, + cx: &AppContext, + ) -> String { + /// Quantity of item paths displayed in prompt prior to cutoff.. + const FILE_NAMES_CUTOFF_POINT: usize = 10; + let mut file_names: Vec<_> = items + .filter_map(|item| { + item.project_path(cx).and_then(|project_path| { + project_path + .path + .file_name() + .and_then(|name| name.to_str().map(ToOwned::to_owned)) + }) + }) + .take(FILE_NAMES_CUTOFF_POINT) + .collect(); + let should_display_followup_text = + all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items; + if should_display_followup_text { + let not_shown_files = all_dirty_items - file_names.len(); + if not_shown_files == 1 { + file_names.push(".. 1 file not shown".into()); + } else { + file_names.push(format!(".. {} files not shown", not_shown_files).into()); + } + } + let file_names = file_names.join("\n"); + format!( + "Do you want to save changes to the following {} files?\n{file_names}", + all_dirty_items + ) + } + pub fn close_items( &mut self, cx: &mut ViewContext, - save_intent: SaveIntent, + mut save_intent: SaveIntent, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { // Find the items to close. @@ -871,6 +907,25 @@ impl Pane { let workspace = self.workspace.clone(); cx.spawn(|pane, mut cx| async move { + if save_intent == SaveIntent::Close && items_to_close.len() > 1 { + let mut answer = pane.update(&mut cx, |_, cx| { + let prompt = Self::file_names_for_prompt( + &mut items_to_close.iter(), + items_to_close.len(), + cx, + ); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => save_intent = SaveIntent::Save, + Some(1) => save_intent = SaveIntent::Skip, + _ => {} + } + } let mut saved_project_items_ids = HashSet::default(); for item in items_to_close.clone() { // Find the item's current index and its set of project item models. Avoid @@ -1013,7 +1068,6 @@ impl Pane { ) -> Result { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?"; if save_intent == SaveIntent::Skip { return Ok(true); @@ -1068,15 +1122,16 @@ impl Pane { if !will_autosave { let mut answer = pane.update(cx, |pane, cx| { pane.activate_item(item_ix, true, true, cx); + let prompt = dirty_message_for(item.project_path(cx)); cx.prompt( PromptLevel::Warning, - DIRTY_MESSAGE, + &prompt, &["Save", "Don't Save", "Cancel"], ) })?; match answer.next().await { Some(0) => {} - Some(1) => return Ok(true), // Don't save this file + Some(1) => return Ok(true), // Don't save his file _ => return Ok(false), // Cancel } } @@ -2154,6 +2209,15 @@ impl Element for PaneBackdrop { } } +fn dirty_message_for(buffer_path: Option) -> String { + let path = buffer_path + .as_ref() + .and_then(|p| p.path.to_str()) + .unwrap_or(&"Untitled buffer"); + let path = truncate_and_remove_front(path, 80); + format!("{path} contains unsaved edits. Do you want to save it?") +} + #[cfg(test)] mod tests { use super::*; @@ -2473,12 +2537,14 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - pane.update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx) - }) - .unwrap() - .await - .unwrap(); + let task = pane + .update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["C*"], cx); } @@ -2499,10 +2565,12 @@ mod tests { add_labeled_item(&pane, "E", false, cx); assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) - .unwrap() - .await + let task = pane + .update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["A^", "C*^"], cx); } @@ -2518,12 +2586,14 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - pane.update(cx, |pane, cx| { - pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) - }) - .unwrap() - .await - .unwrap(); + let task = pane + .update(cx, |pane, cx| { + pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["C*", "D", "E"], cx); } @@ -2539,12 +2609,14 @@ mod tests { set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - pane.update(cx, |pane, cx| { - pane.close_items_to_the_right(&CloseItemsToTheRight, cx) - }) - .unwrap() - .await - .unwrap(); + let task = pane + .update(cx, |pane, cx| { + pane.close_items_to_the_right(&CloseItemsToTheRight, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + task.await.unwrap(); assert_item_labels(&pane, ["A", "B", "C*"], cx); } @@ -2563,12 +2635,14 @@ mod tests { add_labeled_item(&pane, "C", false, cx); assert_item_labels(&pane, ["A", "B", "C*"], cx); - pane.update(cx, |pane, cx| { - pane.close_all_items(&CloseAllItems { save_intent: None }, cx) - }) - .unwrap() - .await - .unwrap(); + let t = pane + .update(cx, |pane, cx| { + pane.close_all_items(&CloseAllItems { save_intent: None }, cx) + }) + .unwrap(); + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); + t.await.unwrap(); assert_item_labels(&pane, [], cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 092286973e867007cedbb0b37c94030a6e906fe6..f081eb9efae77150de27840816828acd8dd72914 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1372,13 +1372,12 @@ impl Workspace { fn save_all_internal( &mut self, - save_intent: SaveIntent, + mut save_intent: SaveIntent, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { return Task::ready(Ok(true)); } - let dirty_items = self .panes .iter() @@ -1394,7 +1393,27 @@ impl Workspace { .collect::>(); let project = self.project.clone(); - cx.spawn(|_, mut cx| async move { + cx.spawn(|workspace, mut cx| async move { + // Override save mode and display "Save all files" prompt + if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let mut answer = workspace.update(&mut cx, |_, cx| { + let prompt = Pane::file_names_for_prompt( + &mut dirty_items.iter().map(|(_, handle)| handle), + dirty_items.len(), + cx, + ); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.next().await { + Some(0) => save_intent = SaveIntent::Save, + Some(1) => save_intent = SaveIntent::Skip, + _ => {} + } + } for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); @@ -4361,7 +4380,9 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - window.simulate_prompt_answer(2, cx); // cancel + window.simulate_prompt_answer(2, cx); // cancel save all + cx.foreground().run_until_parked(); + window.simulate_prompt_answer(2, cx); // cancel save all cx.foreground().run_until_parked(); assert!(!window.has_pending_prompt(cx)); assert!(!task.await.unwrap()); @@ -4419,13 +4440,15 @@ mod tests { }); cx.foreground().run_until_parked(); + assert!(window.has_pending_prompt(cx)); + // Ignore "Save all" prompt + window.simulate_prompt_answer(2, cx); + cx.foreground().run_until_parked(); // There's a prompt to save item 1. pane.read_with(cx, |pane, _| { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); - assert!(window.has_pending_prompt(cx)); - // Confirm saving item 1. window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); @@ -4553,6 +4576,10 @@ mod tests { let close = left_pane.update(cx, |pane, cx| { pane.close_items(cx, SaveIntent::Close, move |_| true) }); + cx.foreground().run_until_parked(); + // Discard "Save all" prompt + window.simulate_prompt_answer(2, cx); + cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( From 667fc25766f62d2a45729cb8cb09f7889c990a0f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 25 Sep 2023 11:31:02 -0700 Subject: [PATCH 53/89] Fix space and copy/paste when editing a channel --- assets/keymaps/default.json | 10 ++++++++-- crates/collab_ui/src/collab_panel.rs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 8fbe87de2bf76f6dbf09febc9145d8db3676c677..cdd8930b17f423effcbb08fdf6d4cce36eb67b8d 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -588,14 +588,20 @@ } }, { - "context": "CollabPanel", + "context": "CollabPanel && not_editing", "bindings": { "ctrl-backspace": "collab_panel::Remove", "space": "menu::Confirm" } }, { - "context": "CollabPanel > Editor", + "context": "(CollabPanel && editing) > Editor", + "bindings": { + "space": "collab_panel::InsertSpace" + } + }, + { + "context": "(CollabPanel && not_editing) > Editor", "bindings": { "cmd-c": "collab_panel::StartLinkChannel", "cmd-x": "collab_panel::StartMoveChannel", diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 6013ea4907dd7d703a7c107b6c726b7cef9ae201..311978578ce67c640b99607019c45df20c256bb4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -136,6 +136,7 @@ actions!( StartMoveChannel, StartLinkChannel, MoveOrLinkToSelected, + InsertSpace, ] ); @@ -184,6 +185,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(CollabPanel::select_next); cx.add_action(CollabPanel::select_prev); cx.add_action(CollabPanel::confirm); + cx.add_action(CollabPanel::insert_space); cx.add_action(CollabPanel::remove); cx.add_action(CollabPanel::remove_selected_channel); cx.add_action(CollabPanel::show_inline_context_menu); @@ -2518,6 +2520,14 @@ impl CollabPanel { } } + fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { + if self.channel_editing_state.is_some() { + self.channel_name_editor.update(cx, |editor, cx| { + editor.insert(" ", cx); + }); + } + } + fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { if let Some(editing_state) = &mut self.channel_editing_state { match editing_state { @@ -3054,6 +3064,19 @@ impl View for CollabPanel { .on_click(MouseButton::Left, |_, _, cx| cx.focus_self()) .into_any_named("collab panel") } + + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + _: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + if self.channel_editing_state.is_some() { + keymap.add_identifier("editing"); + } else { + keymap.add_identifier("not_editing"); + } + } } impl Panel for CollabPanel { From b29e295e1b33ff1e1d46dfdc3b8439220de9f612 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 25 Sep 2023 15:23:14 -0600 Subject: [PATCH 54/89] vim: Add multicursor shortcuts - g n / g N to select next/previous - g > / g < to select next/previous replacing current - g a to select all matches --- assets/keymaps/vim.json | 15 ++++++++++++ crates/vim/src/visual.rs | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 9e69240d27200c591e110fd5578a85b13fe96668..e43a17449fb3be8f7e41490d3b385e1f7b35ef4b 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -125,6 +125,21 @@ "g shift-t": "pane::ActivatePrevItem", "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToTypeDefinition", + "g n": "vim::SelectNext", + "g shift-n": "vim::SelectPrevious", + "g >": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "g <": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], + "g a": "editor::SelectAllMatches", "g s": "outline::Toggle", "g shift-s": "project_symbols::Toggle", "g .": "editor::ToggleCodeActions", // zed specific diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index f59b1fe167f8105c97e6e3cdf2e7b6023e8eeb3c..eac823de610280c24bb83003e007a28c29e81bf5 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use std::{cmp, sync::Arc}; use collections::HashMap; @@ -28,6 +29,8 @@ actions!( VisualDelete, VisualYank, OtherEnd, + SelectNext, + SelectPrevious, ] ); @@ -46,6 +49,9 @@ pub fn init(cx: &mut AppContext) { cx.add_action(other_end); cx.add_action(delete); cx.add_action(yank); + + cx.add_action(select_next); + cx.add_action(select_previous); } pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { @@ -384,6 +390,50 @@ pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { }); } +pub fn select_next( + _: &mut Workspace, + _: &SelectNext, + cx: &mut ViewContext, +) -> Result<()> { + Vim::update(cx, |vim, cx| { + let count = + vim.take_count(cx) + .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); + vim.update_active_editor(cx, |editor, cx| { + for _ in 0..count { + match editor.select_next(&Default::default(), cx) { + Err(a) => return Err(a), + _ => {} + } + } + Ok(()) + }) + }) + .unwrap_or(Ok(())) +} + +pub fn select_previous( + _: &mut Workspace, + _: &SelectPrevious, + cx: &mut ViewContext, +) -> Result<()> { + Vim::update(cx, |vim, cx| { + let count = + vim.take_count(cx) + .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); + vim.update_active_editor(cx, |editor, cx| { + for _ in 0..count { + match editor.select_previous(&Default::default(), cx) { + Err(a) => return Err(a), + _ => {} + } + } + Ok(()) + }) + }) + .unwrap_or(Ok(())) +} + #[cfg(test)] mod test { use indoc::indoc; From b5a39de3e23ddb8ed67b16ce2cab5bd596c930ab Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 25 Sep 2023 21:45:28 -0400 Subject: [PATCH 55/89] Add `reset_db` script --- script/reset_db | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 script/reset_db diff --git a/script/reset_db b/script/reset_db new file mode 100755 index 0000000000000000000000000000000000000000..87ce786aa776cf710f9c80528dc0d49ed82371d3 --- /dev/null +++ b/script/reset_db @@ -0,0 +1,2 @@ +psql -c "DROP DATABASE zed (FORCE);" +script/bootstrap From b131a2cb98514d3442d7cc192f488fcad9510ee9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Sep 2023 10:51:13 +0300 Subject: [PATCH 56/89] Fix another place where Copilot may panic --- crates/project/src/project.rs | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4b31bc31bc78d470107e900857c0df51cc1d947b..a551b985bfb800ddd91bf89c7150162817dedd36 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8043,24 +8043,27 @@ fn subscribe_for_copilot_events( copilot::Event::CopilotLanguageServerStarted => { match copilot.read(cx).language_server() { Some((name, copilot_server)) => { - let new_server_id = copilot_server.server_id(); - let weak_project = cx.weak_handle(); - let copilot_log_subscription = copilot_server - .on_notification::( - move |params, mut cx| { - if let Some(project) = weak_project.upgrade(&mut cx) { - project.update(&mut cx, |_, cx| { - cx.emit(Event::LanguageServerLog( - new_server_id, - params.message, - )); - }) - } - }, - ); - project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server))); - project.copilot_log_subscription = Some(copilot_log_subscription); - cx.emit(Event::LanguageServerAdded(new_server_id)); + // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. + if !copilot_server.has_notification_handler::() { + let new_server_id = copilot_server.server_id(); + let weak_project = cx.weak_handle(); + let copilot_log_subscription = copilot_server + .on_notification::( + move |params, mut cx| { + if let Some(project) = weak_project.upgrade(&mut cx) { + project.update(&mut cx, |_, cx| { + cx.emit(Event::LanguageServerLog( + new_server_id, + params.message, + )); + }) + } + }, + ); + project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server))); + project.copilot_log_subscription = Some(copilot_log_subscription); + cx.emit(Event::LanguageServerAdded(new_server_id)); + } } None => debug_panic!("Received Copilot language server started event, but no language server is running"), } From ea278b5b12bec131bc4b587b71a45e4b74c11308 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 09:53:49 -0400 Subject: [PATCH 57/89] ensure desc sort and cleanup unused imports --- crates/semantic_index/src/db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index 3558bf6b0a0335bf1d4bca0114d08edb9d835d7d..caa70a4cfaaa76cbfc6138f864a12f58acacea02 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -16,19 +16,19 @@ use rpc::proto::Timestamp; use rusqlite::params; use rusqlite::types::Value; use std::{ - cmp::Reverse, future::Future, ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, - time::{Instant, SystemTime}, + time::SystemTime, }; use util::TryFutureExt; pub fn argsort(data: &[T]) -> Vec { let mut indices = (0..data.len()).collect::>(); indices.sort_by_key(|&i| &data[i]); + indices.reverse(); indices } From 342a00b89e4ee37a0937fb3c1887eac84ba3acf2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 26 Sep 2023 10:49:55 -0400 Subject: [PATCH 58/89] Remove `dbg!` from `styleable_helpers!` (#3035) This PR removes a leftover `dbg!` from `styleable_helpers!`. We already removed this in the `gpui2-ui` branch, but getting this on `main` since @KCaverly pointed it out. Release Notes: - N/A --- crates/gpui2_macros/src/styleable_helpers.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/gpui2_macros/src/styleable_helpers.rs b/crates/gpui2_macros/src/styleable_helpers.rs index 238e64ed14f7778ce5f0c1ffa1544c0c5ea26b41..7283870858cfdd54b5f55eda644e0f2aff02c8b5 100644 --- a/crates/gpui2_macros/src/styleable_helpers.rs +++ b/crates/gpui2_macros/src/styleable_helpers.rs @@ -135,10 +135,6 @@ fn generate_predefined_setter( } }; - if negate { - dbg!(method.to_string()); - } - method } From e75f56a0f291c8a773fd5aef701033dcaeac7ddb Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 12:39:22 -0400 Subject: [PATCH 59/89] move to system blas --- Cargo.lock | 1 + crates/semantic_index/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 68c7a159d252df24f2ff7e66322d37080be5cb60..edb2272c1cad7cb7c07cd9a170804c6b9956f163 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6834,6 +6834,7 @@ dependencies = [ "log", "ndarray", "node_runtime", + "openblas-src", "ordered-float", "parking_lot 0.11.2", "picker", diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 7e68399b10c6747df075302856ab3c5e4d9c8380..cd08aa0d63521b16066b5e4d8bfda06f60c88fef 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -41,6 +41,7 @@ globset.workspace = true sha1 = "0.10.5" ndarray = { version = "0.15.0", features = ["blas"] } blas-src = { version = "0.8", features = ["openblas"] } +openblas-src = { version = "0.10", features = ["cblas", "system"] } [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } From 36f022bb587bac5b31a627a816acfaceb14dc9a9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:40:41 +0200 Subject: [PATCH 60/89] project_replace: Fix up key bindings (#3034) Release Notes: - N/A --- assets/keymaps/default.json | 9 ++++++- crates/search/src/project_search.rs | 37 ++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 9c129cb1ac7373f1b8c17847767b7f2b8eeb9251..fa75d0ce1ba88b3144385d63bf7d8890239a5f90 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -262,6 +262,13 @@ "down": "search::NextHistoryQuery" } }, + { + "context": "ProjectSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, { "context": "ProjectSearchView", "bindings": { @@ -563,7 +570,7 @@ } }, { - "context": "ProjectSearchBar", + "context": "ProjectSearchBar && !in_replace", "bindings": { "cmd-enter": "project_search::SearchInNew" } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 863eb72fb319119e524b75ed25431aca1ceb44fc..9df62261a0f21fb25609a5b0ca15791771f9684e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -60,7 +60,7 @@ pub fn init(cx: &mut AppContext) { cx.set_global(ActiveSettings::default()); cx.add_action(ProjectSearchView::deploy); cx.add_action(ProjectSearchView::move_focus_to_results); - cx.add_action(ProjectSearchBar::search); + cx.add_action(ProjectSearchBar::confirm); cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_prev_match); @@ -1371,9 +1371,18 @@ impl ProjectSearchBar { }) } } - fn search(&mut self, _: &Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + let mut should_propagate = true; if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| search_view.search(cx)); + search_view.update(cx, |search_view, cx| { + if !search_view.replacement_editor.is_focused(cx) { + should_propagate = false; + search_view.search(cx); + } + }); + } + if should_propagate { + cx.propagate_action(); } } @@ -1678,6 +1687,28 @@ impl View for ProjectSearchBar { "ProjectSearchBar" } + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + cx: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + let in_replace = self + .active_project_search + .as_ref() + .map(|search| { + search + .read(cx) + .replacement_editor + .read_with(cx, |_, cx| cx.is_self_focused()) + }) + .flatten() + .unwrap_or(false); + if in_replace { + keymap.add_identifier("in_replace"); + } + } + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(_search) = self.active_project_search.as_ref() { let search = _search.read(cx); From 8c47f117dbb9485413939c2ba57d76b484a9051b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 26 Sep 2023 19:21:15 +0200 Subject: [PATCH 61/89] editor: Start transaction in replace impl (#3036) This fixes the undo with replace in project /cc @maxbrunsfeld Release Notes: - N/A --- crates/editor/src/items.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 568ea223cc10e7037414e5ca4b18d15f18779830..504f12c57482b34267b202d6a0017b34ea3bf83a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -996,7 +996,9 @@ impl SearchableItem for Editor { }; if let Some(replacement) = query.replacement_for(&text) { - self.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + self.transact(cx, |this, cx| { + this.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + }); } } fn match_index_for_direction( From e263805847b96d9c8f36b9adb9ddcbee43704eec Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 26 Sep 2023 19:35:10 +0200 Subject: [PATCH 62/89] workspace: change save prompt for unnamed buffers (#3037) Release Notes: - N/A --- crates/workspace/src/pane.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e27100863732b68607e6e6dce0c14ea008bb3298..0f717a0edfbfdd0296cbb1ed839cbfc0f19f4267 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2213,7 +2213,7 @@ fn dirty_message_for(buffer_path: Option) -> String { let path = buffer_path .as_ref() .and_then(|p| p.path.to_str()) - .unwrap_or(&"Untitled buffer"); + .unwrap_or(&"This buffer"); let path = truncate_and_remove_front(path, 80); format!("{path} contains unsaved edits. Do you want to save it?") } From 0897ed561f9acf83cda69d3790c83d58b01d6be2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 26 Sep 2023 14:18:32 -0400 Subject: [PATCH 63/89] Rework call events api There were time when events with bad data were being emitted. What we found was that places where certain collaboration-related code could fail, like sending an, would still send events, and those events be in a bad state, as certain elements weren't constructed as expected, thus missing in the event. The new API guarantees that we have data in the correct configuration. In the future, we will add events for certain types of failures within Zed. Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/call/src/call.rs | 80 ++++++++++++++++------------ crates/collab_ui/src/channel_view.rs | 11 ++-- crates/collab_ui/src/collab_ui.rs | 32 ++++------- 3 files changed, 61 insertions(+), 62 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4db298fe98eb5718f1d641dd6539d2005f90ff4b..6eb50d37e51fc4df3c22aba76ee68af8a7bb8e24 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -206,9 +206,14 @@ impl ActiveCall { cx.spawn(|this, mut cx| async move { let result = invite.await; + if result.is_ok() { + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx)); + } else { + // TODO: Resport collaboration error + } + this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); - this.report_call_event("invite", cx); cx.notify(); }); result @@ -273,13 +278,7 @@ impl ActiveCall { .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; - Self::report_call_event_for_room( - "decline incoming", - Some(call.room_id), - None, - &self.client, - cx, - ); + report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -409,31 +408,46 @@ impl ActiveCall { &self.pending_invites } - fn report_call_event(&self, operation: &'static str, cx: &AppContext) { - let (room_id, channel_id) = match self.room() { - Some(room) => { - let room = room.read(cx); - (Some(room.id()), room.channel_id()) - } - None => (None, None), - }; - Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx) + pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + if let Some(room) = self.room() { + let room = room.read(cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); + } } +} - pub fn report_call_event_for_room( - operation: &'static str, - room_id: Option, - channel_id: Option, - client: &Arc, - cx: &AppContext, - ) { - let telemetry = client.telemetry(); - let telemetry_settings = *settings::get::(cx); - let event = ClickhouseEvent::Call { - operation, - room_id, - channel_id, - }; - telemetry.report_clickhouse_event(event, telemetry_settings); - } +pub fn report_call_event_for_room( + operation: &'static str, + room_id: u64, + channel_id: Option, + client: &Arc, + cx: &AppContext, +) { + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + let event = ClickhouseEvent::Call { + operation, + room_id: Some(room_id), + channel_id, + }; + telemetry.report_clickhouse_event(event, telemetry_settings); +} + +pub fn report_call_event_for_channel( + operation: &'static str, + channel_id: u64, + client: &Arc, + cx: &AppContext, +) { + let room = ActiveCall::global(cx).read(cx).room(); + + let telemetry = client.telemetry(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Call { + operation, + room_id: room.map(|r| r.read(cx).id()), + channel_id: Some(channel_id), + }; + telemetry.report_clickhouse_event(event, telemetry_settings); } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index c6f32cecd271fea3e61101c4cc0d8104ad2329d3..e46d31ac199d70e12bd644b8923183dadfbf53b0 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use call::ActiveCall; +use call::report_call_event_for_channel; use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::proto; use clock::ReplicaId; @@ -42,14 +42,9 @@ impl ChannelView { cx.spawn(|mut cx| async move { let channel_view = channel_view.await?; pane.update(&mut cx, |pane, cx| { - let room_id = ActiveCall::global(cx) - .read(cx) - .room() - .map(|room| room.read(cx).id()); - ActiveCall::report_call_event_for_room( + report_call_event_for_channel( "open channel notes", - room_id, - Some(channel_id), + channel_id, &workspace.read(cx).app_state().client, cx, ); diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 3dca2ec76da0b4ebccdfc415d4a821b01ea21110..84a9b3b6b6427a9b0383e59657e265d0963afd10 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -10,7 +10,7 @@ mod panel_settings; mod project_shared_notification; mod sharing_status_indicator; -use call::{ActiveCall, Room}; +use call::{report_call_event_for_room, ActiveCall, Room}; use gpui::{ actions, geometry::{ @@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { let client = call.client(); let toggle_screen_sharing = room.update(cx, |room, cx| { if room.is_screen_sharing() { - ActiveCall::report_call_event_for_room( + report_call_event_for_room( "disable screen share", - Some(room.id()), + room.id(), room.channel_id(), &client, cx, ); Task::ready(room.unshare_screen(cx)) } else { - ActiveCall::report_call_event_for_room( + report_call_event_for_room( "enable screen share", - Some(room.id()), + room.id(), room.channel_id(), &client, cx, @@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { if let Some(room) = call.room().cloned() { let client = call.client(); room.update(cx, |room, cx| { - if room.is_muted(cx) { - ActiveCall::report_call_event_for_room( - "enable microphone", - Some(room.id()), - room.channel_id(), - &client, - cx, - ); + let operation = if room.is_muted(cx) { + "enable microphone" } else { - ActiveCall::report_call_event_for_room( - "disable microphone", - Some(room.id()), - room.channel_id(), - &client, - cx, - ); - } + "disable microphone" + }; + report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx); + room.toggle_mute(cx) }) .map(|task| task.detach_and_log_err(cx)) From 54c63063e483969ee937417ac3c4eb3805f5cdf8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 16:23:48 -0400 Subject: [PATCH 64/89] changed inline assist generate prompt to leverage outline as opposed to entire prior file Co-Authored-by: Antonio --- crates/assistant/src/assistant.rs | 1 + crates/assistant/src/assistant_panel.rs | 123 +++----- crates/assistant/src/prompts.rs | 378 ++++++++++++++++++++++++ 3 files changed, 412 insertions(+), 90 deletions(-) create mode 100644 crates/assistant/src/prompts.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 258684db47096bac2d3df33d0289462dbc841214..6c9b14333e34cbf5fd49d8299ba7bd891b607526 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -1,6 +1,7 @@ pub mod assistant_panel; mod assistant_settings; mod codegen; +mod prompts; mod streaming_diff; use ai::completion::Role; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 42e5fb78979a6b8136c5c60d29e38e064df3435d..55a1dfe0f6f28e6d745a7d077869a6ae8e9ce8bf 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,6 +1,7 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, + prompts::generate_content_prompt, MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; @@ -541,11 +542,31 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let multi_buffer = editor.read(cx).buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let snapshot = if multi_buffer.all_buffers().len() > 1 { + return; + } else { + multi_buffer + .all_buffers() + .iter() + .next() + .unwrap() + .read(cx) + .snapshot() + }; + let range = pending_assist.codegen.read(cx).range(); - let selected_text = snapshot.text_for_range(range.clone()).collect::(); + let language_range = snapshot.anchor_at( + range.start.to_offset(&multi_buffer_snapshot), + language::Bias::Left, + ) + ..snapshot.anchor_at( + range.end.to_offset(&multi_buffer_snapshot), + language::Bias::Right, + ); - let language = snapshot.language_at(range.start); + let language = snapshot.language_at(language_range.start); let language_name = if let Some(language) = language.as_ref() { if Arc::ptr_eq(language, &language::PLAIN_TEXT) { None @@ -557,93 +578,15 @@ impl AssistantPanel { }; let language_name = language_name.as_deref(); - let mut prompt = String::new(); - if let Some(language_name) = language_name { - writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); - } - match pending_assist.codegen.read(cx).kind() { - CodegenKind::Transform { .. } => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt, "```").unwrap(); - - writeln!( - prompt, - "In particular, the user has selected the following text:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - writeln!(prompt, "{selected_text}").unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!(prompt).unwrap(); - writeln!( - prompt, - "Modify the selected text given the user prompt: {user_prompt}" - ) - .unwrap(); - writeln!( - prompt, - "You MUST reply only with the edited selected text, not the entire file." - ) - .unwrap(); - } - CodegenKind::Generate { .. } => { - writeln!( - prompt, - "You're currently working inside an editor on this file:" - ) - .unwrap(); - if let Some(language_name) = language_name { - writeln!(prompt, "```{language_name}").unwrap(); - } else { - writeln!(prompt, "```").unwrap(); - } - for chunk in snapshot.text_for_range(Anchor::min()..range.start) { - write!(prompt, "{chunk}").unwrap(); - } - write!(prompt, "<|>").unwrap(); - for chunk in snapshot.text_for_range(range.start..Anchor::max()) { - write!(prompt, "{chunk}").unwrap(); - } - writeln!(prompt).unwrap(); - writeln!(prompt, "```").unwrap(); - writeln!( - prompt, - "Assume the cursor is located where the `<|>` marker is." - ) - .unwrap(); - writeln!( - prompt, - "Text can't be replaced, so assume your answer will be inserted at the cursor." - ) - .unwrap(); - writeln!( - prompt, - "Complete the text given the user prompt: {user_prompt}" - ) - .unwrap(); - } - } - if let Some(language_name) = language_name { - writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); - } - writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); - writeln!(prompt, "Never make remarks about the output.").unwrap(); + let codegen_kind = pending_assist.codegen.read(cx).kind().clone(); + let prompt = generate_content_prompt( + user_prompt.to_string(), + language_name, + &snapshot, + language_range, + cx, + codegen_kind, + ); let mut messages = Vec::new(); let mut model = settings::get::(cx) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs new file mode 100644 index 0000000000000000000000000000000000000000..272ae9eac686914faf6b2c0c007f59ac9ff9f77d --- /dev/null +++ b/crates/assistant/src/prompts.rs @@ -0,0 +1,378 @@ +use gpui::AppContext; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use std::cmp; +use std::ops::Range; +use std::{fmt::Write, iter}; + +use crate::codegen::CodegenKind; + +fn outline_for_prompt( + buffer: &BufferSnapshot, + range: Range, + cx: &AppContext, +) -> Option { + let indent = buffer + .language_indent_size_at(0, cx) + .chars() + .collect::(); + let outline = buffer.outline(None)?; + let range = range.to_offset(buffer); + + let mut text = String::new(); + let mut items = outline.items.into_iter().peekable(); + + let mut intersected = false; + let mut intersection_indent = 0; + let mut extended_range = range.clone(); + + while let Some(item) = items.next() { + let item_range = item.range.to_offset(buffer); + if item_range.end < range.start || item_range.start > range.end { + text.extend(iter::repeat(indent.as_str()).take(item.depth)); + text.push_str(&item.text); + text.push('\n'); + } else { + intersected = true; + let is_terminal = items + .peek() + .map_or(true, |next_item| next_item.depth <= item.depth); + if is_terminal { + if item_range.start <= extended_range.start { + extended_range.start = item_range.start; + intersection_indent = item.depth; + } + extended_range.end = cmp::max(extended_range.end, item_range.end); + } else { + let name_start = item_range.start + item.name_ranges.first().unwrap().start; + let name_end = item_range.start + item.name_ranges.last().unwrap().end; + + if range.start > name_end { + text.extend(iter::repeat(indent.as_str()).take(item.depth)); + text.push_str(&item.text); + text.push('\n'); + } else { + if name_start <= extended_range.start { + extended_range.start = item_range.start; + intersection_indent = item.depth; + } + extended_range.end = cmp::max(extended_range.end, name_end); + } + } + } + + if intersected + && items.peek().map_or(true, |next_item| { + next_item.range.start.to_offset(buffer) > range.end + }) + { + intersected = false; + text.extend(iter::repeat(indent.as_str()).take(intersection_indent)); + text.extend(buffer.text_for_range(extended_range.start..range.start)); + text.push_str("<|"); + text.extend(buffer.text_for_range(range.clone())); + text.push_str("|>"); + text.extend(buffer.text_for_range(range.end..extended_range.end)); + text.push('\n'); + } + } + + Some(text) +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: &BufferSnapshot, + range: Range, + cx: &AppContext, + kind: CodegenKind, +) -> String { + let mut prompt = String::new(); + + // General Preamble + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap(); + } else { + writeln!(prompt, "You're an expert engineer.\n").unwrap(); + } + + let outline = outline_for_prompt(buffer, range.clone(), cx); + if let Some(outline) = outline { + writeln!( + prompt, + "The file you are currently working on has the following outline:" + ) + .unwrap(); + if let Some(language_name) = language_name { + let language_name = language_name.to_lowercase(); + writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap(); + } else { + writeln!(prompt, "```\n{outline}\n```").unwrap(); + } + } + + // Assume for now that we are just generating + if range.clone().start == range.end { + writeln!(prompt, "In particular, the user's cursor is current on the '<||>' span in the above outline, with no text selected.").unwrap(); + } else { + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|' and '|>' spans.").unwrap(); + } + + match kind { + CodegenKind::Generate { position } => { + writeln!( + prompt, + "Assume the cursor is located where the `<|` marker is." + ) + .unwrap(); + writeln!( + prompt, + "Text can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); + writeln!( + prompt, + "Generate text based on the users prompt: {user_prompt}" + ) + .unwrap(); + } + CodegenKind::Transform { range } => { + writeln!( + prompt, + "Modify the users code selected text based upon the users prompt: {user_prompt}" + ) + .unwrap(); + writeln!( + prompt, + "You MUST reply with only the adjusted code, not the entire file." + ) + .unwrap(); + } + } + + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap(); + } + writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); + + prompt +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use std::sync::Arc; + + use gpui::AppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use settings::SettingsStore; + + pub(crate) fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_outline_query( + r#" + (struct_item + "struct" @context + name: (_) @name) @item + (enum_item + "enum" @context + name: (_) @name) @item + (enum_variant + name: (_) @name) @item + (field_declaration + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @name + "for"? @context + type: (_) @name) @item + (function_item + "fn" @context + name: (_) @name) @item + (mod_item + "mod" @context + name: (_) @name) @item + "#, + ) + .unwrap() + } + + #[gpui::test] + fn test_outline_for_prompt(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + language_settings::init(cx); + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + self.a + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_before(Point::new(1, 4)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + <||>a: usize + b + impl X + fn new + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(8, 14)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new() -> Self { + let <|a |>= 1; + let b = 2; + Self { a, b } + } + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(6, 0))..snapshot.anchor_before(Point::new(6, 0)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + <||> + fn new + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(8, 12))..snapshot.anchor_before(Point::new(13, 9)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new() -> Self { + let <|a = 1; + let b = 2; + Self { a, b } + } + + pub f|>n a(&self, param: bool) -> usize { + self.a + } + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(5, 6))..snapshot.anchor_before(Point::new(12, 0)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X<| { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + |> + fn a + fn b + "}) + ); + + let outline = outline_for_prompt( + &snapshot, + snapshot.anchor_before(Point::new(18, 8))..snapshot.anchor_before(Point::new(18, 8)), + cx, + ); + assert_eq!( + outline.as_deref(), + Some(indoc! {" + struct X + a + b + impl X + fn new + fn a + pub fn b(&self) -> usize { + <||>self.b + } + "}) + ); + } +} From e8dd412ac12372f2e1f47ae1499c54595ba7f34d Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 17:10:31 -0400 Subject: [PATCH 65/89] update inline generate prompt to leverage more explicit <|START| and |END|> spans --- crates/assistant/src/assistant_panel.rs | 14 +++++--------- crates/assistant/src/prompts.rs | 16 ++++++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 55a1dfe0f6f28e6d745a7d077869a6ae8e9ce8bf..37d0d729fe64e0def057402fb9a24b796ebdf317 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -544,16 +544,10 @@ impl AssistantPanel { let multi_buffer = editor.read(cx).buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let snapshot = if multi_buffer.all_buffers().len() > 1 { - return; + let snapshot = if multi_buffer.is_singleton() { + multi_buffer.as_singleton().unwrap().read(cx).snapshot() } else { - multi_buffer - .all_buffers() - .iter() - .next() - .unwrap() - .read(cx) - .snapshot() + return; }; let range = pending_assist.codegen.read(cx).range(); @@ -588,6 +582,8 @@ impl AssistantPanel { codegen_kind, ); + dbg!(&prompt); + let mut messages = Vec::new(); let mut model = settings::get::(cx) .default_open_ai_model diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 272ae9eac686914faf6b2c0c007f59ac9ff9f77d..00db4c1302e61e776d94adbd27f622b38dff3711 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -68,9 +68,13 @@ fn outline_for_prompt( intersected = false; text.extend(iter::repeat(indent.as_str()).take(intersection_indent)); text.extend(buffer.text_for_range(extended_range.start..range.start)); - text.push_str("<|"); + text.push_str("<|START|"); text.extend(buffer.text_for_range(range.clone())); - text.push_str("|>"); + if range.start != range.end { + text.push_str("|END|>"); + } else { + text.push_str(">"); + } text.extend(buffer.text_for_range(range.end..extended_range.end)); text.push('\n'); } @@ -113,16 +117,16 @@ pub fn generate_content_prompt( // Assume for now that we are just generating if range.clone().start == range.end { - writeln!(prompt, "In particular, the user's cursor is current on the '<||>' span in the above outline, with no text selected.").unwrap(); + writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap(); } else { - writeln!(prompt, "In particular, the user has selected a section of the text between the '<|' and '|>' spans.").unwrap(); + writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap(); } match kind { CodegenKind::Generate { position } => { writeln!( prompt, - "Assume the cursor is located where the `<|` marker is." + "Assume the cursor is located where the `<|START|` marker is." ) .unwrap(); writeln!( @@ -144,7 +148,7 @@ pub fn generate_content_prompt( .unwrap(); writeln!( prompt, - "You MUST reply with only the adjusted code, not the entire file." + "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file." ) .unwrap(); } From 90f17d4a28c716f13b5e6325a2bec3786bf9bd3f Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 26 Sep 2023 17:11:20 -0400 Subject: [PATCH 66/89] updated codegen match to leverage unused values --- crates/assistant/src/prompts.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 00db4c1302e61e776d94adbd27f622b38dff3711..04cf5d089be4d145f9d0e45481e392fb8b415840 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -123,7 +123,7 @@ pub fn generate_content_prompt( } match kind { - CodegenKind::Generate { position } => { + CodegenKind::Generate { position: _ } => { writeln!( prompt, "Assume the cursor is located where the `<|START|` marker is." @@ -140,7 +140,7 @@ pub fn generate_content_prompt( ) .unwrap(); } - CodegenKind::Transform { range } => { + CodegenKind::Transform { range: _ } => { writeln!( prompt, "Modify the users code selected text based upon the users prompt: {user_prompt}" From 7e2cef98a7cfc9c71962b540e607a200f9bdcb6a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Sep 2023 19:20:33 +0300 Subject: [PATCH 67/89] Hide inlay hints toggle if they are not supported by the current editor --- crates/editor/src/editor.rs | 23 ++++++++ .../quick_action_bar/src/quick_action_bar.rs | 52 +++++++++++-------- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ae04e6d9033cf280395493604818d7b4fc097e13..ff34fea917fbd1bef401104c9c09c11cdcc48ba4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8588,6 +8588,29 @@ impl Editor { self.handle_input(text, cx); } + + pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool { + let Some(project) = self.project.as_ref() else { + return false; + }; + let project = project.read(cx); + + let mut supports = false; + self.buffer().read(cx).for_each_buffer(|buffer| { + if !supports { + supports = project + .language_servers_for_buffer(buffer.read(cx), cx) + .any( + |(_, server)| match server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Left(enabled)) => enabled, + Some(lsp::OneOf::Right(_)) => true, + None => false, + }, + ) + } + }); + supports + } } fn inlay_hint_settings( diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 7e0be4d097d3d41cb87930f73d387ca6a86cb7bd..d648e83f8f1acbfe013ab411be89ca1bcd5380fe 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -48,24 +48,26 @@ impl View for QuickActionBar { return Empty::new().into_any(); }; - let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); - let mut bar = Flex::row().with_child(render_quick_action_bar_button( - 0, - "icons/inlay_hint.svg", - inlay_hints_enabled, - ( - "Toggle Inlay Hints".to_string(), - Some(Box::new(editor::ToggleInlayHints)), - ), - cx, - |this, cx| { - if let Some(editor) = this.active_editor() { - editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); - }); - } - }, - )); + let mut bar = Flex::row(); + if editor.read(cx).supports_inlay_hints(cx) { + bar = bar.with_child(render_quick_action_bar_button( + 0, + "icons/inlay_hint.svg", + editor.read(cx).inlay_hints_enabled(), + ( + "Toggle Inlay Hints".to_string(), + Some(Box::new(editor::ToggleInlayHints)), + ), + cx, + |this, cx| { + if let Some(editor) = this.active_editor() { + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); + }); + } + }, + )); + } if editor.read(cx).buffer().read(cx).is_singleton() { let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); @@ -163,12 +165,18 @@ impl ToolbarItemView for QuickActionBar { if let Some(editor) = active_item.downcast::() { let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); self._inlay_hints_enabled_subscription = Some(cx.observe(&editor, move |_, editor, cx| { - let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); - if inlay_hints_enabled != new_inlay_hints_enabled { - inlay_hints_enabled = new_inlay_hints_enabled; - cx.notify(); + let editor = editor.read(cx); + let new_inlay_hints_enabled = editor.inlay_hints_enabled(); + let new_supports_inlay_hints = editor.supports_inlay_hints(cx); + let should_notify = inlay_hints_enabled != new_inlay_hints_enabled + || supports_inlay_hints != new_supports_inlay_hints; + inlay_hints_enabled = new_inlay_hints_enabled; + supports_inlay_hints = new_supports_inlay_hints; + if should_notify { + cx.notify() } })); ToolbarItemLocation::PrimaryRight { flex: None } From 568fec0f5431ad47d65fb426cf5cda18555c29dd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 26 Sep 2023 18:15:41 -0400 Subject: [PATCH 68/89] Add `Sized` bound to `StyleHelpers` (#3042) This PR adds a `Sized` bound to the `StyleHelpers` trait. All of the individual methods on this trait already had a `Self: Sized` bound, so moving it up to the trait level will make it so we don't have to repeat ourselves so much. There's an open question of whether we can hoist the `Sized` bound to `Styleable`, but it's possible there are cases where we'd want to have a `Styleable` trait object. Release Notes: - N/A --- crates/gpui2/src/style.rs | 147 ++++++++------------------------------ 1 file changed, 30 insertions(+), 117 deletions(-) diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 3e4acd57e7cb747be66e9bbb9009ec6ae5c96d43..ecb9d79a6c47f4011550049282616ca68c6193d3 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -320,174 +320,114 @@ use crate as gpui2; // // Example: // // Sets the padding to 0.5rem, just like class="p-2" in Tailwind. -// fn p_2(mut self) -> Self where Self: Sized; -pub trait StyleHelpers: Styleable