@@ -779,7 +779,7 @@ impl CustomMode {
}
pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
- let path = resolve_path(&self.cwd.read(cx).text(cx));
+ let path = self.cwd.read(cx).text(cx);
if cfg!(windows) {
return task::LaunchRequest {
program: self.program.read(cx).text(cx),
@@ -797,12 +797,12 @@ impl CustomMode {
env.insert(lhs.to_string(), rhs.to_string());
}
- let program = resolve_path(&if let Some(program) = args.next() {
+ let program = if let Some(program) = args.next() {
program
} else {
env = FxHashMap::default();
command
- });
+ };
let args = args.collect::<Vec<_>>();
@@ -1145,26 +1145,34 @@ impl PickerDelegate for DebugScenarioDelegate {
}
}
-fn resolve_path(path: &str) -> String {
+pub(crate) fn resolve_path(path: &mut String) {
if path.starts_with('~') {
let home = paths::home_dir().to_string_lossy().to_string();
- let path = path.trim().to_owned();
- path.replace('~', &home)
- } else {
- path.to_owned()
- }
+ let trimmed_path = path.trim().to_owned();
+ *path = trimmed_path.replacen('~', &home, 1);
+ } else if let Some(strip_path) = path.strip_prefix(&format!(".{}", std::path::MAIN_SEPARATOR)) {
+ *path = format!(
+ "$ZED_WORKTREE_ROOT{}{}",
+ std::path::MAIN_SEPARATOR,
+ &strip_path
+ );
+ };
}
#[cfg(test)]
mod tests {
use paths::home_dir;
- use super::*;
-
#[test]
fn test_normalize_paths() {
let sep = std::path::MAIN_SEPARATOR;
let home = home_dir().to_string_lossy().to_string();
+ let resolve_path = |path: &str| -> String {
+ let mut path = path.to_string();
+ super::resolve_path(&mut path);
+ path
+ };
+
assert_eq!(resolve_path("bin"), format!("bin"));
assert_eq!(resolve_path(&format!("{sep}foo")), format!("{sep}foo"));
assert_eq!(resolve_path(""), format!(""));
@@ -7,7 +7,10 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
-use crate::persistence::{self, DebuggerPaneItem, SerializedLayout};
+use crate::{
+ new_session_modal::resolve_path,
+ persistence::{self, DebuggerPaneItem, SerializedLayout},
+};
use super::DebugPanelItemEvent;
use anyhow::{Context as _, Result, anyhow};
@@ -543,6 +546,32 @@ impl RunningState {
}
}
+ pub(crate) fn relativlize_paths(
+ key: Option<&str>,
+ config: &mut serde_json::Value,
+ context: &TaskContext,
+ ) {
+ match config {
+ serde_json::Value::Object(obj) => {
+ obj.iter_mut()
+ .for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
+ }
+ serde_json::Value::Array(array) => {
+ array
+ .iter_mut()
+ .for_each(|value| Self::relativlize_paths(None, value, context));
+ }
+ serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
+ resolve_path(s);
+
+ if let Some(substituted) = substitute_variables_in_str(&s, context) {
+ *s = substituted;
+ }
+ }
+ _ => {}
+ }
+ }
+
pub(crate) fn new(
session: Entity<Session>,
project: Entity<Project>,
@@ -752,6 +781,7 @@ impl RunningState {
mut config,
tcp_connection,
} = scenario;
+ Self::relativlize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry
@@ -0,0 +1,157 @@
+use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
+use project::{FakeFs, Project};
+use serde_json::json;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use task::{DebugScenario, TaskContext, VariableName};
+use util::path;
+
+use crate::tests::{init_test, init_test_workspace};
+
+// todo(tasks) figure out why task replacement is broken on windows
+#[gpui::test]
+async fn test_debug_session_substitutes_variables_and_relativizes_paths(
+ executor: BackgroundExecutor,
+ cx: &mut TestAppContext,
+) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "main.rs": "fn main() {}"
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ // Set up task variables to simulate a real environment
+ let test_variables = vec![(
+ VariableName::WorktreeRoot,
+ "/test/worktree/path".to_string(),
+ )]
+ .into_iter()
+ .collect();
+
+ let task_context = TaskContext {
+ cwd: None,
+ task_variables: test_variables,
+ project_env: Default::default(),
+ };
+
+ let home_dir = paths::home_dir();
+
+ let sep = std::path::MAIN_SEPARATOR;
+
+ // Test cases for different path formats
+ let test_cases: Vec<(Arc<String>, Arc<String>)> = vec![
+ // Absolute path - should not be relativized
+ (
+ Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)),
+ Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)),
+ ),
+ // Relative path - should be prefixed with worktree root
+ (
+ Arc::from(format!(".{0}src{0}program", sep)),
+ Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)),
+ ),
+ // Home directory path - should be prefixed with worktree root
+ (
+ Arc::from(format!("~{0}src{0}program", sep)),
+ Arc::from(format!(
+ "{1}{0}src{0}program",
+ sep,
+ home_dir.to_string_lossy()
+ )),
+ ),
+ // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
+ (
+ Arc::from(format!("$ZED_WORKTREE_ROOT{0}src{0}program", sep)),
+ Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)),
+ ),
+ ];
+
+ let called_launch = Arc::new(AtomicBool::new(false));
+
+ for (input_path, expected_path) in test_cases {
+ let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
+ let called_launch = called_launch.clone();
+ let input_path = input_path.clone();
+ let expected_path = expected_path.clone();
+ move |client| {
+ client.on_request::<dap::requests::Launch, _>({
+ let called_launch = called_launch.clone();
+ let input_path = input_path.clone();
+ let expected_path = expected_path.clone();
+
+ move |_, args| {
+ let config = args.raw.as_object().unwrap();
+
+ // Verify the program path was substituted correctly
+ assert_eq!(
+ config["program"].as_str().unwrap(),
+ expected_path.as_str(),
+ "Program path was not correctly substituted for input: {}",
+ input_path.as_str()
+ );
+
+ // Verify the cwd path was substituted correctly
+ assert_eq!(
+ config["cwd"].as_str().unwrap(),
+ expected_path.as_str(),
+ "CWD path was not correctly substituted for input: {}",
+ input_path.as_str()
+ );
+
+ // Verify that otherField was substituted but not relativized
+ // It should still have $ZED_WORKTREE_ROOT substituted if present
+ let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
+ input_path.replace("$ZED_WORKTREE_ROOT", "/test/worktree/path")
+ } else {
+ input_path.to_string()
+ };
+
+ assert_eq!(
+ config["otherField"].as_str().unwrap(),
+ expected_other_field,
+ "Other field was incorrectly modified for input: {}",
+ input_path
+ );
+
+ called_launch.store(true, Ordering::SeqCst);
+
+ Ok(())
+ }
+ });
+ }
+ });
+
+ let scenario = DebugScenario {
+ adapter: "fake-adapter".into(),
+ label: "test-debug-session".into(),
+ build: None,
+ config: json!({
+ "request": "launch",
+ "program": input_path,
+ "cwd": input_path,
+ "otherField": input_path
+ }),
+ tcp_connection: None,
+ };
+
+ workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.start_debug_session(scenario, task_context.clone(), None, window, cx)
+ })
+ .unwrap();
+
+ cx.run_until_parked();
+
+ assert!(called_launch.load(Ordering::SeqCst));
+ called_launch.store(false, Ordering::SeqCst);
+ }
+}