Fix assorted linux issues (#10061)

Mikayla Maki created

- Fix a bug where modifiers would be dispatched before they changed
- Add a secondary modifier
- Improve keybindings

Release Notes:

- N/A

Change summary

assets/keymaps/default-linux.json                |  25 ++-
assets/settings/default.json                     |   3 
crates/editor/src/editor_settings.rs             |   3 
crates/editor/src/element.rs                     |   9 
crates/editor/src/hover_links.rs                 |  52 +++---
crates/file_finder/src/file_finder_tests.rs      |  14 
crates/gpui/src/elements/div.rs                  |   6 
crates/gpui/src/platform/keystroke.rs            |  87 ++++++++++-
crates/gpui/src/platform/linux/wayland/client.rs | 129 ++++++++---------
crates/gpui/src/platform/linux/x11/event.rs      |   2 
crates/gpui/src/platform/mac/events.rs           |   4 
crates/gpui/src/platform/mac/platform.rs         |   2 
crates/gpui/src/platform/mac/window.rs           |   2 
crates/gpui/src/platform/windows/window.rs       |   2 
crates/picker/src/picker.rs                      |   6 
crates/project_panel/src/project_panel.rs        |   2 
crates/terminal/src/mappings/keys.rs             |   4 
crates/terminal/src/terminal.rs                  |  16 +-
crates/ui/src/components/keybinding.rs           |   2 
crates/vim/src/test/neovim_connection.rs         |   4 
20 files changed, 220 insertions(+), 154 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -219,7 +219,7 @@
     "context": "BufferSearchBar && in_replace",
     "bindings": {
       "enter": "search::ReplaceNext",
-      "cmd-enter": "search::ReplaceAll"
+      "ctrl-enter": "search::ReplaceAll"
     }
   },
   {
@@ -258,7 +258,7 @@
     "bindings": {
       "escape": "project_search::ToggleFocus",
       "alt-tab": "search::CycleMode",
-      "cmd-shift-h": "search::ToggleReplace",
+      "ctrl-shift-h": "search::ToggleReplace",
       "alt-ctrl-g": "search::ActivateRegexMode",
       "alt-ctrl-x": "search::ActivateTextMode"
     }
@@ -304,8 +304,10 @@
         }
       ],
       "ctrl-alt-shift-down": "editor::DuplicateLine",
-      "ctrl-shift-right": "editor::SelectLargerSyntaxNode",
-      "ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
+      "ctrl-shift-left": "editor::SelectToPreviousWordStart",
+      "ctrl-shift-right": "editor::SelectToNextWordEnd",
+      "ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
+      "ctrl-shift-down": "editor::SelectSmallerSyntaxNode", //todo(linux) tmp keybinding
       "ctrl-d": [
         "editor::SelectNext",
         {
@@ -354,14 +356,14 @@
       "ctrl-shift-]": "editor::UnfoldLines",
       "ctrl-space": "editor::ShowCompletions",
       "ctrl-.": "editor::ToggleCodeActions",
-      "alt-cmd-r": "editor::RevealInFinder",
+      "alt-ctrl-r": "editor::RevealInFinder",
       "ctrl-alt-shift-c": "editor::DisplayCursorNames"
     }
   },
   {
     "context": "Editor && mode == full",
     "bindings": {
-      "cmd-shift-o": "outline::Toggle",
+      "ctrl-shift-o": "outline::Toggle",
       "ctrl-g": "go_to_line::Toggle"
     }
   },
@@ -419,7 +421,7 @@
       "ctrl-shift-f": "pane::DeploySearch",
       "ctrl-k ctrl-s": "zed::OpenKeymap",
       "ctrl-k ctrl-t": "theme_selector::Toggle",
-      "ctrl-t": "project_symbols::Toggle",
+      "ctrl-shift-t": "project_symbols::Toggle",
       "ctrl-p": "file_finder::Toggle",
       "ctrl-tab": "tab_switcher::Toggle",
       "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
@@ -549,7 +551,7 @@
       "delete": "project_panel::Delete",
       "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
       "ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
-      "alt-cmd-r": "project_panel::RevealInFinder",
+      "alt-ctrl-r": "project_panel::RevealInFinder",
       "alt-shift-f": "project_panel::NewSearchInDirectory"
     }
   },
