Add ability to open files with system default application (#17231)

KorigamiK created

Change summary

assets/keymaps/default-linux.json            |  1 +
assets/keymaps/default-macos.json            |  2 +-
assets/keymaps/vim.json                      |  1 +
crates/gpui/src/app.rs                       |  5 +++++
crates/gpui/src/platform.rs                  |  1 +
crates/gpui/src/platform/linux/platform.rs   | 13 +++++++++++++
crates/gpui/src/platform/mac/platform.rs     | 14 ++++++++++++++
crates/gpui/src/platform/test/platform.rs    |  4 ++++
crates/gpui/src/platform/windows/platform.rs | 13 +++++++++++++
crates/project_panel/src/project_panel.rs    | 10 ++++++++++
10 files changed, 63 insertions(+), 1 deletion(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -553,6 +553,7 @@
       "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
       "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
       "alt-ctrl-r": "project_panel::RevealInFileManager",
+      "ctrl-shift-enter": "project_panel::OpenWithSystem",
       "alt-shift-f": "project_panel::NewSearchInDirectory",
       "shift-down": "menu::SelectNext",
       "shift-up": "menu::SelectPrev",

assets/keymaps/default-macos.json 🔗

@@ -563,8 +563,8 @@
       "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
       "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
       "alt-cmd-r": "project_panel::RevealInFileManager",
+      "ctrl-shift-enter": "project_panel::OpenWithSystem",
       "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
-
       "alt-shift-f": "project_panel::NewSearchInDirectory",
       "shift-down": "menu::SelectNext",
       "shift-up": "menu::SelectPrev",

assets/keymaps/vim.json 🔗

@@ -493,6 +493,7 @@
       "v": "project_panel::OpenPermanent",
       "p": "project_panel::Open",
       "x": "project_panel::RevealInFileManager",
+      "s": "project_panel::OpenWithSystem",
       "shift-g": "menu::SelectLast",
       "g g": "menu::SelectFirst",
       "-": "project_panel::SelectParent",

crates/gpui/src/app.rs 🔗

@@ -657,6 +657,11 @@ impl AppContext {
         self.platform.reveal_path(path)
     }
 
+    /// Opens the specified path with the system's default application.
+    pub fn open_with_system(&self, path: &Path) {
+        self.platform.open_with_system(path)
+    }
+
     /// Returns whether the user has configured scrollbars to auto-hide at the platform level.
     pub fn should_auto_hide_scrollbars(&self) -> bool {
         self.platform.should_auto_hide_scrollbars()

crates/gpui/src/platform.rs 🔗

@@ -149,6 +149,7 @@ pub(crate) trait Platform: 'static {
     ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
     fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
     fn reveal_path(&self, path: &Path);
+    fn open_with_system(&self, path: &Path);
 
     fn on_quit(&self, callback: Box<dyn FnMut()>);
     fn on_reopen(&self, callback: Box<dyn FnMut()>);

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

@@ -351,6 +351,19 @@ impl<P: LinuxClient + 'static> Platform for P {
         self.reveal_path(path.to_owned());
     }
 
+    fn open_with_system(&self, path: &Path) {
+        let executor = self.background_executor().clone();
+        let path = path.to_owned();
+        executor
+            .spawn(async move {
+                let _ = std::process::Command::new("xdg-open")
+                    .arg(path)
+                    .spawn()
+                    .expect("Failed to open file with xdg-open");
+            })
+            .detach();
+    }
+
     fn on_quit(&self, callback: Box<dyn FnMut()>) {
         self.with_common(|common| {
             common.callbacks.quit = Some(callback);

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

@@ -718,6 +718,20 @@ impl Platform for MacPlatform {
         }
     }
 
+    fn open_with_system(&self, path: &Path) {
+        let path = path.to_path_buf();
+        self.0
+            .lock()
+            .background_executor
+            .spawn(async move {
+                std::process::Command::new("open")
+                    .arg(path)
+                    .spawn()
+                    .expect("Failed to open file");
+            })
+            .detach();
+    }
+
     fn on_quit(&self, callback: Box<dyn FnMut()>) {
         self.0.lock().quit = Some(callback);
     }

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

@@ -318,6 +318,10 @@ impl Platform for TestPlatform {
     fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
         unimplemented!()
     }
+
+    fn open_with_system(&self, _path: &Path) {
+        unimplemented!()
+    }
 }
 
 #[cfg(target_os = "windows")]

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

@@ -400,6 +400,19 @@ impl Platform for WindowsPlatform {
             .detach();
     }
 
+    fn open_with_system(&self, path: &Path) {
+        let executor = self.background_executor().clone();
+        let path = path.to_owned();
+        executor
+            .spawn(async move {
+                let _ = std::process::Command::new("cmd")
+                    .args(&["/c", "start", "", path.to_str().expect("path to string")])
+                    .spawn()
+                    .expect("Failed to open file");
+            })
+            .detach();
+    }
+
     fn on_quit(&self, callback: Box<dyn FnMut()>) {
         self.state.borrow_mut().callbacks.quit = Some(callback);
     }

crates/project_panel/src/project_panel.rs 🔗

@@ -146,6 +146,7 @@ actions!(
         CopyRelativePath,
         Duplicate,
         RevealInFileManager,
+        OpenWithSystem,
         Cut,
         Paste,
         Rename,
@@ -500,6 +501,7 @@ impl ProjectPanel {
                             .when(cfg!(not(target_os = "macos")), |menu| {
                                 menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
                             })
+                            .action("Open in Default App", Box::new(OpenWithSystem))
                             .action("Open in Terminal", Box::new(OpenInTerminal))
                             .when(is_dir, |menu| {
                                 menu.separator()
@@ -1497,6 +1499,13 @@ impl ProjectPanel {
         }
     }
 
+    fn open_system(&mut self, _: &OpenWithSystem, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            let abs_path = worktree.abs_path().join(&entry.path);
+            cx.open_with_system(&abs_path);
+        }
+    }
+
     fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
         if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
             let abs_path = worktree.abs_path().join(&entry.path);
@@ -2711,6 +2720,7 @@ impl Render for ProjectPanel {
                 })
                 .when(project.is_local_or_ssh(), |el| {
                     el.on_action(cx.listener(Self::reveal_in_finder))
+                        .on_action(cx.listener(Self::open_system))
                         .on_action(cx.listener(Self::open_in_terminal))
                 })
                 .on_mouse_down(