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 883b0c1872a47ea9242716cfbd39298ccdfacb94..5c841d19b2ca1b5bf54e5ca93c5f7d10341876b5 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 0ebd01e1f7a706e1e2baf3b15ff1f35a51a3fb7f..6694cc06a32ff416323cea047539759a8ee62e39 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 f0c8917aa28d300e1aea521f7c936c3ce918a984..175b3d931532bce1fbe9c4f41bbcfab557e88fdf 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 fa824115cb82b02c2428f2f1bd9889a0b93a5757..ca13a4eb2fd2b0af34a115274ea4347c57883259 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 c5bf313701e76f1ea7084f7299f7596b7b0306df..88185acfc29f03f8e3cae0e51488c3e2234a684b 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 365e8a30232e5dc9193b1077b16186f3c44e9d19..9ba0f2755240e0e22357ffc2b8ea2fd6dda02dd3 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 0000000000000000000000000000000000000000..778f0a6f050a72eb231d374eeff09bff3591cbdd --- /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 0000000000000000000000000000000000000000..d63a46411ae830a0b86d55a18e405a8086af6548 --- /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 84e3e5868a42f8b3f360af2d7b0e17a8f17fc015..639a7594f113c61bc8c232325ac8d769b2ac89e5 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 e05837740d4c50fbe7e02e51272bd6340884ec8a..7af3f5460def3d55a9d661e3b29724e0d392e637 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 df16ea7db97fbbd2acfb005be15ff45af7ef4ad1..b9fefb89a73555a16a03d9d7e34fa5369d3abde4 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 7c6a213d0d4e36a427fc4352b2e417258f12de8f..9de3e6f4a69fb14351504b66a59f293069978381 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 fc46286de0c78f286e9d110aed41fe41bf3902d3..19d43a1b95bc3f7ebf4c482dad8d1a4607faa8c8 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 9de3e6f4a69fb14351504b66a59f293069978381..04ee8c2212f0296c8f1af116823296bb51b49e59 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 19d43a1b95bc3f7ebf4c482dad8d1a4607faa8c8..157db0635f7bcc490ba207c6c0932899cfd6abf0 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 5c841d19b2ca1b5bf54e5ca93c5f7d10341876b5..090385458efd1bb43cce6d83615fbbaf8dbc6166 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 2972c307f2624a6bb23bef0f1919aab5a43eb66d..ee658c9cc92b3a4fa24a56f986e895547a190ad1 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 6694cc06a32ff416323cea047539759a8ee62e39..827b0b8427c0bf4e0f25237d664489f747199592 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 827b0b8427c0bf4e0f25237d664489f747199592..a8ba017655a8a6546ed641e5714f2db1f5663cc5 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 16e706a77eeb9b4a92a368d7bb653639d24e9814..259c10ca057c8bb29ad5b2d805107eb982239441 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 4b4126fef2e1f8f4cb403c809ca62a136cff5afb..71a0b70b81b7a6f08a2584064a55a07743096b2c 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 52ee12c26de17274d5cd8c5d39a69843cbcb7869..d6d69e575fdfa01a77c92b62efacfa45c31de23d 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 d180f5e8314291593b1b343cd96527b1aea1d838..e8c929c99500c6a1da445b36d49ec007b4a6d997 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 bd114de216a0a30b3271b56c2b627439a7e70a0e..f1450eb7b0200d2f2dbedca9f46acb7db2a615a3 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 acf5a9d72b43a1123102e105da2b4b039fba87c6..6acb25d98a911a061a9b4e8e2f6a387fc73204cc 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 e05837740d4c50fbe7e02e51272bd6340884ec8a..5404610b1da4213d217c6e5f8f77d4261941ef0e 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 9054d9e1213dcc0544af0faeb35eb8bd74a3f117..ade60a6d348d8522517ac601588a04b5ecac5b6b 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 6c3b3d57c0b49453c66597b133b2893e898f398e..f767324e4f6e2bc2edc825af512bbb5561ac6b62 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 ce4938ed0d9e31c8130625b526095aeb0283e12c..ab2d861190ff98fb7b4da954a7b92bfb43d75a9d 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 94858df880e28eeaf44a4899e75395aea47cca6a..d43c14ec7b6e8bcfdc6452df67120efc086f103a 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(); - - task.await.map_err(|e| anyhow!("{}", e)) - } + log::info!("Node runtime install_if_needed"); - 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 b9fefb89a73555a16a03d9d7e34fa5369d3abde4..e44ab3e33acbb213f69b230a80075aea07890c85 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 a29567ac385633af04b7e7c3f4b2c0de595d892c..4b0bf1cd4c6aec42aa2d636b474828ef452cbe03 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 b9bf74ee85f8bbc3c338a75b4cd1bf39bc0a53cb..cb46e74af0b1e8d1480dedba1ae51db417fbf5b6 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 bcd3f76dca99d5cf22ee01ebe1e1d51cc13e103f..3fa0e003e744b656139958acea89a7b107c68579 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 e6e1cff5981cf7450c154cd7173ab195c5190751..b650d272fbdb7cbb05b477e5033e1f127be8d07b 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 b650d272fbdb7cbb05b477e5033e1f127be8d07b..b2b8b2e4bd721fa225db6b8d6f902cb0e0b560d6 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 b2b8b2e4bd721fa225db6b8d6f902cb0e0b560d6..0be52646e631ef1446f168dc3ecf1ce7b9ef0075 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 8a4c04d3387784e0e2b5e4e7d745c690f72c02aa..957c5e1c063aad3de12657448267d0f813f38887 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 b4145edb6483dd0d8c25d0f645cdcef1927ed374..5270d6f951870fa952e9a0a33f2f229a1d71ff98 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 832e77626491ff94f183d5ce1fe195f5a55a3780..deab45d4b21df8b7d130479c277ed317ba78d8d4 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 0ccf9c2bb7c7b4eb12f6ca83646ab9a38c661490..67791aaecb42f20a6a57035266a9bedaa4d7b415 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 | 343 ++++++++++++------------ 1 file changed, 176 insertions(+), 167 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 67791aaecb42f20a6a57035266a9bedaa4d7b415..9ea75d07309d853f37fed0192ca83bfd7d1099d8 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" - }, - "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" + "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" + }, + "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 0000000000000000000000000000000000000000..d4b3375b0d21f95128c6bffb2c7a92f8bf97916c --- /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 cb46e74af0b1e8d1480dedba1ae51db417fbf5b6..750beaea1380ee676c15a3d895c633a91bbe6c12 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 adc55f8c91e8cd39ec72745975c988aaaac9fb7c..57fde112bfc5a9ab4d15f5a17f91afd6a89cbf82 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 7af363d596b63abe06f78717d8945dcba820d7fd..da601ba3510e3624b7acd0954f922082b2197755 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 54293050989c146e5bb39363c12281c46eea1a53..45842aa5617b4f87cf306aebdead2d9ae96d455f 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 58cda0c7dc5f819ec1b34cd97ff577331cc030b0..18e39155274c650d4f2dde860a4998e398a8bbbe 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 d584c575d2970e3fa1131acb08b1adfac7ca38a9..614866d9c9c4ce7b3aca398dcc978f79298dfc81 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 98d8cb8749996909757367c36e2591817030bebe..474f2128fc5194857e2c5436a802c5ce99791cc8 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 57fde112bfc5a9ab4d15f5a17f91afd6a89cbf82..38ec8ffb4057d2404c5e38e1150b27bb3acd155d 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 87307264f507070ced1303c072c8b9a59cee5cc9..1b4e32f4b832483c160293a5d792b39d62a1a628 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 18e39155274c650d4f2dde860a4998e398a8bbbe..f1711afec20c73bd9965bd77140994aa1c1145b8 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,