@@ -610,7 +612,12 @@
       "pagedown": ["terminal::SendKeystroke", "pagedown"],
       "escape": ["terminal::SendKeystroke", "escape"],
       "enter": ["terminal::SendKeystroke", "enter"],
-      "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
+      "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
+
+      // Some nice conveniences
+      "ctrl-backspace": ["terminal::SendText", "\u0015"],
+      "ctrl-right": ["terminal::SendText", "\u0005"],
+      "ctrl-left": ["terminal::SendText", "\u0001"]
     }
   }
 ]

assets/settings/default.json 🔗

@@ -48,7 +48,8 @@
   // which gives the same size as all other panes.
   "active_pane_magnification": 1.0,
   // The key to use for adding multiple cursors
-  // Currently "alt" or "cmd" are supported.
+  // Currently "alt" or "cmd_or_ctrl"  (also aliased as
+  // "cmd" and "ctrl") are supported.
   "multi_cursor_modifier": "alt",
   // Whether to enable vim modes and key bindings
   "vim_mode": false,

crates/editor/src/editor_settings.rs 🔗

@@ -92,7 +92,8 @@ pub enum ShowScrollbar {
 #[serde(rename_all = "snake_case")]
 pub enum MultiCursorModifier {
     Alt,
-    Cmd,
+    #[serde(alias = "cmd", alias = "ctrl")]
+    CmdOrCtrl,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]

crates/editor/src/element.rs 🔗

@@ -449,7 +449,8 @@ impl EditorElement {
                 },
                 cx,
             );
-        } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.command {
+        } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary()
+        {
             editor.select(
                 SelectPhase::Extend {
                     position,
@@ -461,7 +462,7 @@ impl EditorElement {
             let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
             let multi_cursor_modifier = match multi_cursor_setting {
                 MultiCursorModifier::Alt => modifiers.alt,
-                MultiCursorModifier::Cmd => modifiers.command,
+                MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
             };
             editor.select(
                 SelectPhase::Begin {
@@ -513,8 +514,8 @@ impl EditorElement {
 
         let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
         let multi_cursor_modifier = match multi_cursor_setting {
-            MultiCursorModifier::Alt => event.modifiers.command,
-            MultiCursorModifier::Cmd => event.modifiers.alt,
+            MultiCursorModifier::Alt => event.modifiers.secondary(),
+            MultiCursorModifier::CmdOrCtrl => event.modifiers.alt,
         };
 
         if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(cx) {

crates/editor/src/hover_links.rs 🔗

@@ -93,7 +93,7 @@ impl Editor {
         modifiers: Modifiers,
         cx: &mut ViewContext<Self>,
     ) {
-        if !modifiers.command || self.has_pending_selection() {
+        if !modifiers.secondary() || self.has_pending_selection() {
             self.hide_hovered_link(cx);
             return;
         }
@@ -113,7 +113,7 @@ impl Editor {
                     &snapshot,
                     point_for_position,
                     self,
-                    modifiers.command,
+                    modifiers.secondary(),
                     modifiers.shift,
                     cx,
                 );
@@ -256,7 +256,7 @@ pub fn update_inlay_link_and_hover_points(
     snapshot: &EditorSnapshot,
     point_for_position: PointForPosition,
     editor: &mut Editor,
-    cmd_held: bool,
+    secondary_held: bool,
     shift_held: bool,
     cx: &mut ViewContext<'_, Editor>,
 ) {
@@ -394,7 +394,9 @@ pub fn update_inlay_link_and_hover_points(
                                     if let Some((language_server_id, location)) =
                                         hovered_hint_part.location
                                     {
-                                        if cmd_held && !editor.has_pending_nonempty_selection() {
+                                        if secondary_held
+                                            && !editor.has_pending_nonempty_selection()
+                                        {
                                             go_to_definition_updated = true;
                                             show_link_definition(
                                                 shift_held,
@@ -762,7 +764,7 @@ mod tests {
             let «variable» = A;
         "});
 
-        cx.simulate_modifiers_change(Modifiers::command());
+        cx.simulate_modifiers_change(Modifiers::secondary_key());
         cx.run_until_parked();
         // Assert no link highlights
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -823,7 +825,7 @@ mod tests {
             ])))
         });
 
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
         requests.next().await;
         cx.background_executor.run_until_parked();
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -849,7 +851,7 @@ mod tests {
             ])))
         });
 
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
         requests.next().await;
         cx.background_executor.run_until_parked();
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -868,7 +870,7 @@ mod tests {
                 // No definitions returned
                 Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
             });
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
 
         requests.next().await;
         cx.background_executor.run_until_parked();
@@ -912,7 +914,7 @@ mod tests {
             ])))
         });
 
-        cx.simulate_modifiers_change(Modifiers::command());
+        cx.simulate_modifiers_change(Modifiers::secondary_key());
 
         requests.next().await;
         cx.background_executor.run_until_parked();
@@ -928,7 +930,7 @@ mod tests {
                 fn do_work() { test(); }
             "});
 
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
         cx.background_executor.run_until_parked();
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
@@ -940,7 +942,7 @@ mod tests {
                 fn test() { do_work(); }
                 fn do_work() { tesˇt(); }
             "});
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
         cx.background_executor.run_until_parked();
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
                 fn test() { do_work(); }
@@ -948,7 +950,7 @@ mod tests {
             "});
 
         // Cmd click with existing definition doesn't re-request and dismisses highlight
-        cx.simulate_click(hover_point, Modifiers::command());
+        cx.simulate_click(hover_point, Modifiers::secondary_key());
         cx.lsp
             .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
                 // Empty definition response to make sure we aren't hitting the lsp and using
@@ -987,7 +989,7 @@ mod tests {
                 },
             ])))
         });
