From a1fe5abeaf500b1e4fb484f0733fdbb9114d68e8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Jul 2023 12:31:20 +0200 Subject: [PATCH 01/42] Add rudimentary PHP syntax highlighting --- Cargo.lock | 10 ++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 1 + crates/zed/src/languages/php/config.toml | 11 ++ crates/zed/src/languages/php/highlights.scm | 122 ++++++++++++++++++++ crates/zed/src/languages/php/injections.scm | 3 + crates/zed/src/languages/php/outline.scm | 8 ++ crates/zed/src/languages/php/tags.scm | 40 +++++++ crates/zed/src/languages/python/outline.scm | 2 +- 9 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 crates/zed/src/languages/php/config.toml create mode 100644 crates/zed/src/languages/php/highlights.scm create mode 100644 crates/zed/src/languages/php/injections.scm create mode 100644 crates/zed/src/languages/php/outline.scm create mode 100644 crates/zed/src/languages/php/tags.scm diff --git a/Cargo.lock b/Cargo.lock index 60ed830683a5f91d0c8f4806b5d6758b2afbc31e..9806c86c96e00f3a05e7e038a24d3a2aec721897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8004,6 +8004,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-php" +version = "0.19.1" +source = "git+https://github.com/tree-sitter/tree-sitter-php?rev=d38adb26304d9b9d38e9a3b4aae0ec4b29bf9462#d38adb26304d9b9d38e9a3b4aae0ec4b29bf9462" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-python" version = "0.20.2" @@ -9432,6 +9441,7 @@ dependencies = [ "tree-sitter-json 0.20.0", "tree-sitter-lua", "tree-sitter-markdown", + "tree-sitter-php", "tree-sitter-python", "tree-sitter-racket", "tree-sitter-ruby", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d016525af40a9e247517dc1aebd64fa2c99c703c..0e92b2e3ea58b03fd2fe2e0e39795bff5edffb25 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -114,6 +114,7 @@ tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", re tree-sitter-rust = "0.20.3" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-python = "0.20.2" +tree-sitter-php = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "d38adb26304d9b9d38e9a3b4aae0ec4b29bf9462" } tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" } tree-sitter-ruby = "0.20.0" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 44e144e89b37695e792b0b1f43fdf88f7e2b674d..81edd92d374b56e5d656a649cb3da96459b33e0f 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -137,6 +137,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { tree_sitter_yaml::language(), vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime))], ); + language("php", tree_sitter_php::language(), vec![]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/php/config.toml b/crates/zed/src/languages/php/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..e9de52745a153b63df0ed99c260d64b9aea65aab --- /dev/null +++ b/crates/zed/src/languages/php/config.toml @@ -0,0 +1,11 @@ +name = "PHP" +path_suffixes = ["php"] +first_line_pattern = '^#!.*php' +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] diff --git a/crates/zed/src/languages/php/highlights.scm b/crates/zed/src/languages/php/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..666a49be2a4795bfc923695417fdecf9a5100740 --- /dev/null +++ b/crates/zed/src/languages/php/highlights.scm @@ -0,0 +1,122 @@ +(php_tag) @tag +"?>" @tag + +; Types + +(primitive_type) @type.builtin +(cast_type) @type.builtin +(named_type (name) @type) @type +(named_type (qualified_name) @type) @type + +; Functions + +(array_creation_expression "array" @function.builtin) +(list_literal "list" @function.builtin) + +(method_declaration + name: (name) @function.method) + +(function_call_expression + function: [(qualified_name (name)) (name)] @function) + +(scoped_call_expression + name: (name) @function) + +(member_call_expression + name: (name) @function.method) + +(function_definition + name: (name) @function) + +; Member + +(property_element + (variable_name) @property) + +(member_access_expression + name: (variable_name (name)) @property) +(member_access_expression + name: (name) @property) + +; Variables + +(relative_scope) @variable.builtin + +((name) @constant + (#match? @constant "^_?[A-Z][A-Z\\d_]+$")) +((name) @constant.builtin + (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$")) + +((name) @constructor + (#match? @constructor "^[A-Z]")) + +((name) @variable.builtin + (#eq? @variable.builtin "this")) + +(variable_name) @variable + +; Basic tokens +[ + (string) + (string_value) + (encapsed_string) + (heredoc) + (heredoc_body) + (nowdoc_body) +] @string +(boolean) @constant.builtin +(null) @constant.builtin +(integer) @number +(float) @number +(comment) @comment + +"$" @operator + +; Keywords + +"abstract" @keyword +"as" @keyword +"break" @keyword +"case" @keyword +"catch" @keyword +"class" @keyword +"const" @keyword +"continue" @keyword +"declare" @keyword +"default" @keyword +"do" @keyword +"echo" @keyword +"else" @keyword +"elseif" @keyword +"enddeclare" @keyword +"endforeach" @keyword +"endif" @keyword +"endswitch" @keyword +"endwhile" @keyword +"extends" @keyword +"final" @keyword +"finally" @keyword +"foreach" @keyword +"function" @keyword +"global" @keyword +"if" @keyword +"implements" @keyword +"include_once" @keyword +"include" @keyword +"insteadof" @keyword +"interface" @keyword +"namespace" @keyword +"new" @keyword +"private" @keyword +"protected" @keyword +"public" @keyword +"require_once" @keyword +"require" @keyword +"return" @keyword +"static" @keyword +"switch" @keyword +"throw" @keyword +"trait" @keyword +"try" @keyword +"use" @keyword +"while" @keyword diff --git a/crates/zed/src/languages/php/injections.scm b/crates/zed/src/languages/php/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..16d5736bebad153cece89b21f09dfae951e7457f --- /dev/null +++ b/crates/zed/src/languages/php/injections.scm @@ -0,0 +1,3 @@ +((text) @injection.content + (#set! injection.language "html") + (#set! injection.combined)) diff --git a/crates/zed/src/languages/php/outline.scm b/crates/zed/src/languages/php/outline.scm new file mode 100644 index 0000000000000000000000000000000000000000..57ea2ae33485c4b8390c62e8d6aa11c3fb639015 --- /dev/null +++ b/crates/zed/src/languages/php/outline.scm @@ -0,0 +1,8 @@ +(class_declaration + "class" @context + name: (name) @name + ) @item + +(function_definition + "function" @context + name: (_) @name) @item diff --git a/crates/zed/src/languages/php/tags.scm b/crates/zed/src/languages/php/tags.scm new file mode 100644 index 0000000000000000000000000000000000000000..66d594c254748c26623ce40456bab1304305be43 --- /dev/null +++ b/crates/zed/src/languages/php/tags.scm @@ -0,0 +1,40 @@ +(namespace_definition + name: (namespace_name) @name) @module + +(interface_declaration + name: (name) @name) @definition.interface + +(trait_declaration + name: (name) @name) @definition.interface + +(class_declaration + name: (name) @name) @definition.class + +(class_interface_clause [(name) (qualified_name)] @name) @impl + +(property_declaration + (property_element (variable_name (name) @name))) @definition.field + +(function_definition + name: (name) @name) @definition.function + +(method_declaration + name: (name) @name) @definition.function + +(object_creation_expression + [ + (qualified_name (name) @name) + (variable_name (name) @name) + ]) @reference.class + +(function_call_expression + function: [ + (qualified_name (name) @name) + (variable_name (name)) @name + ]) @reference.call + +(scoped_call_expression + name: (name) @name) @reference.call + +(member_call_expression + name: (name) @name) @reference.call diff --git a/crates/zed/src/languages/python/outline.scm b/crates/zed/src/languages/python/outline.scm index 373c7c7c68b3b2a97e753c4fca09b6c2e42c68e6..e3efb3dbf67c28083f6311b2337ecfe97ccebe27 100644 --- a/crates/zed/src/languages/python/outline.scm +++ b/crates/zed/src/languages/python/outline.scm @@ -6,4 +6,4 @@ (function_definition "async"? @context "def" @context - name: (_) @name) @item \ No newline at end of file + name: (_) @name) @item From 1cc8ecad1215e7d9e8b7411cb2e03f7d6b1c52ed Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 12 Jul 2023 19:33:09 +0200 Subject: [PATCH 02/42] Fix HTML injections (Thanks Max!) Co-authored-by: Max --- crates/zed/src/languages/php/injections.scm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/languages/php/injections.scm b/crates/zed/src/languages/php/injections.scm index 16d5736bebad153cece89b21f09dfae951e7457f..57abd8ea2b0576e7b936788b4a9880bc57fea798 100644 --- a/crates/zed/src/languages/php/injections.scm +++ b/crates/zed/src/languages/php/injections.scm @@ -1,3 +1,3 @@ -((text) @injection.content - (#set! injection.language "html") - (#set! injection.combined)) +((text) @content + (#set! "language" "html") + (#set! "combined")) From 608c16342c0ecdec4373e9d848a88ab1c37e5214 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 13 Jul 2023 12:23:49 +0200 Subject: [PATCH 03/42] Update outline queries; add enum as a highlighted keyword --- crates/zed/src/languages/php/highlights.scm | 1 + crates/zed/src/languages/php/outline.scm | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/languages/php/highlights.scm b/crates/zed/src/languages/php/highlights.scm index 666a49be2a4795bfc923695417fdecf9a5100740..fcb087c47d14dbc036ed79a50be7ff1b57ebc4e8 100644 --- a/crates/zed/src/languages/php/highlights.scm +++ b/crates/zed/src/languages/php/highlights.scm @@ -88,6 +88,7 @@ "echo" @keyword "else" @keyword "elseif" @keyword +"enum" @keyword "enddeclare" @keyword "endforeach" @keyword "endif" @keyword diff --git a/crates/zed/src/languages/php/outline.scm b/crates/zed/src/languages/php/outline.scm index 57ea2ae33485c4b8390c62e8d6aa11c3fb639015..4934bc494d0ae709ec52ea3fb46db518b8dc35d8 100644 --- a/crates/zed/src/languages/php/outline.scm +++ b/crates/zed/src/languages/php/outline.scm @@ -5,4 +5,22 @@ (function_definition "function" @context - name: (_) @name) @item + name: (_) @name + ) @item + + + +(method_declaration + "function" @context + name: (_) @name + ) @item + +(interface_declaration + "interface" @context + name: (_) @name + ) @item + +(enum_declaration + "enum" @context + name: (_) @name + ) @item From 9a1a9813cbc5936f0da02088b3b70d5f5c445083 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 13 Jul 2023 11:56:53 -0400 Subject: [PATCH 04/42] WIP --- Cargo.lock | 10 ++++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 1 + crates/zed/src/languages/bash/brackets.scm | 3 ++ crates/zed/src/languages/bash/config.toml | 7 +++ crates/zed/src/languages/bash/highlights.scm | 56 ++++++++++++++++++++ 6 files changed, 78 insertions(+) create mode 100644 crates/zed/src/languages/bash/brackets.scm create mode 100644 crates/zed/src/languages/bash/config.toml create mode 100644 crates/zed/src/languages/bash/highlights.scm diff --git a/Cargo.lock b/Cargo.lock index 0ac6a2ee890418104d6851a961d321f1ef7e8f36..ec674aee2ce721077e3f5b9f6054afa9ac92d2cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7953,6 +7953,15 @@ dependencies = [ "regex", ] +[[package]] +name = "tree-sitter-bash" +version = "0.19.0" +source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-c" version = "0.20.2" @@ -9539,6 +9548,7 @@ dependencies = [ "tiny_http", "toml", "tree-sitter", + "tree-sitter-bash", "tree-sitter-c", "tree-sitter-cpp", "tree-sitter-css", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 597e40161fb029eee16cf53208ce0e20d0c0a603..fb7cd6addab2d164bdfd60600ed582cb1f838f6c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -104,6 +104,7 @@ thiserror.workspace = true tiny_http = "0.8" toml.workspace = true tree-sitter.workspace = true +tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 820f5641510d8cc0f0d0eb216a8c2056e4916428..225bc4d0bce8f735a5f87cf5232c0787eb987445 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -38,6 +38,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { languages.register(name, load_config(name), grammar, adapters, load_queries) }; + language("bash", tree_sitter_bash::language(), vec![]); language( "c", tree_sitter_c::language(), diff --git a/crates/zed/src/languages/bash/brackets.scm b/crates/zed/src/languages/bash/brackets.scm new file mode 100644 index 0000000000000000000000000000000000000000..191fd9c084a52eced37428281971ff9e569a4932 --- /dev/null +++ b/crates/zed/src/languages/bash/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..f3dcee9b73ecc4ea272febfc40128a1bad4d5a2a --- /dev/null +++ b/crates/zed/src/languages/bash/config.toml @@ -0,0 +1,7 @@ +name = "Shell Script" +path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"] +brackets = [ + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/zed/src/languages/bash/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..f33a7c2d3ae8c63364845dbd8cef1242ffa07f04 --- /dev/null +++ b/crates/zed/src/languages/bash/highlights.scm @@ -0,0 +1,56 @@ +[ + (string) + (raw_string) + (heredoc_body) + (heredoc_start) +] @string + +(command_name) @function + +(variable_name) @property + +[ + "case" + "do" + "done" + "elif" + "else" + "esac" + "export" + "fi" + "for" + "function" + "if" + "in" + "select" + "then" + "unset" + "until" + "while" +] @keyword + +(comment) @comment + +(function_definition name: (word) @function) + +(file_descriptor) @number + +[ + (command_substitution) + (process_substitution) + (expansion) +]@embedded + +[ + "$" + "&&" + ">" + ">>" + "<" + "|" +] @operator + +( + (command (_) @constant) + (#match? @constant "^-") +) From ef7aa66959ca44fe99293d8352360f94d569e33c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 13 Jul 2023 12:09:43 -0400 Subject: [PATCH 05/42] Add first line pattern --- crates/zed/src/languages/bash/config.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml index f3dcee9b73ecc4ea272febfc40128a1bad4d5a2a..80b8753d80105f91ea9c0b802a2fe11ea61557c4 100644 --- a/crates/zed/src/languages/bash/config.toml +++ b/crates/zed/src/languages/bash/config.toml @@ -1,5 +1,6 @@ name = "Shell Script" path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"] +first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" brackets = [ { start = "[", end = "]", close = true, newline = false }, { start = "(", end = ")", close = true, newline = false }, From eaa8224076e20b5d4df70b0cf4c5b8f049fac94c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 17 Jul 2023 12:24:56 +0300 Subject: [PATCH 06/42] Use id instead of type_id for actions Currently, both are the same thing, so the logic is not changed. --- crates/gpui/src/app.rs | 12 +++++----- crates/gpui/src/app/window.rs | 20 +++++++--------- crates/gpui/src/keymap_matcher.rs | 4 ++-- crates/gpui/src/keymap_matcher/keymap.rs | 29 +++++++++++------------- crates/gpui/src/platform/mac/platform.rs | 2 +- 5 files changed, 30 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 640614324f65dcdcae29c711a698713b48cfd9d0..b40a67db61cf2077d3b22ccc72d8afc9b96e9819 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1073,7 +1073,7 @@ impl AppContext { pub fn is_action_available(&self, action: &dyn Action) -> bool { let mut available_in_window = false; - let action_type = action.as_any().type_id(); + let action_id = action.id(); if let Some(window_id) = self.platform.main_window_id() { available_in_window = self .read_window(window_id, |cx| { @@ -1083,7 +1083,7 @@ impl AppContext { cx.views_metadata.get(&(window_id, view_id)) { if let Some(actions) = cx.actions.get(&view_metadata.type_id) { - if actions.contains_key(&action_type) { + if actions.contains_key(&action_id) { return true; } } @@ -1094,7 +1094,7 @@ impl AppContext { }) .unwrap_or(false); } - available_in_window || self.global_actions.contains_key(&action_type) + available_in_window || self.global_actions.contains_key(&action_id) } fn actions_mut( @@ -3399,7 +3399,7 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { for (i, view_id) in self.ancestors(view_id).enumerate() { if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) { if let Some(actions) = self.actions.get(&view_metadata.type_id) { - if actions.contains_key(&action.as_any().type_id()) { + if actions.contains_key(&action.id()) { handler_depth = Some(i); } } @@ -3407,12 +3407,12 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { } } - if self.global_actions.contains_key(&action.as_any().type_id()) { + if self.global_actions.contains_key(&action.id()) { handler_depth = Some(contexts.len()) } self.keystroke_matcher - .bindings_for_action_type(action.as_any().type_id()) + .bindings_for_action(action.id()) .find_map(|b| { let highest_handler = handler_depth?; if action.eq(b.action()) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 49b12d823e69832eb42ae382c3d6b37c39fd1957..1a5611f6aed0fcc17d8509124b1583a9255dcbc0 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -363,17 +363,13 @@ impl<'a> WindowContext<'a> { ) -> Vec<(&'static str, Box, SmallVec<[Binding; 1]>)> { let window_id = self.window_id; let mut contexts = Vec::new(); - let mut handler_depths_by_action_type = HashMap::::default(); + let mut handler_depths_by_action_id = HashMap::::default(); for (depth, view_id) in self.ancestors(view_id).enumerate() { if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) { contexts.push(view_metadata.keymap_context.clone()); if let Some(actions) = self.actions.get(&view_metadata.type_id) { - handler_depths_by_action_type.extend( - actions - .keys() - .copied() - .map(|action_type| (action_type, depth)), - ); + handler_depths_by_action_id + .extend(actions.keys().copied().map(|action_id| (action_id, depth))); } } else { log::error!( @@ -383,21 +379,21 @@ impl<'a> WindowContext<'a> { } } - handler_depths_by_action_type.extend( + handler_depths_by_action_id.extend( self.global_actions .keys() .copied() - .map(|action_type| (action_type, contexts.len())), + .map(|action_id| (action_id, contexts.len())), ); self.action_deserializers .iter() - .filter_map(move |(name, (type_id, deserialize))| { - if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() { + .filter_map(move |(name, (action_id, deserialize))| { + if let Some(action_depth) = handler_depths_by_action_id.get(action_id).copied() { let action = deserialize(serde_json::Value::Object(Default::default())).ok()?; let bindings = self .keystroke_matcher - .bindings_for_action_type(*type_id) + .bindings_for_action(*action_id) .filter(|b| { action.eq(b.action()) && (0..=action_depth) diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index bc70638b2c077bf5aee53f9fedd22e96938d6b4b..9e5d45987dd69cd2ef825d232c18c0c51095487a 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -47,8 +47,8 @@ impl KeymapMatcher { self.keymap.clear(); } - pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator { - self.keymap.bindings_for_action_type(action_type) + pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { + self.keymap.bindings_for_action(action_id) } pub fn clear_pending(&mut self) { diff --git a/crates/gpui/src/keymap_matcher/keymap.rs b/crates/gpui/src/keymap_matcher/keymap.rs index 6f358aad3939d3f5dbb4be3dba52e3d5020874ce..bc02ae10facb5e2d67cf07598e627f19068cb5b1 100644 --- a/crates/gpui/src/keymap_matcher/keymap.rs +++ b/crates/gpui/src/keymap_matcher/keymap.rs @@ -1,39 +1,36 @@ use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - collections::HashMap, -}; +use std::{any::TypeId, collections::HashMap}; use super::Binding; #[derive(Default)] pub struct Keymap { bindings: Vec, - binding_indices_by_action_type: HashMap>, + binding_indices_by_action_id: HashMap>, } impl Keymap { pub fn new(bindings: Vec) -> Self { - let mut binding_indices_by_action_type = HashMap::new(); + let mut binding_indices_by_action_id = HashMap::new(); for (ix, binding) in bindings.iter().enumerate() { - binding_indices_by_action_type - .entry(binding.action().type_id()) + binding_indices_by_action_id + .entry(binding.action().id()) .or_insert_with(SmallVec::new) .push(ix); } Self { - binding_indices_by_action_type, + binding_indices_by_action_id, bindings, } } - pub(crate) fn bindings_for_action_type( + pub(crate) fn bindings_for_action( &self, - action_type: TypeId, + action_id: TypeId, ) -> impl Iterator { - self.binding_indices_by_action_type - .get(&action_type) + self.binding_indices_by_action_id + .get(&action_id) .map(SmallVec::as_slice) .unwrap_or(&[]) .iter() @@ -42,8 +39,8 @@ impl Keymap { pub(crate) fn add_bindings>(&mut self, bindings: T) { for binding in bindings { - self.binding_indices_by_action_type - .entry(binding.action().as_any().type_id()) + self.binding_indices_by_action_id + .entry(binding.action().id()) .or_default() .push(self.bindings.len()); self.bindings.push(binding); @@ -52,7 +49,7 @@ impl Keymap { pub(crate) fn clear(&mut self) { self.bindings.clear(); - self.binding_indices_by_action_type.clear(); + self.binding_indices_by_action_id.clear(); } pub fn bindings(&self) -> &Vec { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index e1d80fe25ceead41828aa18d90f5938eb518a17f..509c979b8536dfde31a9246b34edd7bd0ca88e0e 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -231,7 +231,7 @@ impl MacForegroundPlatform { } => { // TODO let keystrokes = keystroke_matcher - .bindings_for_action_type(action.as_any().type_id()) + .bindings_for_action(action.id()) .find(|binding| binding.action().eq(action.as_ref())) .map(|binding| binding.keystrokes()); let selector = match os_action { From f5eac82e813a7f5bbfda1b6f4d0d6eda4e97be16 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 17 Jul 2023 12:30:42 +0300 Subject: [PATCH 07/42] Reload menu after keybindings change --- crates/zed/src/zed.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1621ae276a993bfb347825970d2afa652ccd659d..9dffc644ae7fd63a5a722db94fd4bdb25a979a99 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -517,11 +517,7 @@ pub fn handle_keymap_file_changes( let mut settings_subscription = None; while let Some(user_keymap_content) = user_keymap_file_rx.next().await { if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) { - cx.update(|cx| { - cx.clear_bindings(); - load_default_keymap(cx); - keymap_content.clone().add_to_cx(cx).log_err(); - }); + cx.update(|cx| reload_keymaps(cx, &keymap_content)); let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); drop(settings_subscription); @@ -530,10 +526,7 @@ pub fn handle_keymap_file_changes( let new_base_keymap = *settings::get::(cx); if new_base_keymap != old_base_keymap { old_base_keymap = new_base_keymap.clone(); - - cx.clear_bindings(); - load_default_keymap(cx); - keymap_content.clone().add_to_cx(cx).log_err(); + reload_keymaps(cx, &keymap_content); } }) .detach(); @@ -544,6 +537,13 @@ pub fn handle_keymap_file_changes( .detach(); } +fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { + cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + cx.set_menus(menus::menus()); +} + fn open_local_settings_file( workspace: &mut Workspace, _: &OpenLocalSettings, From dc557e16478ba0fe570cef208185ac104842bdde Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:34:22 +0200 Subject: [PATCH 08/42] Add scaffolding of php language server --- crates/language/src/language.rs | 23 +++- crates/lsp/src/lsp.rs | 7 +- crates/project/src/project.rs | 2 +- crates/zed/src/languages.rs | 9 +- crates/zed/src/languages/php.rs | 203 ++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 crates/zed/src/languages/php.rs diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e8450344b8a627994cbe173a52c900339aa93cf3..af6a6e5045897052aa8062b02c7b0f124a084f1a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -262,24 +262,31 @@ pub trait LspAdapter: 'static + Send + Sync { container_dir: PathBuf, ) -> Option; - async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + async fn process_diagnostics(&self, d: &mut lsp::PublishDiagnosticsParams) { + dbg!(d); + } - async fn process_completion(&self, _: &mut lsp::CompletionItem) {} + async fn process_completion(&self, d: &mut lsp::CompletionItem) { + dbg!(d); + } async fn label_for_completion( &self, - _: &lsp::CompletionItem, + item: &lsp::CompletionItem, _: &Arc, ) -> Option { + dbg!(item); None } async fn label_for_symbol( &self, - _: &str, - _: lsp::SymbolKind, + name: &str, + kind: lsp::SymbolKind, _: &Arc, ) -> Option { + dbg!(name); + dbg!(kind); None } @@ -321,7 +328,7 @@ pub struct CodeLabel { pub filter_range: Range, } -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Debug)] pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, @@ -810,6 +817,7 @@ impl LanguageRegistry { .spawn(async move { let id = language.id; let queries = (language.get_queries)(&language.path); + dbg!(&language.path); let language = Language::new(language.config, Some(language.grammar)) .with_lsp_adapters(language.lsp_adapters) @@ -819,9 +827,11 @@ impl LanguageRegistry { Ok(language) => { let language = Arc::new(language); let mut state = this.state.write(); + state.add(language.clone()); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { + dbg!(&name); for tx in txs.drain(..) { let _ = tx.send(Ok(language.clone())); } @@ -829,6 +839,7 @@ impl LanguageRegistry { } Err(err) => { log::error!("failed to load language {name} - {err}"); + dbg!(&name); let mut state = this.state.write(); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a01f6e8a49ea744cd4c1d699ada7c27474490907..78c858a90c46e2af349027ed3f4f361fe8805752 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -151,16 +151,17 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stout = server.stdout.take().unwrap(); let mut server = Self::new_internal( - server_id, + server_id.clone(), stdin, stout, Some(server), root_path, code_action_kinds, cx, - |notification| { + move |notification| { log::info!( - "unhandled notification {}:\n{}", + "{} unhandled notification {}:\n{}", + server_id, notification.method, serde_json::to_string_pretty( ¬ification diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 81db0c7ed7d3b6ffe61df7104e53796d9dd43b54..666503e210ea93731506675004465488dd1149cc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2680,6 +2680,7 @@ impl Project { key: (WorktreeId, LanguageServerName), cx: &mut AsyncAppContext, ) -> Result>> { + dbg!(language.name()); let setup = Self::setup_pending_language_server( this, initialization_options, @@ -2694,7 +2695,6 @@ impl Project { Some(language_server) => language_server, None => return Ok(None), }; - let this = match this.upgrade(cx) { Some(this) => this, None => return Err(anyhow!("failed to upgrade project handle")), diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 81edd92d374b56e5d656a649cb3da96459b33e0f..2a29fd537bf7a9a378c4f2dd4aa30b9fec816913 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -13,6 +13,7 @@ mod json; #[cfg(feature = "plugin_runtime")] mod language_plugin; mod lua; +mod php; mod python; mod ruby; mod rust; @@ -135,9 +136,13 @@ pub fn init(languages: Arc, node_runtime: Arc) { language( "yaml", tree_sitter_yaml::language(), - vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime))], + vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))], + ); + language( + "php", + tree_sitter_php::language(), + vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], ); - language("php", tree_sitter_php::language(), vec![]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs new file mode 100644 index 0000000000000000000000000000000000000000..b42ad18a5397b5fbd3ed04c7d495b4cdb342da62 --- /dev/null +++ b/crates/zed/src/languages/php.rs @@ -0,0 +1,203 @@ +use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{future::BoxFuture, FutureExt}; +use gpui::AppContext; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use serde_json::{json, Value}; +use smol::{fs, io::BufReader, stream::StreamExt}; +use std::{ + any::Any, + ffi::OsString, + future, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +fn intelephense_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct IntelephenseVersion(String); + +pub struct IntelephenseLspAdapter { + node: Arc, +} + +impl IntelephenseLspAdapter { + const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js"; + + #[allow(unused)] + pub fn new(node: Arc) -> Self { + Self { node } + } +} + +#[async_trait] +impl LspAdapter for IntelephenseLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("intelephense".into()) + } + + async fn fetch_latest_server_version( + &self, + _delegate: &dyn LspAdapterDelegate, + ) -> Result> { + // At the time of writing the latest vscode-eslint release was released in 2020 and requires + // special custom LSP protocol extensions be handled to fully initialize. Download the latest + // prerelease instead to sidestep this issue + dbg!("Strarting fetching server binary version"); + Ok(Box::new(IntelephenseVersion( + self.node.npm_package_latest_version("intelephense").await?, + )) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _delegate: &dyn LspAdapterDelegate, + ) -> Result { + dbg!("Strarting fetching server binary"); + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())]) + .await?; + } + dbg!("Fetched server binary"); + Ok(LanguageServerBinary { + path: dbg!(self.node.binary_path().await)?, + arguments: intelephense_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + dbg!("cached_server_binary"); + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + dbg!("installation_test_binary"); + get_cached_server_binary(container_dir, &self.node).await + } + + async fn label_for_completion( + &self, + _item: &lsp::CompletionItem, + _language: &Arc, + ) -> Option { + dbg!(_item.kind); + None + } + + async fn initialization_options(&self) -> Option { + dbg!("init_options"); + None + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: intelephense_server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + use unindent::Unindent; + + #[gpui::test] + async fn test_outline(cx: &mut TestAppContext) { + let language = crate::languages::language("php", tree_sitter_php::language(), None).await; + + /*let text = r#" + function a() { + // local variables are omitted + let a1 = 1; + // all functions are included + async function a2() {} + } + // top-level variables are included + let b: C + function getB() {} + // exported variables are included + export const d = e; + "# + .unindent();*/ + let text = r#" + function a() { + // local variables are omitted + $a1 = 1; + // all functions are included + function a2() {} + } + class Foo {} + "# + .unindent(); + let buffer = + cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); + panic!( + "{:?}", + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>() + ); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[ + ("function a()", 0), + ("async function a2()", 1), + ("let b", 0), + ("function getB()", 0), + ("const d", 0), + ] + ); + } +} From 965cc2efbc6fbc6316d82a27af0d39deb535c539 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:07:25 +0200 Subject: [PATCH 09/42] Fix a crash in tree-sitter-php --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c972dc2530351bff31d5cc7e88bb818907201377..d433805a1f77787740c87b50135628f69f27b0e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8053,7 +8053,7 @@ dependencies = [ [[package]] name = "tree-sitter-php" version = "0.19.1" -source = "git+https://github.com/tree-sitter/tree-sitter-php?rev=d38adb26304d9b9d38e9a3b4aae0ec4b29bf9462#d38adb26304d9b9d38e9a3b4aae0ec4b29bf9462" +source = "git+https://github.com/tree-sitter/tree-sitter-php?rev=d43130fd1525301e9826f420c5393a4d169819fc#d43130fd1525301e9826f420c5393a4d169819fc" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index 9a4fc8160782f0985a85cd7e87afabb5c96a5a60..7a32938c378e8bbbfcd9d3fad6de1a79366c0b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } tree-sitter-rust = "0.20.3" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } -tree-sitter-php = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "d38adb26304d9b9d38e9a3b4aae0ec4b29bf9462" } +tree-sitter-php = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "d43130fd1525301e9826f420c5393a4d169819fc" } tree-sitter-python = "0.20.2" tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" } From 94796e943b16053886a282dd944d431cf6046340 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:36:08 +0200 Subject: [PATCH 10/42] Set language id for PHP. LSP works! --- crates/zed/src/languages/php.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index b42ad18a5397b5fbd3ed04c7d495b4cdb342da62..ee6493b5761a9d4053bc8a1b362293ae9c08bdc0 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; +use collections::HashMap; use futures::{future::BoxFuture, FutureExt}; use gpui::AppContext; use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; @@ -50,7 +51,6 @@ impl LspAdapter for IntelephenseLspAdapter { // At the time of writing the latest vscode-eslint release was released in 2020 and requires // special custom LSP protocol extensions be handled to fully initialize. Download the latest // prerelease instead to sidestep this issue - dbg!("Strarting fetching server binary version"); Ok(Box::new(IntelephenseVersion( self.node.npm_package_latest_version("intelephense").await?, )) as Box<_>) @@ -62,7 +62,6 @@ impl LspAdapter for IntelephenseLspAdapter { container_dir: PathBuf, _delegate: &dyn LspAdapterDelegate, ) -> Result { - dbg!("Strarting fetching server binary"); let version = version.downcast::().unwrap(); let server_path = container_dir.join(Self::SERVER_PATH); @@ -71,9 +70,8 @@ impl LspAdapter for IntelephenseLspAdapter { .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())]) .await?; } - dbg!("Fetched server binary"); Ok(LanguageServerBinary { - path: dbg!(self.node.binary_path().await)?, + path: self.node.binary_path().await?, arguments: intelephense_server_binary_arguments(&server_path), }) } @@ -83,7 +81,6 @@ impl LspAdapter for IntelephenseLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - dbg!("cached_server_binary"); get_cached_server_binary(container_dir, &self.node).await } @@ -91,7 +88,6 @@ impl LspAdapter for IntelephenseLspAdapter { &self, container_dir: PathBuf, ) -> Option { - dbg!("installation_test_binary"); get_cached_server_binary(container_dir, &self.node).await } @@ -100,14 +96,15 @@ impl LspAdapter for IntelephenseLspAdapter { _item: &lsp::CompletionItem, _language: &Arc, ) -> Option { - dbg!(_item.kind); None } async fn initialization_options(&self) -> Option { - dbg!("init_options"); None } + async fn language_ids(&self) -> HashMap { + HashMap::from_iter([("PHP".into(), "php".into())]) + } } async fn get_cached_server_binary( From 6c7a6d43fce52334b9309a70c02a98f911956bd4 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:38:35 +0200 Subject: [PATCH 11/42] Cargo fix --- crates/zed/src/languages/php.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index ee6493b5761a9d4053bc8a1b362293ae9c08bdc0..84115b45288034500e824aa5e644ff81e207a135 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -1,19 +1,18 @@ use anyhow::{anyhow, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; + + use async_trait::async_trait; use collections::HashMap; -use futures::{future::BoxFuture, FutureExt}; -use gpui::AppContext; + + use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp::{CodeActionKind, LanguageServerBinary}; +use lsp::{LanguageServerBinary}; use node_runtime::NodeRuntime; -use serde_json::{json, Value}; -use smol::{fs, io::BufReader, stream::StreamExt}; + +use smol::{fs, stream::StreamExt}; use std::{ any::Any, ffi::OsString, - future, path::{Path, PathBuf}, sync::Arc, }; From 5b6582a7c28b9afd98172a9cb25a82653e90eb2c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:51:00 +0200 Subject: [PATCH 12/42] rustfmt --- crates/zed/src/languages/php.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 84115b45288034500e824aa5e644ff81e207a135..75eef29e5055d64b78b9fc7dcd7d318227a42051 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -1,12 +1,10 @@ use anyhow::{anyhow, Result}; - use async_trait::async_trait; use collections::HashMap; - use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp::{LanguageServerBinary}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use smol::{fs, stream::StreamExt}; From ee9123a7da70eed702da7496276ba785268f03af Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:56:25 +0200 Subject: [PATCH 13/42] Remove test --- crates/zed/src/languages/php.rs | 61 --------------------------------- 1 file changed, 61 deletions(-) diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 75eef29e5055d64b78b9fc7dcd7d318227a42051..2607dc410bc0352eb801d923ebfd30ca7e0f704c 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -134,64 +134,3 @@ async fn get_cached_server_binary( .await .log_err() } - -#[cfg(test)] -mod tests { - use gpui::TestAppContext; - use unindent::Unindent; - - #[gpui::test] - async fn test_outline(cx: &mut TestAppContext) { - let language = crate::languages::language("php", tree_sitter_php::language(), None).await; - - /*let text = r#" - function a() { - // local variables are omitted - let a1 = 1; - // all functions are included - async function a2() {} - } - // top-level variables are included - let b: C - function getB() {} - // exported variables are included - export const d = e; - "# - .unindent();*/ - let text = r#" - function a() { - // local variables are omitted - $a1 = 1; - // all functions are included - function a2() {} - } - class Foo {} - "# - .unindent(); - let buffer = - cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); - let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); - panic!( - "{:?}", - outline - .items - .iter() - .map(|item| (item.text.as_str(), item.depth)) - .collect::>() - ); - assert_eq!( - outline - .items - .iter() - .map(|item| (item.text.as_str(), item.depth)) - .collect::>(), - &[ - ("function a()", 0), - ("async function a2()", 1), - ("let b", 0), - ("function getB()", 0), - ("const d", 0), - ] - ); - } -} From 8642a1d074c404ea00cd52a3cfde00a5b43d6f9f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:03:57 +0200 Subject: [PATCH 14/42] Remove dbg! calls --- crates/language/src/language.rs | 5 ----- crates/project/src/project.rs | 1 - 2 files changed, 6 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index dec01daea514e5ec28832b08556060ae20308537..08a377d280c024fc08227daa32ab3926daa31941 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -285,8 +285,6 @@ pub trait LspAdapter: 'static + Send + Sync { kind: lsp::SymbolKind, _: &Arc, ) -> Option { - dbg!(name); - dbg!(kind); None } @@ -829,7 +827,6 @@ impl LanguageRegistry { .spawn(async move { let id = language.id; let queries = (language.get_queries)(&language.path); - dbg!(&language.path); let language = Language::new(language.config, Some(language.grammar)) .with_lsp_adapters(language.lsp_adapters) @@ -843,7 +840,6 @@ impl LanguageRegistry { state.add(language.clone()); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { - dbg!(&name); for tx in txs.drain(..) { let _ = tx.send(Ok(language.clone())); } @@ -851,7 +847,6 @@ impl LanguageRegistry { } Err(err) => { log::error!("failed to load language {name} - {err}"); - dbg!(&name); let mut state = this.state.write(); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8b46f0fd7fdb0d8d6a53f33a6591b8ad8d24fb1a..b931560d25ed5393b70ca419c5aaf2b0e7b64bb0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2695,7 +2695,6 @@ impl Project { key: (WorktreeId, LanguageServerName), cx: &mut AsyncAppContext, ) -> Result>> { - dbg!(language.name()); let setup = Self::setup_pending_language_server( this, initialization_options, From dd6b674e7ee3cc11346d1756348f8051f6e09d3a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:08:41 +0200 Subject: [PATCH 15/42] Remove dbg calls --- crates/language/src/language.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 08a377d280c024fc08227daa32ab3926daa31941..f8e25fbce1f85a6ba0aea1e035dc62ccd5a7d3bc 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -262,27 +262,22 @@ pub trait LspAdapter: 'static + Send + Sync { container_dir: PathBuf, ) -> Option; - async fn process_diagnostics(&self, d: &mut lsp::PublishDiagnosticsParams) { - dbg!(d); - } + async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} - async fn process_completion(&self, d: &mut lsp::CompletionItem) { - dbg!(d); - } + async fn process_completion(&self, _: &mut lsp::CompletionItem) {} async fn label_for_completion( &self, - item: &lsp::CompletionItem, + _: &lsp::CompletionItem, _: &Arc, ) -> Option { - dbg!(item); None } async fn label_for_symbol( &self, - name: &str, - kind: lsp::SymbolKind, + _: &str, + _: lsp::SymbolKind, _: &Arc, ) -> Option { None From 4cc06748c909dced565de925c1198bff95bff911 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 17 Jul 2023 14:22:50 +0300 Subject: [PATCH 16/42] Ignore keybindings with NoAction in config overrides --- crates/gpui/src/keymap_matcher/binding.rs | 4 +- crates/gpui/src/keymap_matcher/keymap.rs | 372 +++++++++++++++++- .../gpui/src/keymap_matcher/keymap_context.rs | 2 +- crates/gpui/src/keymap_matcher/keystroke.rs | 2 +- 4 files changed, 355 insertions(+), 25 deletions(-) diff --git a/crates/gpui/src/keymap_matcher/binding.rs b/crates/gpui/src/keymap_matcher/binding.rs index 4d8334128ba5b0f419e095b173e2568207d18560..527052c85d3668d0716da84c1445880d81faca47 100644 --- a/crates/gpui/src/keymap_matcher/binding.rs +++ b/crates/gpui/src/keymap_matcher/binding.rs @@ -7,8 +7,8 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke}; pub struct Binding { action: Box, - keystrokes: SmallVec<[Keystroke; 2]>, - context_predicate: Option, + pub(super) keystrokes: SmallVec<[Keystroke; 2]>, + pub(super) context_predicate: Option, } impl std::fmt::Debug for Binding { diff --git a/crates/gpui/src/keymap_matcher/keymap.rs b/crates/gpui/src/keymap_matcher/keymap.rs index bc02ae10facb5e2d67cf07598e627f19068cb5b1..7cb95cab3a28aac3a41f7515e1551d22b1a776f5 100644 --- a/crates/gpui/src/keymap_matcher/keymap.rs +++ b/crates/gpui/src/keymap_matcher/keymap.rs @@ -1,28 +1,24 @@ +use collections::HashSet; use smallvec::SmallVec; use std::{any::TypeId, collections::HashMap}; -use super::Binding; +use crate::{Action, NoAction}; + +use super::{Binding, KeymapContextPredicate, Keystroke}; #[derive(Default)] pub struct Keymap { bindings: Vec, binding_indices_by_action_id: HashMap>, + disabled_keystrokes: HashMap, HashSet>>, } impl Keymap { - pub fn new(bindings: Vec) -> Self { - let mut binding_indices_by_action_id = HashMap::new(); - for (ix, binding) in bindings.iter().enumerate() { - binding_indices_by_action_id - .entry(binding.action().id()) - .or_insert_with(SmallVec::new) - .push(ix); - } - - Self { - binding_indices_by_action_id, - bindings, - } + #[cfg(test)] + pub(super) fn new(bindings: Vec) -> Self { + let mut this = Self::default(); + this.add_bindings(bindings); + this } pub(crate) fn bindings_for_action( @@ -35,24 +31,358 @@ impl Keymap { .unwrap_or(&[]) .iter() .map(|ix| &self.bindings[*ix]) + .filter(|binding| !self.binding_disabled(binding)) } pub(crate) fn add_bindings>(&mut self, bindings: T) { + let no_action_id = (NoAction {}).id(); + let mut new_bindings = Vec::new(); + let mut has_new_disabled_keystrokes = false; for binding in bindings { - self.binding_indices_by_action_id - .entry(binding.action().id()) - .or_default() - .push(self.bindings.len()); - self.bindings.push(binding); + if binding.action().id() == no_action_id { + has_new_disabled_keystrokes |= self + .disabled_keystrokes + .entry(binding.keystrokes) + .or_default() + .insert(binding.context_predicate); + } else { + new_bindings.push(binding); + } + } + + if has_new_disabled_keystrokes { + self.binding_indices_by_action_id.retain(|_, indices| { + indices.retain(|ix| { + let binding = &self.bindings[*ix]; + match self.disabled_keystrokes.get(&binding.keystrokes) { + Some(disabled_predicates) => { + !disabled_predicates.contains(&binding.context_predicate) + } + None => true, + } + }); + !indices.is_empty() + }); + } + + for new_binding in new_bindings { + if !self.binding_disabled(&new_binding) { + self.binding_indices_by_action_id + .entry(new_binding.action().id()) + .or_default() + .push(self.bindings.len()); + self.bindings.push(new_binding); + } } } pub(crate) fn clear(&mut self) { self.bindings.clear(); self.binding_indices_by_action_id.clear(); + self.disabled_keystrokes.clear(); + } + + pub fn bindings(&self) -> Vec<&Binding> { + self.bindings + .iter() + .filter(|binding| !self.binding_disabled(binding)) + .collect() + } + + fn binding_disabled(&self, binding: &Binding) -> bool { + match self.disabled_keystrokes.get(&binding.keystrokes) { + Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate), + None => false, + } + } +} + +#[cfg(test)] +mod tests { + use crate::actions; + + use super::*; + + actions!( + keymap_test, + [Present1, Present2, Present3, Duplicate, Missing] + ); + + #[test] + fn regular_keymap() { + let present_1 = Binding::new("ctrl-q", Present1 {}, None); + let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); + let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor")); + let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None); + let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); + let missing = Binding::new("ctrl-r", Missing {}, None); + let all_bindings = [ + &present_1, + &present_2, + &present_3, + &keystroke_duplicate_to_1, + &full_duplicate_to_2, + &missing, + ]; + + let mut keymap = Keymap::default(); + assert_absent(&keymap, &all_bindings); + assert!(keymap.bindings().is_empty()); + + keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]); + assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]); + assert_present( + &keymap, + &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")], + ); + + keymap.add_bindings([ + keystroke_duplicate_to_1.clone(), + full_duplicate_to_2.clone(), + ]); + assert_absent(&keymap, &[&missing]); + assert!( + !keymap.binding_disabled(&keystroke_duplicate_to_1), + "Duplicate binding 1 was added and should not be disabled" + ); + assert!( + !keymap.binding_disabled(&full_duplicate_to_2), + "Duplicate binding 2 was added and should not be disabled" + ); + + assert_eq!( + keymap + .bindings_for_action(keystroke_duplicate_to_1.action().id()) + .map(|binding| &binding.keystrokes) + .flatten() + .collect::>(), + vec![&Keystroke { + ctrl: true, + alt: false, + shift: false, + cmd: false, + function: false, + key: "q".to_string() + }], + "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap" + ); + assert_eq!( + keymap + .bindings_for_action(full_duplicate_to_2.action().id()) + .map(|binding| &binding.keystrokes) + .flatten() + .collect::>(), + vec![ + &Keystroke { + ctrl: true, + alt: false, + shift: false, + cmd: false, + function: false, + key: "w".to_string() + }, + &Keystroke { + ctrl: true, + alt: false, + shift: false, + cmd: false, + function: false, + key: "w".to_string() + } + ], + "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap" + ); + + let updated_bindings = keymap.bindings(); + let expected_updated_bindings = vec![ + &present_1, + &present_2, + &present_3, + &keystroke_duplicate_to_1, + &full_duplicate_to_2, + ]; + assert_eq!( + updated_bindings.len(), + expected_updated_bindings.len(), + "Unexpected updated keymap bindings {updated_bindings:?}" + ); + for (i, expected) in expected_updated_bindings.iter().enumerate() { + let keymap_binding = &updated_bindings[i]; + assert_eq!( + keymap_binding.context_predicate, expected.context_predicate, + "Unexpected context predicate for keymap {i} element: {keymap_binding:?}" + ); + assert_eq!( + keymap_binding.keystrokes, expected.keystrokes, + "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}" + ); + } + + keymap.clear(); + assert_absent(&keymap, &all_bindings); + assert!(keymap.bindings().is_empty()); } - pub fn bindings(&self) -> &Vec { - &self.bindings + #[test] + fn keymap_with_ignored() { + let present_1 = Binding::new("ctrl-q", Present1 {}, None); + let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); + let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor")); + let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None); + let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); + let ignored_1 = Binding::new("ctrl-q", NoAction {}, None); + let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane")); + let ignored_3_with_other_context = + Binding::new("ctrl-e", NoAction {}, Some("other_context")); + + let mut keymap = Keymap::default(); + + keymap.add_bindings([ + ignored_1.clone(), + ignored_2.clone(), + ignored_3_with_other_context.clone(), + ]); + assert_absent(&keymap, &[&present_3]); + assert_disabled( + &keymap, + &[ + &present_1, + &present_2, + &ignored_1, + &ignored_2, + &ignored_3_with_other_context, + ], + ); + assert!(keymap.bindings().is_empty()); + keymap.clear(); + + keymap.add_bindings([ + present_1.clone(), + present_2.clone(), + present_3.clone(), + ignored_1.clone(), + ignored_2.clone(), + ignored_3_with_other_context.clone(), + ]); + assert_present(&keymap, &[(&present_3, "e")]); + assert_disabled( + &keymap, + &[ + &present_1, + &present_2, + &ignored_1, + &ignored_2, + &ignored_3_with_other_context, + ], + ); + keymap.clear(); + + keymap.add_bindings([ + present_1.clone(), + present_2.clone(), + present_3.clone(), + ignored_1.clone(), + ]); + assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]); + assert_disabled(&keymap, &[&present_1, &ignored_1]); + assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]); + keymap.clear(); + + keymap.add_bindings([ + present_1.clone(), + present_2.clone(), + present_3.clone(), + keystroke_duplicate_to_1.clone(), + full_duplicate_to_2.clone(), + ignored_1.clone(), + ignored_2.clone(), + ignored_3_with_other_context.clone(), + ]); + assert_present(&keymap, &[(&present_3, "e")]); + assert_disabled( + &keymap, + &[ + &present_1, + &present_2, + &keystroke_duplicate_to_1, + &full_duplicate_to_2, + &ignored_1, + &ignored_2, + &ignored_3_with_other_context, + ], + ); + keymap.clear(); + } + + #[track_caller] + fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) { + let keymap_bindings = keymap.bindings(); + assert_eq!( + expected_bindings.len(), + keymap_bindings.len(), + "Unexpected keymap bindings {keymap_bindings:?}" + ); + for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() { + assert!( + !keymap.binding_disabled(expected), + "{expected:?} should not be disabled as it was added into keymap for element {i}" + ); + assert_eq!( + keymap + .bindings_for_action(expected.action().id()) + .map(|binding| &binding.keystrokes) + .flatten() + .collect::>(), + vec![&Keystroke { + ctrl: true, + alt: false, + shift: false, + cmd: false, + function: false, + key: expected_key.to_string() + }], + "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}" + ); + + let keymap_binding = &keymap_bindings[i]; + assert_eq!( + keymap_binding.context_predicate, expected.context_predicate, + "Unexpected context predicate for keymap {i} element: {keymap_binding:?}" + ); + assert_eq!( + keymap_binding.keystrokes, expected.keystrokes, + "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}" + ); + } + } + + #[track_caller] + fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) { + for binding in bindings.iter() { + assert!( + !keymap.binding_disabled(binding), + "{binding:?} should not be disabled in the keymap where was not added" + ); + assert_eq!( + keymap.bindings_for_action(binding.action().id()).count(), + 0, + "{binding:?} should have no actions in the keymap where was not added" + ); + } + } + + #[track_caller] + fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) { + for binding in bindings.iter() { + assert!( + keymap.binding_disabled(binding), + "{binding:?} should be disabled in the keymap" + ); + assert_eq!( + keymap.bindings_for_action(binding.action().id()).count(), + 0, + "{binding:?} should have no actions in the keymap where it was disabled" + ); + } } } diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index be61fea531656fb5a3163ba2804dfce1b4fb0e63..fd60a8f4b5d385eb94b7edf0bfeb407a9dce8c20 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -44,7 +44,7 @@ impl KeymapContext { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum KeymapContextPredicate { Identifier(String), Equal(String, String), diff --git a/crates/gpui/src/keymap_matcher/keystroke.rs b/crates/gpui/src/keymap_matcher/keystroke.rs index ed3c3f69140f2da02268c5d44ac9ecd77d813761..164dea8aba145aec263dd1c95afbbb79824bb83c 100644 --- a/crates/gpui/src/keymap_matcher/keystroke.rs +++ b/crates/gpui/src/keymap_matcher/keystroke.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use anyhow::anyhow; use serde::Deserialize; -#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { pub ctrl: bool, pub alt: bool, From a4bf19c5bd1fba45785c31d93068cd4f5ecb0da1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 17 Jul 2023 20:37:58 +0300 Subject: [PATCH 17/42] Simplify NoAction filtering logic co-authored-by: Max Brunsfeld --- crates/gpui/src/app/window.rs | 8 ++------ crates/gpui/src/keymap_matcher.rs | 7 +++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 1a5611f6aed0fcc17d8509124b1583a9255dcbc0..1dc88d2e717a985be4aed9984597f4abf7b92a1a 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -14,8 +14,8 @@ use crate::{ text_layout::TextLayoutCache, util::post_inc, Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, - Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder, - Subscription, View, ViewContext, ViewHandle, WindowInvalidation, + Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription, + View, ViewContext, ViewHandle, WindowInvalidation, }; use anyhow::{anyhow, bail, Result}; use collections::{HashMap, HashSet}; @@ -430,11 +430,7 @@ impl<'a> WindowContext<'a> { MatchResult::None => false, MatchResult::Pending => true, MatchResult::Matches(matches) => { - let no_action_id = (NoAction {}).id(); for (view_id, action) in matches { - if action.id() == no_action_id { - return false; - } if self.dispatch_action(Some(*view_id), action.as_ref()) { self.keystroke_matcher.clear_pending(); handled_by = Some(action.boxed_clone()); diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index 9e5d45987dd69cd2ef825d232c18c0c51095487a..8cb7d30dfe4a78012023ee724becc09ced6d3212 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -8,7 +8,7 @@ use std::{any::TypeId, fmt::Debug}; use collections::HashMap; use smallvec::SmallVec; -use crate::Action; +use crate::{Action, NoAction}; pub use binding::{Binding, BindingMatchResult}; pub use keymap::Keymap; @@ -81,6 +81,7 @@ impl KeymapMatcher { // The key is the reverse position of the binding in the bindings list so that later bindings // match before earlier ones in the user's config let mut matched_bindings: Vec<(usize, Box)> = Default::default(); + let no_action_id = (NoAction {}).id(); let first_keystroke = self.pending_keystrokes.is_empty(); self.pending_keystrokes.push(keystroke.clone()); @@ -108,7 +109,9 @@ impl KeymapMatcher { match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..]) { BindingMatchResult::Complete(action) => { - matched_bindings.push((*view_id, action)); + if action.id() != no_action_id { + matched_bindings.push((*view_id, action)); + } } BindingMatchResult::Partial => { self.pending_views From 6770aeeb3c4c267f491da5d39feac11d1d853f43 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 14 Jul 2023 12:58:50 -0400 Subject: [PATCH 18/42] After first panic, ignore others and tear down process even if in thread --- crates/zed/src/main.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 4c75d370d517423e395119d2ceb4f3c47b61a21b..ccf381b5b11b21f674c9e068973efe53cfa98af0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -36,7 +36,7 @@ use std::{ path::{Path, PathBuf}, str, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU32, Ordering}, Arc, Weak, }, thread, @@ -405,11 +405,18 @@ struct PanicRequest { token: String, } +static PANIC_COUNT: AtomicU32 = AtomicU32::new(0); + fn init_panic_hook(app: &App, installation_id: Option) { let is_pty = stdout_is_a_pty(); let platform = app.platform(); panic::set_hook(Box::new(move |info| { + let prior_panic_count = PANIC_COUNT.fetch_add(1, Ordering::SeqCst); + if prior_panic_count > 0 { + std::panic::resume_unwind(Box::new(())); + } + let app_version = ZED_APP_VERSION .or_else(|| platform.app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); @@ -464,7 +471,6 @@ fn init_panic_hook(app: &App, installation_id: Option) { if is_pty { if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { eprintln!("{}", panic_data_json); - return; } } else { if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { @@ -481,6 +487,8 @@ fn init_panic_hook(app: &App, installation_id: Option) { } } } + + std::process::abort(); })); } From 6793d4b6b82ceea2f16d4894229ba4187bb81c7a Mon Sep 17 00:00:00 2001 From: Alex Viscreanu Date: Mon, 17 Jul 2023 18:48:57 +0200 Subject: [PATCH 19/42] feat(workspace): show git status on editor tabs --- assets/settings/default.json | 4 ++++ crates/theme/src/theme.rs | 5 +++-- crates/workspace/src/item.rs | 27 +++++++++++++++++++++++++++ crates/workspace/src/pane.rs | 30 +++++++++++++++++++++++++----- crates/workspace/src/workspace.rs | 1 + styles/src/style_tree/tab_bar.ts | 14 ++++++++++++++ 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2e6361ce7ee3a72d8c1d5ad19d82de18b01c33f7..9b8dce10d8bf38cbebea17cd1de486f6365ec03a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -128,6 +128,10 @@ // 4. Save when idle for a certain amount of time: // "autosave": { "after_delay": {"milliseconds": 500} }, "autosave": "off", + // Color tab titles based on the git status of the buffer. + "tabs": { + "git_status": false + }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. "remove_trailing_whitespace_on_save": true, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cdf3cadf594b68e29a6ce45451a1a4229d7eba7e..191fe6332af61711863d06da2855dc93a218b674 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -350,6 +350,7 @@ pub struct Tab { pub icon_close_active: Color, pub icon_dirty: Color, pub icon_conflict: Color, + pub git: GitProjectStatus, } #[derive(Clone, Deserialize, Default, JsonSchema)] @@ -722,12 +723,12 @@ pub struct Scrollbar { pub thumb: ContainerStyle, pub width: f32, pub min_height_factor: f32, - pub git: GitDiffColors, + pub git: FileGitDiffColors, pub selections: Color, } #[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct GitDiffColors { +pub struct FileGitDiffColors { pub inserted: Color, pub modified: Color, pub deleted: Color, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 0c7a478e31a5cd7291054fedfda8f30f7c1d225f..c4b9d8e879557bcd8d479c3ce1bbb12706f62a9c 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -10,6 +10,9 @@ use gpui::{ ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use project::{Project, ProjectEntryId, ProjectPath}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Setting; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -27,6 +30,30 @@ use std::{ }; use theme::Theme; +#[derive(Deserialize)] +pub struct ItemSettings { + pub git_status: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ItemSettingsContent { + git_status: Option, +} + +impl Setting for ItemSettings { + const KEY: Option<&'static str> = Some("tabs"); + + type FileContent = ItemSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + #[derive(Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { CloseItem, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8e6e10748824b373fc93ab8a6af943c9df836e0d..373c30fb529c1b9e854eebd9ac6592c99a2d9586 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3,14 +3,16 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; pub use crate::toolbar::Toolbar; use crate::{ - item::WeakItemHandle, notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, - NewSearch, ToggleZoom, Workspace, WorkspaceSettings, + item::{ItemSettings, WeakItemHandle}, + notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, NewSearch, ToggleZoom, + Workspace, WorkspaceSettings, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use context_menu::{ContextMenu, ContextMenuItem}; use drag_and_drop::{DragAndDrop, Draggable}; use dragged_item_receiver::dragged_item_receiver; +use fs::repository::GitFileStatus; use futures::StreamExt; use gpui::{ actions, @@ -866,6 +868,7 @@ impl Pane { .paths_by_item .get(&item.id()) .and_then(|(_, abs_path)| abs_path.clone()); + self.nav_history .0 .borrow_mut() @@ -1157,6 +1160,11 @@ impl Pane { .zip(self.tab_details(cx)) .enumerate() { + let git_status = item + .project_path(cx) + .and_then(|path| self.project.read(cx).entry_for_path(&path, cx)) + .and_then(|entry| entry.git_status()); + let detail = if detail == 0 { None } else { Some(detail) }; let tab_active = ix == self.active_item_index; @@ -1174,9 +1182,21 @@ impl Pane { let tab_tooltip_text = item.tab_tooltip_text(cx).map(|text| text.into_owned()); + let mut tab_style = theme + .workspace + .tab_bar + .tab_style(pane_active, tab_active) + .clone(); + let should_show_status = settings::get::(cx).git_status; + if should_show_status && git_status != None { + tab_style.label.text.color = match git_status.unwrap() { + GitFileStatus::Added => tab_style.git.inserted, + GitFileStatus::Modified => tab_style.git.modified, + GitFileStatus::Conflict => tab_style.git.conflict, + }; + } + move |mouse_state, cx| { - let tab_style = - theme.workspace.tab_bar.tab_style(pane_active, tab_active); let hovered = mouse_state.hovered(); enum Tab {} @@ -1188,7 +1208,7 @@ impl Pane { ix == 0, detail, hovered, - tab_style, + &tab_style, cx, ) }) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a23efdf628082125d362803959c829319b8f7af7..3e62af8ea65db48618e129cf25bce97417c58c48 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -203,6 +203,7 @@ pub type WorkspaceId = i64; pub fn init_settings(cx: &mut AppContext) { settings::register::(cx); + settings::register::(cx); } pub fn init(app_state: Arc, cx: &mut AppContext) { diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 29769f9bae27ba5cc69cf81dae95f915051f4bd8..e7b04246c492ed7c0faeb9f0cda5644fc8854ec8 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -6,6 +6,8 @@ import { useTheme } from "../common" export default function tab_bar(): any { const theme = useTheme() + const { is_light } = theme + const height = 32 const active_layer = theme.highest @@ -38,6 +40,18 @@ export default function tab_bar(): any { icon_conflict: foreground(layer, "warning"), icon_dirty: foreground(layer, "accent"), + git: { + modified: is_light + ? theme.ramps.yellow(0.6).hex() + : theme.ramps.yellow(0.5).hex(), + inserted: is_light + ? theme.ramps.green(0.45).hex() + : theme.ramps.green(0.5).hex(), + conflict: is_light + ? theme.ramps.red(0.6).hex() + : theme.ramps.red(0.5).hex(), + }, + // When two tabs of the same name are open, a label appears next to them description: { margin: { left: 8 }, From 4efcf492ee2bcc0391ad9e26c851f3a2310c8090 Mon Sep 17 00:00:00 2001 From: Alex Viscreanu Date: Mon, 17 Jul 2023 21:17:04 +0200 Subject: [PATCH 20/42] feat(workspace): add option for moving the tab close button to the left --- assets/settings/default.json | 7 +- crates/theme/src/theme.rs | 4 +- crates/workspace/src/item.rs | 19 +++++ crates/workspace/src/pane.rs | 149 +++++++++++++++++++---------------- 4 files changed, 107 insertions(+), 72 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 9b8dce10d8bf38cbebea17cd1de486f6365ec03a..5cf5f59f76239cbe22738e9471606a883cd5b98e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -128,9 +128,12 @@ // 4. Save when idle for a certain amount of time: // "autosave": { "after_delay": {"milliseconds": 500} }, "autosave": "off", - // Color tab titles based on the git status of the buffer. + // Settings related to the editor's tabs "tabs": { - "git_status": false + // Show git status colors in the editor tabs. + "git_status": false, + // Position of the close button on the editor tabs. + "close_position": "right" }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 191fe6332af61711863d06da2855dc93a218b674..b7a7408bef1f0ad6c5afe9748f1f6033f72e5f6d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -723,12 +723,12 @@ pub struct Scrollbar { pub thumb: ContainerStyle, pub width: f32, pub min_height_factor: f32, - pub git: FileGitDiffColors, + pub git: BufferGitDiffColors, pub selections: Color, } #[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct FileGitDiffColors { +pub struct BufferGitDiffColors { pub inserted: Color, pub modified: Color, pub deleted: Color, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index c4b9d8e879557bcd8d479c3ce1bbb12706f62a9c..460698efb80aa4fc41912810649964477a8a48f4 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -33,11 +33,30 @@ use theme::Theme; #[derive(Deserialize)] pub struct ItemSettings { pub git_status: bool, + pub close_position: ClosePosition, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum ClosePosition { + Left, + #[default] + Right, +} + +impl ClosePosition { + pub fn right(&self) -> bool { + match self { + ClosePosition::Left => false, + ClosePosition::Right => true, + } + } } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ItemSettingsContent { git_status: Option, + close_position: Option, } impl Setting for ItemSettings { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 373c30fb529c1b9e854eebd9ac6592c99a2d9586..f5b96fd421b6cea5e997dc524d7dc95e969d5286 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1370,81 +1370,94 @@ impl Pane { container.border.left = false; } - Flex::row() - .with_child({ - let diameter = 7.0; - let icon_color = if item.has_conflict(cx) { - Some(tab_style.icon_conflict) - } else if item.is_dirty(cx) { - Some(tab_style.icon_dirty) - } else { - None - }; + let buffer_jewel_element = { + let diameter = 7.0; + let icon_color = if item.has_conflict(cx) { + Some(tab_style.icon_conflict) + } else if item.is_dirty(cx) { + Some(tab_style.icon_dirty) + } else { + None + }; - Canvas::new(move |scene, bounds, _, _, _| { - if let Some(color) = icon_color { - let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); - scene.push_quad(Quad { - bounds: square, - background: Some(color), - border: Default::default(), - corner_radius: diameter / 2., - }); - } - }) - .constrained() - .with_width(diameter) - .with_height(diameter) - .aligned() + Canvas::new(move |scene, bounds, _, _, _| { + if let Some(color) = icon_color { + let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); + scene.push_quad(Quad { + bounds: square, + background: Some(color), + border: Default::default(), + corner_radius: diameter / 2., + }); + } }) - .with_child(title.aligned().contained().with_style(ContainerStyle { - margin: Margin { - left: tab_style.spacing, - right: tab_style.spacing, - ..Default::default() - }, + .constrained() + .with_width(diameter) + .with_height(diameter) + .aligned() + }; + + let title_element = title.aligned().contained().with_style(ContainerStyle { + margin: Margin { + left: tab_style.spacing, + right: tab_style.spacing, ..Default::default() - })) - .with_child( - if hovered { - let item_id = item.id(); - enum TabCloseButton {} - let icon = Svg::new("icons/x_mark_8.svg"); - MouseEventHandler::::new(item_id, cx, |mouse_state, _| { - if mouse_state.hovered() { - icon.with_color(tab_style.icon_close_active) - } else { - icon.with_color(tab_style.icon_close) - } - }) - .with_padding(Padding::uniform(4.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, { - let pane = pane.clone(); - move |_, _, cx| { - let pane = pane.clone(); - cx.window_context().defer(move |cx| { - if let Some(pane) = pane.upgrade(cx) { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(item_id, cx).detach_and_log_err(cx); - }); - } + }, + ..Default::default() + }); + + let close_element = if hovered { + let item_id = item.id(); + enum TabCloseButton {} + let icon = Svg::new("icons/x_mark_8.svg"); + MouseEventHandler::::new(item_id, cx, |mouse_state, _| { + if mouse_state.hovered() { + icon.with_color(tab_style.icon_close_active) + } else { + icon.with_color(tab_style.icon_close) + } + }) + .with_padding(Padding::uniform(4.)) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let pane = pane.clone(); + move |_, _, cx| { + let pane = pane.clone(); + cx.window_context().defer(move |cx| { + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item_id, cx).detach_and_log_err(cx); }); } - }) - .into_any_named("close-tab-icon") - .constrained() - } else { - Empty::new().constrained() + }); } - .with_width(tab_style.close_icon_width) - .aligned(), - ) - .contained() - .with_style(container) + }) + .into_any_named("close-tab-icon") .constrained() - .with_height(tab_style.height) - .into_any() + } else { + Empty::new().constrained() + } + .with_width(tab_style.close_icon_width) + .aligned(); + + let close_right = settings::get::(cx).close_position.right(); + + if close_right { + Flex::row() + .with_child(buffer_jewel_element) + .with_child(title_element) + .with_child(close_element) + } else { + Flex::row() + .with_child(close_element) + .with_child(title_element) + .with_child(buffer_jewel_element) + } + .contained() + .with_style(container) + .constrained() + .with_height(tab_style.height) + .into_any() } pub fn render_tab_bar_button< From ede86a686ca5b48a3c4c946ed715ee118aff5506 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 17 Jul 2023 16:06:14 -0400 Subject: [PATCH 21/42] Prevent multiple submissions of the same feedback text Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/feedback/src/feedback_editor.rs | 20 ++++++++++++++++- crates/feedback/src/submit_feedback_button.rs | 22 +++++++++++++++++-- styles/src/style_tree/feedback.ts | 5 +++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index bea398d3eb14a1630632f1d4726bd74fa3bdc39f..47cb90875a4f83cfb5950d68cfaac1f5b38c2de6 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -60,6 +60,7 @@ pub(crate) struct FeedbackEditor { system_specs: SystemSpecs, editor: ViewHandle, project: ModelHandle, + pub allow_submission: bool, } impl FeedbackEditor { @@ -82,10 +83,15 @@ impl FeedbackEditor { system_specs: system_specs.clone(), editor, project, + allow_submission: true, } } pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { + if !self.allow_submission { + return Task::ready(Ok(())); + } + let feedback_text = self.editor.read(cx).text(cx); let feedback_char_count = feedback_text.chars().count(); let feedback_text = feedback_text.trim().to_string(); @@ -122,19 +128,26 @@ impl FeedbackEditor { let answer = answer.recv().await; if answer == Some(0) { + this.update(&mut cx, |feedback_editor, cx| { + feedback_editor.set_allow_submission(false, cx); + }) + .log_err(); + match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await { Ok(_) => { this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed)) .log_err(); } + Err(error) => { log::error!("{}", error); - this.update(&mut cx, |_, cx| { + this.update(&mut cx, |feedback_editor, cx| { cx.prompt( PromptLevel::Critical, FEEDBACK_SUBMISSION_ERROR_TEXT, &["OK"], ); + feedback_editor.set_allow_submission(true, cx); }) .log_err(); } @@ -146,6 +159,11 @@ impl FeedbackEditor { Task::ready(Ok(())) } + fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext) { + self.allow_submission = allow_submission; + cx.notify(); + } + async fn submit_feedback( feedback_text: &str, zed_client: Arc, diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 15f77bd561eb900bbdeb213f4fb37adc3be22697..03a2cd51eddb94774d02438c2d4d23ff25b00806 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -46,10 +46,28 @@ impl View for SubmitFeedbackButton { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); + let allow_submission = self + .active_item + .as_ref() + .map_or(true, |i| i.read(cx).allow_submission); + enum SubmitFeedbackButton {} MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.feedback.submit_button.style_for(state); - Label::new("Submit as Markdown", style.text.clone()) + let text; + let style = if allow_submission { + text = "Submit as Markdown"; + theme.feedback.submit_button.style_for(state) + } else { + text = "Submitting..."; + theme + .feedback + .submit_button + .disabled + .as_ref() + .unwrap_or(&theme.feedback.submit_button.default) + }; + + Label::new(text, style.text.clone()) .contained() .with_style(style.container) }) diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts index 2bb63e951be4764a6cdb33544abf404a84a11d7e..b1bd96e165466521804f86d547664c209c7c4671 100644 --- a/styles/src/style_tree/feedback.ts +++ b/styles/src/style_tree/feedback.ts @@ -33,6 +33,11 @@ export default function feedback(): any { background: background(theme.highest, "on", "hovered"), border: border(theme.highest, "on", "hovered"), }, + disabled: { + ...text(theme.highest, "mono", "on", "disabled"), + background: background(theme.highest, "on", "disabled"), + border: border(theme.highest, "on", "disabled"), + } }, }), button_margin: 8, From 9e44de90afaae38416510d6c4f1e8db0be67ae97 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 17 Jul 2023 14:57:21 -0600 Subject: [PATCH 22/42] Allow ctrl+[ as an alias for escape Also remove unneeded mappings in `g` and `z` modes Fixes: zed-industries/community#358 --- assets/keymaps/vim.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 639daef614332a7103b2195e298fe0fcf8f2eef3..e6421114ec800ec80face8b97f9e4ebaa608027b 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -99,6 +99,10 @@ "vim::SwitchMode", "Normal" ], + "ctrl+[": [ + "vim::SwitchMode", + "Normal" + ], "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -234,10 +238,6 @@ "h": "editor::Hover", "t": "pane::ActivateNextItem", "shift-t": "pane::ActivatePrevItem", - "escape": [ - "vim::SwitchMode", - "Normal" - ], "d": "editor::GoToDefinition" } }, @@ -265,10 +265,6 @@ "t": "editor::ScrollCursorTop", "z": "editor::ScrollCursorCenter", "b": "editor::ScrollCursorBottom", - "escape": [ - "vim::SwitchMode", - "Normal" - ] } }, { @@ -322,7 +318,8 @@ "context": "Editor && vim_mode == insert", "bindings": { "escape": "vim::NormalBefore", - "ctrl-c": "vim::NormalBefore" + "ctrl-c": "vim::NormalBefore", + "ctrl-[": "vim::NormalBefore", } }, { @@ -333,6 +330,10 @@ "escape": [ "vim::SwitchMode", "Normal" + ], + "ctrl+[": [ + "vim::SwitchMode", + "Normal" ] } } From 7fde3614fec4ff8a7806fe5544e3e40ae3a8d114 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:19:35 +0200 Subject: [PATCH 23/42] Remove leftover comment --- crates/zed/src/languages/php.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 2607dc410bc0352eb801d923ebfd30ca7e0f704c..6a01d00300997862532178f24cc6ad518c2c093b 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -45,9 +45,6 @@ impl LspAdapter for IntelephenseLspAdapter { &self, _delegate: &dyn LspAdapterDelegate, ) -> Result> { - // At the time of writing the latest vscode-eslint release was released in 2020 and requires - // special custom LSP protocol extensions be handled to fully initialize. Download the latest - // prerelease instead to sidestep this issue Ok(Box::new(IntelephenseVersion( self.node.npm_package_latest_version("intelephense").await?, )) as Box<_>) From fa529d9590dffffc753d46829ec2ccf2c6db8865 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:21:00 +0200 Subject: [PATCH 24/42] Remove redundant debug impl --- crates/language/src/language.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index f8e25fbce1f85a6ba0aea1e035dc62ccd5a7d3bc..50a0b4b161cb2253602d0c28c769e872a331bd2e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -321,7 +321,7 @@ pub struct CodeLabel { pub filter_range: Range, } -#[derive(Clone, Deserialize, Debug)] +#[derive(Clone, Deserialize)] pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, From a884bd77e11970b6094de04aa1774e9d672a55c0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 14:06:57 +0300 Subject: [PATCH 25/42] Slightly tidy up vector_db code Avoid panicking when truncating code with special chars --- crates/vector_store/src/embedding.rs | 12 +++++++----- crates/vector_store/src/parsing.rs | 7 ++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/vector_store/src/embedding.rs b/crates/vector_store/src/embedding.rs index ea349c8afa4a8d908d60760f8ff1eb6839e3120b..2ddade6bb290a7cf93f14ba2bdf4eceb89554c89 100644 --- a/crates/vector_store/src/embedding.rs +++ b/crates/vector_store/src/embedding.rs @@ -67,11 +67,13 @@ impl EmbeddingProvider for DummyEmbeddings { } } +const INPUT_LIMIT: usize = 8190; + impl OpenAIEmbeddings { - async fn truncate(span: String) -> String { + fn truncate(span: String) -> String { let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref()); - if tokens.len() > 8190 { - tokens.truncate(8190); + if tokens.len() > INPUT_LIMIT { + tokens.truncate(INPUT_LIMIT); let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone()); if result.is_ok() { let transformed = result.unwrap(); @@ -80,7 +82,7 @@ impl OpenAIEmbeddings { } } - return span.to_string(); + span } async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result> { @@ -137,7 +139,7 @@ impl EmbeddingProvider for OpenAIEmbeddings { // Don't worry about delaying bad request, as we can assume // we haven't been rate limited yet. for span in spans.iter_mut() { - *span = Self::truncate(span.to_string()).await; + *span = Self::truncate(span.to_string()); } } StatusCode::OK => { diff --git a/crates/vector_store/src/parsing.rs b/crates/vector_store/src/parsing.rs index 91dcf699f8c3add9088b2af4f0d4df59b7551ac2..12e590b35f464ad960a6e779b7c56d872760657e 100644 --- a/crates/vector_store/src/parsing.rs +++ b/crates/vector_store/src/parsing.rs @@ -63,7 +63,7 @@ impl CodeContextRetriever { ) { // log::info!("-----MATCH-----"); - let mut name: Vec<&str> = vec![]; + let mut name = Vec::new(); let mut item: Option<&str> = None; let mut offset: Option = None; for capture in mat.captures { @@ -91,11 +91,8 @@ impl CodeContextRetriever { .replace("", &pending_file.language.name().to_lowercase()) .replace("", item.unwrap()); - let mut truncated_span = context_span.clone(); - truncated_span.truncate(100); - // log::info!("Name: {:?}", name); - // log::info!("Span: {:?}", truncated_span); + // log::info!("Span: {:?}", util::truncate(&context_span, 100)); context_spans.push(context_span); documents.push(Document { From aee008440b0fe18e1024e0d14f5e58debfff1ae1 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 18 Jul 2023 10:02:14 -0400 Subject: [PATCH 26/42] Detect Node broken-ness initially This will help cases where Node is broken causing Copilot to fail to start but because it doesn't install via NPM we would not have caught it prior. Co-Authored-By: Antonio Scandurra --- crates/node_runtime/src/node_runtime.rs | 45 +++++++++---------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 27a763e7f8851722907749c1a28a958827c3f9f5..de9cf501ac7515e0ac8352e408c3f1e7dd22fa89 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -6,13 +6,13 @@ use futures::{future::Shared, FutureExt}; use gpui::{executor::Background, Task}; use serde::Deserialize; use smol::{fs, io::BufReader, process::Command}; -use std::process::Output; +use std::process::{Output, Stdio}; use std::{ env::consts, path::{Path, PathBuf}, sync::{Arc, OnceLock}, }; -use util::{http::HttpClient, ResultExt}; +use util::http::HttpClient; const VERSION: &str = "v18.15.0"; @@ -84,9 +84,8 @@ impl NodeRuntime { }; let installation_path = self.install_if_needed().await?; - let mut output = attempt(installation_path).await; + let mut output = attempt(installation_path.clone()).await; if output.is_err() { - let installation_path = self.reinstall().await?; output = attempt(installation_path).await; if output.is_err() { return Err(anyhow!( @@ -158,29 +157,6 @@ impl NodeRuntime { Ok(()) } - async fn reinstall(&self) -> Result { - log::info!("beginnning to reinstall Node runtime"); - let mut installation_path = self.installation_path.lock().await; - - if let Some(task) = installation_path.as_ref().cloned() { - if let Ok(installation_path) = task.await { - smol::fs::remove_dir_all(&installation_path) - .await - .context("node dir removal") - .log_err(); - } - } - - let http = self.http.clone(); - let task = self - .background - .spawn(async move { Self::install(http).await.map_err(Arc::new) }) - .shared(); - - *installation_path = Some(task.clone()); - task.await.map_err(|e| anyhow!("{}", e)) - } - async fn install_if_needed(&self) -> Result { let task = self .installation_path @@ -209,8 +185,19 @@ impl NodeRuntime { let node_containing_dir = util::paths::SUPPORT_DIR.join("node"); let node_dir = node_containing_dir.join(folder_name); let node_binary = node_dir.join("bin/node"); - - if fs::metadata(&node_binary).await.is_err() { + let npm_file = node_dir.join("bin/npm"); + + let result = Command::new(&node_binary) + .arg(npm_file) + .arg("--version") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .await; + let valid = matches!(result, Ok(status) if status.success()); + + if !valid { _ = fs::remove_dir_all(&node_containing_dir).await; fs::create_dir(&node_containing_dir) .await From bf2dcd4582adb2332c1298eff7d5d34185baf9cc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 18 Jul 2023 12:15:03 -0400 Subject: [PATCH 27/42] Update cargo.toml --- Cargo.toml | 1 + crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7a32938c378e8bbbfcd9d3fad6de1a79366c0b43..fa824115cb82b02c2428f2f1bd9889a0b93a5757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ tree-sitter = "0.20" unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" +tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 478cdf226618473c38361999496a3021d82f2679..f749fb6e68df122a859f2a983dc2d5fa87afe27a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -104,7 +104,7 @@ thiserror.workspace = true tiny_http = "0.8" toml.workspace = true tree-sitter.workspace = true -tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } +tree-sitter-bash.workspace = true tree-sitter-c.workspace = true tree-sitter-cpp.workspace = true tree-sitter-css.workspace = true From 018eb06091d437fe293024cd6d9a75c129c59b8d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 18 Jul 2023 12:32:53 -0400 Subject: [PATCH 28/42] Add is_staff to events --- crates/client/src/telemetry.rs | 2 ++ script/start-local-collaboration | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 959f4cc7835bcd159b00d05940b8b6cff32db3c8..dc5154d96f96f9657947da92af6dae1ac873012a 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -40,6 +40,7 @@ lazy_static! { struct ClickhouseEventRequestBody { token: &'static str, installation_id: Option>, + is_staff: Option, app_version: Option>, os_name: &'static str, os_version: Option>, @@ -224,6 +225,7 @@ impl Telemetry { &ClickhouseEventRequestBody { token: ZED_SECRET_CLIENT_TOKEN, installation_id: state.installation_id.clone(), + is_staff: state.is_staff.clone(), app_version: state.app_version.clone(), os_name: state.os_name, os_version: state.os_version.clone(), diff --git a/script/start-local-collaboration b/script/start-local-collaboration index b702fb4e02f9d0e3ae2a70ca99054b7bea2a711b..6c256abefe15119f3a8561eaba3824580fe57ac4 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -45,7 +45,7 @@ position_2=${half_width},${y} # Authenticate using the collab server's admin secret. export ZED_STATELESS=1 export ZED_ADMIN_API_TOKEN=secret -export ZED_SERVER_URL=http://localhost:8080 +export ZED_SERVER_URL=http://localhost:3000 export ZED_WINDOW_SIZE=${half_width},${height} cargo build From 7cb5326ba08f33e31f44d8780860f955e743eced Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 18 Jul 2023 12:43:27 -0400 Subject: [PATCH 29/42] Fix ZED_SERVER_URL port number This change accidentally slipped into https://github.com/zed-industries/zed/pull/2746 --- script/start-local-collaboration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/start-local-collaboration b/script/start-local-collaboration index 6c256abefe15119f3a8561eaba3824580fe57ac4..b702fb4e02f9d0e3ae2a70ca99054b7bea2a711b 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -45,7 +45,7 @@ position_2=${half_width},${y} # Authenticate using the collab server's admin secret. export ZED_STATELESS=1 export ZED_ADMIN_API_TOKEN=secret -export ZED_SERVER_URL=http://localhost:3000 +export ZED_SERVER_URL=http://localhost:8080 export ZED_WINDOW_SIZE=${half_width},${height} cargo build From 9aeb970f09845061d03fe1e33c64f6311ae7f7fe Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 18:50:55 +0300 Subject: [PATCH 30/42] Ignore empty hover contents, trim final hover label text --- crates/editor/src/hover_popover.rs | 116 ++++++++++++++++++++++++++++- crates/project/src/project.rs | 6 ++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7a203d54a96764c5ccca9e354f85e648a8529c00..92ed9ef77d52225eb60e1ff1c119bcefdb2f3385 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -198,7 +198,7 @@ fn show_hover( // Construct new hover popover from hover request let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { - if hover_result.contents.is_empty() { + if hover_result.is_empty() { return None; } @@ -420,7 +420,7 @@ fn render_blocks( RenderedInfo { theme_id, - text, + text: text.trim().to_string(), highlights, region_ranges, regions, @@ -816,6 +816,118 @@ mod tests { }); } + #[gpui::test] + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("regular text for hover to show".to_string()), + lsp::MarkedString::String("".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "".to_string(), + }), + ]), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "regular text for hover to show".to_string(), + kind: HoverBlockKind::Markdown, + }], + "No empty string hovers should be shown" + ); + }); + } + + #[gpui::test] + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; + let markdown_string = format!("\n```rust\n{code_str}```"); + + let closure_markdown_string = markdown_string.clone(); + cx.handle_request::(move |_, _, _| { + let future_markdown_string = closure_markdown_string.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: future_markdown_string, + }), + range: Some(symbol_range), + })) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, cx| { + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; + assert_eq!( + blocks, + vec![HoverBlock { + text: markdown_string, + kind: HoverBlockKind::Markdown, + }], + ); + + let style = editor.style(cx); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + assert_eq!( + rendered.text, + code_str.trim(), + "Should not have extra line breaks at end of rendered hover" + ); + }); + } + #[gpui::test] async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b931560d25ed5393b70ca419c5aaf2b0e7b64bb0..5bb8af3f389d9787a55734727e15c8310c3b99fa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -425,6 +425,12 @@ pub struct Hover { pub language: Option>, } +impl Hover { + pub fn is_empty(&self) -> bool { + self.contents.iter().all(|block| block.text.is_empty()) + } +} + #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); From 75d900704eb838ea2e07f90f012e4871049b398c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 12 Jul 2023 22:08:44 +0300 Subject: [PATCH 31/42] Refactor terminal highlights and open mechanisms Co-authored-by: Mikayla --- crates/terminal/src/terminal.rs | 14 ++++++++------ crates/terminal_view/src/terminal_element.rs | 4 ++++ crates/terminal_view/src/terminal_view.rs | 8 +++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 39e77b590b1077fd9cc917d2538ec0e563df453a..739f52db01ce92a34d94e2a3f034098934ffa480 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -75,11 +75,11 @@ const DEBUG_LINE_HEIGHT: f32 = 5.; // Regex Copied from alacritty's ui_config.rs lazy_static! { - static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } ///Upward flowing events, for changing the title and such -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum Event { TitleChanged, BreadcrumbsChanged, @@ -88,6 +88,7 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, + Open(String), } #[derive(Clone)] @@ -806,6 +807,7 @@ impl Terminal { term.scroll_to_point(*point); self.refresh_hyperlink(); } + // We need to change this to a word boundary check InternalEvent::FindHyperlink(position, open) => { let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); @@ -848,7 +850,7 @@ impl Terminal { let url_match = min_index..=max_index; Some((url, url_match)) - } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { + } else if let Some(url_match) = regex_match_at(term, point, &WORD_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); Some((url, url_match)) @@ -858,7 +860,7 @@ impl Terminal { if let Some((url, url_match)) = found_url { if *open { - cx.platform().open_url(url.as_str()); + cx.emit(Event::Open(url)) } else { self.update_hyperlink(prev_hyperlink, url, url_match); } @@ -1089,7 +1091,7 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else { + } else if e.cmd { self.hyperlink_from_position(Some(position)); } } @@ -1208,7 +1210,7 @@ impl Terminal { let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { cx.platform().open_url(link.uri()); - } else { + } else if e.cmd { self.events .push_back(InternalEvent::FindHyperlink(position, true)); } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index b92059f5d605bc16c6578b254ce0431113cce200..72517b5a10485999c2d66153f9fb699feb382e75 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -392,6 +392,10 @@ impl TerminalElement { let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); + // 1. Get file:linenumber syntax working ✔️ + // 2. Switch terminal to look for word boundaries, on cmd-hover + // 3. Send those query strings to the resolver thing above + // Terminal Emulator controlled behavior: region = region // Start selections diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3dd401e392c32481f2f5da76cd7acebed1d79cd3..5f6fabcf1c596e144e4a8a61988a036d650619e6 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -158,7 +158,13 @@ impl TerminalView { .detach(); } } - _ => cx.emit(*event), + Event::Open(url) => { + // Get a workspace pointer from the new() function above + // Guess for project path or url + // Either run open buffer action OR platform open depending on whatever happens + cx.platform().open_url(url); + } + _ => cx.emit(event.clone()), }) .detach(); From f52722b6a4bf1a5db39bbffa73c858f1a1ccdc11 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 14 Jul 2023 18:26:35 +0300 Subject: [PATCH 32/42] Properly handle Cmd press for terminal highlights --- crates/terminal/src/terminal.rs | 24 ++++++++++++++++---- crates/terminal_view/src/terminal_element.rs | 4 ---- crates/terminal_view/src/terminal_view.rs | 17 +++++++++++++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 739f52db01ce92a34d94e2a3f034098934ffa480..fcbd02096c264d12aebda66e975d1ed93bf76e78 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -51,7 +51,7 @@ use gpui::{ fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, - platform::{MouseButton, MouseMovedEvent, TouchPhase}, + platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, AppContext, ClipboardItem, Entity, ModelContext, Task, }; @@ -494,6 +494,7 @@ impl TerminalBuilder { last_mouse_position: None, next_link_id: 0, selection_phase: SelectionPhase::Ended, + cmd_pressed: false, }; Ok(TerminalBuilder { @@ -638,6 +639,7 @@ pub struct Terminal { scroll_px: f32, next_link_id: usize, selection_phase: SelectionPhase, + cmd_pressed: bool, } impl Terminal { @@ -807,7 +809,6 @@ impl Terminal { term.scroll_to_point(*point); self.refresh_hyperlink(); } - // We need to change this to a word boundary check InternalEvent::FindHyperlink(position, open) => { let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); @@ -966,6 +967,21 @@ impl Terminal { } } + pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool { + let cmd = modifiers.cmd; + let changed = self.cmd_pressed != cmd; + if changed { + self.cmd_pressed = cmd; + if cmd { + self.refresh_hyperlink(); + } else { + self.last_content.last_hovered_hyperlink.take(); + } + } + + changed + } + ///Paste text into the terminal pub fn paste(&mut self, text: &str) { let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) { @@ -1091,7 +1107,7 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else if e.cmd { + } else if self.cmd_pressed { self.hyperlink_from_position(Some(position)); } } @@ -1210,7 +1226,7 @@ impl Terminal { let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { cx.platform().open_url(link.uri()); - } else if e.cmd { + } else if self.cmd_pressed { self.events .push_back(InternalEvent::FindHyperlink(position, true)); } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 72517b5a10485999c2d66153f9fb699feb382e75..b92059f5d605bc16c6578b254ce0431113cce200 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -392,10 +392,6 @@ impl TerminalElement { let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); - // 1. Get file:linenumber syntax working ✔️ - // 2. Switch terminal to look for word boundaries, on cmd-hover - // 3. Send those query strings to the resolver thing above - // Terminal Emulator controlled behavior: region = region // Start selections diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5f6fabcf1c596e144e4a8a61988a036d650619e6..4dbeb190339a0d143dc86d5f3d7d2f08903d7565 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -11,7 +11,7 @@ use gpui::{ geometry::vector::Vector2F, impl_actions, keymap_matcher::{KeymapContext, Keystroke}, - platform::KeyDownEvent, + platform::{KeyDownEvent, ModifiersChangedEvent}, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -159,6 +159,7 @@ impl TerminalView { } } Event::Open(url) => { + // TODO kb // Get a workspace pointer from the new() function above // Guess for project path or url // Either run open buffer action OR platform open depending on whatever happens @@ -399,6 +400,20 @@ impl View for TerminalView { cx.notify(); } + fn modifiers_changed( + &mut self, + event: &ModifiersChangedEvent, + cx: &mut ViewContext, + ) -> bool { + let handled = self + .terminal() + .update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); + if handled { + cx.notify(); + } + handled + } + fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext) -> bool { self.clear_bel(cx); self.pause_cursor_blinking(cx); From 23f25562b535abcb49dc938fefe1ab33bb70cb01 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 14 Jul 2023 19:34:41 +0300 Subject: [PATCH 33/42] Map initial approach to string opening --- crates/terminal/src/terminal.rs | 5 ++- crates/terminal_view/src/terminal_panel.rs | 12 ++++-- crates/terminal_view/src/terminal_view.rs | 46 +++++++++++++++++----- crates/zed/src/main.rs | 9 ++++- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index fcbd02096c264d12aebda66e975d1ed93bf76e78..9d73f7d1269c4fa86ddca620660b5ced535f5eb3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -72,9 +72,10 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -// Regex Copied from alacritty's ui_config.rs - lazy_static! { + // Regex Copied from alacritty's ui_config.rs + pub static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ad61903a9d83e98e619ea5330e98e25021a8e2cc..6ad321c735deb00bb557058db5082761da9f7bbb 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -261,10 +261,14 @@ impl TerminalPanel { .create_terminal(working_directory, window_id, cx) .log_err() }) { - let terminal = - Box::new(cx.add_view(|cx| { - TerminalView::new(terminal, workspace.database_id(), cx) - })); + let terminal = Box::new(cx.add_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + })); pane.update(cx, |pane, cx| { let focus = pane.has_focus(); pane.add_item(terminal, true, focus, None, cx); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 4dbeb190339a0d143dc86d5f3d7d2f08903d7565..7038eb284b62248ef447f9e867f69c9961cd43bd 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -32,7 +32,7 @@ use terminal::{ }, Event, Terminal, TerminalBlink, WorkingDirectory, }; -use util::ResultExt; +use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, notifications::NotifyResultExt, @@ -117,19 +117,27 @@ impl TerminalView { .notify_err(workspace, cx); if let Some(terminal) = terminal { - let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + let view = cx.add_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + }); workspace.add_item(Box::new(view), cx) } } pub fn new( terminal: ModelHandle, + workspace: WeakViewHandle, workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { let view_id = cx.view_id(); cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&terminal, |this, _, event, cx| match event { + cx.subscribe(&terminal, move |this, _, event, cx| match event { Event::Wakeup => { if !cx.is_self_focused() { this.has_new_content = true; @@ -158,12 +166,30 @@ impl TerminalView { .detach(); } } - Event::Open(url) => { - // TODO kb - // Get a workspace pointer from the new() function above - // Guess for project path or url - // Either run open buffer action OR platform open depending on whatever happens - cx.platform().open_url(url); + Event::Open(maybe_url_or_path) => { + // TODO kb, what is the API for this? + // terminal::URL_REGEX.matches(maybe_url_or_path) + if maybe_url_or_path.starts_with("http") { + cx.platform().open_url(maybe_url_or_path); + } else if let Some(workspace) = workspace.upgrade(cx) { + let path_like = + PathLikeWithPosition::parse_str(maybe_url_or_path.as_str(), |path_str| { + Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) + }) + .expect("infallible"); + let maybe_path = path_like.path_like; + workspace.update(cx, |workspace, cx| { + if false { //&& workspace.contains_path() { + // + } else if maybe_path.exists() { + workspace + .open_abs_path(maybe_path, true, cx) + .detach_and_log_err(cx); + } + }); + } + + // TODO kb let terminal know if we cannot open the string } _ => cx.emit(event.clone()), }) @@ -639,7 +665,7 @@ impl Item for TerminalView { project.create_terminal(cwd, window_id, cx) })?; Ok(pane.update(&mut cx, |_, cx| { - cx.add_view(|cx| TerminalView::new(terminal, workspace_id, cx)) + cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx)) })?) }) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ccf381b5b11b21f674c9e068973efe53cfa98af0..8f528771c9ed95cb95ce7d25ce4e8a2762cac1ea 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -895,7 +895,14 @@ pub fn dock_default_item_factory( }) .notify_err(workspace, cx)?; - let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + let terminal_view = cx.add_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + }); Some(Box::new(terminal_view)) } From 6123c67de94cec57f687af5746420a94a5bc947b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 15 Jul 2023 01:11:20 +0300 Subject: [PATCH 34/42] Detect and open URLs properly --- crates/terminal/src/terminal.rs | 92 +++++++++++++------- crates/terminal_view/src/terminal_element.rs | 18 ++-- crates/terminal_view/src/terminal_view.rs | 14 +-- 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9d73f7d1269c4fa86ddca620660b5ced535f5eb3..fae79eda1dc92c9c328562ee5b23a6a8fad88e51 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -74,7 +74,7 @@ const DEBUG_LINE_HEIGHT: f32 = 5.; lazy_static! { // Regex Copied from alacritty's ui_config.rs - pub static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } @@ -89,7 +89,10 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - Open(String), + Open { + is_url: bool, + maybe_url_or_path: String, + }, } #[derive(Clone)] @@ -592,7 +595,14 @@ pub struct TerminalContent { pub cursor: RenderableCursor, pub cursor_char: char, pub size: TerminalSize, - pub last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, + pub last_hovered_word: Option, +} + +#[derive(Clone)] +pub struct HoveredWord { + pub word: String, + pub word_match: RangeInclusive, + pub id: usize, } impl Default for TerminalContent { @@ -609,7 +619,7 @@ impl Default for TerminalContent { }, cursor_char: Default::default(), size: Default::default(), - last_hovered_hyperlink: None, + last_hovered_word: None, } } } @@ -626,7 +636,7 @@ pub struct Terminal { events: VecDeque, /// This is only used for mouse mode cell change detection last_mouse: Option<(Point, AlacDirection)>, - /// This is only used for terminal hyperlink checking + /// This is only used for terminal hovered word checking last_mouse_position: Option, pub matches: Vec>, pub last_content: TerminalContent, @@ -773,7 +783,7 @@ impl Terminal { } InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); - self.refresh_hyperlink(); + self.refresh_hovered_word(); } InternalEvent::SetSelection(selection) => { term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); @@ -808,10 +818,10 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => { term.scroll_to_point(*point); - self.refresh_hyperlink(); + self.refresh_hovered_word(); } InternalEvent::FindHyperlink(position, open) => { - let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); + let prev_hovered_word = self.last_content.last_hovered_word.take(); let point = grid_point( *position, @@ -851,41 +861,57 @@ impl Terminal { let url = link.unwrap().uri().to_owned(); let url_match = min_index..=max_index; - Some((url, url_match)) - } else if let Some(url_match) = regex_match_at(term, point, &WORD_REGEX) { - let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + Some((url, true, url_match)) + } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { + let maybe_url_or_path = + term.bounds_to_string(*word_match.start(), *word_match.end()); + let is_url = regex_match_at(term, point, &URL_REGEX).is_some(); - Some((url, url_match)) + Some((maybe_url_or_path, is_url, word_match)) } else { None }; - if let Some((url, url_match)) = found_url { + if let Some((maybe_url_or_path, is_url, url_match)) = found_url { if *open { - cx.emit(Event::Open(url)) + cx.emit(Event::Open { + is_url, + maybe_url_or_path, + }) } else { - self.update_hyperlink(prev_hyperlink, url, url_match); + self.update_selected_word(prev_hovered_word, maybe_url_or_path, url_match); } } } } } - fn update_hyperlink( + fn update_selected_word( &mut self, - prev_hyperlink: Option<(String, RangeInclusive, usize)>, - url: String, - url_match: RangeInclusive, + prev_word: Option, + word: String, + word_match: RangeInclusive, ) { - if let Some(prev_hyperlink) = prev_hyperlink { - if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match { - self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2)); + if let Some(prev_word) = prev_word { + if prev_word.word == word && prev_word.word_match == word_match { + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: prev_word.id, + }); } else { - self.last_content.last_hovered_hyperlink = - Some((url, url_match, self.next_link_id())); + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: self.next_link_id(), + }); } } else { - self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id())); + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: self.next_link_id(), + }); } } @@ -974,9 +1000,9 @@ impl Terminal { if changed { self.cmd_pressed = cmd; if cmd { - self.refresh_hyperlink(); + self.refresh_hovered_word(); } else { - self.last_content.last_hovered_hyperlink.take(); + self.last_content.last_hovered_word.take(); } } @@ -1054,7 +1080,7 @@ impl Terminal { cursor: content.cursor, cursor_char: term.grid()[content.cursor.point].c, size: last_content.size, - last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(), + last_hovered_word: last_content.last_hovered_word.clone(), } } @@ -1109,13 +1135,13 @@ impl Terminal { } } } else if self.cmd_pressed { - self.hyperlink_from_position(Some(position)); + self.word_from_position(Some(position)); } } - fn hyperlink_from_position(&mut self, position: Option) { + fn word_from_position(&mut self, position: Option) { if self.selection_phase == SelectionPhase::Selecting { - self.last_content.last_hovered_hyperlink = None; + self.last_content.last_hovered_word = None; } else if let Some(position) = position { self.events .push_back(InternalEvent::FindHyperlink(position, false)); @@ -1274,8 +1300,8 @@ impl Terminal { } } - pub fn refresh_hyperlink(&mut self) { - self.hyperlink_from_position(self.last_mouse_position); + pub fn refresh_hovered_word(&mut self) { + self.word_from_position(self.last_mouse_position); } fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index b92059f5d605bc16c6578b254ce0431113cce200..aabfb999225df1f93d0b6b8072b9a79e52db677c 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -583,17 +583,23 @@ impl Element for TerminalElement { let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); - terminal.last_content.last_hovered_hyperlink.clone() + terminal.last_content.last_hovered_word.clone() }); - let hyperlink_tooltip = last_hovered_hyperlink.map(|(uri, _, id)| { + let hyperlink_tooltip = last_hovered_hyperlink.map(|hovered_word| { let mut tooltip = Overlay::new( Empty::new() .contained() .constrained() .with_width(dimensions.width()) .with_height(dimensions.height()) - .with_tooltip::(id, uri, None, tooltip_style, cx), + .with_tooltip::( + hovered_word.id, + hovered_word.word, + None, + tooltip_style, + cx, + ), ) .with_position_mode(gpui::elements::OverlayPositionMode::Local) .into_any(); @@ -613,7 +619,7 @@ impl Element for TerminalElement { cursor_char, selection, cursor, - last_hovered_hyperlink, + last_hovered_word, .. } = { &terminal_handle.read(cx).last_content }; @@ -634,9 +640,9 @@ impl Element for TerminalElement { &terminal_theme, cx.text_layout_cache(), cx.font_cache(), - last_hovered_hyperlink + last_hovered_word .as_ref() - .map(|(_, range, _)| (link_style, range)), + .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7038eb284b62248ef447f9e867f69c9961cd43bd..476ff49a57efae8dc2f8838ef26b9de3a9828d9e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -166,10 +166,11 @@ impl TerminalView { .detach(); } } - Event::Open(maybe_url_or_path) => { - // TODO kb, what is the API for this? - // terminal::URL_REGEX.matches(maybe_url_or_path) - if maybe_url_or_path.starts_with("http") { + Event::Open { + is_url, + maybe_url_or_path, + } => { + if *is_url { cx.platform().open_url(maybe_url_or_path); } else if let Some(workspace) = workspace.upgrade(cx) { let path_like = @@ -180,10 +181,11 @@ impl TerminalView { let maybe_path = path_like.path_like; workspace.update(cx, |workspace, cx| { if false { //&& workspace.contains_path() { - // + // TODO kb } else if maybe_path.exists() { + let visible = maybe_path.is_dir(); workspace - .open_abs_path(maybe_path, true, cx) + .open_abs_path(maybe_path, visible, cx) .detach_and_log_err(cx); } }); From 6349d90cac88808809184ca012d8f5ecf4a68953 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 00:37:03 +0300 Subject: [PATCH 35/42] Properly open project directories --- crates/project/src/project.rs | 4 +++- crates/terminal_view/src/terminal_view.rs | 24 ++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5bb8af3f389d9787a55734727e15c8310c3b99fa..b3255df812a80736ffed7b9ebdc32239a3414167 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1915,7 +1915,9 @@ impl Project { return; } - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let abs_path = file.abs_path(cx); + let uri = lsp::Url::from_file_path(&abs_path) + .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}")); let initial_snapshot = buffer.text_snapshot(); let language = buffer.language().cloned(); let worktree_id = file.worktree_id(cx); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 476ff49a57efae8dc2f8838ef26b9de3a9828d9e..49f334b2d9d785a342321d4afac723fd3d76ca3c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -180,18 +180,28 @@ impl TerminalView { .expect("infallible"); let maybe_path = path_like.path_like; workspace.update(cx, |workspace, cx| { - if false { //&& workspace.contains_path() { - // TODO kb - } else if maybe_path.exists() { - let visible = maybe_path.is_dir(); + let potential_abs_paths = if maybe_path.is_absolute() { + vec![maybe_path] + } else { workspace - .open_abs_path(maybe_path, visible, cx) - .detach_and_log_err(cx); + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) + .collect() + }; + + for path in potential_abs_paths { + if path.exists() { + let visible = path.is_dir(); + workspace + .open_abs_path(path, visible, cx) + .detach_and_log_err(cx); + break; + } } }); } - // TODO kb let terminal know if we cannot open the string + // TODO kb let terminal know if we cannot open the string + remove the error message when folder open returns None } _ => cx.emit(event.clone()), }) From 82a9d53c8aca67ca511baf0c94300608ab6a0ec8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 01:13:27 +0300 Subject: [PATCH 36/42] Only highlight the openable things --- crates/terminal/src/terminal.rs | 50 ++++++++++--- crates/terminal_view/src/terminal_view.rs | 85 ++++++++++++++--------- 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index fae79eda1dc92c9c328562ee5b23a6a8fad88e51..17bfa1550e544943aad1ae86fcffc49fd4dd9963 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -33,6 +33,7 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use smol::channel::Sender; use util::truncate_and_trailoff; use std::{ @@ -89,10 +90,12 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - Open { - is_url: bool, - maybe_url_or_path: String, + OpenUrl(String), + ProbePathOpen { + maybe_path: String, + can_open_tx: Sender, }, + OpenPath(String), } #[derive(Clone)] @@ -874,12 +877,43 @@ impl Terminal { if let Some((maybe_url_or_path, is_url, url_match)) = found_url { if *open { - cx.emit(Event::Open { - is_url, - maybe_url_or_path, - }) + let event = if is_url { + Event::OpenUrl(maybe_url_or_path) + } else { + Event::OpenPath(maybe_url_or_path) + }; + cx.emit(event); } else { - self.update_selected_word(prev_hovered_word, maybe_url_or_path, url_match); + if is_url { + self.update_selected_word( + prev_hovered_word, + maybe_url_or_path, + url_match, + ); + } else { + let (can_open_tx, can_open_rx) = smol::channel::bounded(1); + cx.emit(Event::ProbePathOpen { + maybe_path: maybe_url_or_path.clone(), + can_open_tx, + }); + + cx.spawn(|terminal, mut cx| async move { + let can_open = can_open_rx.recv().await.unwrap_or(false); + terminal.update(&mut cx, |terminal, cx| { + if can_open { + terminal.update_selected_word( + prev_hovered_word, + maybe_url_or_path, + url_match, + ); + } else { + terminal.last_content.last_hovered_word.take(); + } + cx.notify(); + }); + }) + .detach(); + }; } } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 49f334b2d9d785a342321d4afac723fd3d76ca3c..f0371fcb3eb92ef70789ca255ed80684686af468 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -166,42 +166,27 @@ impl TerminalView { .detach(); } } - Event::Open { - is_url, - maybe_url_or_path, + Event::ProbePathOpen { + maybe_path, + can_open_tx, } => { - if *is_url { - cx.platform().open_url(maybe_url_or_path); - } else if let Some(workspace) = workspace.upgrade(cx) { - let path_like = - PathLikeWithPosition::parse_str(maybe_url_or_path.as_str(), |path_str| { - Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) - }) - .expect("infallible"); - let maybe_path = path_like.path_like; - workspace.update(cx, |workspace, cx| { - let potential_abs_paths = if maybe_path.is_absolute() { - vec![maybe_path] - } else { + let can_open = !possible_open_targets(&workspace, maybe_path, cx).is_empty(); + can_open_tx.send_blocking(can_open).ok(); + } + Event::OpenUrl(url) => cx.platform().open_url(url), + Event::OpenPath(maybe_path) => { + let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); + if let Some(path) = potential_abs_paths.into_iter().next() { + // TODO kb change selections using path_like row & column + let visible = path.path_like.is_dir(); + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { workspace - .worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) - .collect() - }; - - for path in potential_abs_paths { - if path.exists() { - let visible = path.is_dir(); - workspace - .open_abs_path(path, visible, cx) - .detach_and_log_err(cx); - break; - } - } - }); + .open_abs_path(path.path_like, visible, cx) + .detach_and_log_err(cx); + }); + } } - - // TODO kb let terminal know if we cannot open the string + remove the error message when folder open returns None } _ => cx.emit(event.clone()), }) @@ -389,6 +374,40 @@ impl TerminalView { } } +fn possible_open_targets( + workspace: &WeakViewHandle, + maybe_path: &String, + cx: &mut ViewContext<'_, '_, TerminalView>, +) -> Vec> { + let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| { + Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) + }) + .expect("infallible"); + let maybe_path = path_like.path_like; + let potential_abs_paths = if maybe_path.is_absolute() { + vec![maybe_path] + } else if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) + .collect() + }) + } else { + Vec::new() + }; + + potential_abs_paths + .into_iter() + .filter(|path| path.exists()) + .map(|path| PathLikeWithPosition { + path_like: path, + row: path_like.row, + column: path_like.column, + }) + .collect() +} + pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { let searcher = match query { project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query), From 94358ffb16491c204b14fc6976709d6f2ec189ad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 01:31:14 +0300 Subject: [PATCH 37/42] Use lines and columns from the file url strings --- crates/terminal_view/src/terminal_view.rs | 46 +++++++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index f0371fcb3eb92ef70789ca255ed80684686af468..7c79a76cff34432b272a5f75cf99f0769721ef8a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -3,8 +3,10 @@ pub mod terminal_element; pub mod terminal_panel; use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; +use anyhow::Context; use context_menu::{ContextMenu, ContextMenuItem}; use dirs::home_dir; +use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack}, @@ -15,6 +17,7 @@ use gpui::{ AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::Bias; use project::{LocalWorktree, Project}; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; @@ -177,15 +180,42 @@ impl TerminalView { Event::OpenPath(maybe_path) => { let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); if let Some(path) = potential_abs_paths.into_iter().next() { - // TODO kb change selections using path_like row & column let visible = path.path_like.is_dir(); - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path(path.path_like, visible, cx) - .detach_and_log_err(cx); - }); - } + let task_workspace = workspace.clone(); + cx.spawn(|_, mut cx| async move { + let opened_item = task_workspace + .update(&mut cx, |workspace, cx| { + workspace.open_abs_path(path.path_like, visible, cx) + }) + .context("workspace update")? + .await + .context("workspace update")?; + if let Some(row) = path.row { + let col = path.column.unwrap_or(0); + if let Some(active_editor) = opened_item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + Bias::Left, + ); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } _ => cx.emit(event.clone()), From 6f7a6e57fcefa7e7c8f78a27dd6e604d73a76e9c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 02:03:27 +0300 Subject: [PATCH 38/42] Avoid excessive blinking on cmd-hover --- crates/terminal/src/terminal.rs | 62 +++++++++++++++++---------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 17bfa1550e544943aad1ae86fcffc49fd4dd9963..256cbe652d2c8cc629941613b4f3c5c72a30ec13 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -884,36 +884,13 @@ impl Terminal { }; cx.emit(event); } else { - if is_url { - self.update_selected_word( - prev_hovered_word, - maybe_url_or_path, - url_match, - ); - } else { - let (can_open_tx, can_open_rx) = smol::channel::bounded(1); - cx.emit(Event::ProbePathOpen { - maybe_path: maybe_url_or_path.clone(), - can_open_tx, - }); - - cx.spawn(|terminal, mut cx| async move { - let can_open = can_open_rx.recv().await.unwrap_or(false); - terminal.update(&mut cx, |terminal, cx| { - if can_open { - terminal.update_selected_word( - prev_hovered_word, - maybe_url_or_path, - url_match, - ); - } else { - terminal.last_content.last_hovered_word.take(); - } - cx.notify(); - }); - }) - .detach(); - }; + self.update_selected_word( + prev_hovered_word, + maybe_url_or_path, + url_match, + !is_url, + cx, + ); } } } @@ -925,6 +902,8 @@ impl Terminal { prev_word: Option, word: String, word_match: RangeInclusive, + should_probe_word: bool, + cx: &mut ModelContext, ) { if let Some(prev_word) = prev_word { if prev_word.word == word && prev_word.word_match == word_match { @@ -933,6 +912,29 @@ impl Terminal { word_match, id: prev_word.id, }); + } else if should_probe_word { + let (can_open_tx, can_open_rx) = smol::channel::bounded(1); + cx.emit(Event::ProbePathOpen { + maybe_path: word.clone(), + can_open_tx, + }); + + cx.spawn(|terminal, mut cx| async move { + let can_open = can_open_rx.recv().await.unwrap_or(false); + terminal.update(&mut cx, |terminal, cx| { + if can_open { + terminal.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: terminal.next_link_id(), + }); + } else { + terminal.last_content.last_hovered_word.take(); + } + cx.notify(); + }); + }) + .detach(); } else { self.last_content.last_hovered_word = Some(HoveredWord { word, From 10db05f87f1a00a616bd8fcfcbc37ca427d46d6b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 13:28:44 +0300 Subject: [PATCH 39/42] Rework terminal highlight event flow --- crates/terminal/src/terminal.rs | 109 ++++++++----------- crates/terminal_view/src/terminal_element.rs | 14 ++- crates/terminal_view/src/terminal_view.rs | 103 ++++++++++-------- 3 files changed, 111 insertions(+), 115 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 256cbe652d2c8cc629941613b4f3c5c72a30ec13..438224c81ef5acd47d7ede671d30ca00f20f7b5b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -33,7 +33,6 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use smol::channel::Sender; use util::truncate_and_trailoff; use std::{ @@ -90,12 +89,18 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - OpenUrl(String), - ProbePathOpen { - maybe_path: String, - can_open_tx: Sender, - }, - OpenPath(String), + NewNavigationTarget(MaybeNavigationTarget), + Open(MaybeNavigationTarget), +} + +/// A string inside terminal, potentially useful as a URI that can be opened. +#[derive(Clone, Debug)] +pub enum MaybeNavigationTarget { + /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex. + Url(String), + /// File system path, absolute or relative, existing or not. + /// Might have line and column number(s) attached as `file.rs:1:23` + PathLike(String), } #[derive(Clone)] @@ -502,6 +507,7 @@ impl TerminalBuilder { next_link_id: 0, selection_phase: SelectionPhase::Ended, cmd_pressed: false, + found_word: false, }; Ok(TerminalBuilder { @@ -654,6 +660,7 @@ pub struct Terminal { next_link_id: usize, selection_phase: SelectionPhase, cmd_pressed: bool, + found_word: bool, } impl Terminal { @@ -834,7 +841,7 @@ impl Terminal { .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor); let link = term.grid().index(point).hyperlink(); - let found_url = if link.is_some() { + let found_word = if link.is_some() { let mut min_index = point; loop { let new_min_index = @@ -875,20 +882,21 @@ impl Terminal { None }; - if let Some((maybe_url_or_path, is_url, url_match)) = found_url { + self.found_word = found_word.is_some(); + if let Some((maybe_url_or_path, is_url, url_match)) = found_word { if *open { - let event = if is_url { - Event::OpenUrl(maybe_url_or_path) + let target = if is_url { + MaybeNavigationTarget::Url(maybe_url_or_path) } else { - Event::OpenPath(maybe_url_or_path) + MaybeNavigationTarget::PathLike(maybe_url_or_path) }; - cx.emit(event); + cx.emit(Event::Open(target)); } else { self.update_selected_word( prev_hovered_word, - maybe_url_or_path, url_match, - !is_url, + maybe_url_or_path, + is_url, cx, ); } @@ -900,9 +908,9 @@ impl Terminal { fn update_selected_word( &mut self, prev_word: Option, - word: String, word_match: RangeInclusive, - should_probe_word: bool, + word: String, + is_url: bool, cx: &mut ModelContext, ) { if let Some(prev_word) = prev_word { @@ -912,43 +920,21 @@ impl Terminal { word_match, id: prev_word.id, }); - } else if should_probe_word { - let (can_open_tx, can_open_rx) = smol::channel::bounded(1); - cx.emit(Event::ProbePathOpen { - maybe_path: word.clone(), - can_open_tx, - }); - - cx.spawn(|terminal, mut cx| async move { - let can_open = can_open_rx.recv().await.unwrap_or(false); - terminal.update(&mut cx, |terminal, cx| { - if can_open { - terminal.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: terminal.next_link_id(), - }); - } else { - terminal.last_content.last_hovered_word.take(); - } - cx.notify(); - }); - }) - .detach(); - } else { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: self.next_link_id(), - }); + return; } - } else { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: self.next_link_id(), - }); } + + self.last_content.last_hovered_word = Some(HoveredWord { + word: word.clone(), + word_match, + id: self.next_link_id(), + }); + let navigation_target = if is_url { + MaybeNavigationTarget::Url(word) + } else { + MaybeNavigationTarget::PathLike(word) + }; + cx.emit(Event::NewNavigationTarget(navigation_target)); } fn next_link_id(&mut self) -> usize { @@ -1031,17 +1017,8 @@ impl Terminal { } pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool { - let cmd = modifiers.cmd; - let changed = self.cmd_pressed != cmd; - if changed { - self.cmd_pressed = cmd; - if cmd { - self.refresh_hovered_word(); - } else { - self.last_content.last_hovered_word.take(); - } - } - + let changed = self.cmd_pressed != modifiers.cmd; + self.cmd_pressed = modifiers.cmd; changed } @@ -1336,7 +1313,7 @@ impl Terminal { } } - pub fn refresh_hovered_word(&mut self) { + fn refresh_hovered_word(&mut self) { self.word_from_position(self.last_mouse_position); } @@ -1415,6 +1392,10 @@ impl Terminal { }) .unwrap_or_else(|| "Terminal".to_string()) } + + pub fn can_navigate_to_selected_word(&self) -> bool { + self.cmd_pressed && self.found_word + } } impl Drop for Terminal { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index aabfb999225df1f93d0b6b8072b9a79e52db677c..e29beb3ad52592bee273a34c24aa25b60c172754 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -163,6 +163,7 @@ pub struct TerminalElement { terminal: WeakModelHandle, focused: bool, cursor_visible: bool, + can_navigate_to_selected_word: bool, } impl TerminalElement { @@ -170,11 +171,13 @@ impl TerminalElement { terminal: WeakModelHandle, focused: bool, cursor_visible: bool, + can_navigate_to_selected_word: bool, ) -> TerminalElement { TerminalElement { terminal, focused, cursor_visible, + can_navigate_to_selected_word, } } @@ -580,13 +583,17 @@ impl Element for TerminalElement { let background_color = terminal_theme.background; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| { + let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); - terminal.last_content.last_hovered_word.clone() + if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { + terminal.last_content.last_hovered_word.clone() + } else { + None + } }); - let hyperlink_tooltip = last_hovered_hyperlink.map(|hovered_word| { + let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { let mut tooltip = Overlay::new( Empty::new() .contained() @@ -619,7 +626,6 @@ impl Element for TerminalElement { cursor_char, selection, cursor, - last_hovered_word, .. } = { &terminal_handle.read(cx).last_content }; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7c79a76cff34432b272a5f75cf99f0769721ef8a..3f4101d16c3a51bec32411bc8cae78922cc52d56 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,7 +33,7 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, Terminal, TerminalBlink, WorkingDirectory, + Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory, }; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -93,6 +93,7 @@ pub struct TerminalView { blinking_on: bool, blinking_paused: bool, blink_epoch: usize, + can_navigate_to_selected_word: bool, workspace_id: WorkspaceId, } @@ -169,55 +170,61 @@ impl TerminalView { .detach(); } } - Event::ProbePathOpen { - maybe_path, - can_open_tx, - } => { - let can_open = !possible_open_targets(&workspace, maybe_path, cx).is_empty(); - can_open_tx.send_blocking(can_open).ok(); + Event::NewNavigationTarget(maybe_navigation_target) => { + this.can_navigate_to_selected_word = match maybe_navigation_target { + MaybeNavigationTarget::Url(_) => true, + MaybeNavigationTarget::PathLike(maybe_path) => { + !possible_open_targets(&workspace, maybe_path, cx).is_empty() + } + } } - Event::OpenUrl(url) => cx.platform().open_url(url), - Event::OpenPath(maybe_path) => { - let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); - if let Some(path) = potential_abs_paths.into_iter().next() { - let visible = path.path_like.is_dir(); - let task_workspace = workspace.clone(); - cx.spawn(|_, mut cx| async move { - let opened_item = task_workspace - .update(&mut cx, |workspace, cx| { - workspace.open_abs_path(path.path_like, visible, cx) - }) - .context("workspace update")? - .await - .context("workspace update")?; - if let Some(row) = path.row { - let col = path.column.unwrap_or(0); - if let Some(active_editor) = opened_item.downcast::() { - active_editor - .downgrade() - .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - Bias::Left, - ); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }) - .log_err(); + Event::Open(maybe_navigation_target) => match maybe_navigation_target { + MaybeNavigationTarget::Url(url) => cx.platform().open_url(url), + MaybeNavigationTarget::PathLike(maybe_path) => { + if !this.can_navigate_to_selected_word { + return; + } + let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); + if let Some(path) = potential_abs_paths.into_iter().next() { + let visible = path.path_like.is_dir(); + let task_workspace = workspace.clone(); + cx.spawn(|_, mut cx| async move { + let opened_item = task_workspace + .update(&mut cx, |workspace, cx| { + workspace.open_abs_path(path.path_like, visible, cx) + }) + .context("workspace update")? + .await + .context("workspace update")?; + if let Some(row) = path.row { + let col = path.column.unwrap_or(0); + if let Some(active_editor) = opened_item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + Bias::Left, + ); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } } - } + }, _ => cx.emit(event.clone()), }) .detach(); @@ -231,6 +238,7 @@ impl TerminalView { blinking_on: false, blinking_paused: false, blink_epoch: 0, + can_navigate_to_selected_word: false, workspace_id, } } @@ -466,6 +474,7 @@ impl View for TerminalView { terminal_handle, focused, self.should_show_cursor(focused, cx), + self.can_navigate_to_selected_word, ) .contained(), ) From 6ed7820f7ca2d73d2a2c2e1baf84fa5c7325cb25 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 16:08:17 +0300 Subject: [PATCH 40/42] Consider all terminal when searching for words --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 438224c81ef5acd47d7ede671d30ca00f20f7b5b..450ca35b449e6531e702a13422b0d333df7333d4 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -838,7 +838,7 @@ impl Terminal { self.last_content.size, term.grid().display_offset(), ) - .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor); + .grid_clamp(term, alacritty_terminal::index::Boundary::Grid); let link = term.grid().index(point).hyperlink(); let found_word = if link.is_some() { From 33921183dc31170585b09d598a9ca6799724bbc2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 17:05:52 +0300 Subject: [PATCH 41/42] Avoid extra blinking on mouse moves --- crates/terminal/src/terminal.rs | 53 ++++++++++++++--------- crates/terminal_view/src/terminal_view.rs | 5 ++- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 450ca35b449e6531e702a13422b0d333df7333d4..3a64cff24f870eab763addb8936daa14e285fda3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -89,7 +89,7 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - NewNavigationTarget(MaybeNavigationTarget), + NewNavigationTarget(Option), Open(MaybeNavigationTarget), } @@ -507,7 +507,7 @@ impl TerminalBuilder { next_link_id: 0, selection_phase: SelectionPhase::Ended, cmd_pressed: false, - found_word: false, + hovered_word: false, }; Ok(TerminalBuilder { @@ -660,7 +660,7 @@ pub struct Terminal { next_link_id: usize, selection_phase: SelectionPhase, cmd_pressed: bool, - found_word: bool, + hovered_word: bool, } impl Terminal { @@ -882,23 +882,31 @@ impl Terminal { None }; - self.found_word = found_word.is_some(); - if let Some((maybe_url_or_path, is_url, url_match)) = found_word { - if *open { - let target = if is_url { - MaybeNavigationTarget::Url(maybe_url_or_path) + match found_word { + Some((maybe_url_or_path, is_url, url_match)) => { + if *open { + let target = if is_url { + MaybeNavigationTarget::Url(maybe_url_or_path) + } else { + MaybeNavigationTarget::PathLike(maybe_url_or_path) + }; + cx.emit(Event::Open(target)); } else { - MaybeNavigationTarget::PathLike(maybe_url_or_path) - }; - cx.emit(Event::Open(target)); - } else { - self.update_selected_word( - prev_hovered_word, - url_match, - maybe_url_or_path, - is_url, - cx, - ); + self.update_selected_word( + prev_hovered_word, + url_match, + maybe_url_or_path, + is_url, + cx, + ); + } + self.hovered_word = true; + } + None => { + if self.hovered_word { + cx.emit(Event::NewNavigationTarget(None)); + } + self.hovered_word = false; } } } @@ -934,7 +942,7 @@ impl Terminal { } else { MaybeNavigationTarget::PathLike(word) }; - cx.emit(Event::NewNavigationTarget(navigation_target)); + cx.emit(Event::NewNavigationTarget(Some(navigation_target))); } fn next_link_id(&mut self) -> usize { @@ -1018,6 +1026,9 @@ impl Terminal { pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool { let changed = self.cmd_pressed != modifiers.cmd; + if !self.cmd_pressed && modifiers.cmd { + self.refresh_hovered_word(); + } self.cmd_pressed = modifiers.cmd; changed } @@ -1394,7 +1405,7 @@ impl Terminal { } pub fn can_navigate_to_selected_word(&self) -> bool { - self.cmd_pressed && self.found_word + self.cmd_pressed && self.hovered_word } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3f4101d16c3a51bec32411bc8cae78922cc52d56..cdb1d40efcfbaaf8b8a710cd11bd03d2f3ec9950 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -172,10 +172,11 @@ impl TerminalView { } Event::NewNavigationTarget(maybe_navigation_target) => { this.can_navigate_to_selected_word = match maybe_navigation_target { - MaybeNavigationTarget::Url(_) => true, - MaybeNavigationTarget::PathLike(maybe_path) => { + Some(MaybeNavigationTarget::Url(_)) => true, + Some(MaybeNavigationTarget::PathLike(maybe_path)) => { !possible_open_targets(&workspace, maybe_path, cx).is_empty() } + None => false, } } Event::Open(maybe_navigation_target) => match maybe_navigation_target { From 3058a96deeac15d3ccb1c17d14eb7712ae397e4b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 19 Jul 2023 14:06:00 +0300 Subject: [PATCH 42/42] Clean up stale conflicting hints --- crates/editor/src/editor.rs | 9 +- crates/editor/src/inlay_hint_cache.rs | 375 ++++++++++++++++++++++---- 2 files changed, 323 insertions(+), 61 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b8a7853a93e8f35aeab7016b7e61c4fe629ea4c6..379f43e2e5be3b91533296b9f3ca3e4ad9795ac7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2659,11 +2659,16 @@ impl Editor { InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None), }; - self.inlay_hint_cache.refresh_inlay_hints( + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.spawn_hint_refresh( self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, - ) + ) { + self.splice_inlay_hints(to_remove, to_insert, cx); + } } fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 52473f9971c60ee5af15e8cfdff6142268cb5aa9..63076ba234d52c53556bb9fc9a8a43acbf314a96 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -195,20 +195,41 @@ impl InlayHintCache { } } - pub fn refresh_inlay_hints( + pub fn spawn_hint_refresh( &mut self, mut excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, - ) { - if !self.enabled || excerpts_to_query.is_empty() { - return; + ) -> Option { + if !self.enabled { + return None; } + let update_tasks = &mut self.update_tasks; + let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { - update_tasks - .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); + let mut changed = false; + update_tasks.retain(|task_excerpt_id, _| { + let retain = excerpts_to_query.contains_key(task_excerpt_id); + changed |= !retain; + retain + }); + self.hints.retain(|cached_excerpt, cached_hints| { + let retain = excerpts_to_query.contains_key(cached_excerpt); + changed |= !retain; + if !retain { + invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); + } + retain + }); + if changed { + self.version += 1; + } } + if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { + return None; + } + let cache_version = self.version; excerpts_to_query.retain(|visible_excerpt_id, _| { match update_tasks.entry(*visible_excerpt_id) { @@ -229,6 +250,15 @@ impl InlayHintCache { .ok(); }) .detach(); + + if invalidated_hints.is_empty() { + None + } else { + Some(InlaySplice { + to_remove: invalidated_hints, + to_insert: Vec::new(), + }) + } } fn new_allowed_hint_kinds_splice( @@ -684,7 +714,7 @@ async fn fetch_and_update_hints( if query.invalidate.should_invalidate() { let mut outdated_excerpt_caches = HashSet::default(); - for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() { + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { let excerpt_hints = excerpt_hints.read(); if excerpt_hints.buffer_id == query.buffer_id && excerpt_id != &query.excerpt_id @@ -1022,9 +1052,9 @@ mod tests { "Should get its first hints when opening the editor" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "The editor update the cache version after every cache/view change" ); }); @@ -1053,9 +1083,9 @@ mod tests { "Should not update hints while the work task is running" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "Should not update the cache while the work task is running" ); }); @@ -1077,9 +1107,9 @@ mod tests { "New hints should be queried after the work task is done" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "Cache version should udpate once after the work task is done" ); }); @@ -1194,9 +1224,9 @@ mod tests { "Should get its first hints when opening the editor" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 1, + editor.inlay_hint_cache().version, + 1, "Rust editor update the cache version after every cache/view change" ); }); @@ -1252,8 +1282,7 @@ mod tests { "Markdown editor should have a separate verison, repeating Rust editor rules" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 1); + assert_eq!(editor.inlay_hint_cache().version, 1); }); rs_editor.update(cx, |editor, cx| { @@ -1269,9 +1298,9 @@ mod tests { "Rust inlay cache should change after the edit" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 2, + editor.inlay_hint_cache().version, + 2, "Every time hint cache changes, cache version should be incremented" ); }); @@ -1283,8 +1312,7 @@ mod tests { "Markdown editor should not be affected by Rust editor changes" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 1); + assert_eq!(editor.inlay_hint_cache().version, 1); }); md_editor.update(cx, |editor, cx| { @@ -1300,8 +1328,7 @@ mod tests { "Rust editor should not be affected by Markdown editor changes" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 2); + assert_eq!(editor.inlay_hint_cache().version, 2); }); rs_editor.update(cx, |editor, cx| { let expected_layers = vec!["1".to_string()]; @@ -1311,8 +1338,7 @@ mod tests { "Markdown editor should also change independently" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 2); + assert_eq!(editor.inlay_hint_cache().version, 2); }); } @@ -1433,9 +1459,9 @@ mod tests { vec!["other hint".to_string(), "type hint".to_string()], visible_hint_labels(editor, cx) ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "Should not update cache version due to new loaded hints being the same" ); }); @@ -1568,9 +1594,8 @@ mod tests { ); assert!(cached_hint_labels(editor).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty()); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, edits_made, "The editor should not update the cache version after /refresh query without updates" ); }); @@ -1641,8 +1666,7 @@ mod tests { vec!["parameter hint".to_string()], visible_hint_labels(editor, cx), ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(editor.inlay_hint_cache().version, edits_made); }); } @@ -1720,9 +1744,8 @@ mod tests { "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 1, + editor.inlay_hint_cache().version, 1, "Only one update should be registered in the cache after all cancellations" ); }); @@ -1766,9 +1789,9 @@ mod tests { "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 2, + editor.inlay_hint_cache().version, + 2, "Should update the cache version once more, for the new change" ); }); @@ -1886,9 +1909,8 @@ mod tests { "Should have hints from both LSP requests made for a big file" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 2, + editor.inlay_hint_cache().version, 2, "Both LSP queries should've bumped the cache version" ); }); @@ -1918,8 +1940,7 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "Should have hints from the new LSP response after edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added"); }); } @@ -2075,6 +2096,7 @@ mod tests { panic!("unexpected uri: {:?}", params.text_document.uri); }; + // one hint per excerpt let positions = [ lsp::Position::new(0, 2), lsp::Position::new(4, 2), @@ -2138,8 +2160,7 @@ mod tests { "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison"); + assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison"); }); editor.update(cx, |editor, cx| { @@ -2169,8 +2190,8 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 9); + assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); }); editor.update(cx, |editor, cx| { @@ -2179,7 +2200,7 @@ mod tests { }); }); cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + let last_scroll_update_version = editor.update(cx, |editor, cx| { let expected_layers = vec![ "main hint #0".to_string(), "main hint #1".to_string(), @@ -2197,8 +2218,8 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 12); + assert_eq!(editor.inlay_hint_cache().version, expected_layers.len()); + expected_layers.len() }); editor.update(cx, |editor, cx| { @@ -2225,12 +2246,14 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer"); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); }); editor_edited.store(true, Ordering::Release); editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); editor.handle_input("++++more text++++", cx); }); cx.foreground().run_until_parked(); @@ -2240,19 +2263,253 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), - "other hint #0".to_string(), - "other hint #1".to_string(), - "other hint #2".to_string(), - "other hint #3".to_string(), - "other hint #4".to_string(), - "other hint #5".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), - "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \ -unedited (2nd) buffer should have the same hint"); + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ +all hints should be invalidated and requeried for all of its visible excerpts" + ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 16); + assert_eq!( + editor.inlay_hint_cache().version, + last_scroll_update_version + expected_layers.len() + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal" + ); + }); + } + + #[gpui::test] + async fn test_excerpts_removed( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { + let buffer_1_excerpts = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + let buffer_2_excerpts = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }], + cx, + ); + (buffer_1_excerpts, buffer_2_excerpts) + }); + + assert!(!buffer_1_excerpts.is_empty()); + assert!(!buffer_2_excerpts.is_empty()); + + deterministic.run_until_parked(); + cx.foreground().run_until_parked(); + let (_, editor) = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string(), "other hint #0".to_string()], + cached_hint_labels(editor), + "Cache should update for both excerpts despite hints display was disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Cache should update once per excerpt query" + ); + }); + + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(buffer_2_excerpts, cx) + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string()], + cached_hint_labels(editor), + "For the removed excerpt, should clean corresponding cached hints" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Excerpt removal should trigger cache update" + ); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["main hint #0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Hint display settings change should not change the cache" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Settings change should make cached hints visible" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 4, + "Settings change should trigger cache update" + ); }); }