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 b42e11d326..8a87cb9986 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 86def54d32..126407a32d 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 15406cf63e..2994cb29b6 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 b2339f998f..fb41f7a349 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 0b1fa750c0..75674e78e0 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 c800e4e110..7c69bc2db1 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 d968a92646..8a74522df1 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 9e69240d27..1a6a752e23 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 4f9bb231ce..06f76bbc5c 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 0000000000..a1f4777ec6 --- /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 e2fa6e989a..6ff997a161 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 a3e6a547dd..5275a2664a 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 feab53d094..c297962684 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 6b5f7b3a35..da3f7e4c32 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 e44e8d0e4c..38af2d1555 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 700b69117a..89bfaa4b70 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 0bae32f1f7..0ef54dc3d5 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 e43b0ab22b..82e4cc6863 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 5275a2664a..285cf1da60 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 c297962684..a8e7f12b3a 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 1a6a752e23..5db0fe748e 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 0827e13264..446ddfeab0 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 a8e7f12b3a..6043b94621 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 3cced78c42..bfae8cae0b 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 06f76bbc5c..90c155cdf8 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 ecaee4534e..97175cb55e 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 0dc1d1eba4..5b15b5274c 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 142b548a93..97f9d7a7c8 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 5d40032024..509efc5825 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 a1f4777ec6..10c2599dd9 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 c8d12f8ee3..a23091c7a7 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 c9c04007d1..a9e5e66b9e 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 18de029fdc..15fce99aad 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 e58f805a02..227d39bb63 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 0000000000..669d34409f --- /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 0000000000..2f7ed10eeb --- /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 0000000000..13928d5c7e --- /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 da3f7e4c32..acffbc29ab 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 d968a92646..72804cb523 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 c1876434ad..3e4c5911ed 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 97f9d7a7c8..11c35a6d75 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 10c2599dd9..7aaf3d8024 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 a9e5e66b9e..8ef0718c32 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 13928d5c7e..91827c0285 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 7aaf3d8024..2ad8f0522e 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -355,8 +355,33 @@ mod test { dd ˇcc"}) .await; + } - cx.simulate_shared_keystrokes([":", "%", "s", "/", "/", "/", "enter"]) + #[gpui::test] + async fn test_command_search(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + 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 8ef0718c32..e76da1dfc5 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 0000000000..705ce51fb7 --- /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 2ad8f0522e..6bed7f5bb4 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 ea747b3a36..24bed4c8d1 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 285cf1da60..40e6d36f3b 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 || 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 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 { - 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 6043b94621..a853691b76 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 72804cb523..5363262e3f 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 3e4c5911ed..c1876434ad 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 5db0fe748e..9453607ce9 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 90c155cdf8..90c4481374 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 c2d8cc52b2..64ef31cd30 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 5b15b5274c..0dc1d1eba4 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 b79f655f81..cd939b5604 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 6bed7f5bb4..092d72c2fc 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 e76da1dfc5..f74625c8b3 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 40e6d36f3b..fbe018409b 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 a853691b76..092286973e 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 acffbc29ab..4e01693dbf 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 5363262e3f..11e80ffb4a 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 1944219117..c1127a5389 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 c808d33f23..b6ad75a34e 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 b0124bf7df..2b6e94854e 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 b6ad75a34e..1ba0001cfd 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 9035327b2e..f386665915 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 6c502ebc74..b3d4987526 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 6e424a6f81..22810e3e07 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 056ad56a2b..f8f0e7a65f 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 49a1268863..26fa015847 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 e79a9c5986..3f76af0d15 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 a261fffcd6..a1cead7df9 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 3cced78c42..05b29aabf4 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 c1876434ad..b9622a1c3e 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 f634ea4eca..c1096a87ba 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 87fd536391..30d15a5b04 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 df1db7b8c2..92d178fad2 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 f8f0e7a65f..0000000000 --- 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 26fa015847..0000000000 --- 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 3127bcf837..95152ec832 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 0000000000..5018a91739 --- /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 1aafefc1a6..a82d28eb8c 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 73ab231c07..d9566c216c 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 5d79b8fc06..75e24e9135 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 46df86bfa1..868b58e449 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 8237aac004..e812a26cd5 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 72ec5b328a..67352f0779 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 83edc73deb..068ec7a28a 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 5d06862439..f156199f9e 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 5a9e8fb6cb..dbe30cb4f9 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 f82979ab7c..4270e0ca5d 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 b905892f1a..2ee40a57ac 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 310287797c..5a3da16fdd 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 bbbedd0b10..d3e94297ad 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 24fd1eb5d3..851efdd4f6 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 a923628def..2ef29b225f 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 0000000000..415cdcbaf3 --- /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 de4428826b..77c5b2ef20 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 5a9608d3e3..e17ff4c8ed 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 f34cfa88ef..763a585176 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 9b96171f2f..08caad3107 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 a41d56476f..705d0d8866 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 0a86a61499..71235625d6 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 415cdcbaf3..eac16aa776 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 eac16aa776..c7bab1e0b0 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 dcfce4f1fb..4aa0e5cd10 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 b4e698e08a..98508e1bee 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 e88aee5dcf..3273d5c6e6 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 75674e78e0..0d1c2a9d36 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 0000000000..a25ada92b8 --- /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 3cced78c42..58095343f1 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 c1876434ad..1c05c810f6 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 8002b0d35d..a3ee412548 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 dfd9a523b4..48e31bc55a 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 1f8ec4e92b..c2f54e1078 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 8a40325203..7e0be4d097 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 7478f5cca1..fc06e8865a 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 d22e26c1f5..0032c24cbb 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 d968a92646..dcdbd004b7 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 05b29aabf4..9d6358edb4 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 c1096a87ba..882a1a1bdb 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 0000000000..57674c3c70 --- /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 0000000000..5e3d309419 --- /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 0000000000..6bde4539ed --- /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 0000000000..124369cf9d --- /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 0000000000..3239dbec9c --- /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 92d178fad2..089ff33049 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 95152ec832..58c5fed62d 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 58095343f1..103b9c2c2a 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 1c05c810f6..e86960daf4 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 0000000000..c4e129c1f5 --- /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 0000000000..c893d109ab --- /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 0000000000..170b2268f9 --- /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 a3ee412548..5d141b32d5 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 48e31bc55a..258684db47 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 263382c03e..42e5fb7897 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 e7da46cdf9..e956d72260 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 dcdbd004b7..bdf060205a 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 5e3d309419..89ee5a92ea 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 0000000000..252caf99f6 --- /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 089ff33049..b72c4a5681 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 a82d28eb8c..b400a491e4 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 0000000000..128af9976f --- /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 705d0d8866..82c5c0234b 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 0000000000..bd34de3396 --- /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 a82d28eb8c..1d513db7d2 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 868b58e449..19e5b26abe 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 0000000000..76a0d4cee7 --- /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 0000000000..9e7883d700 --- /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 dbe30cb4f9..08b8e3e7c5 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 d3e94297ad..90e6b525eb 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 c7bab1e0b0..0bcd961643 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 a1cead7df9..d29e31072b 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 0000000000..a7fb06132f --- /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 0000000000..a540d29169 --- /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 e17ff4c8ed..0000000000 --- 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 61dbb676e2..70b9ab4a5e 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 0000000000..de946dab28 --- /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 0000000000..f09a8a7ea4 --- /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 77c5b2ef20..7c3462b3fe 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 0000000000..7e76cb6835 --- /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 0000000000..303e2d6de9 --- /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 0000000000..8204ad26c0 --- /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 763a585176..79265a7572 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 08caad3107..e1ca6b1dc7 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 0000000000..fb785a317f --- /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 0000000000..7912820533 --- /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 7dc3110ced..0000000000 --- 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 d0e9e93839..3f29d6f79b 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 dcfce4f1fb..9b0d6c98b0 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 | 58 +++++++++++++------ 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, 78 insertions(+), 35 deletions(-) rename crates/{semantic_index => ai}/src/embedding.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index 103b9c2c2a..0216bea496 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 c4e129c1f5..a2c70ce8c6 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 c893d109ab..5256a6a643 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 2b6e94854e..332470aa54 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 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 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 44afecb0c2..e38ae1f06d 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 15406cf63e..a0cdbeea05 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 3e35284027..8280dc7d65 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 3026eef9ae..6ae8faa4cd 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 9f5a339b23..498ad6187e 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 1ba0001cfd..3bb6e17fb5 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 f386665915..f2cae8a557 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 3bb6e17fb5..fd41cb1500 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 8aa7a1e40e..dd75d2bab6 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 6bde4539ed..96c3aff2af 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 252caf99f6..9f84446eed 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 3239dbec9c..a21ee0a0bc 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 0000000000..7a3145cf08 --- /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 b72c4a5681..3974bbce40 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 626a969bd8..4eca6f3a30 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 30d15a5b04..a6be248d6a 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 7e76cb6835..3d6efe54f4 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 79265a7572..22a99fdbf8 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 71235625d6..d4c8c26273 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 5c01bbc4a9..186427e863 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 882a1a1bdb..49dd05ba30 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 3974bbce40..a57be4fd89 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 4f0d4fd461..b2dc79c87b 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 96c3aff2af..37893a9a0a 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 9f84446eed..f3c7b93005 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 a21ee0a0bc..a5caf717e7 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 7a3145cf08..c758518762 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 d9566c216c..39167498a9 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 75e24e9135..a522859868 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 89ee5a92ea..35bed24c01 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 0000000000..c702eb3def --- /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 0000000000..7a7688a9d8 --- /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 a57be4fd89..59aca22be7 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 58c5fed62d..3ddaa5caa6 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 a8fbc02265..15790fe9f8 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 0000000000..1883e35ae9 --- /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 0000000000..cc493c00f4 --- /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 186427e863..878604f360 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 49dd05ba30..5c73235d3e 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 0000000000..d12368706a --- /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 59aca22be7..d52219f61e 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 cc493c00f4..08e671311f 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 08b8e3e7c5..7d2bb45b49 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 4270e0ca5d..893d41871d 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 7c3462b3fe..2e3f73d30f 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 22a99fdbf8..21f792c358 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 e1ca6b1dc7..c8892b4697 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 82c5c0234b..f926adbd26 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 15790fe9f8..50a680e0f7 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 3f76af0d15..0ed40e147e 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 0bcd961643..ae06009238 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 d29e31072b..0000000000 --- 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 f09a8a7ea4..0000000000 --- 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 8a87cb9986..4f43215bc7 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 126407a32d..fbc40b4756 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 587e6ed25a..d2ad8fac90 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 fb41f7a349..ab6844d2de 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 0d1c2a9d36..2a21e4035f 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 a25ada92b8..9b12572ec3 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 9b12572ec3..cd0d13c712 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 3f83f8e37a..629f950014 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 a3e6a547dd..a191adcc05 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 feab53d094..263652184a 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 0827e13264..0c9d8de969 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 0c9d8de969..6070590d4f 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 8fbe87de2b..110502c259 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 2a21e4035f..be8d05256a 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 cd0d13c712..6293d4bfb9 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 629f950014..3f83f8e37a 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 a191adcc05..a3e6a547dd 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 263652184a..feab53d094 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 878604f360..96cac48f7a 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 e38ae1f06d..7e68399b10 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 8280dc7d65..3558bf6b0a 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(?) + ", + )?; - anyhow::Ok(results) + 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); + } + + 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 fd41cb1500..59cf596e7f 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 e9a96a02b4..8dcb53c399 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 6293d4bfb9..f5a77c7568 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 8a74522df1..58eb1e453e 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 72893656d6..4f0deda6e0 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 3f83f8e37a..629f950014 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 fbe018409b..e271008637 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 092286973e..f081eb9efa 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 8fbe87de2b..cdd8930b17 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 6013ea4907..311978578c 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 9e69240d27..e43a17449f 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 f59b1fe167..eac823de61 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 0000000000..87ce786aa7 --- /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 4b31bc31bc..a551b985bf 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 3558bf6b0a..caa70a4cfa 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 238e64ed14..7283870858 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 68c7a159d2..edb2272c1c 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 7e68399b10..cd08aa0d63 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 9c129cb1ac..fa75d0ce1b 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 863eb72fb3..9df62261a0 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 568ea223cc..504f12c574 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 e271008637..0f717a0edf 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 | 82 ++++++++++++++++------------ crates/collab_ui/src/channel_view.rs | 11 +--- crates/collab_ui/src/collab_ui.rs | 32 ++++------- 3 files changed, 62 insertions(+), 63 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4db298fe98..6eb50d37e5 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_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(&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: 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 c6f32cecd2..e46d31ac19 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 3dca2ec76d..84a9b3b6b6 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 | 125 +++----- crates/assistant/src/prompts.rs | 378 ++++++++++++++++++++++++ 3 files changed, 413 insertions(+), 91 deletions(-) create mode 100644 crates/assistant/src/prompts.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 258684db47..6c9b14333e 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 42e5fb7897..55a1dfe0f6 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 range = pending_assist.codegen.read(cx).range(); - let selected_text = snapshot.text_for_range(range.clone()).collect::(); + 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 language = snapshot.language_at(range.start); + let range = pending_assist.codegen.read(cx).range(); + 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(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 0000000000..272ae9eac6 --- /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 55a1dfe0f6..37d0d729fe 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 272ae9eac6..00db4c1302 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 00db4c1302..04cf5d089b 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 ae04e6d903..ff34fea917 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 7e0be4d097..d648e83f8f 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 3e4acd57e7..ecb9d79a6c 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