-        cx.simulate_click(hover_point, Modifiers::command());
+        cx.simulate_click(hover_point, Modifiers::secondary_key());
         requests.next().await;
         cx.background_executor.run_until_parked();
         cx.assert_editor_state(indoc! {"
@@ -1030,7 +1032,7 @@ mod tests {
                 s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
             });
         });
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
         cx.background_executor.run_until_parked();
         assert!(requests.try_next().is_err());
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
@@ -1144,7 +1146,7 @@ mod tests {
         });
         // Press cmd to trigger highlight
         let hover_point = cx.pixel_position_for(midpoint);
-        cx.simulate_mouse_move(hover_point, Modifiers::command());
+        cx.simulate_mouse_move(hover_point, Modifiers::secondary_key());
         cx.background_executor.run_until_parked();
         cx.update_editor(|editor, cx| {
             let snapshot = editor.snapshot(cx);
@@ -1175,9 +1177,9 @@ mod tests {
                 assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
             });
 
-        cx.simulate_modifiers_change(Modifiers::command());
+        cx.simulate_modifiers_change(Modifiers::secondary_key());
         cx.background_executor.run_until_parked();
-        cx.simulate_click(hover_point, Modifiers::command());
+        cx.simulate_click(hover_point, Modifiers::secondary_key());
         cx.background_executor.run_until_parked();
         cx.assert_editor_state(indoc! {"
                 struct «TestStructˇ»;
@@ -1207,12 +1209,12 @@ mod tests {
             Let's test a [complex](https://zed.dev/channel/had-(ˇoops)) case.
             "});
 
-        cx.simulate_mouse_move(screen_coord, Modifiers::command());
+        cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
         cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
             Let's test a [complex](«https://zed.dev/channel/had-(oops)ˇ») case.
         "});
 
-        cx.simulate_click(screen_coord, Modifiers::command());
+        cx.simulate_click(screen_coord, Modifiers::secondary_key());
         assert_eq!(
             cx.opened_url(),
             Some("https://zed.dev/channel/had-(oops)".into())
@@ -1235,12 +1237,12 @@ mod tests {
         let screen_coord =
             cx.pixel_position(indoc! {"https://zed.dev/relˇeases is a cool webpage."});
 
-        cx.simulate_mouse_move(screen_coord, Modifiers::command());
+        cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
         cx.assert_editor_text_highlights::<HoveredLinkState>(
             indoc! {"«https://zed.dev/releasesˇ» is a cool webpage."},
         );
 
-        cx.simulate_click(screen_coord, Modifiers::command());
+        cx.simulate_click(screen_coord, Modifiers::secondary_key());
         assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
     }
 
@@ -1260,12 +1262,12 @@ mod tests {
         let screen_coord =
             cx.pixel_position(indoc! {"A cool webpage is https://zed.dev/releˇases"});
 
-        cx.simulate_mouse_move(screen_coord, Modifiers::command());
+        cx.simulate_mouse_move(screen_coord, Modifiers::secondary_key());
         cx.assert_editor_text_highlights::<HoveredLinkState>(
             indoc! {"A cool webpage is «https://zed.dev/releasesˇ»"},
         );
 
-        cx.simulate_click(screen_coord, Modifiers::command());
+        cx.simulate_click(screen_coord, Modifiers::secondary_key());
         assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
     }
 
@@ -1386,7 +1388,7 @@ mod tests {
         });
 
         for _ in 0..5 {
-            cx.simulate_click(definition_hover_point, Modifiers::command());
+            cx.simulate_click(definition_hover_point, Modifiers::secondary_key());
             cx.background_executor.run_until_parked();
             cx.assert_editor_state(indoc! {"
                 fn test() {
@@ -1398,7 +1400,7 @@ mod tests {
                 }
             "});
 
-            cx.simulate_click(reference_hover_point, Modifiers::command());
+            cx.simulate_click(reference_hover_point, Modifiers::secondary_key());
             cx.background_executor.run_until_parked();
             cx.assert_editor_state(indoc! {"
                 fn «testˇ»() {

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -1490,7 +1490,7 @@ async fn test_keeps_file_finder_open_after_modifier_keys_release(cx: &mut gpui::
 
     open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
 
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     open_file_picker(&workspace, cx);
 
     cx.simulate_modifiers_change(Modifiers::none());
@@ -1519,7 +1519,7 @@ async fn test_opens_file_on_modifier_keys_release(cx: &mut gpui::TestAppContext)
     open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
     open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
 
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     let picker = open_file_picker(&workspace, cx);
     picker.update(cx, |finder, _| {
         assert_eq!(finder.delegate.matches.len(), 2);
@@ -1560,7 +1560,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav(
     open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
 
     // Open with a shortcut
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     let picker = open_file_picker(&workspace, cx);
     picker.update(cx, |finder, _| {
         assert_eq!(finder.delegate.matches.len(), 2);
@@ -1581,7 +1581,7 @@ async fn test_switches_between_release_norelease_modes_on_forward_nav(
 
     // Back to navigation with initial shortcut
     // Open file on modifiers release
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     cx.dispatch_action(Toggle);
     cx.simulate_modifiers_change(Modifiers::none());
     cx.read(|cx| {
@@ -1617,7 +1617,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
     open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
 
     // Open with a shortcut
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     let picker = open_file_picker(&workspace, cx);
     picker.update(cx, |finder, _| {
         assert_eq!(finder.delegate.matches.len(), 3);
@@ -1640,7 +1640,7 @@ async fn test_switches_between_release_norelease_modes_on_backward_nav(
 
     // Back to navigation with initial shortcut
     // Open file on modifiers release
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     cx.dispatch_action(SelectPrev); // <-- File Finder's SelectPrev, not menu's
     cx.simulate_modifiers_change(Modifiers::none());
     cx.read(|cx| {
@@ -1669,7 +1669,7 @@ async fn test_extending_modifiers_does_not_confirm_selection(cx: &mut gpui::Test
 
     open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
 
-    cx.simulate_modifiers_change(Modifiers::command());
+    cx.simulate_modifiers_change(Modifiers::secondary_key());
     open_file_picker(&workspace, cx);
 
     cx.simulate_modifiers_change(Modifiers::command_shift());

crates/gpui/src/elements/div.rs 🔗

@@ -1510,12 +1510,12 @@ impl Interactivity {
                     };
                     if self.location.is_some()
                         && text_bounds.contains(&cx.mouse_position())
-                        && cx.modifiers().command
+                        && cx.modifiers().secondary()
                     {
-                        let command_held = cx.modifiers().command;
+                        let secondary_held = cx.modifiers().secondary();
                         cx.on_key_event({
                             move |e: &crate::ModifiersChangedEvent, _phase, cx| {
-                                if e.modifiers.command != command_held
+                                if e.modifiers.secondary() != secondary_held
                                     && text_bounds.contains(&cx.mouse_position())
                                 {
                                     cx.refresh();

crates/gpui/src/platform/keystroke.rs 🔗

@@ -37,7 +37,7 @@ impl Keystroke {
                             control: self.modifiers.control,
                             alt: false,
                             shift: false,
-                            command: false,
+                            platform: false,
                             function: false,
                         },
                         key: ime_key.to_string(),
@@ -62,7 +62,7 @@ impl Keystroke {
         let mut control = false;
         let mut alt = false;
         let mut shift = false;
-        let mut command = false;
+        let mut platform = false;
         let mut function = false;
         let mut key = None;
         let mut ime_key = None;
@@ -73,8 +73,13 @@ impl Keystroke {
                 "ctrl" => control = true,
                 "alt" => alt = true,
                 "shift" => shift = true,
-                "cmd" => command = true,
                 "fn" => function = true,
+                #[cfg(target_os = "macos")]
+                "cmd" => platform = true,
+                #[cfg(target_os = "linux")]
+                "super" => platform = true,
+                #[cfg(target_os = "windows")]
+                "win" => platform = true,
                 _ => {
                     if let Some(next) = components.peek() {
                         if next.is_empty() && source.ends_with('-') {
@@ -101,7 +106,7 @@ impl Keystroke {
                 control,
                 alt,
                 shift,
-                command,
+                platform,
                 function,
             },
             key,
@@ -114,7 +119,7 @@ impl Keystroke {
     /// be able to simulate typing "space", etc.
     pub fn with_simulated_ime(mut self) -> Self {
         if self.ime_key.is_none()
-            && !self.modifiers.command
+            && !self.modifiers.platform
             && !self.modifiers.control
             && !self.modifiers.function
             && !self.modifiers.alt
@@ -147,8 +152,15 @@ impl std::fmt::Display for Keystroke {
         if self.modifiers.alt {
             f.write_char('⌥')?;
         }
-        if self.modifiers.command {
+        if self.modifiers.platform {
+            #[cfg(target_os = "macos")]
             f.write_char('⌘')?;
+
+            #[cfg(target_os = "linux")]
+            f.write_char('❖')?;
+
+            #[cfg(target_os = "windows")]
+            f.write_char('⊞')?;
         }
         if self.modifiers.shift {
             f.write_char('⇧')?;
@@ -188,7 +200,8 @@ pub struct Modifiers {
 
     /// The command key, on macos
     /// the windows key, on windows
-    pub command: bool,
+    /// the super key, on linux
+    pub platform: bool,
 
     /// The function key
     pub function: bool,
@@ -197,7 +210,22 @@ pub struct Modifiers {
 impl Modifiers {
     /// Returns true if any modifier key is pressed
     pub fn modified(&self) -> bool {
-        self.control || self.alt || self.shift || self.command || self.function
+        self.control || self.alt || self.shift || self.platform || self.function
+    }
+
+    /// Whether the semantically 'secondary' modifier key is pressed
+    /// On macos, this is the command key
+    /// On windows and linux, this is the control key
+    pub fn secondary(&self) -> bool {
+        #[cfg(target_os = "macos")]
+        {
+            return self.platform;
+        }
+
+        #[cfg(not(target_os = "macos"))]
+        {
+            return self.control;
+        }
     }
 
     /// helper method for Modifiers with no modifiers
@@ -205,10 +233,45 @@ impl Modifiers {
         Default::default()
     }
 
-    /// helper method for Modifiers with just command
+    /// helper method for Modifiers with just the command key
     pub fn command() -> Modifiers {
         Modifiers {
-            command: true,
+            platform: true,
+            ..Default::default()
+        }
+    }
+
+    /// A helper method for Modifiers with just the secondary key pressed
+    pub fn secondary_key() -> Modifiers {
+        #[cfg(target_os = "macos")]
+        {
+            Modifiers {
+                platform: true,
+                ..Default::default()
+            }
+        }
+
+        #[cfg(not(target_os = "macos"))]
+        {
+            Modifiers {
+                control: true,
+                ..Default::default()
+            }
+        }
+    }
+
+    /// helper method for Modifiers with just the windows key
+    pub fn windows() -> Modifiers {
+        Modifiers {
+            platform: true,
+            ..Default::default()
+        }
+    }
+
+    /// helper method for Modifiers with just the super key
+    pub fn super_key() -> Modifiers {
+        Modifiers {
+            platform: true,
             ..Default::default()
         }
     }
@@ -233,7 +296,7 @@ impl Modifiers {
     pub fn command_shift() -> Modifiers {
         Modifiers {
             shift: true,
-            command: true,
+            platform: true,
             ..Default::default()
         }
     }
@@ -243,7 +306,7 @@ impl Modifiers {
         (other.control || !self.control)
             && (other.alt || !self.alt)
             && (other.shift || !self.shift)
-            && (other.command || !self.command)
+            && (other.platform || !self.platform)
             && (other.function || !self.function)
     }
 }

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -190,7 +190,7 @@ impl WaylandClient {
                 control: false,
                 alt: false,
                 function: false,
-                command: false,
+                platform: false,
             },
             scroll_direction: -1.0,
             axis_source: AxisSource::Wheel,
@@ -692,6 +692,11 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
                 group,
                 ..
             } => {
+                let focused_window = state.keyboard_focused_window.clone();
+                let Some(focused_window) = focused_window else {
+                    return;
+                };
+
                 let keymap_state = state.keymap_state.as_mut().unwrap();
                 keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
 
@@ -707,14 +712,22 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
                 state.modifiers.shift = shift;
                 state.modifiers.alt = alt;
                 state.modifiers.control = control;
-                state.modifiers.command = command;
+                state.modifiers.platform = command;
+
+                let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
+                    modifiers: state.modifiers,
+                });
+
+                drop(state);
+
+                focused_window.handle_input(input);
             }
             wl_keyboard::Event::Key {
                 key,
                 state: WEnum::Value(key_state),
                 ..
             } => {
-                let focused_window = &state.keyboard_focused_window;
+                let focused_window = state.keyboard_focused_window.clone();
                 let Some(focused_window) = focused_window else {
                     return;
                 };
@@ -725,80 +738,56 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
                 let keysym = keymap_state.key_get_one_sym(keycode);
 
                 match key_state {
-                    wl_keyboard::KeyState::Pressed => {
-                        let input = if keysym.is_modifier_key() {
-                            PlatformInput::ModifiersChanged(ModifiersChangedEvent {
-                                modifiers: state.modifiers,
+                    wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
+                        let input = PlatformInput::KeyDown(KeyDownEvent {
+                            keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
+                            is_held: false, // todo(linux)
+                        });
+
+                        state.repeat.current_id += 1;
+                        state.repeat.current_keysym = Some(keysym);
+
+                        let rate = state.repeat.characters_per_second;
+                        let delay = state.repeat.delay;
+                        let id = state.repeat.current_id;
+                        let this = this.clone();
+
+                        let timer = Timer::from_duration(delay);
+                        let state_ = Rc::clone(&this.client_state_inner);
+                        let input_ = input.clone();
+                        state
+                            .loop_handle
+                            .insert_source(timer, move |event, _metadata, shared_data| {
+                                let state_ = state_.borrow_mut();
+                                let is_repeating = id == state_.repeat.current_id
+                                    && state_.repeat.current_keysym.is_some()
+                                    && state_.keyboard_focused_window.is_some();
+
+                                if !is_repeating {
+                                    return TimeoutAction::Drop;
+                                }
+
+                                let focused_window =
+                                    state_.keyboard_focused_window.as_ref().unwrap().clone();
+
+                                drop(state_);
+
+                                focused_window.handle_input(input_.clone());
+
+                                TimeoutAction::ToDuration(Duration::from_secs(1) / rate)
                             })
-                        } else {
-                            PlatformInput::KeyDown(KeyDownEvent {
-                                keystroke: Keystroke::from_xkb(
-                                    keymap_state,
-                                    state.modifiers,
-                                    keycode,
-                                ),
-                                is_held: false, // todo(linux)
-                            })
-                        };
-
-                        if !keysym.is_modifier_key() {
-                            state.repeat.current_id += 1;
-                            state.repeat.current_keysym = Some(keysym);
-
-                            let rate = state.repeat.characters_per_second;
-                            let delay = state.repeat.delay;
-                            let id = state.repeat.current_id;
-                            let this = this.clone();
-
-                            let timer = Timer::from_duration(delay);
-                            let state_ = Rc::clone(&this.client_state_inner);
-                            let input_ = input.clone();
-                            state
-                                .loop_handle
-                                .insert_source(timer, move |event, _metadata, shared_data| {
-                                    let state_ = state_.borrow_mut();
-                                    let is_repeating = id == state_.repeat.current_id
-                                        && state_.repeat.current_keysym.is_some()
-                                        && state_.keyboard_focused_window.is_some();
-
-                                    if !is_repeating {
-                                        return TimeoutAction::Drop;
-                                    }
-
-                                    let focused_window =
-                                        state_.keyboard_focused_window.as_ref().unwrap().clone();
-
-                                    drop(state_);
-
-                                    focused_window.handle_input(input_.clone());
-
-                                    TimeoutAction::ToDuration(Duration::from_secs(1) / rate)
-                                })
-                                .unwrap();
-                        }
+                            .unwrap();
 
                         drop(state);
 
                         focused_window.handle_input(input);
                     }
-                    wl_keyboard::KeyState::Released => {
-                        let input = if keysym.is_modifier_key() {
-                            PlatformInput::ModifiersChanged(ModifiersChangedEvent {
-                                modifiers: state.modifiers,
-                            })
-                        } else {
-                            PlatformInput::KeyUp(KeyUpEvent {
-                                keystroke: Keystroke::from_xkb(
-                                    keymap_state,
-                                    state.modifiers,
-                                    keycode,
-                                ),
-                            })
-                        };
+                    wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => {
+                        let input = PlatformInput::KeyUp(KeyUpEvent {
+                            keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
+                        });
 
-                        if !keysym.is_modifier_key() {
-                            state.repeat.current_keysym = None;
-                        }
+                        state.repeat.current_keysym = None;
 
                         drop(state);
 

crates/gpui/src/platform/linux/x11/event.rs 🔗

@@ -18,7 +18,7 @@ pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers {
         control: state.contains(xproto::KeyButMask::CONTROL),
         alt: state.contains(xproto::KeyButMask::MOD1),
         shift: state.contains(xproto::KeyButMask::SHIFT),
-        command: state.contains(xproto::KeyButMask::MOD4),
+        platform: state.contains(xproto::KeyButMask::MOD4),
         function: false,
     }
 }

crates/gpui/src/platform/mac/events.rs 🔗

@@ -77,7 +77,7 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers {
         control,
         alt,
         shift,
-        command,
+        platform: command,
         function,
     }
 }
@@ -323,7 +323,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
             control,
             alt,
             shift,
-            command,
+            platform: command,
             function,
         },
         key,

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -279,7 +279,7 @@ impl MacPlatform {
                         let mut mask = NSEventModifierFlags::empty();
                         for (modifier, flag) in &[
                             (
-                                keystroke.modifiers.command,
+                                keystroke.modifiers.platform,
                                 NSEventModifierFlags::NSCommandKeyMask,
                             ),
                             (

crates/gpui/src/platform/windows/window.rs 🔗

@@ -239,7 +239,7 @@ impl WindowsWindowInner {
             control: self.is_virtual_key_pressed(VK_CONTROL),
             alt: self.is_virtual_key_pressed(VK_MENU),
             shift: self.is_virtual_key_pressed(VK_SHIFT),
-            command: self.is_virtual_key_pressed(VK_LWIN) || self.is_virtual_key_pressed(VK_RWIN),
+            platform: self.is_virtual_key_pressed(VK_LWIN) || self.is_virtual_key_pressed(VK_RWIN),
             function: false,
         }
     }

crates/picker/src/picker.rs 🔗

@@ -418,7 +418,7 @@ impl<D: PickerDelegate> Picker<D> {
             .id(("item", ix))
             .cursor_pointer()
             .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
-                this.handle_click(ix, event.down.modifiers.command, cx)
+                this.handle_click(ix, event.down.modifiers.secondary(), cx)
             }))
             // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
             // and produces right mouse button events. This matches platforms norms
@@ -427,7 +427,9 @@ impl<D: PickerDelegate> Picker<D> {
             .on_mouse_up(
                 MouseButton::Right,
                 cx.listener(move |this, event: &MouseUpEvent, cx| {
-                    this.handle_click(ix, event.modifiers.command, cx)
+                    // We specficially want to use the platform key here, as
+                    // ctrl will already be held down for the tab switcher.
+                    this.handle_click(ix, event.modifiers.platform, cx)
                 }),
             )
             .children(

crates/project_panel/src/project_panel.rs 🔗

@@ -1456,7 +1456,7 @@ impl ProjectPanel {
                             if kind.is_dir() {
                                 this.toggle_expanded(entry_id, cx);
                             } else {
-                                if event.down.modifiers.command {
+                                if event.down.modifiers.secondary() {
                                     this.split_entry(entry_id, cx);
                                 } else {
                                     this.open_entry(entry_id, event.up.click_count > 1, cx);

crates/terminal/src/mappings/keys.rs 🔗

@@ -18,7 +18,7 @@ impl AlacModifiers {
             ks.modifiers.alt,
             ks.modifiers.control,
             ks.modifiers.shift,
-            ks.modifiers.command,
+            ks.modifiers.platform,
         ) {
             (false, false, false, false) => AlacModifiers::None,
             (true, false, false, false) => AlacModifiers::Alt,
@@ -336,7 +336,7 @@ mod test {
                 control: false,
                 alt: false,
                 shift: false,
-                command: false,
+                platform: false,
                 function: false,
             },
             key: "🖖🏻".to_string(), //2 char string

crates/terminal/src/terminal.rs 🔗

@@ -432,7 +432,7 @@ impl TerminalBuilder {
             last_mouse_position: None,
             next_link_id: 0,
             selection_phase: SelectionPhase::Ended,
-            cmd_pressed: false,
+            secondary_pressed: false,
             hovered_word: false,
             url_regex,
             word_regex,
@@ -585,7 +585,7 @@ pub struct Terminal {
     scroll_px: Pixels,
     next_link_id: usize,
     selection_phase: SelectionPhase,
-    cmd_pressed: bool,
+    secondary_pressed: bool,
     hovered_word: bool,
     url_regex: RegexSearch,
     word_regex: RegexSearch,
@@ -1029,11 +1029,11 @@ impl Terminal {
     }
 
     pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
-        let changed = self.cmd_pressed != modifiers.command;
-        if !self.cmd_pressed && modifiers.command {
+        let changed = self.secondary_pressed != modifiers.secondary();
+        if !self.secondary_pressed && modifiers.secondary() {
             self.refresh_hovered_word();
         }
-        self.cmd_pressed = modifiers.command;
+        self.secondary_pressed = modifiers.secondary();
         changed
     }
 
@@ -1136,7 +1136,7 @@ impl Terminal {
                     self.pty_tx.notify(bytes);
                 }
             }
-        } else if self.cmd_pressed {
+        } else if self.secondary_pressed {
             self.word_from_position(Some(position));
         }
     }
@@ -1266,7 +1266,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.open_url(link.uri());
-                } else if self.cmd_pressed {
+                } else if self.secondary_pressed {
                     self.events
                         .push_back(InternalEvent::FindHyperlink(position, true));
                 }
@@ -1402,7 +1402,7 @@ impl Terminal {
     }
 
     pub fn can_navigate_to_selected_word(&self) -> bool {
-        self.cmd_pressed && self.hovered_word
+        self.secondary_pressed && self.hovered_word
     }
 
     pub fn task(&self) -> Option<&TaskState> {

crates/ui/src/components/keybinding.rs 🔗

@@ -113,7 +113,7 @@ impl RenderOnce for KeyBinding {
                             el.child(Key::new("Alt")).child(Key::new("+"))
                         }
                     })
-                    .when(keystroke.modifiers.command, |el| {
+                    .when(keystroke.modifiers.platform, |el| {
                         match self.platform_style {
                             PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Command)),
                             PlatformStyle::Linux => {

crates/vim/src/test/neovim_connection.rs 🔗

@@ -120,7 +120,7 @@ impl NeovimConnection {
         let special = keystroke.modifiers.shift
             || keystroke.modifiers.control
             || keystroke.modifiers.alt
-            || keystroke.modifiers.command
+            || keystroke.modifiers.platform
             || keystroke.key.len() > 1;
         let start = if special { "<" } else { "" };
         let shift = if keystroke.modifiers.shift { "S-" } else { "" };
@@ -130,7 +130,7 @@ impl NeovimConnection {
             ""
         };
         let alt = if keystroke.modifiers.alt { "M-" } else { "" };
-        let cmd = if keystroke.modifiers.command {
+        let cmd = if keystroke.modifiers.platform {
             "D-"
         } else {
             ""