Set the working directory according to the editor file path (#14688)

Kyle Kelley created

Kernels now launch in the same directory as the script invoking them,
similar to notebook behavior.


![image](https://github.com/user-attachments/assets/def86308-bea4-4fa3-8211-132a282a5ecc)


Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs      | 18 +++++++++++++++++-
crates/repl/src/kernels.rs       |  5 +++++
crates/repl/src/runtime_panel.rs | 24 ++++++++++--------------
crates/repl/src/session.rs       | 22 ++++++++++++++++++++--
4 files changed, 52 insertions(+), 17 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -131,7 +131,7 @@ use std::{
     mem,
     num::NonZeroU32,
     ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
-    path::Path,
+    path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
     time::{Duration, Instant},
@@ -10386,6 +10386,22 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn working_directory(&self, cx: &WindowContext) -> Option<PathBuf> {
+        if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+            if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+                if let Some(dir) = file.abs_path(cx).parent() {
+                    return Some(dir.to_owned());
+                }
+            }
+
+            if let Some(project_path) = buffer.read(cx).project_path(cx) {
+                return Some(project_path.path.to_path_buf());
+            }
+        }
+
+        None
+    }
+
     pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
         if let Some(buffer) = self.buffer().read(cx).as_singleton() {
             if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {

crates/repl/src/kernels.rs 🔗

@@ -171,6 +171,7 @@ pub struct RunningKernel {
     _control_task: Task<anyhow::Result<()>>,
     _routing_task: Task<anyhow::Result<()>>,
     connection_path: PathBuf,
+    pub working_directory: PathBuf,
     pub request_tx: mpsc::Sender<JupyterMessage>,
     pub execution_state: ExecutionState,
     pub kernel_info: Option<KernelInfoReply>,
@@ -190,6 +191,7 @@ impl RunningKernel {
     pub fn new(
         kernel_specification: KernelSpecification,
         entity_id: EntityId,
+        working_directory: PathBuf,
         fs: Arc<dyn Fs>,
         cx: &mut AppContext,
     ) -> Task<anyhow::Result<(Self, JupyterMessageChannel)>> {
@@ -220,7 +222,9 @@ impl RunningKernel {
             fs.atomic_write(connection_path.clone(), content).await?;
 
             let mut cmd = kernel_specification.command(&connection_path)?;
+
             let process = cmd
+                .current_dir(&working_directory)
                 // .stdout(Stdio::null())
                 // .stderr(Stdio::null())
                 .kill_on_drop(true)
@@ -301,6 +305,7 @@ impl RunningKernel {
                 Self {
                     process,
                     request_tx,
+                    working_directory,
                     _shell_task,
                     _iopub_task,
                     _control_task,

crates/repl/src/runtime_panel.rs 🔗

@@ -208,9 +208,16 @@ impl RuntimePanel {
         cx.notify();
     }
 
-    // Gets the active selection in the editor or the current line
-    fn selection(&self, editor: View<Editor>, cx: &mut ViewContext<Self>) -> Range<Anchor> {
+    pub fn snippet(
+        &self,
+        editor: WeakView<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<(String, Arc<Language>, Range<Anchor>)> {
+        let editor = editor.upgrade()?;
         let editor = editor.read(cx);
+
+        let buffer = editor.buffer().read(cx).snapshot(cx);
+
         let selection = editor.selections.newest::<usize>(cx);
         let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
 
@@ -232,18 +239,7 @@ impl RuntimePanel {
             selection.range()
         };
 
-        range.to_anchors(&multi_buffer_snapshot)
-    }
-
-    pub fn snippet(
-        &self,
-        editor: WeakView<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<(String, Arc<Language>, Range<Anchor>)> {
-        let editor = editor.upgrade()?;
-
-        let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
-        let anchor_range = self.selection(editor, cx);
+        let anchor_range = range.to_anchors(&multi_buffer_snapshot);
 
         let selected_text = buffer
             .text_for_range(anchor_range.clone())

crates/repl/src/session.rs 🔗

@@ -19,7 +19,7 @@ use runtimelib::{
     ExecuteRequest, InterruptRequest, JupyterMessage, JupyterMessageContent, ShutdownRequest,
 };
 use settings::Settings as _;
-use std::{ops::Range, sync::Arc, time::Duration};
+use std::{env::temp_dir, ops::Range, path::PathBuf, sync::Arc, time::Duration};
 use theme::{ActiveTheme, ThemeSettings};
 use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
 
@@ -144,6 +144,17 @@ impl EditorBlock {
 }
 
 impl Session {
+    pub fn working_directory(editor: WeakView<Editor>, cx: &WindowContext) -> PathBuf {
+        if let Some(working_directory) = editor
+            .upgrade()
+            .and_then(|editor| editor.read(cx).working_directory(cx))
+        {
+            working_directory
+        } else {
+            temp_dir()
+        }
+    }
+
     pub fn new(
         editor: WeakView<Editor>,
         fs: Arc<dyn Fs>,
@@ -151,7 +162,14 @@ impl Session {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let entity_id = editor.entity_id();
-        let kernel = RunningKernel::new(kernel_specification.clone(), entity_id, fs.clone(), cx);
+
+        let kernel = RunningKernel::new(
+            kernel_specification.clone(),
+            entity_id,
+            Self::working_directory(editor.clone(), cx),
+            fs.clone(),
+            cx,
+        );
 
         let pending_kernel = cx
             .spawn(|this, mut cx| async move {