From 1610e270d6422271bd39006214c2a5410bc320c9 Mon Sep 17 00:00:00 2001 From: Alex Viscreanu Date: Fri, 21 Jul 2023 13:16:00 +0200 Subject: [PATCH 01/26] feat(workspace): add action for closing inactive editors on all panes --- assets/keymaps/default.json | 1 + crates/workspace/src/workspace.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 883b0c1872..5c841d19b2 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -22,6 +22,7 @@ "alt-cmd-right": "pane::ActivateNextItem", "cmd-w": "pane::CloseActiveItem", "alt-cmd-t": "pane::CloseInactiveItems", + "ctrl-alt-cmd-w": "workspace::CloseInactiveEditors", "cmd-k u": "pane::CloseCleanItems", "cmd-k cmd-w": "pane::CloseAllItems", "cmd-shift-w": "workspace::CloseWindow", diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0ebd01e1f7..6694cc06a3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -122,6 +122,7 @@ actions!( NewFile, NewWindow, CloseWindow, + CloseInactiveEditors, AddFolderToProject, Unfollow, Save, @@ -239,6 +240,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_editors); cx.add_global_action(Workspace::close_global); cx.add_global_action(restart); cx.add_async_action(Workspace::save_all); @@ -1633,6 +1635,34 @@ impl Workspace { } } + pub fn close_inactive_editors( + &mut self, + _: &CloseInactiveEditors, + cx: &mut ViewContext, + ) -> Option>> { + let current_pane = self.active_pane(); + + // let mut tasks: Vec>> = Vec::new(); + current_pane + .update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx).unwrap() + }) + .detach_and_log_err(cx); + + for pane in self.panes() { + if pane.id() == current_pane.id() { + continue; + } + + pane.update(cx, |pane: &mut Pane, cx| { + pane.close_all_items(&CloseAllItems, cx).unwrap() + }) + .detach_and_log_err(cx); + } + + Some(Task::ready(Ok(()))) + } + pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { let dock = match dock_side { DockPosition::Left => &self.left_dock, From fe388ed71ef9d8877fbef216061ac807acc1ba3d Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Sun, 23 Jul 2023 14:39:43 -0700 Subject: [PATCH 02/26] Add tree-sitter-nix --- Cargo.lock | 10 +++ Cargo.toml | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 1 + crates/zed/src/languages/nix/config.toml | 11 +++ crates/zed/src/languages/nix/highlights.scm | 95 +++++++++++++++++++++ 6 files changed, 119 insertions(+) create mode 100644 crates/zed/src/languages/nix/config.toml create mode 100644 crates/zed/src/languages/nix/highlights.scm diff --git a/Cargo.lock b/Cargo.lock index f0c8917aa2..175b3d9315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8065,6 +8065,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-nix" +version = "0.0.1" +source = "git+https://github.com/nix-community/tree-sitter-nix?rev=66e3e9ce9180ae08fc57372061006ef83f0abde7#66e3e9ce9180ae08fc57372061006ef83f0abde7" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-php" version = "0.19.1" @@ -9550,6 +9559,7 @@ dependencies = [ "tree-sitter-json 0.20.0", "tree-sitter-lua", "tree-sitter-markdown", + "tree-sitter-nix", "tree-sitter-php", "tree-sitter-python", "tree-sitter-racket", diff --git a/Cargo.toml b/Cargo.toml index fa824115cb..ca13a4eb2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", r tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"} tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"} tree-sitter-lua = "0.0.14" +tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c5bf313701..88185acfc2 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,6 +126,7 @@ tree-sitter-svelte.workspace = true tree-sitter-racket.workspace = true tree-sitter-yaml.workspace = true tree-sitter-lua.workspace = true +tree-sitter-nix.workspace = true url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 365e8a3023..9ba0f27552 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -152,6 +152,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { tree_sitter_php::language(), vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], ); + language("nix", tree_sitter_nix::language(), vec![]) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/nix/config.toml b/crates/zed/src/languages/nix/config.toml new file mode 100644 index 0000000000..778f0a6f05 --- /dev/null +++ b/crates/zed/src/languages/nix/config.toml @@ -0,0 +1,11 @@ +name = "Nix" +path_suffixes = ["nix"] +line_comment = "# " +block_comment = ["/* ", " */"] +autoclose_before = ";:.,=}])>` \n\t\"" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = true, newline = true }, +] diff --git a/crates/zed/src/languages/nix/highlights.scm b/crates/zed/src/languages/nix/highlights.scm new file mode 100644 index 0000000000..d63a46411a --- /dev/null +++ b/crates/zed/src/languages/nix/highlights.scm @@ -0,0 +1,95 @@ +(comment) @comment + +[ + "if" + "then" + "else" + "let" + "inherit" + "in" + "rec" + "with" + "assert" + "or" +] @keyword + +[ + (string_expression) + (indented_string_expression) +] @string + +[ + (path_expression) + (hpath_expression) + (spath_expression) +] @string.special.path + +(uri_expression) @link_uri + +[ + (integer_expression) + (float_expression) +] @number + +(interpolation + "${" @punctuation.special + "}" @punctuation.special) @embedded + +(escape_sequence) @escape +(dollar_escape) @escape + +(function_expression + universal: (identifier) @parameter +) + +(formal + name: (identifier) @parameter + "?"? @punctuation.delimiter) + +(select_expression + attrpath: (attrpath (identifier)) @property) + +(apply_expression + function: [ + (variable_expression (identifier)) @function + (select_expression + attrpath: (attrpath + attr: (identifier) @function .))]) + +(unary_expression + operator: _ @operator) + +(binary_expression + operator: _ @operator) + +(variable_expression (identifier) @variable) + +(binding + attrpath: (attrpath (identifier)) @property) + +"=" @operator + +[ + ";" + "." + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(identifier) @variable + +((identifier) @function.builtin + (#match? @function.builtin "^(__add|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__langVersion|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__partition|__path|__pathExists|__readDir|__readFile|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__valueSize|abort|baseNameOf|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fromTOML|import|isNull|map|placeholder|removeAttrs|scopedImport|throw|toString)$") + (#is-not? local)) + +((identifier) @variable.builtin + (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$") + (#is-not? local)) From 03bc430bdd82422dc01813f859a8ff8e201212a6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 16:14:56 -0700 Subject: [PATCH 03/26] Make mode indicator follow vim enabled state --- crates/vim/src/mode_indicator.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 84e3e5868a..639a7594f1 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -44,7 +44,11 @@ impl ModeIndicator { // Vim doesn't exist in some tests let mode = cx .has_global::() - .then(|| cx.global::().state.mode); + .then(|| { + let vim = cx.global::(); + vim.enabled.then(|| vim.state.mode) + }) + .flatten(); Self { mode, From 1935307b4f6b80438df46719ab5202ea7bd26850 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 27 Jul 2023 18:08:15 -0600 Subject: [PATCH 04/26] Fix jumping to definition in a new file This is broken because vim currently sets settings only on the active editor. Fix this by correcting the range on the currently active editor. It would be nice (at some point) to refactor how vim sets settings, but that's for another day. --- crates/editor/src/editor.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e05837740d..7af3f5460d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6374,8 +6374,8 @@ impl Editor { .range .to_offset(definition.target.buffer.read(cx)); + let range = self.range_for_match(&range); if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { - let range = self.range_for_match(&range); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); }); @@ -6392,7 +6392,6 @@ impl Editor { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. pane.update(cx, |pane, _| pane.disable_history()); - let range = target_editor.range_for_match(&range); target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); }); From f15a03816f9357522a3793fbba449818dc9ed67e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 17:19:32 -0700 Subject: [PATCH 05/26] underscore arguments --- crates/zed/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index df16ea7db9..b9fefb89a7 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -717,7 +717,7 @@ async fn watch_languages(_: Arc, _: Arc) -> Option<()> } #[cfg(not(debug_assertions))] -fn watch_file_types(fs: Arc, cx: &mut AppContext) {} +fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} fn connect_to_cli( server_name: &str, From 0dffb728db4287376c1fc9214e2de3b7aee2d06e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 17:36:02 -0700 Subject: [PATCH 06/26] Update elixir depedency co-authored-by: Alex --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c6a213d0d..9de3e6f4a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3851,7 +3851,7 @@ dependencies = [ "text", "theme", "tree-sitter", - "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e)", + "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=2616034f78ffa83ca6a521ebd7eee1868cb5c14c)", "tree-sitter-embedded-template", "tree-sitter-heex", "tree-sitter-html", @@ -8287,7 +8287,7 @@ dependencies = [ [[package]] name = "tree-sitter-elixir" version = "0.1.0" -source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e" +source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=2616034f78ffa83ca6a521ebd7eee1868cb5c14c#2616034f78ffa83ca6a521ebd7eee1868cb5c14c" dependencies = [ "cc", "tree-sitter", @@ -9923,7 +9923,7 @@ dependencies = [ "tree-sitter-c", "tree-sitter-cpp 0.20.0", "tree-sitter-css", - "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e)", + "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=2616034f78ffa83ca6a521ebd7eee1868cb5c14c)", "tree-sitter-elm", "tree-sitter-embedded-template", "tree-sitter-glsl", diff --git a/Cargo.toml b/Cargo.toml index fc46286de0..19d43a1b95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,7 +111,7 @@ tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", re tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } -tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" } +tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "2616034f78ffa83ca6a521ebd7eee1868cb5c14c" } tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"} tree-sitter-embedded-template = "0.20.0" tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" } From 45e5d816649242b058c19979779c0b04c3d9de17 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 17:41:13 -0700 Subject: [PATCH 07/26] update to dependency without symbol conflict --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9de3e6f4a6..04ee8c2212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3851,7 +3851,7 @@ dependencies = [ "text", "theme", "tree-sitter", - "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=2616034f78ffa83ca6a521ebd7eee1868cb5c14c)", + "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=a2861e88a730287a60c11ea9299c033c7d076e30)", "tree-sitter-embedded-template", "tree-sitter-heex", "tree-sitter-html", @@ -8287,7 +8287,7 @@ dependencies = [ [[package]] name = "tree-sitter-elixir" version = "0.1.0" -source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=2616034f78ffa83ca6a521ebd7eee1868cb5c14c#2616034f78ffa83ca6a521ebd7eee1868cb5c14c" +source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=a2861e88a730287a60c11ea9299c033c7d076e30#a2861e88a730287a60c11ea9299c033c7d076e30" dependencies = [ "cc", "tree-sitter", @@ -9923,7 +9923,7 @@ dependencies = [ "tree-sitter-c", "tree-sitter-cpp 0.20.0", "tree-sitter-css", - "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=2616034f78ffa83ca6a521ebd7eee1868cb5c14c)", + "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=a2861e88a730287a60c11ea9299c033c7d076e30)", "tree-sitter-elm", "tree-sitter-embedded-template", "tree-sitter-glsl", diff --git a/Cargo.toml b/Cargo.toml index 19d43a1b95..157db0635f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,7 +111,7 @@ tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", re tree-sitter-c = "0.20.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } -tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "2616034f78ffa83ca6a521ebd7eee1868cb5c14c" } +tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" } tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"} tree-sitter-embedded-template = "0.20.0" tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" } From a0fc515cfca9677dde32cef8be22321d1304a13e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 17:58:43 -0700 Subject: [PATCH 08/26] Rework close_inactive_items to await all tasks Update action name to be more accurate --- assets/keymaps/default.json | 2 +- crates/workspace/src/pane.rs | 4 +++ crates/workspace/src/workspace.rs | 42 ++++++++++++++++++++----------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 5c841d19b2..090385458e 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -22,7 +22,7 @@ "alt-cmd-right": "pane::ActivateNextItem", "cmd-w": "pane::CloseActiveItem", "alt-cmd-t": "pane::CloseInactiveItems", - "ctrl-alt-cmd-w": "workspace::CloseInactiveEditors", + "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", "cmd-k u": "pane::CloseCleanItems", "cmd-k cmd-w": "pane::CloseAllItems", "cmd-shift-w": "workspace::CloseWindow", diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2972c307f2..ee658c9cc9 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -746,6 +746,10 @@ impl Pane { _: &CloseAllItems, cx: &mut ViewContext, ) -> Option>> { + if self.items.is_empty() { + return None; + } + Some(self.close_items(cx, move |_| true)) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6694cc06a3..827b0b8427 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -21,6 +21,7 @@ use drag_and_drop::DragAndDrop; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, + stream::FuturesUnordered, FutureExt, StreamExt, }; use gpui::{ @@ -122,7 +123,7 @@ actions!( NewFile, NewWindow, CloseWindow, - CloseInactiveEditors, + CloseInactiveTabsAndPanes, AddFolderToProject, Unfollow, Save, @@ -240,7 +241,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_editors); + cx.add_async_action(Workspace::close_inactive_items_and_panes); cx.add_global_action(Workspace::close_global); cx.add_global_action(restart); cx.add_async_action(Workspace::save_all); @@ -1635,32 +1636,43 @@ impl Workspace { } } - pub fn close_inactive_editors( + pub fn close_inactive_items_and_panes( &mut self, - _: &CloseInactiveEditors, + _: &CloseInactiveTabsAndPanes, cx: &mut ViewContext, ) -> Option>> { let current_pane = self.active_pane(); - // let mut tasks: Vec>> = Vec::new(); - current_pane - .update(cx, |pane, cx| { - pane.close_inactive_items(&CloseInactiveItems, cx).unwrap() - }) - .detach_and_log_err(cx); + 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); + }; for pane in self.panes() { if pane.id() == current_pane.id() { continue; } - pane.update(cx, |pane: &mut Pane, cx| { - pane.close_all_items(&CloseAllItems, cx).unwrap() - }) - .detach_and_log_err(cx); + if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { + pane.close_all_items(&CloseAllItems, cx) + }) { + tasks.push(close_pane_items) + } } - Some(Task::ready(Ok(()))) + if tasks.is_empty() { + None + } else { + Some(cx.spawn(|_, _| async move { + for task in tasks { + task.await? + } + Ok(()) + })) + } } pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { From 4735b07088269e5b99e9299dbb9dd620e70f4497 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 27 Jul 2023 18:00:33 -0700 Subject: [PATCH 09/26] Fix warning --- crates/workspace/src/workspace.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 827b0b8427..a8ba017655 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -21,7 +21,6 @@ use drag_and_drop::DragAndDrop; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, - stream::FuturesUnordered, FutureExt, StreamExt, }; use gpui::{ From cf6e524c9a5f2fecbfbad9675e37cafd746c002c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 28 Jul 2023 12:56:44 +0300 Subject: [PATCH 10/26] Make project search includes and excludes more user-friendly Allow search results that start with the include/exclude path part --- crates/project/src/project_tests.rs | 55 ++++---- crates/project/src/search.rs | 117 +++++++++++++++--- crates/search/src/project_search.rs | 84 ++++--------- crates/semantic_index/src/db.rs | 17 +-- crates/semantic_index/src/semantic_index.rs | 14 +-- .../src/semantic_index_tests.rs | 7 +- 6 files changed, 160 insertions(+), 134 deletions(-) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 16e706a77e..259c10ca05 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,7 +1,6 @@ -use crate::{worktree::WorktreeHandle, Event, *}; +use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *}; use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; -use globset::Glob; use gpui::{executor::Deterministic, test::subscribe, AppContext}; use language::{ language_settings::{AllLanguageSettings, LanguageSettingsContent}, @@ -3641,7 +3640,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { search_query, false, true, - vec![Glob::new("*.odd").unwrap().compile_matcher()], + vec![PathMatcher::new("*.odd").unwrap()], Vec::new() ), cx @@ -3659,7 +3658,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { search_query, false, true, - vec![Glob::new("*.rs").unwrap().compile_matcher()], + vec![PathMatcher::new("*.rs").unwrap()], Vec::new() ), cx @@ -3681,8 +3680,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { false, true, vec![ - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher(), + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), ], Vec::new() ), @@ -3705,9 +3704,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { false, true, vec![ - Glob::new("*.rs").unwrap().compile_matcher(), - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher(), + PathMatcher::new("*.rs").unwrap(), + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), ], Vec::new() ), @@ -3752,7 +3751,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { false, true, Vec::new(), - vec![Glob::new("*.odd").unwrap().compile_matcher()], + vec![PathMatcher::new("*.odd").unwrap()], ), cx ) @@ -3775,7 +3774,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { false, true, Vec::new(), - vec![Glob::new("*.rs").unwrap().compile_matcher()], + vec![PathMatcher::new("*.rs").unwrap()], ), cx ) @@ -3797,8 +3796,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { true, Vec::new(), vec![ - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher(), + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), ], ), cx @@ -3821,9 +3820,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { true, Vec::new(), vec![ - Glob::new("*.rs").unwrap().compile_matcher(), - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher(), + PathMatcher::new("*.rs").unwrap(), + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap(), ], ), cx @@ -3860,8 +3859,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex search_query, false, true, - vec![Glob::new("*.odd").unwrap().compile_matcher()], - vec![Glob::new("*.odd").unwrap().compile_matcher()], + vec![PathMatcher::new("*.odd").unwrap()], + vec![PathMatcher::new("*.odd").unwrap()], ), cx ) @@ -3878,8 +3877,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex search_query, false, true, - vec![Glob::new("*.ts").unwrap().compile_matcher()], - vec![Glob::new("*.ts").unwrap().compile_matcher()], + vec![PathMatcher::new("*.ts").unwrap()], + vec![PathMatcher::new("*.ts").unwrap()], ), cx ) @@ -3897,12 +3896,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex false, true, vec![ - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher() + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap() ], vec![ - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher() + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap() ], ), cx @@ -3921,12 +3920,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex false, true, vec![ - Glob::new("*.ts").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher() + PathMatcher::new("*.ts").unwrap(), + PathMatcher::new("*.odd").unwrap() ], vec![ - Glob::new("*.rs").unwrap().compile_matcher(), - Glob::new("*.odd").unwrap().compile_matcher() + PathMatcher::new("*.rs").unwrap(), + PathMatcher::new("*.odd").unwrap() ], ), cx diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 4b4126fef2..71a0b70b81 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,5 +1,5 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; -use anyhow::Result; +use anyhow::{Context, Result}; use client::proto; use globset::{Glob, GlobMatcher}; use itertools::Itertools; @@ -9,7 +9,7 @@ use smol::future::yield_now; use std::{ io::{BufRead, BufReader, Read}, ops::Range, - path::Path, + path::{Path, PathBuf}, sync::Arc, }; @@ -20,8 +20,8 @@ pub enum SearchQuery { query: Arc, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, }, Regex { regex: Regex, @@ -29,18 +29,43 @@ pub enum SearchQuery { multiline: bool, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, }, } +#[derive(Clone, Debug)] +pub struct PathMatcher { + maybe_path: PathBuf, + glob: GlobMatcher, +} + +impl std::fmt::Display for PathMatcher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.maybe_path.to_string_lossy().fmt(f) + } +} + +impl PathMatcher { + pub fn new(maybe_glob: &str) -> Result { + Ok(PathMatcher { + glob: Glob::new(&maybe_glob)?.compile_matcher(), + maybe_path: PathBuf::from(maybe_glob), + }) + } + + pub fn is_match>(&self, other: P) -> bool { + other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other) + } +} + impl SearchQuery { pub fn text( query: impl ToString, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, ) -> Self { let query = query.to_string(); let search = AhoCorasickBuilder::new() @@ -61,8 +86,8 @@ impl SearchQuery { query: impl ToString, whole_word: bool, case_sensitive: bool, - files_to_include: Vec, - files_to_exclude: Vec, + files_to_include: Vec, + files_to_exclude: Vec, ) -> Result { let mut query = query.to_string(); let initial_query = Arc::from(query.as_str()); @@ -96,16 +121,16 @@ impl SearchQuery { message.query, message.whole_word, message.case_sensitive, - deserialize_globs(&message.files_to_include)?, - deserialize_globs(&message.files_to_exclude)?, + deserialize_path_matches(&message.files_to_include)?, + deserialize_path_matches(&message.files_to_exclude)?, ) } else { Ok(Self::text( message.query, message.whole_word, message.case_sensitive, - deserialize_globs(&message.files_to_include)?, - deserialize_globs(&message.files_to_exclude)?, + deserialize_path_matches(&message.files_to_include)?, + deserialize_path_matches(&message.files_to_exclude)?, )) } } @@ -120,12 +145,12 @@ impl SearchQuery { files_to_include: self .files_to_include() .iter() - .map(|g| g.glob().to_string()) + .map(|matcher| matcher.to_string()) .join(","), files_to_exclude: self .files_to_exclude() .iter() - .map(|g| g.glob().to_string()) + .map(|matcher| matcher.to_string()) .join(","), } } @@ -266,7 +291,7 @@ impl SearchQuery { matches!(self, Self::Regex { .. }) } - pub fn files_to_include(&self) -> &[GlobMatcher] { + pub fn files_to_include(&self) -> &[PathMatcher] { match self { Self::Text { files_to_include, .. @@ -277,7 +302,7 @@ impl SearchQuery { } } - pub fn files_to_exclude(&self) -> &[GlobMatcher] { + pub fn files_to_exclude(&self) -> &[PathMatcher] { match self { Self::Text { files_to_exclude, .. @@ -306,11 +331,63 @@ impl SearchQuery { } } -fn deserialize_globs(glob_set: &str) -> Result> { +fn deserialize_path_matches(glob_set: &str) -> anyhow::Result> { glob_set .split(',') .map(str::trim) .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher())) + .map(|glob_str| { + PathMatcher::new(glob_str) + .with_context(|| format!("deserializing path match glob {glob_str}")) + }) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_matcher_creation_for_valid_paths() { + for valid_path in [ + "file", + "Cargo.toml", + ".DS_Store", + "~/dir/another_dir/", + "./dir/file", + "dir/[a-z].txt", + "../dir/filé", + ] { + let path_matcher = PathMatcher::new(valid_path).unwrap_or_else(|e| { + panic!("Valid path {valid_path} should be accepted, but got: {e}") + }); + assert!( + path_matcher.is_match(valid_path), + "Path matcher for valid path {valid_path} should match itself" + ) + } + } + + #[test] + fn path_matcher_creation_for_globs() { + for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] { + match PathMatcher::new(invalid_glob) { + Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"), + Err(_expected) => {} + } + } + + for valid_glob in [ + "dir/?ile", + "dir/*.txt", + "dir/**/file", + "dir/[a-z].txt", + "{dir,file}", + ] { + match PathMatcher::new(valid_glob) { + Ok(_expected) => {} + Err(e) => panic!("Valid glob {valid_glob} should be accepted, but got: {e}"), + } + } + } +} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 52ee12c26d..d6d69e575f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2,14 +2,13 @@ use crate::{ SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; -use anyhow::Result; +use anyhow::Context; use collections::HashMap; use editor::{ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, }; use futures::StreamExt; -use globset::{Glob, GlobMatcher}; use gpui::{ actions, elements::*, @@ -19,7 +18,10 @@ use gpui::{ }; use menu::Confirm; use postage::stream::Stream; -use project::{search::SearchQuery, Entry, Project}; +use project::{ + search::{PathMatcher, SearchQuery}, + Entry, Project, +}; use semantic_index::SemanticIndex; use smallvec::SmallVec; use std::{ @@ -185,21 +187,15 @@ impl ProjectSearch { cx.notify(); } - fn semantic_search( - &mut self, - query: String, - include_files: Vec, - exclude_files: Vec, - cx: &mut ModelContext, - ) { + fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext) { let search = SemanticIndex::global(cx).map(|index| { index.update(cx, |semantic_index, cx| { semantic_index.search_project( self.project.clone(), - query.clone(), + query.as_str().to_owned(), 10, - include_files, - exclude_files, + query.files_to_include().to_vec(), + query.files_to_exclude().to_vec(), cx, ) }) @@ -590,8 +586,7 @@ impl ProjectSearchView { if !dir_entry.is_dir() { return; } - let filter_path = dir_entry.path.join("**"); - let Some(filter_str) = filter_path.to_str() else { return; }; + let Some(filter_str) = dir_entry.path.to_str() else { return; }; let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); @@ -662,16 +657,10 @@ impl ProjectSearchView { if semantic.outstanding_file_count > 0 { return; } - - let query = self.query_editor.read(cx).text(cx); - if let Some((included_files, exclude_files)) = - self.get_included_and_excluded_globsets(cx) - { - self.model.update(cx, |model, cx| { - model.semantic_search(query, included_files, exclude_files, cx) - }); + if let Some(query) = self.build_search_query(cx) { + self.model + .update(cx, |model, cx| model.semantic_search(query, cx)); } - return; } if let Some(query) = self.build_search_query(cx) { @@ -679,42 +668,10 @@ impl ProjectSearchView { } } - fn get_included_and_excluded_globsets( - &mut self, - cx: &mut ViewContext, - ) -> Option<(Vec, Vec)> { - let included_files = - match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) { - Ok(included_files) => { - self.panels_with_errors.remove(&InputPanel::Include); - included_files - } - Err(_e) => { - self.panels_with_errors.insert(InputPanel::Include); - cx.notify(); - return None; - } - }; - let excluded_files = - match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) { - Ok(excluded_files) => { - self.panels_with_errors.remove(&InputPanel::Exclude); - excluded_files - } - Err(_e) => { - self.panels_with_errors.insert(InputPanel::Exclude); - cx.notify(); - return None; - } - }; - - Some((included_files, excluded_files)) - } - fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { let text = self.query_editor.read(cx).text(cx); let included_files = - match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) { + match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) { Ok(included_files) => { self.panels_with_errors.remove(&InputPanel::Include); included_files @@ -726,7 +683,7 @@ impl ProjectSearchView { } }; let excluded_files = - match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) { + match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) { Ok(excluded_files) => { self.panels_with_errors.remove(&InputPanel::Exclude); excluded_files @@ -766,11 +723,14 @@ impl ProjectSearchView { } } - fn load_glob_set(text: &str) -> Result> { + fn parse_path_matches(text: &str) -> anyhow::Result> { text.split(',') .map(str::trim) - .filter(|glob_str| !glob_str.is_empty()) - .map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher())) + .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) + .map(|maybe_glob_str| { + PathMatcher::new(maybe_glob_str) + .with_context(|| format!("parsing {maybe_glob_str} as path matcher")) + }) .collect() } @@ -1769,7 +1729,7 @@ pub mod tests { search_view.included_files_editor.update(cx, |editor, cx| { assert_eq!( editor.display_text(cx), - a_dir_entry.path.join("**").display().to_string(), + a_dir_entry.path.to_str().unwrap(), "New search in directory should have included dir entry path" ); }); diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index d180f5e831..e8c929c995 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -1,7 +1,6 @@ use crate::{parsing::Document, SEMANTIC_INDEX_VERSION}; use anyhow::{anyhow, Context, Result}; -use globset::GlobMatcher; -use project::Fs; +use project::{search::PathMatcher, Fs}; use rpc::proto::Timestamp; use rusqlite::{ params, @@ -290,8 +289,8 @@ impl VectorDatabase { pub fn retrieve_included_file_ids( &self, worktree_ids: &[i64], - include_globs: Vec, - exclude_globs: Vec, + includes: &[PathMatcher], + excludes: &[PathMatcher], ) -> Result> { let mut file_query = self.db.prepare( " @@ -310,13 +309,9 @@ impl VectorDatabase { while let Some(row) = rows.next()? { let file_id = row.get(0)?; let relative_path = row.get_ref(1)?.as_str()?; - let included = include_globs.is_empty() - || include_globs - .iter() - .any(|glob| glob.is_match(relative_path)); - let excluded = exclude_globs - .iter() - .any(|glob| glob.is_match(relative_path)); + let included = + includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path)); + let excluded = excludes.iter().any(|glob| glob.is_match(relative_path)); if included && !excluded { file_ids.push(file_id); } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index bd114de216..f1450eb7b0 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -11,13 +11,12 @@ use anyhow::{anyhow, Result}; use db::VectorDatabase; use embedding::{EmbeddingProvider, OpenAIEmbeddings}; use futures::{channel::oneshot, Future}; -use globset::GlobMatcher; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; use postage::watch; -use project::{Fs, Project, WorktreeId}; +use project::{search::PathMatcher, Fs, Project, WorktreeId}; use smol::channel; use std::{ cmp::Ordering, @@ -682,8 +681,8 @@ impl SemanticIndex { project: ModelHandle, phrase: String, limit: usize, - include_globs: Vec, - exclude_globs: Vec, + includes: Vec, + excludes: Vec, cx: &mut ModelContext, ) -> Task>> { let project_state = if let Some(state) = self.projects.get(&project.downgrade()) { @@ -714,11 +713,8 @@ impl SemanticIndex { .next() .unwrap(); - let file_ids = database.retrieve_included_file_ids( - &worktree_db_ids, - include_globs, - exclude_globs, - )?; + let file_ids = + database.retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)?; let batch_n = cx.background().num_cpus(); let ids_len = file_ids.clone().len(); diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index acf5a9d72b..6acb25d98a 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -7,11 +7,10 @@ use crate::{ }; use anyhow::Result; use async_trait::async_trait; -use globset::Glob; use gpui::{Task, TestAppContext}; use language::{Language, LanguageConfig, LanguageRegistry, ToOffset}; use pretty_assertions::assert_eq; -use project::{project_settings::ProjectSettings, FakeFs, Fs, Project}; +use project::{project_settings::ProjectSettings, search::PathMatcher, FakeFs, Fs, Project}; use rand::{rngs::StdRng, Rng}; use serde_json::json; use settings::SettingsStore; @@ -121,8 +120,8 @@ async fn test_semantic_index(cx: &mut TestAppContext) { ); // Test Include Files Functonality - let include_files = vec![Glob::new("*.rs").unwrap().compile_matcher()]; - let exclude_files = vec![Glob::new("*.rs").unwrap().compile_matcher()]; + let include_files = vec![PathMatcher::new("*.rs").unwrap()]; + let exclude_files = vec![PathMatcher::new("*.rs").unwrap()]; let rust_only_search_results = store .update(cx, |store, cx| { store.search_project( From fac0e2dd56bfa44dbeea1baf814d2fad23e08c63 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 28 Jul 2023 12:10:04 -0600 Subject: [PATCH 11/26] Don't highlight project search matches either --- crates/editor/src/editor.rs | 2 +- crates/search/src/project_search.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e05837740d..5404610b1d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1537,7 +1537,7 @@ impl Editor { self.collapse_matches = collapse_matches; } - fn range_for_match(&self, range: &Range) -> Range { + pub fn range_for_match(&self, range: &Range) -> Range { if self.collapse_matches { return range.start..range.start; } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 9054d9e121..ade60a6d34 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -654,6 +654,7 @@ impl ProjectSearchView { let range_to_select = match_ranges[new_index].clone(); self.results_editor.update(cx, |editor, cx| { + let range_to_select = editor.range_for_match(&range_to_select); editor.unfold_ranges([range_to_select.clone()], false, true, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range_to_select]) @@ -695,8 +696,12 @@ impl ProjectSearchView { let is_new_search = self.search_id != prev_search_id; self.results_editor.update(cx, |editor, cx| { if is_new_search { + let range_to_select = match_ranges + .first() + .clone() + .map(|range| editor.range_for_match(range)); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges(match_ranges.first().cloned()) + s.select_ranges(range_to_select) }); } editor.highlight_background::( From b8690ec1d1df2e5f3d7a018070be0180dfa3257e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 15:12:37 -0400 Subject: [PATCH 12/26] Update release action to choose between preview and stable URL in Discord announcements This is what ChatGPT told me, so we'll see. --- .github/workflows/release_actions.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 6c3b3d57c0..f767324e4f 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,6 +6,16 @@ jobs: discord_release: runs-on: ubuntu-latest steps: + - name: Get appropriate URL + id: get-appropriate-url + run: | + if [ "${{ github.event.release.prerelease }}" == "true" ]; then + URL="https://zed.dev/releases/preview/latest" + else + URL="https://zed.dev/releases/stable/latest" + fi + echo "::set-output name=URL::$URL" + - name: Discord Webhook Action uses: tsickert/discord-webhook@v5.3.0 with: @@ -13,6 +23,6 @@ jobs: content: | 📣 Zed ${{ github.event.release.tag_name }} was just released! - Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. + Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it. ${{ github.event.release.body }} From 46101bf1103721088f16da845dae3a79bf30e856 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 28 Jul 2023 15:24:40 -0400 Subject: [PATCH 13/26] Reattempt Node installation if the installation itself errors This also makes us a bit more aggressive about reinstalling Node --- crates/copilot/src/copilot.rs | 4 +-- crates/node_runtime/src/node_runtime.rs | 44 ++++++------------------- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ce4938ed0d..ab2d861190 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -338,9 +338,9 @@ impl Copilot { let (server, fake_server) = LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); - let this = cx.add_model(|cx| Self { + let this = cx.add_model(|_| Self { http: http.clone(), - node_runtime: NodeRuntime::instance(http, cx.background().clone()), + node_runtime: NodeRuntime::instance(http), server: CopilotServer::Running(RunningCopilotServer { lsp: Arc::new(server), sign_in_status: SignInStatus::Authorized, diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 94858df880..d43c14ec7b 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -1,9 +1,6 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use futures::lock::Mutex; -use futures::{future::Shared, FutureExt}; -use gpui::{executor::Background, Task}; use serde::Deserialize; use smol::{fs, io::BufReader, process::Command}; use std::process::{Output, Stdio}; @@ -33,20 +30,12 @@ pub struct NpmInfoDistTags { pub struct NodeRuntime { http: Arc, - background: Arc, - installation_path: Mutex>>>>>, } impl NodeRuntime { - pub fn instance(http: Arc, background: Arc) -> Arc { + pub fn instance(http: Arc) -> Arc { RUNTIME_INSTANCE - .get_or_init(|| { - Arc::new(NodeRuntime { - http, - background, - installation_path: Mutex::new(None), - }) - }) + .get_or_init(|| Arc::new(NodeRuntime { http })) .clone() } @@ -61,7 +50,9 @@ impl NodeRuntime { subcommand: &str, args: &[&str], ) -> Result { - let attempt = |installation_path: PathBuf| async move { + let attempt = || async move { + let installation_path = self.install_if_needed().await?; + let mut env_path = installation_path.join("bin").into_os_string(); if let Some(existing_path) = std::env::var_os("PATH") { if !existing_path.is_empty() { @@ -92,10 +83,9 @@ impl NodeRuntime { command.output().await.map_err(|e| anyhow!("{e}")) }; - let installation_path = self.install_if_needed().await?; - let mut output = attempt(installation_path.clone()).await; + let mut output = attempt().await; if output.is_err() { - output = attempt(installation_path).await; + output = attempt().await; if output.is_err() { return Err(anyhow!( "failed to launch npm subcommand {subcommand} subcommand" @@ -167,23 +157,8 @@ impl NodeRuntime { } async fn install_if_needed(&self) -> Result { - let task = self - .installation_path - .lock() - .await - .get_or_insert_with(|| { - let http = self.http.clone(); - self.background - .spawn(async move { Self::install(http).await.map_err(Arc::new) }) - .shared() - }) - .clone(); + log::info!("Node runtime install_if_needed"); - task.await.map_err(|e| anyhow!("{}", e)) - } - - async fn install(http: Arc) -> Result { - log::info!("installing Node runtime"); let arch = match consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", @@ -214,7 +189,8 @@ impl NodeRuntime { let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz"); let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); - let mut response = http + let mut response = self + .http .get(&url, Default::default(), true) .await .context("error downloading Node binary tarball")?; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b9fefb89a7..e44ab3e33a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -136,7 +136,7 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned()); + let node_runtime = NodeRuntime::instance(http.clone()); languages::init(languages.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a29567ac38..4b0bf1cd4c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2364,7 +2364,7 @@ mod tests { languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let http = FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::instance(http, cx.background().to_owned()); + let node_runtime = NodeRuntime::instance(http); languages::init(languages.clone(), node_runtime); for name in languages.language_names() { languages.language_for_name(&name); From d3b89e16f26d24965267637000e642382cb4b675 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 28 Jul 2023 14:56:13 -0700 Subject: [PATCH 14/26] Make wrap guides respect scroll position --- crates/editor/src/element.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b9bf74ee85..cb46e74af0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -546,8 +546,18 @@ impl EditorElement { }); } + let scroll_left = + layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width; + for (wrap_position, active) in layout.wrap_guides.iter() { - let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.; + let x = + (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.) + - scroll_left; + + if x < text_bounds.origin_x() { + continue; + } + let color = if *active { self.style.active_wrap_guide } else { From fe43bacb6fa9a22d57d174df92a05a8cb6739e9d Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 28 Jul 2023 18:53:24 -0400 Subject: [PATCH 15/26] Put LiveKitBridge Swift build directory in `target` Helps it get caught in a cargo clean --- crates/live_kit_client/build.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/live_kit_client/build.rs b/crates/live_kit_client/build.rs index bcd3f76dca..3fa0e003e7 100644 --- a/crates/live_kit_client/build.rs +++ b/crates/live_kit_client/build.rs @@ -58,11 +58,14 @@ fn build_bridge(swift_target: &SwiftTarget) { "cargo:rerun-if-changed={}/Package.resolved", SWIFT_PACKAGE_NAME ); + let swift_package_root = swift_package_root(); + let swift_target_folder = swift_target_folder(); if !Command::new("swift") .arg("build") .args(["--configuration", &env::var("PROFILE").unwrap()]) .args(["--triple", &swift_target.target.triple]) + .args(["--build-path".into(), swift_target_folder]) .current_dir(&swift_package_root) .status() .unwrap() @@ -128,6 +131,12 @@ fn swift_package_root() -> PathBuf { env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME) } +fn swift_target_folder() -> PathBuf { + env::current_dir() + .unwrap() + .join(format!("../../target/{SWIFT_PACKAGE_NAME}")) +} + fn copy_dir(source: &Path, destination: &Path) { assert!( Command::new("rm") @@ -155,8 +164,7 @@ fn copy_dir(source: &Path, destination: &Path) { impl SwiftTarget { fn out_dir_path(&self) -> PathBuf { - swift_package_root() - .join(".build") + swift_target_folder() .join(&self.target.unversioned_triple) .join(env::var("PROFILE").unwrap()) } From 2c47efcce91328ee8d567f2871fae1e3cd107b63 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 22:36:15 -0400 Subject: [PATCH 16/26] Add a command to collapse all entires --- crates/project_panel/src/project_panel.rs | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e6e1cff598..b650d272fb 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -115,6 +115,7 @@ actions!( [ ExpandSelectedEntry, CollapseSelectedEntry, + CollapseAllEntries, NewDirectory, NewFile, Copy, @@ -140,6 +141,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { file_associations::init(assets, cx); cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry); + cx.add_action(ProjectPanel::collapse_all_entries); cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::new_file); @@ -514,6 +516,12 @@ impl ProjectPanel { } } + pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { + self.expanded_dir_ids.clear(); + self.update_visible_entries(None, cx); + cx.notify(); + } + fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { @@ -2678,6 +2686,73 @@ mod tests { ); } + #[gpui::test] + async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + "file_b.py": "# File contents", + "file_c.py": "# File contents", + }, + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + + let new_search_events_count = Arc::new(AtomicUsize::new(0)); + let _subscription = panel.update(cx, |_, cx| { + let subcription_count = Arc::clone(&new_search_events_count); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::NewSearchInDirectory { .. }) { + subcription_count.fetch_add(1, atomic::Ordering::SeqCst); + } + }) + }); + + panel.update(cx, |panel, cx| { + panel.collapse_all_entries(&CollapseAllEntries, cx) + }); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v project_root", " > dir_1", " > dir_2",] + ); + + // Open dir_1 and make sure nested_dir was collapsed during + toggle_expand_dir(&panel, "project_root/dir_1", cx); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " v dir_1 <== selected", + " > nested_dir", + " file_1.py", + " file_2.py", + " file_3.py", + " > dir_2", + ] + ); + } + fn toggle_expand_dir( panel: &ViewHandle, path: impl AsRef, @@ -2878,3 +2953,4 @@ mod tests { }); } } +// TODO - a workspace command? From b0e81c58dc9d85a93153563b1c71753c425c4247 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 23:06:40 -0400 Subject: [PATCH 17/26] Remove unused code in test --- crates/project_panel/src/project_panel.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b650d272fb..b2b8b2e4bd 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2717,16 +2717,6 @@ mod tests { let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - let new_search_events_count = Arc::new(AtomicUsize::new(0)); - let _subscription = panel.update(cx, |_, cx| { - let subcription_count = Arc::clone(&new_search_events_count); - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!(event, Event::NewSearchInDirectory { .. }) { - subcription_count.fetch_add(1, atomic::Ordering::SeqCst); - } - }) - }); - panel.update(cx, |panel, cx| { panel.collapse_all_entries(&CollapseAllEntries, cx) }); From 0bd6e7bac3179b49f69125b573c78dce438f286a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 23:13:36 -0400 Subject: [PATCH 18/26] Fix comment --- crates/project_panel/src/project_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b2b8b2e4bd..0be52646e6 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2726,7 +2726,7 @@ mod tests { &["v project_root", " > dir_1", " > dir_2",] ); - // Open dir_1 and make sure nested_dir was collapsed during + // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries toggle_expand_dir(&panel, "project_root/dir_1", cx); cx.foreground().run_until_parked(); assert_eq!( From d58f031696ce039c0068344803551558c391d85c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 28 Jul 2023 22:27:36 -0700 Subject: [PATCH 19/26] disable wrap guides in the assitant panel --- crates/ai/src/assistant.rs | 1 + crates/editor/src/editor.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 8a4c04d338..957c5e1c06 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1637,6 +1637,7 @@ impl ConversationEditor { let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); + editor.set_show_wrap_guides(false, cx); editor }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b4145edb64..5270d6f951 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -543,6 +543,7 @@ pub struct Editor { show_local_selections: bool, mode: EditorMode, show_gutter: bool, + show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, #[allow(clippy::type_complexity)] @@ -1375,6 +1376,7 @@ impl Editor { show_local_selections: true, mode, show_gutter: mode == EditorMode::Full, + show_wrap_guides: None, placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), @@ -7187,6 +7189,10 @@ impl Editor { pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { let mut wrap_guides = smallvec::smallvec![]; + if self.show_wrap_guides == Some(false) { + return wrap_guides; + } + let settings = self.buffer.read(cx).settings_at(0, cx); if settings.show_wrap_guides { if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { @@ -7244,6 +7250,11 @@ impl Editor { cx.notify(); } + pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext) { + self.show_wrap_guides = Some(show_gutter); + cx.notify(); + } + pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { From 8926266952948063a666a248f2e4b7bf8edcfde6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 29 Jul 2023 23:53:16 -0700 Subject: [PATCH 20/26] Halve opacity on wrap guides --- styles/src/style_tree/editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 832e776264..deab45d4b2 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -182,8 +182,8 @@ export default function editor(): any { line_number: with_opacity(foreground(layer), 0.35), line_number_active: foreground(layer), rename_fade: 0.6, - wrap_guide: with_opacity(foreground(layer), 0.1), - active_wrap_guide: with_opacity(foreground(layer), 0.2), + wrap_guide: with_opacity(foreground(layer), 0.05), + active_wrap_guide: with_opacity(foreground(layer), 0.1), unnecessary_code_fade: 0.5, selection: theme.players[0], whitespace: theme.ramps.neutral(0.5).hex(), From e07a81b22590257bb7bc8c4362d6afe3a01d401c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 31 Jul 2023 12:49:55 -0400 Subject: [PATCH 21/26] Add additional storage filetypes --- assets/icons/file_icons/file_types.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 0ccf9c2bb7..67791aaecb 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -1,5 +1,16 @@ { "suffixes": { + "db": "storage", + "sqlite": "storage", + "myi": "storage", + "myd": "storage", + "mdf": "storage", + "csv": "storage", + "bak": "backup", + "dat": "storage", + "dll": "storage", + "sav": "storage", + "tsv": "storage", "aac": "audio", "bash": "terminal", "bmp": "image", From c4709418d142888b5d94ae61c5aad98f28c6b21a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 31 Jul 2023 12:50:30 -0400 Subject: [PATCH 22/26] Format --- assets/icons/file_icons/file_types.json | 341 ++++++++++++------------ 1 file changed, 175 insertions(+), 166 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 67791aaecb..9ea75d0730 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -1,170 +1,179 @@ { - "suffixes": { - "db": "storage", - "sqlite": "storage", - "myi": "storage", - "myd": "storage", - "mdf": "storage", - "csv": "storage", - "bak": "backup", - "dat": "storage", - "dll": "storage", - "sav": "storage", - "tsv": "storage", - "aac": "audio", - "bash": "terminal", - "bmp": "image", - "c": "code", - "conf": "settings", - "cpp": "code", - "cc": "code", - "css": "code", - "doc": "document", - "docx": "document", - "eslintrc": "eslint", - "eslintrc.js": "eslint", - "eslintrc.json": "eslint", - "flac": "audio", - "fish": "terminal", - "gitattributes": "vcs", - "gitignore": "vcs", - "gitmodules": "vcs", - "gif": "image", - "go": "code", - "h": "code", - "handlebars": "code", - "hbs": "template", - "htm": "template", - "html": "template", - "svelte": "template", - "hpp": "code", - "ico": "image", - "ini": "settings", - "java": "code", - "jpeg": "image", - "jpg": "image", - "js": "code", - "json": "storage", - "lock": "lock", - "log": "log", - "md": "document", - "mdx": "document", - "mp3": "audio", - "mp4": "video", - "ods": "document", - "odp": "document", - "odt": "document", - "ogg": "video", - "pdf": "document", - "php": "code", - "png": "image", - "ppt": "document", - "pptx": "document", - "prettierrc": "prettier", - "prettierignore": "prettier", - "ps1": "terminal", - "psd": "image", - "py": "code", - "rb": "code", - "rkt": "code", - "rs": "rust", - "rtf": "document", - "scm": "code", - "sh": "terminal", - "bashrc": "terminal", - "bash_profile": "terminal", - "bash_aliases": "terminal", - "bash_logout": "terminal", - "profile": "terminal", - "zshrc": "terminal", - "zshenv": "terminal", - "zsh_profile": "terminal", - "zsh_aliases": "terminal", - "zsh_histfile": "terminal", - "zlogin": "terminal", - "sql": "code", - "svg": "image", - "swift": "code", - "tiff": "image", - "toml": "toml", - "ts": "typescript", - "tsx": "code", - "txt": "document", - "wav": "audio", - "webm": "video", - "xls": "document", - "xlsx": "document", - "xml": "template", - "yaml": "settings", - "yml": "settings", - "zsh": "terminal" - }, - "types": { - "audio": { - "icon": "icons/file_icons/audio.svg" + "suffixes": { + "aac": "audio", + "accdb": "storage", + "bak": "backup", + "bash": "terminal", + "bash_aliases": "terminal", + "bash_logout": "terminal", + "bash_profile": "terminal", + "bashrc": "terminal", + "bmp": "image", + "c": "code", + "cc": "code", + "conf": "settings", + "cpp": "code", + "css": "code", + "csv": "storage", + "dat": "storage", + "db": "storage", + "dbf": "storage", + "dll": "storage", + "doc": "document", + "docx": "document", + "eslintrc": "eslint", + "eslintrc.js": "eslint", + "eslintrc.json": "eslint", + "fmp": "storage", + "fp7": "storage", + "flac": "audio", + "fish": "terminal", + "frm": "storage", + "gdb": "storage", + "gitattributes": "vcs", + "gitignore": "vcs", + "gitmodules": "vcs", + "gif": "image", + "go": "code", + "h": "code", + "handlebars": "code", + "hbs": "template", + "htm": "template", + "html": "template", + "ib": "storage", + "ico": "image", + "ini": "settings", + "java": "code", + "jpeg": "image", + "jpg": "image", + "js": "code", + "json": "storage", + "ldf": "storage", + "lock": "lock", + "log": "log", + "mdb": "storage", + "md": "document", + "mdf": "storage", + "mdx": "document", + "mp3": "audio", + "mp4": "video", + "myd": "storage", + "myi": "storage", + "ods": "document", + "odp": "document", + "odt": "document", + "ogg": "video", + "pdb": "storage", + "pdf": "document", + "php": "code", + "png": "image", + "ppt": "document", + "pptx": "document", + "prettierignore": "prettier", + "prettierrc": "prettier", + "profile": "terminal", + "ps1": "terminal", + "psd": "image", + "py": "code", + "rb": "code", + "rkt": "code", + "rs": "rust", + "rtf": "document", + "sav": "storage", + "scm": "code", + "sh": "terminal", + "sqlite": "storage", + "sdf": "storage", + "svelte": "template", + "svg": "image", + "swift": "code", + "ts": "typescript", + "tsx": "code", + "tiff": "image", + "toml": "toml", + "tsv": "storage", + "txt": "document", + "wav": "audio", + "webm": "video", + "xls": "document", + "xlsx": "document", + "xml": "template", + "yaml": "settings", + "yml": "settings", + "zlogin": "terminal", + "zsh": "terminal", + "zsh_aliases": "terminal", + "zshenv": "terminal", + "zsh_histfile": "terminal", + "zsh_profile": "terminal", + "zshrc": "terminal" }, - "code": { - "icon": "icons/file_icons/code.svg" - }, - "collapsed_chevron": { - "icon": "icons/file_icons/chevron_right.svg" - }, - "collapsed_folder": { - "icon": "icons/file_icons/folder.svg" - }, - "default": { - "icon": "icons/file_icons/file.svg" - }, - "document": { - "icon": "icons/file_icons/book.svg" - }, - "eslint": { - "icon": "icons/file_icons/eslint.svg" - }, - "expanded_chevron": { - "icon": "icons/file_icons/chevron_down.svg" - }, - "expanded_folder": { - "icon": "icons/file_icons/folder_open.svg" - }, - "image": { - "icon": "icons/file_icons/image.svg" - }, - "lock": { - "icon": "icons/file_icons/lock.svg" - }, - "log": { - "icon": "icons/file_icons/info.svg" - }, - "prettier": { - "icon": "icons/file_icons/prettier.svg" - }, - "rust": { - "icon": "icons/file_icons/rust.svg" - }, - "settings": { - "icon": "icons/file_icons/settings.svg" - }, - "storage": { - "icon": "icons/file_icons/database.svg" - }, - "template": { - "icon": "icons/file_icons/html.svg" - }, - "terminal": { - "icon": "icons/file_icons/terminal.svg" - }, - "toml": { - "icon": "icons/file_icons/toml.svg" - }, - "typescript": { - "icon": "icons/file_icons/typescript.svg" - }, - "vcs": { - "icon": "icons/file_icons/git.svg" - }, - "video": { - "icon": "icons/file_icons/video.svg" + "types": { + "audio": { + "icon": "icons/file_icons/audio.svg" + }, + "code": { + "icon": "icons/file_icons/code.svg" + }, + "collapsed_chevron": { + "icon": "icons/file_icons/chevron_right.svg" + }, + "collapsed_folder": { + "icon": "icons/file_icons/folder.svg" + }, + "default": { + "icon": "icons/file_icons/file.svg" + }, + "document": { + "icon": "icons/file_icons/book.svg" + }, + "eslint": { + "icon": "icons/file_icons/eslint.svg" + }, + "expanded_chevron": { + "icon": "icons/file_icons/chevron_down.svg" + }, + "expanded_folder": { + "icon": "icons/file_icons/folder_open.svg" + }, + "image": { + "icon": "icons/file_icons/image.svg" + }, + "lock": { + "icon": "icons/file_icons/lock.svg" + }, + "log": { + "icon": "icons/file_icons/info.svg" + }, + "prettier": { + "icon": "icons/file_icons/prettier.svg" + }, + "rust": { + "icon": "icons/file_icons/rust.svg" + }, + "settings": { + "icon": "icons/file_icons/settings.svg" + }, + "storage": { + "icon": "icons/file_icons/database.svg" + }, + "template": { + "icon": "icons/file_icons/html.svg" + }, + "terminal": { + "icon": "icons/file_icons/terminal.svg" + }, + "toml": { + "icon": "icons/file_icons/toml.svg" + }, + "typescript": { + "icon": "icons/file_icons/typescript.svg" + }, + "vcs": { + "icon": "icons/file_icons/git.svg" + }, + "video": { + "icon": "icons/file_icons/video.svg" + } } - } } From bb288eb941fba31ba98a89f9b2b32940ed493ac5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 31 Jul 2023 13:08:40 -0400 Subject: [PATCH 23/26] Ensure json uses a tab size of 4 --- .zed/settings.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000000..d4b3375b0d --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,5 @@ +{ + "JSON": { + "tab_size": 4 + } +} From 88474a60485257814313b4a6ff799642feeafe6a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 31 Jul 2023 10:54:29 -0700 Subject: [PATCH 24/26] Clip wrap guides from under the scrollbar --- crates/editor/src/element.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cb46e74af0..750beaea13 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -554,7 +554,9 @@ impl EditorElement { (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.) - scroll_left; - if x < text_bounds.origin_x() { + if x < text_bounds.origin_x() + || (layout.show_scrollbars && x > self.scrollbar_left(&bounds)) + { continue; } @@ -1046,6 +1048,10 @@ impl EditorElement { scene.pop_layer(); } + fn scrollbar_left(&self, bounds: &RectF) -> f32 { + bounds.max_x() - self.style.theme.scrollbar.width + } + fn paint_scrollbar( &mut self, scene: &mut SceneBuilder, @@ -1064,7 +1070,7 @@ impl EditorElement { let top = bounds.min_y(); let bottom = bounds.max_y(); let right = bounds.max_x(); - let left = right - style.width; + let left = self.scrollbar_left(&bounds); let row_range = &layout.scrollbar_row_range; let max_row = layout.max_row as f32 + (row_range.end - row_range.start); From 646dabe1133b32c852bf9b41b22234723f442602 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 31 Jul 2023 16:40:03 +0300 Subject: [PATCH 25/26] Add buffer search history --- assets/keymaps/default.json | 7 + crates/gpui/src/app.rs | 6 + crates/search/src/buffer_search.rs | 230 ++++++++++++++++++++++++++++- crates/search/src/search.rs | 187 +++++++++++++++++++++++ crates/vim/src/normal/search.rs | 2 +- crates/vim/src/test.rs | 4 +- 6 files changed, 428 insertions(+), 8 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index adc55f8c91..57fde112bf 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -227,6 +227,13 @@ "alt-enter": "search::SelectAllMatches" } }, + { + "context": "BufferSearchBar > Editor", + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, { "context": "ProjectSearchBar", "bindings": { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7af363d596..da601ba351 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1128,6 +1128,12 @@ impl AppContext { self.keystroke_matcher.clear_bindings(); } + pub fn binding_for_action(&self, action: &dyn Action) -> Option<&Binding> { + self.keystroke_matcher + .bindings_for_action(action.id()) + .find(|binding| binding.action().eq(action)) + } + pub fn default_global(&mut self) -> &T { let type_id = TypeId::of::(); self.update(|this| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5429305098..45842aa561 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ - SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, - ToggleRegex, ToggleWholeWord, + NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -46,6 +46,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::select_prev_match_on_pane); cx.add_action(BufferSearchBar::select_all_matches_on_pane); cx.add_action(BufferSearchBar::handle_editor_cancel); + cx.add_action(BufferSearchBar::next_history_query); + cx.add_action(BufferSearchBar::previous_history_query); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); add_toggle_option_action::(SearchOptions::REGEX, cx); @@ -65,7 +67,7 @@ fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContex } pub struct BufferSearchBar { - pub query_editor: ViewHandle, + query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, @@ -76,6 +78,7 @@ pub struct BufferSearchBar { default_options: SearchOptions, query_contains_error: bool, dismissed: bool, + search_history: SearchHistory, } impl Entity for BufferSearchBar { @@ -106,6 +109,48 @@ impl View for BufferSearchBar { .map(|active_searchable_item| active_searchable_item.supported_options()) .unwrap_or_default(); + let previous_query_keystrokes = + cx.binding_for_action(&PreviousHistoryQuery {}) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + Flex::row() .with_child( Flex::row() @@ -258,6 +303,7 @@ impl BufferSearchBar { pending_search: None, query_contains_error: false, dismissed: true, + search_history: SearchHistory::default(), } } @@ -341,7 +387,7 @@ impl BufferSearchBar { cx: &mut ViewContext, ) -> oneshot::Receiver<()> { let options = options.unwrap_or(self.default_options); - if query != self.query_editor.read(cx).text(cx) || self.search_options != options { + if query != self.query(cx) || self.search_options != options { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { let len = query_buffer.len(cx); @@ -674,7 +720,7 @@ impl BufferSearchBar { fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { let (done_tx, done_rx) = oneshot::channel(); - let query = self.query_editor.read(cx).text(cx); + let query = self.query(cx); self.pending_search.take(); if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { @@ -707,6 +753,7 @@ impl BufferSearchBar { ) }; + let query_text = query.as_str().to_string(); let matches = active_searchable_item.find_matches(query, cx); let active_searchable_item = active_searchable_item.downgrade(); @@ -720,6 +767,7 @@ impl BufferSearchBar { .insert(active_searchable_item.downgrade(), matches); this.update_match_index(cx); + this.search_history.add(query_text); if !this.dismissed { let matches = this .searchable_items_with_matches @@ -753,6 +801,28 @@ impl BufferSearchBar { cx.notify(); } } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(new_query) = self.search_history.next().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } else { + self.search_history.reset_selection(); + let _ = self.search("", Some(self.search_options), cx); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if self.query(cx).is_empty() { + if let Some(new_query) = self.search_history.current().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + return; + } + } + + if let Some(new_query) = self.search_history.previous().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } + } } #[cfg(test)] @@ -1333,4 +1403,154 @@ mod tests { ); }); } + + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + crate::project_search::tests::init_test(cx); + + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(); + let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let (window_id, _root_view) = cx.add_window(|_| EmptyView); + + let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.add_view(window_id, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history. + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| search_bar.search("b", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + // Ensure that the latest search is active. + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_bar + .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) + .await + .unwrap(); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 58cda0c7dc..18e3915527 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -3,6 +3,7 @@ pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext}; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; +use smallvec::SmallVec; pub mod buffer_search; pub mod project_search; @@ -21,6 +22,8 @@ actions!( SelectNextMatch, SelectPrevMatch, SelectAllMatches, + NextHistoryQuery, + PreviousHistoryQuery, ] ); @@ -65,3 +68,187 @@ impl SearchOptions { options } } + +const SEARCH_HISTORY_LIMIT: usize = 20; + +#[derive(Default, Debug)] +pub struct SearchHistory { + history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, + selected: Option, +} + +impl SearchHistory { + pub fn add(&mut self, search_string: String) { + if let Some(i) = self.selected { + if search_string == self.history[i] { + return; + } + } + + if let Some(previously_searched) = self.history.last_mut() { + if search_string.find(previously_searched.as_str()).is_some() { + *previously_searched = search_string; + self.selected = Some(self.history.len() - 1); + return; + } + } + + self.history.push(search_string); + if self.history.len() > SEARCH_HISTORY_LIMIT { + self.history.remove(0); + } + self.selected = Some(self.history.len() - 1); + } + + pub fn next(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let selected = self.selected?; + if selected == history_size - 1 { + return None; + } + let next_index = selected + 1; + self.selected = Some(next_index); + Some(&self.history[next_index]) + } + + pub fn current(&self) -> Option<&str> { + Some(&self.history[self.selected?]) + } + + pub fn previous(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let prev_index = match self.selected { + Some(selected_index) => { + if selected_index == 0 { + return None; + } else { + selected_index - 1 + } + } + None => history_size - 1, + }; + + self.selected = Some(prev_index); + Some(&self.history[prev_index]) + } + + pub fn reset_selection(&mut self) { + self.selected = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.current(), + None, + "No current selection should be set fo the default search history" + ); + + search_history.add("rust".to_string()); + assert_eq!( + search_history.current(), + Some("rust"), + "Newly added item should be selected" + ); + + // check if duplicates are not added + search_history.add("rust".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should not add a duplicate" + ); + assert_eq!(search_history.current(), Some("rust")); + + // check if new string containing the previous string replaces it + search_history.add("rustlang".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should replace previous item if it's a substring" + ); + assert_eq!(search_history.current(), Some("rustlang")); + + // push enough items to test SEARCH_HISTORY_LIMIT + for i in 0..SEARCH_HISTORY_LIMIT * 2 { + search_history.add(format!("item{i}")); + } + assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); + } + + #[test] + fn test_next_and_previous() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.next(), + None, + "Default search history should not have a next item" + ); + + search_history.add("Rust".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("JavaScript".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("TypeScript".to_string()); + assert_eq!(search_history.next(), None); + + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.previous(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.previous(), Some("Rust")); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.previous(), None); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.next(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.next(), Some("TypeScript")); + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.next(), None); + assert_eq!(search_history.current(), Some("TypeScript")); + } + + #[test] + fn test_reset_selection() { + let mut search_history = SearchHistory::default(); + search_history.add("Rust".to_string()); + search_history.add("JavaScript".to_string()); + search_history.add("TypeScript".to_string()); + + assert_eq!(search_history.current(), Some("TypeScript")); + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + assert_eq!( + search_history.previous(), + Some("TypeScript"), + "Should start from the end after reset on previous item query" + ); + + search_history.previous(); + assert_eq!(search_history.current(), Some("JavaScript")); + search_history.previous(); + assert_eq!(search_history.current(), Some("Rust")); + + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + } +} diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index d584c575d2..614866d9c9 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -222,7 +222,7 @@ mod test { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + assert_eq!(bar.query(cx), "cc"); }); deterministic.run_until_parked(); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 98d8cb8749..474f2128fc 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -99,7 +99,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), ""); + assert_eq!(bar.query(cx), ""); }) } @@ -175,7 +175,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + assert_eq!(bar.query(cx), "cc"); }); // wait for the query editor change event to fire. From 634baeedb4a0a59b303182519ef1279b72319c8f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 1 Aug 2023 01:23:51 +0300 Subject: [PATCH 26/26] Add project search history --- assets/keymaps/default.json | 7 + crates/search/src/project_search.rs | 283 +++++++++++++++++++++++++++- crates/search/src/search.rs | 2 +- 3 files changed, 289 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 57fde112bf..38ec8ffb40 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -240,6 +240,13 @@ "escape": "project_search::ToggleFocus" } }, + { + "context": "ProjectSearchBar > Editor", + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, { "context": "ProjectSearchView", "bindings": { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 87307264f5..1b4e32f4b8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,6 @@ use crate::{ - SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, - ToggleWholeWord, + NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, + SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use anyhow::Context; use collections::HashMap; @@ -56,6 +56,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_prev_match); + cx.add_action(ProjectSearchBar::next_history_query); + cx.add_action(ProjectSearchBar::previous_history_query); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); @@ -83,6 +85,7 @@ struct ProjectSearch { match_ranges: Vec>, active_query: Option, search_id: usize, + search_history: SearchHistory, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -131,6 +134,7 @@ impl ProjectSearch { match_ranges: Default::default(), active_query: None, search_id: 0, + search_history: SearchHistory::default(), } } @@ -144,6 +148,7 @@ impl ProjectSearch { match_ranges: self.match_ranges.clone(), active_query: self.active_query.clone(), search_id: self.search_id, + search_history: self.search_history.clone(), }) } @@ -152,6 +157,7 @@ impl ProjectSearch { .project .update(cx, |project, cx| project.search(query.clone(), cx)); self.search_id += 1; + self.search_history.add(query.as_str().to_string()); self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { @@ -202,6 +208,7 @@ impl ProjectSearch { }); self.search_id += 1; self.match_ranges.clear(); + self.search_history.add(query.as_str().to_string()); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; @@ -278,6 +285,49 @@ impl View for ProjectSearchView { Cow::Borrowed("No results") }; + let previous_query_keystrokes = + cx.binding_for_action(&PreviousHistoryQuery {}) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = + cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + MouseEventHandler::::new(0, cx, |_, _| { Label::new(text, theme.search.results_status.clone()) .aligned() @@ -1152,6 +1202,47 @@ impl ProjectSearchBar { false } } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + let new_query = search_view.model.update(cx, |model, _| { + if let Some(new_query) = model.search_history.next().map(str::to_string) { + new_query + } else { + model.search_history.reset_selection(); + String::new() + } + }); + search_view.set_query(&new_query, cx); + }); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if search_view.query_editor.read(cx).text(cx).is_empty() { + if let Some(new_query) = search_view + .model + .read(cx) + .search_history + .current() + .map(str::to_string) + { + search_view.set_query(&new_query, cx); + return; + } + } + + if let Some(new_query) = search_view.model.update(cx, |model, _| { + model.search_history.previous().map(str::to_string) + }) { + search_view.set_query(&new_query, cx); + } + }); + } + } } impl Entity for ProjectSearchBar { @@ -1333,6 +1424,7 @@ pub mod tests { use editor::DisplayPoint; use gpui::{color::Color, executor::Deterministic, TestAppContext}; use project::FakeFs; + use semantic_index::semantic_index_settings::SemanticIndexSettings; use serde_json::json; use settings::SettingsStore; use std::sync::Arc; @@ -1758,6 +1850,192 @@ pub mod tests { }); } + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + + let search_view = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Search view expected to appear after new search event trigger") + }); + + let search_bar = cx.add_view(window_id, |cx| { + let mut search_bar = ProjectSearchBar::new(); + search_bar.set_active_pane_item(Some(&search_view), cx); + // search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history + another unsubmitted one. + search_view.update(cx, |search_view, cx| { + search_view.search_options = SearchOptions::CASE_SENSITIVE; + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("JUST_TEXT_INPUT", cx) + }); + }); + cx.foreground().run_until_parked(); + + // Ensure that the latest input with search settings is active. + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view.query_editor.read(cx).text(cx), + "JUST_TEXT_INPUT" + ); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest submitted one. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + } + pub fn init_test(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); let fonts = cx.font_cache(); @@ -1767,6 +2045,7 @@ pub mod tests { cx.update(|cx| { cx.set_global(SettingsStore::test(cx)); cx.set_global(ActiveSearches::default()); + settings::register::(cx); theme::init((), cx); cx.update_global::(|store, _| { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 18e3915527..f1711afec2 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -71,7 +71,7 @@ impl SearchOptions { const SEARCH_HISTORY_LIMIT: usize = 20; -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct SearchHistory { history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, selected: Option,