1136 lines · 42.4 KB · Rust
RawDownload
use crate::*;use anyhow::{Context as _, bail};use collections::HashMap;use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};use fs::RemoveOptions;use futures::{StreamExt, TryStreamExt};use gpui::http_client::AsyncBody;use gpui::{AsyncApp, SharedString};use json_dotpath::DotPaths;use language::{LanguageName, Toolchain};use paths::debug_adapters_dir;use serde_json::Value;use smol::fs::File;use smol::io::AsyncReadExt;use smol::lock::OnceCell;use std::ffi::OsString;use std::net::Ipv4Addr;use std::str::FromStr;use std::{    ffi::OsStr,    path::{Path, PathBuf},};use util::command::new_command;use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; enum DebugpyLaunchMode<'a> {    Normal,    AttachWithConnect { host: Option<&'a str> },} #[derive(Default)]pub(crate) struct PythonDebugAdapter {    base_venv_path: OnceCell<Result<Arc<Path>, String>>,    debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,} impl PythonDebugAdapter {    const ADAPTER_NAME: &'static str = "Debugpy";    const DEBUG_ADAPTER_NAME: DebugAdapterName =        DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME));     const LANGUAGE_NAME: &'static str = "Python";     async fn generate_debugpy_arguments<'a>(        host: &'a Ipv4Addr,        port: u16,        launch_mode: DebugpyLaunchMode<'a>,        user_installed_path: Option<&'a Path>,        user_args: Option<Vec<String>>,    ) -> Result<Vec<String>> {        let mut args = if let Some(user_installed_path) = user_installed_path {            log::debug!(                "Using user-installed debugpy adapter from: {}",                user_installed_path.display()            );            vec![user_installed_path.to_string_lossy().into_owned()]        } else {            let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());            let path = adapter_path                .join("debugpy")                .join("adapter")                .to_string_lossy()                .into_owned();            log::debug!("Using pip debugpy adapter from: {path}");            vec![path]        };         args.extend(if let Some(args) = user_args {            args        } else {            match launch_mode {                DebugpyLaunchMode::Normal => {                    vec![format!("--host={}", host), format!("--port={}", port)]                }                DebugpyLaunchMode::AttachWithConnect { host } => {                    let mut args = vec!["connect".to_string()];                     if let Some(host) = host {                        args.push(format!("{host}:"));                    }                    args.push(format!("{port}"));                    args                }            }        });        Ok(args)    }     async fn request_args(        &self,        delegate: &Arc<dyn DapDelegate>,        task_definition: &DebugTaskDefinition,    ) -> Result<StartDebuggingRequestArguments> {        let request = self.request_kind(&task_definition.config).await?;         let mut configuration = task_definition.config.clone();        if let Ok(console) = configuration.dot_get_mut("console") {            // Use built-in Zed terminal if user did not explicitly provide a setting for console.            if console.is_null() {                *console = Value::String("integratedTerminal".into());            }        }         if let Some(obj) = configuration.as_object_mut() {            obj.entry("cwd")                .or_insert(delegate.worktree_root_path().to_string_lossy().into());        }         Ok(StartDebuggingRequestArguments {            configuration,            request,        })    }     async fn fetch_wheel(        &self,        toolchain: Option<Toolchain>,        delegate: &Arc<dyn DapDelegate>,    ) -> Result<Arc<Path>> {        let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");        std::fs::create_dir_all(&download_dir)?;        let venv_python = self.base_venv_path(toolchain, delegate).await?;         let installation_succeeded = util::command::new_command(venv_python.as_ref())            .args([                "-m",                "pip",                "download",                "debugpy",                "--only-binary=:all:",                "-d",                download_dir.to_string_lossy().as_ref(),            ])            .output()            .await            .context("spawn system python")?            .status            .success();        if !installation_succeeded {            bail!("debugpy installation failed (could not fetch Debugpy's wheel)");        }         let wheel_path = std::fs::read_dir(&download_dir)?            .find_map(|entry| {                entry.ok().filter(|e| {                    e.file_type().is_ok_and(|typ| typ.is_file())                        && Path::new(&e.file_name()).extension() == Some("whl".as_ref())                })            })            .with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;         util::archive::extract_zip(            &debug_adapters_dir().join(Self::ADAPTER_NAME),            File::open(&wheel_path.path()).await?,        )        .await?;         Ok(Arc::from(wheel_path.path()))    }     async fn maybe_fetch_new_wheel(        &self,        toolchain: Option<Toolchain>,        delegate: &Arc<dyn DapDelegate>,    ) -> Result<()> {        let latest_release = delegate            .http_client()            .get(                "https://pypi.org/pypi/debugpy/json",                AsyncBody::empty(),                false,            )            .await            .log_err();        let response = latest_release            .filter(|response| response.status().is_success())            .context("getting latest release")?;         let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);        std::fs::create_dir_all(&download_dir)?;         let mut output = String::new();        response.into_body().read_to_string(&mut output).await?;        let as_json = serde_json::Value::from_str(&output)?;        let latest_version = as_json            .get("info")            .and_then(|info| {                info.get("version")                    .and_then(|version| version.as_str())                    .map(ToOwned::to_owned)            })            .context("parsing latest release information")?;        let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();        let is_up_to_date = delegate            .fs()            .read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))            .await?            .into_stream()            .any(async |entry| {                entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))            })            .await;         if !is_up_to_date {            delegate                .fs()                .remove_dir(                    &debug_adapters_dir().join(Self::ADAPTER_NAME),                    RemoveOptions {                        recursive: true,                        ignore_if_not_exists: true,                    },                )                .await?;            self.fetch_wheel(toolchain, delegate).await?;        }        anyhow::Ok(())    }     async fn fetch_debugpy_whl(        &self,        toolchain: Option<Toolchain>,        delegate: &Arc<dyn DapDelegate>,    ) -> Result<Arc<Path>, String> {        self.debugpy_whl_base_path            .get_or_init(|| async move {                let adapter_path = debug_adapters_dir()                    .join(Self::ADAPTER_NAME)                    .join("debugpy")                    .join("adapter");                 if let Err(error) = self.maybe_fetch_new_wheel(toolchain, delegate).await {                    if delegate                        .fs()                        .metadata(&adapter_path)                        .await                        .is_ok_and(|m| m.is_some())                    {                        log::warn!(                            "Failed to fetch latest debugpy, using cached version: {error:#}"                        );                    } else {                        return Err(format!("{error}"));                    }                }                 Ok(Arc::from(adapter_path.as_ref()))            })            .await            .clone()    }     async fn base_venv_path(        &self,        toolchain: Option<Toolchain>,        delegate: &Arc<dyn DapDelegate>,    ) -> Result<Arc<Path>> {        let result = self.base_venv_path            .get_or_init(|| async {                let base_python = if let Some(toolchain) = toolchain {                    toolchain.path.to_string()                } else {                    Self::system_python_name(delegate).await.ok_or_else(|| {                        let mut message = "Could not find a Python installation".to_owned();                        if cfg!(windows){                            message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")                        }                        message                    })?                };                 let debug_adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());                let output = util::command::new_command(&base_python)                    .args(["-m", "venv", "zed_base_venv"])                    .current_dir(                        &debug_adapter_path,                    )                    .spawn()                    .map_err(|e| format!("{e:#?}"))?                    .output()                    .await                    .map_err(|e| format!("{e:#?}"))?;                 if !output.status.success() {                    let stderr = String::from_utf8_lossy(&output.stderr);                    let stdout = String::from_utf8_lossy(&output.stdout);                    let debug_adapter_path = debug_adapter_path.display();                    return Err(format!("Failed to create base virtual environment with {base_python} in:\n{debug_adapter_path}\nstderr:\n{stderr}\nstdout:\n{stdout}\n"));                }                 const PYTHON_PATH: &str = if cfg!(target_os = "windows") {                    "Scripts/python.exe"                } else {                    "bin/python3"                };                Ok(Arc::from(                    paths::debug_adapters_dir()                        .join(Self::DEBUG_ADAPTER_NAME.as_ref())                        .join("zed_base_venv")                        .join(PYTHON_PATH)                        .as_ref(),                ))            })            .await            .clone();        match result {            Ok(path) => Ok(path),            Err(e) => Err(anyhow::anyhow!("{e}")),        }    }    async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {        const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];        let mut name = None;         for cmd in BINARY_NAMES {            let Some(path) = delegate.which(OsStr::new(cmd)).await else {                continue;            };            // Try to detect situations where `python3` exists but is not a real Python interpreter.            // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app            // when run with no arguments, and just fails otherwise.            let Some(output) = new_command(&path)                .args(["-c", "print(1 + 2)"])                .output()                .await                .ok()            else {                continue;            };            if output.stdout.trim_ascii() != b"3" {                continue;            }            name = Some(path.to_string_lossy().into_owned());            break;        }        name    }     async fn get_installed_binary(        &self,        delegate: &Arc<dyn DapDelegate>,        config: &DebugTaskDefinition,        user_installed_path: Option<PathBuf>,        user_args: Option<Vec<String>>,        user_env: Option<HashMap<String, String>>,        python_from_toolchain: Option<String>,    ) -> Result<DebugAdapterBinary> {        let mut tcp_connection = config.tcp_connection.clone().unwrap_or_default();         let (config_port, config_host) = config            .config            .get("connect")            .map(|value| {                (                    value                        .get("port")                        .and_then(|val| val.as_u64().map(|p| p as u16)),                    value.get("host").and_then(|val| val.as_str()),                )            })            .unwrap_or_else(|| {                (                    config                        .config                        .get("port")                        .and_then(|port| port.as_u64().map(|p| p as u16)),                    config.config.get("host").and_then(|host| host.as_str()),                )            });         let is_attach_with_connect = if config            .config            .get("request")            .is_some_and(|val| val.as_str().is_some_and(|request| request == "attach"))        {            if tcp_connection.host.is_some() && config_host.is_some() {                bail!("Cannot have two different hosts in debug configuration")            } else if tcp_connection.port.is_some() && config_port.is_some() {                bail!("Cannot have two different ports in debug configuration")            }             if let Some(hostname) = config_host {                tcp_connection.host = Some(hostname.parse().context("hostname must be IPv4")?);            }            tcp_connection.port = config_port;            DebugpyLaunchMode::AttachWithConnect { host: config_host }        } else {            DebugpyLaunchMode::Normal        };         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;         let python_path = if let Some(toolchain) = python_from_toolchain {            Some(toolchain)        } else {            Self::system_python_name(delegate).await        };         let python_command = python_path.context("failed to find binary path for Python")?;        log::debug!("Using Python executable: {}", python_command);         let arguments = Self::generate_debugpy_arguments(            &host,            port,            is_attach_with_connect,            user_installed_path.as_deref(),            user_args,        )        .await?;         log::debug!(            "Starting debugpy adapter with command: {} {}",            python_command,            arguments.join(" ")        );         Ok(DebugAdapterBinary {            command: Some(python_command),            arguments,            connection: Some(adapters::TcpArguments {                host,                port,                timeout,            }),            cwd: Some(delegate.worktree_root_path().to_path_buf()),            envs: user_env.unwrap_or_default(),            request_args: self.request_args(delegate, config).await?,        })    }} #[async_trait(?Send)]impl DebugAdapter for PythonDebugAdapter {    fn name(&self) -> DebugAdapterName {        Self::DEBUG_ADAPTER_NAME    }     fn adapter_language_name(&self) -> Option<LanguageName> {        Some(SharedString::new_static("Python").into())    }     async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {        let mut args = json!({            "request": match zed_scenario.request {                DebugRequest::Launch(_) => "launch",                DebugRequest::Attach(_) => "attach",            },            "subProcess": true,            "redirectOutput": true,        });         let map = args.as_object_mut().unwrap();        match &zed_scenario.request {            DebugRequest::Attach(attach) => {                map.insert("processId".into(), attach.process_id.into());            }            DebugRequest::Launch(launch) => {                map.insert("program".into(), launch.program.clone().into());                map.insert("args".into(), launch.args.clone().into());                if !launch.env.is_empty() {                    map.insert("env".into(), launch.env_json());                }                 if let Some(stop_on_entry) = zed_scenario.stop_on_entry {                    map.insert("stopOnEntry".into(), stop_on_entry.into());                }                if let Some(cwd) = launch.cwd.as_ref() {                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());                }            }        }         Ok(DebugScenario {            adapter: zed_scenario.adapter,            label: zed_scenario.label,            config: args,            build: None,            tcp_connection: None,        })    }     fn dap_schema(&self) -> serde_json::Value {        json!({            "properties": {                "request": {                    "type": "string",                    "enum": ["attach", "launch"],                    "description": "Debug adapter request type"                },                "autoReload": {                    "default": {},                    "description": "Configures automatic reload of code on edit.",                    "properties": {                        "enable": {                            "default": false,                            "description": "Automatically reload code on edit.",                            "type": "boolean"                        },                        "exclude": {                            "default": [

Lines 1–500 of 1136.