1use anyhow::{Context as _, ensure};
   2use anyhow::{Result, anyhow};
   3use async_trait::async_trait;
   4use collections::HashMap;
   5use futures::{AsyncBufReadExt, StreamExt as _};
   6use gpui::{App, AsyncApp, SharedString, Task};
   7use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
   8use language::language_settings::language_settings;
   9use language::{ContextLocation, LanguageToolchainStore, LspInstaller};
  10use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
  11use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
  12use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
  13use lsp::LanguageServerBinary;
  14use lsp::LanguageServerName;
  15use node_runtime::{NodeRuntime, VersionStrategy};
  16use pet_core::Configuration;
  17use pet_core::os_environment::Environment;
  18use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
  19use pet_virtualenv::is_virtualenv_dir;
  20use project::Fs;
  21use project::lsp_store::language_server_settings;
  22use rope::Rope;
  23use serde::{Deserialize, Serialize};
  24use serde_json::{Value, json};
  25use smol::lock::OnceCell;
  26use std::cmp::Ordering;
  27use std::env::consts;
  28use util::command::new_smol_command;
  29use util::fs::{make_file_executable, remove_matching};
  30use util::rel_path::RelPath;
  31
  32use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
  33use parking_lot::Mutex;
  34use std::str::FromStr;
  35use std::{
  36    borrow::Cow,
  37    fmt::Write,
  38    path::{Path, PathBuf},
  39    sync::Arc,
  40};
  41use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
  42use util::{ResultExt, maybe};
  43
  44#[derive(Debug, Serialize, Deserialize)]
  45pub(crate) struct PythonToolchainData {
  46    #[serde(flatten)]
  47    environment: PythonEnvironment,
  48    #[serde(skip_serializing_if = "Option::is_none")]
  49    activation_scripts: Option<HashMap<ShellKind, PathBuf>>,
  50}
  51
  52pub(crate) struct PyprojectTomlManifestProvider;
  53
  54impl ManifestProvider for PyprojectTomlManifestProvider {
  55    fn name(&self) -> ManifestName {
  56        SharedString::new_static("pyproject.toml").into()
  57    }
  58
  59    fn search(
  60        &self,
  61        ManifestQuery {
  62            path,
  63            depth,
  64            delegate,
  65        }: ManifestQuery,
  66    ) -> Option<Arc<RelPath>> {
  67        for path in path.ancestors().take(depth) {
  68            let p = path.join(RelPath::unix("pyproject.toml").unwrap());
  69            if delegate.exists(&p, Some(false)) {
  70                return Some(path.into());
  71            }
  72        }
  73
  74        None
  75    }
  76}
  77
  78enum TestRunner {
  79    UNITTEST,
  80    PYTEST,
  81}
  82
  83impl FromStr for TestRunner {
  84    type Err = ();
  85
  86    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
  87        match s {
  88            "unittest" => Ok(Self::UNITTEST),
  89            "pytest" => Ok(Self::PYTEST),
  90            _ => Err(()),
  91        }
  92    }
  93}
  94
  95/// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
  96/// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
  97/// and `name` is the symbol name itself.
  98///
  99/// The problem with it is that Pyright adjusts the sort text based on previous resolutions (items for which we've issued `completion/resolve` call have their sortText adjusted),
 100/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail.
 101/// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
 102fn process_pyright_completions(items: &mut [lsp::CompletionItem]) {
 103    for item in items {
 104        item.sort_text.take();
 105    }
 106}
 107
 108pub struct TyLspAdapter {
 109    fs: Arc<dyn Fs>,
 110}
 111
 112#[cfg(target_os = "macos")]
 113impl TyLspAdapter {
 114    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 115    const ARCH_SERVER_NAME: &str = "apple-darwin";
 116}
 117
 118#[cfg(target_os = "linux")]
 119impl TyLspAdapter {
 120    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 121    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
 122}
 123
 124#[cfg(target_os = "freebsd")]
 125impl TyLspAdapter {
 126    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 127    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
 128}
 129
 130#[cfg(target_os = "windows")]
 131impl TyLspAdapter {
 132    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
 133    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
 134}
 135
 136impl TyLspAdapter {
 137    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty");
 138
 139    pub fn new(fs: Arc<dyn Fs>) -> TyLspAdapter {
 140        TyLspAdapter { fs }
 141    }
 142
 143    fn build_asset_name() -> Result<(String, String)> {
 144        let arch = match consts::ARCH {
 145            "x86" => "i686",
 146            _ => consts::ARCH,
 147        };
 148        let os = Self::ARCH_SERVER_NAME;
 149        let suffix = match consts::OS {
 150            "windows" => "zip",
 151            _ => "tar.gz",
 152        };
 153        let asset_name = format!("ty-{arch}-{os}.{suffix}");
 154        let asset_stem = format!("ty-{arch}-{os}");
 155        Ok((asset_stem, asset_name))
 156    }
 157}
 158
 159#[async_trait(?Send)]
 160impl LspAdapter for TyLspAdapter {
 161    fn name(&self) -> LanguageServerName {
 162        Self::SERVER_NAME
 163    }
 164
 165    async fn workspace_configuration(
 166        self: Arc<Self>,
 167        delegate: &Arc<dyn LspAdapterDelegate>,
 168        toolchain: Option<Toolchain>,
 169        cx: &mut AsyncApp,
 170    ) -> Result<Value> {
 171        let mut ret = cx
 172            .update(|cx| {
 173                language_server_settings(delegate.as_ref(), &self.name(), cx)
 174                    .and_then(|s| s.settings.clone())
 175            })?
 176            .unwrap_or_else(|| json!({}));
 177        if let Some(toolchain) = toolchain.and_then(|toolchain| {
 178            serde_json::from_value::<PythonToolchainData>(toolchain.as_json).ok()
 179        }) {
 180            _ = maybe!({
 181                let uri =
 182                    url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?;
 183                let sys_prefix = toolchain.environment.prefix.clone()?;
 184                let environment = json!({
 185                    "executable": {
 186                        "uri": uri,
 187                        "sysPrefix": sys_prefix
 188                    }
 189                });
 190                ret.as_object_mut()?
 191                    .entry("pythonExtension")
 192                    .or_insert_with(|| json!({ "activeEnvironment": environment }));
 193                Some(())
 194            });
 195        }
 196        Ok(json!({"ty": ret}))
 197    }
 198}
 199
 200impl LspInstaller for TyLspAdapter {
 201    type BinaryVersion = GitHubLspBinaryVersion;
 202    async fn fetch_latest_server_version(
 203        &self,
 204        delegate: &dyn LspAdapterDelegate,
 205        _: bool,
 206        _: &mut AsyncApp,
 207    ) -> Result<Self::BinaryVersion> {
 208        let release =
 209            latest_github_release("astral-sh/ty", true, true, delegate.http_client()).await?;
 210        let (_, asset_name) = Self::build_asset_name()?;
 211        let asset = release
 212            .assets
 213            .into_iter()
 214            .find(|asset| asset.name == asset_name)
 215            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 216        Ok(GitHubLspBinaryVersion {
 217            name: release.tag_name,
 218            url: asset.browser_download_url,
 219            digest: asset.digest,
 220        })
 221    }
 222
 223    async fn fetch_server_binary(
 224        &self,
 225        latest_version: Self::BinaryVersion,
 226        container_dir: PathBuf,
 227        delegate: &dyn LspAdapterDelegate,
 228    ) -> Result<LanguageServerBinary> {
 229        let GitHubLspBinaryVersion {
 230            name,
 231            url,
 232            digest: expected_digest,
 233        } = latest_version;
 234        let destination_path = container_dir.join(format!("ty-{name}"));
 235
 236        async_fs::create_dir_all(&destination_path).await?;
 237
 238        let server_path = match Self::GITHUB_ASSET_KIND {
 239            AssetKind::TarGz | AssetKind::Gz => destination_path
 240                .join(Self::build_asset_name()?.0)
 241                .join("ty"),
 242            AssetKind::Zip => destination_path.clone().join("ty.exe"),
 243        };
 244
 245        let binary = LanguageServerBinary {
 246            path: server_path.clone(),
 247            env: None,
 248            arguments: vec!["server".into()],
 249        };
 250
 251        let metadata_path = destination_path.with_extension("metadata");
 252        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
 253            .await
 254            .ok();
 255        if let Some(metadata) = metadata {
 256            let validity_check = async || {
 257                delegate
 258                    .try_exec(LanguageServerBinary {
 259                        path: server_path.clone(),
 260                        arguments: vec!["--version".into()],
 261                        env: None,
 262                    })
 263                    .await
 264                    .inspect_err(|err| {
 265                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
 266                    })
 267            };
 268            if let (Some(actual_digest), Some(expected_digest)) =
 269                (&metadata.digest, &expected_digest)
 270            {
 271                if actual_digest == expected_digest {
 272                    if validity_check().await.is_ok() {
 273                        return Ok(binary);
 274                    }
 275                } else {
 276                    log::info!(
 277                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
 278                    );
 279                }
 280            } else if validity_check().await.is_ok() {
 281                return Ok(binary);
 282            }
 283        }
 284
 285        download_server_binary(
 286            &*delegate.http_client(),
 287            &url,
 288            expected_digest.as_deref(),
 289            &destination_path,
 290            Self::GITHUB_ASSET_KIND,
 291        )
 292        .await?;
 293        make_file_executable(&server_path).await?;
 294        remove_matching(&container_dir, |path| path != destination_path).await;
 295        GithubBinaryMetadata::write_to_file(
 296            &GithubBinaryMetadata {
 297                metadata_version: 1,
 298                digest: expected_digest,
 299            },
 300            &metadata_path,
 301        )
 302        .await?;
 303
 304        Ok(LanguageServerBinary {
 305            path: server_path,
 306            env: None,
 307            arguments: vec!["server".into()],
 308        })
 309    }
 310
 311    async fn cached_server_binary(
 312        &self,
 313        container_dir: PathBuf,
 314        _: &dyn LspAdapterDelegate,
 315    ) -> Option<LanguageServerBinary> {
 316        maybe!(async {
 317            let mut last = None;
 318            let mut entries = self.fs.read_dir(&container_dir).await?;
 319            while let Some(entry) = entries.next().await {
 320                let path = entry?;
 321                if path.extension().is_some_and(|ext| ext == "metadata") {
 322                    continue;
 323                }
 324                last = Some(path);
 325            }
 326
 327            let path = last.context("no cached binary")?;
 328            let path = match TyLspAdapter::GITHUB_ASSET_KIND {
 329                AssetKind::TarGz | AssetKind::Gz => {
 330                    path.join(Self::build_asset_name()?.0).join("ty")
 331                }
 332                AssetKind::Zip => path.join("ty.exe"),
 333            };
 334
 335            anyhow::Ok(LanguageServerBinary {
 336                path,
 337                env: None,
 338                arguments: vec!["server".into()],
 339            })
 340        })
 341        .await
 342        .log_err()
 343    }
 344}
 345
 346pub struct PyrightLspAdapter {
 347    node: NodeRuntime,
 348}
 349
 350impl PyrightLspAdapter {
 351    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
 352    const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
 353    const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
 354
 355    pub fn new(node: NodeRuntime) -> Self {
 356        PyrightLspAdapter { node }
 357    }
 358
 359    async fn get_cached_server_binary(
 360        container_dir: PathBuf,
 361        node: &NodeRuntime,
 362    ) -> Option<LanguageServerBinary> {
 363        let server_path = container_dir.join(Self::SERVER_PATH);
 364        if server_path.exists() {
 365            Some(LanguageServerBinary {
 366                path: node.binary_path().await.log_err()?,
 367                env: None,
 368                arguments: vec![server_path.into(), "--stdio".into()],
 369            })
 370        } else {
 371            log::error!("missing executable in directory {:?}", server_path);
 372            None
 373        }
 374    }
 375}
 376
 377#[async_trait(?Send)]
 378impl LspAdapter for PyrightLspAdapter {
 379    fn name(&self) -> LanguageServerName {
 380        Self::SERVER_NAME
 381    }
 382
 383    async fn initialization_options(
 384        self: Arc<Self>,
 385        _: &Arc<dyn LspAdapterDelegate>,
 386    ) -> Result<Option<Value>> {
 387        // Provide minimal initialization options
 388        // Virtual environment configuration will be handled through workspace configuration
 389        Ok(Some(json!({
 390            "python": {
 391                "analysis": {
 392                    "autoSearchPaths": true,
 393                    "useLibraryCodeForTypes": true,
 394                    "autoImportCompletions": true
 395                }
 396            }
 397        })))
 398    }
 399
 400    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
 401        process_pyright_completions(items);
 402    }
 403
 404    async fn label_for_completion(
 405        &self,
 406        item: &lsp::CompletionItem,
 407        language: &Arc<language::Language>,
 408    ) -> Option<language::CodeLabel> {
 409        let label = &item.label;
 410        let grammar = language.grammar()?;
 411        let highlight_id = match item.kind? {
 412            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
 413            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
 414            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
 415            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
 416            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
 417            _ => {
 418                return None;
 419            }
 420        };
 421        let mut text = label.clone();
 422        if let Some(completion_details) = item
 423            .label_details
 424            .as_ref()
 425            .and_then(|details| details.description.as_ref())
 426        {
 427            write!(&mut text, " {}", completion_details).ok();
 428        }
 429        Some(language::CodeLabel::filtered(
 430            text,
 431            item.filter_text.as_deref(),
 432            highlight_id
 433                .map(|id| (0..label.len(), id))
 434                .into_iter()
 435                .collect(),
 436        ))
 437    }
 438
 439    async fn label_for_symbol(
 440        &self,
 441        name: &str,
 442        kind: lsp::SymbolKind,
 443        language: &Arc<language::Language>,
 444    ) -> Option<language::CodeLabel> {
 445        let (text, filter_range, display_range) = match kind {
 446            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 447                let text = format!("def {}():\n", name);
 448                let filter_range = 4..4 + name.len();
 449                let display_range = 0..filter_range.end;
 450                (text, filter_range, display_range)
 451            }
 452            lsp::SymbolKind::CLASS => {
 453                let text = format!("class {}:", name);
 454                let filter_range = 6..6 + name.len();
 455                let display_range = 0..filter_range.end;
 456                (text, filter_range, display_range)
 457            }
 458            lsp::SymbolKind::CONSTANT => {
 459                let text = format!("{} = 0", name);
 460                let filter_range = 0..name.len();
 461                let display_range = 0..filter_range.end;
 462                (text, filter_range, display_range)
 463            }
 464            _ => return None,
 465        };
 466
 467        Some(language::CodeLabel::new(
 468            text[display_range.clone()].to_string(),
 469            filter_range,
 470            language.highlight_text(&Rope::from_str_small(text.as_str()), display_range),
 471        ))
 472    }
 473
 474    async fn workspace_configuration(
 475        self: Arc<Self>,
 476        adapter: &Arc<dyn LspAdapterDelegate>,
 477        toolchain: Option<Toolchain>,
 478        cx: &mut AsyncApp,
 479    ) -> Result<Value> {
 480        cx.update(move |cx| {
 481            let mut user_settings =
 482                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
 483                    .and_then(|s| s.settings.clone())
 484                    .unwrap_or_default();
 485
 486            // If we have a detected toolchain, configure Pyright to use it
 487            if let Some(toolchain) = toolchain
 488                && let Ok(env) =
 489                    serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
 490            {
 491                if !user_settings.is_object() {
 492                    user_settings = Value::Object(serde_json::Map::default());
 493                }
 494                let object = user_settings.as_object_mut().unwrap();
 495
 496                let interpreter_path = toolchain.path.to_string();
 497                if let Some(venv_dir) = &env.environment.prefix {
 498                    // Set venvPath and venv at the root level
 499                    // This matches the format of a pyrightconfig.json file
 500                    if let Some(parent) = venv_dir.parent() {
 501                        // Use relative path if the venv is inside the workspace
 502                        let venv_path = if parent == adapter.worktree_root_path() {
 503                            ".".to_string()
 504                        } else {
 505                            parent.to_string_lossy().into_owned()
 506                        };
 507                        object.insert("venvPath".to_string(), Value::String(venv_path));
 508                    }
 509
 510                    if let Some(venv_name) = venv_dir.file_name() {
 511                        object.insert(
 512                            "venv".to_owned(),
 513                            Value::String(venv_name.to_string_lossy().into_owned()),
 514                        );
 515                    }
 516                }
 517
 518                // Always set the python interpreter path
 519                // Get or create the python section
 520                let python = object
 521                    .entry("python")
 522                    .and_modify(|v| {
 523                        if !v.is_object() {
 524                            *v = Value::Object(serde_json::Map::default());
 525                        }
 526                    })
 527                    .or_insert(Value::Object(serde_json::Map::default()));
 528                let python = python.as_object_mut().unwrap();
 529
 530                // Set both pythonPath and defaultInterpreterPath for compatibility
 531                python.insert(
 532                    "pythonPath".to_owned(),
 533                    Value::String(interpreter_path.clone()),
 534                );
 535                python.insert(
 536                    "defaultInterpreterPath".to_owned(),
 537                    Value::String(interpreter_path),
 538                );
 539            }
 540
 541            user_settings
 542        })
 543    }
 544}
 545
 546impl LspInstaller for PyrightLspAdapter {
 547    type BinaryVersion = String;
 548
 549    async fn fetch_latest_server_version(
 550        &self,
 551        _: &dyn LspAdapterDelegate,
 552        _: bool,
 553        _: &mut AsyncApp,
 554    ) -> Result<String> {
 555        self.node
 556            .npm_package_latest_version(Self::SERVER_NAME.as_ref())
 557            .await
 558    }
 559
 560    async fn check_if_user_installed(
 561        &self,
 562        delegate: &dyn LspAdapterDelegate,
 563        _: Option<Toolchain>,
 564        _: &AsyncApp,
 565    ) -> Option<LanguageServerBinary> {
 566        if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
 567            let env = delegate.shell_env().await;
 568            Some(LanguageServerBinary {
 569                path: pyright_bin,
 570                env: Some(env),
 571                arguments: vec!["--stdio".into()],
 572            })
 573        } else {
 574            let node = delegate.which("node".as_ref()).await?;
 575            let (node_modules_path, _) = delegate
 576                .npm_package_installed_version(Self::SERVER_NAME.as_ref())
 577                .await
 578                .log_err()??;
 579
 580            let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
 581
 582            let env = delegate.shell_env().await;
 583            Some(LanguageServerBinary {
 584                path: node,
 585                env: Some(env),
 586                arguments: vec![path.into(), "--stdio".into()],
 587            })
 588        }
 589    }
 590
 591    async fn fetch_server_binary(
 592        &self,
 593        latest_version: Self::BinaryVersion,
 594        container_dir: PathBuf,
 595        delegate: &dyn LspAdapterDelegate,
 596    ) -> Result<LanguageServerBinary> {
 597        let server_path = container_dir.join(Self::SERVER_PATH);
 598
 599        self.node
 600            .npm_install_packages(
 601                &container_dir,
 602                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
 603            )
 604            .await?;
 605
 606        let env = delegate.shell_env().await;
 607        Ok(LanguageServerBinary {
 608            path: self.node.binary_path().await?,
 609            env: Some(env),
 610            arguments: vec![server_path.into(), "--stdio".into()],
 611        })
 612    }
 613
 614    async fn check_if_version_installed(
 615        &self,
 616        version: &Self::BinaryVersion,
 617        container_dir: &PathBuf,
 618        delegate: &dyn LspAdapterDelegate,
 619    ) -> Option<LanguageServerBinary> {
 620        let server_path = container_dir.join(Self::SERVER_PATH);
 621
 622        let should_install_language_server = self
 623            .node
 624            .should_install_npm_package(
 625                Self::SERVER_NAME.as_ref(),
 626                &server_path,
 627                container_dir,
 628                VersionStrategy::Latest(version),
 629            )
 630            .await;
 631
 632        if should_install_language_server {
 633            None
 634        } else {
 635            let env = delegate.shell_env().await;
 636            Some(LanguageServerBinary {
 637                path: self.node.binary_path().await.ok()?,
 638                env: Some(env),
 639                arguments: vec![server_path.into(), "--stdio".into()],
 640            })
 641        }
 642    }
 643
 644    async fn cached_server_binary(
 645        &self,
 646        container_dir: PathBuf,
 647        delegate: &dyn LspAdapterDelegate,
 648    ) -> Option<LanguageServerBinary> {
 649        let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
 650        binary.env = Some(delegate.shell_env().await);
 651        Some(binary)
 652    }
 653}
 654
 655pub(crate) struct PythonContextProvider;
 656
 657const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
 658    VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET"));
 659
 660const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
 661    VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
 662
 663const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
 664    VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
 665
 666impl ContextProvider for PythonContextProvider {
 667    fn build_context(
 668        &self,
 669        variables: &task::TaskVariables,
 670        location: ContextLocation<'_>,
 671        _: Option<HashMap<String, String>>,
 672        toolchains: Arc<dyn LanguageToolchainStore>,
 673        cx: &mut gpui::App,
 674    ) -> Task<Result<task::TaskVariables>> {
 675        let test_target =
 676            match selected_test_runner(location.file_location.buffer.read(cx).file(), cx) {
 677                TestRunner::UNITTEST => self.build_unittest_target(variables),
 678                TestRunner::PYTEST => self.build_pytest_target(variables),
 679            };
 680
 681        let module_target = self.build_module_target(variables);
 682        let location_file = location.file_location.buffer.read(cx).file().cloned();
 683        let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
 684
 685        cx.spawn(async move |cx| {
 686            let active_toolchain = if let Some(worktree_id) = worktree_id {
 687                let file_path = location_file
 688                    .as_ref()
 689                    .and_then(|f| f.path().parent())
 690                    .map(Arc::from)
 691                    .unwrap_or_else(|| RelPath::empty().into());
 692
 693                toolchains
 694                    .active_toolchain(worktree_id, file_path, "Python".into(), cx)
 695                    .await
 696                    .map_or_else(
 697                        || String::from("python3"),
 698                        |toolchain| toolchain.path.to_string(),
 699                    )
 700            } else {
 701                String::from("python3")
 702            };
 703
 704            let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
 705
 706            Ok(task::TaskVariables::from_iter(
 707                test_target
 708                    .into_iter()
 709                    .chain(module_target.into_iter())
 710                    .chain([toolchain]),
 711            ))
 712        })
 713    }
 714
 715    fn associated_tasks(
 716        &self,
 717        file: Option<Arc<dyn language::File>>,
 718        cx: &App,
 719    ) -> Task<Option<TaskTemplates>> {
 720        let test_runner = selected_test_runner(file.as_ref(), cx);
 721
 722        let mut tasks = vec![
 723            // Execute a selection
 724            TaskTemplate {
 725                label: "execute selection".to_owned(),
 726                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 727                args: vec![
 728                    "-c".to_owned(),
 729                    VariableName::SelectedText.template_value_with_whitespace(),
 730                ],
 731                cwd: Some(VariableName::WorktreeRoot.template_value()),
 732                ..TaskTemplate::default()
 733            },
 734            // Execute an entire file
 735            TaskTemplate {
 736                label: format!("run '{}'", VariableName::File.template_value()),
 737                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 738                args: vec![VariableName::File.template_value_with_whitespace()],
 739                cwd: Some(VariableName::WorktreeRoot.template_value()),
 740                ..TaskTemplate::default()
 741            },
 742            // Execute a file as module
 743            TaskTemplate {
 744                label: format!("run module '{}'", VariableName::File.template_value()),
 745                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 746                args: vec![
 747                    "-m".to_owned(),
 748                    PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
 749                ],
 750                cwd: Some(VariableName::WorktreeRoot.template_value()),
 751                tags: vec!["python-module-main-method".to_owned()],
 752                ..TaskTemplate::default()
 753            },
 754        ];
 755
 756        tasks.extend(match test_runner {
 757            TestRunner::UNITTEST => {
 758                [
 759                    // Run tests for an entire file
 760                    TaskTemplate {
 761                        label: format!("unittest '{}'", VariableName::File.template_value()),
 762                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 763                        args: vec![
 764                            "-m".to_owned(),
 765                            "unittest".to_owned(),
 766                            VariableName::File.template_value_with_whitespace(),
 767                        ],
 768                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 769                        ..TaskTemplate::default()
 770                    },
 771                    // Run test(s) for a specific target within a file
 772                    TaskTemplate {
 773                        label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
 774                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 775                        args: vec![
 776                            "-m".to_owned(),
 777                            "unittest".to_owned(),
 778                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
 779                        ],
 780                        tags: vec![
 781                            "python-unittest-class".to_owned(),
 782                            "python-unittest-method".to_owned(),
 783                        ],
 784                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 785                        ..TaskTemplate::default()
 786                    },
 787                ]
 788            }
 789            TestRunner::PYTEST => {
 790                [
 791                    // Run tests for an entire file
 792                    TaskTemplate {
 793                        label: format!("pytest '{}'", VariableName::File.template_value()),
 794                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 795                        args: vec![
 796                            "-m".to_owned(),
 797                            "pytest".to_owned(),
 798                            VariableName::File.template_value_with_whitespace(),
 799                        ],
 800                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 801                        ..TaskTemplate::default()
 802                    },
 803                    // Run test(s) for a specific target within a file
 804                    TaskTemplate {
 805                        label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
 806                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 807                        args: vec![
 808                            "-m".to_owned(),
 809                            "pytest".to_owned(),
 810                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
 811                        ],
 812                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 813                        tags: vec![
 814                            "python-pytest-class".to_owned(),
 815                            "python-pytest-method".to_owned(),
 816                        ],
 817                        ..TaskTemplate::default()
 818                    },
 819                ]
 820            }
 821        });
 822
 823        Task::ready(Some(TaskTemplates(tasks)))
 824    }
 825}
 826
 827fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {
 828    const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";
 829    language_settings(Some(LanguageName::new("Python")), location, cx)
 830        .tasks
 831        .variables
 832        .get(TEST_RUNNER_VARIABLE)
 833        .and_then(|val| TestRunner::from_str(val).ok())
 834        .unwrap_or(TestRunner::PYTEST)
 835}
 836
 837impl PythonContextProvider {
 838    fn build_unittest_target(
 839        &self,
 840        variables: &task::TaskVariables,
 841    ) -> Option<(VariableName, String)> {
 842        let python_module_name =
 843            python_module_name_from_relative_path(variables.get(&VariableName::RelativeFile)?);
 844
 845        let unittest_class_name =
 846            variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
 847
 848        let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
 849            "_unittest_method_name",
 850        )));
 851
 852        let unittest_target_str = match (unittest_class_name, unittest_method_name) {
 853            (Some(class_name), Some(method_name)) => {
 854                format!("{python_module_name}.{class_name}.{method_name}")
 855            }
 856            (Some(class_name), None) => format!("{python_module_name}.{class_name}"),
 857            (None, None) => python_module_name,
 858            // should never happen, a TestCase class is the unit of testing
 859            (None, Some(_)) => return None,
 860        };
 861
 862        Some((
 863            PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),
 864            unittest_target_str,
 865        ))
 866    }
 867
 868    fn build_pytest_target(
 869        &self,
 870        variables: &task::TaskVariables,
 871    ) -> Option<(VariableName, String)> {
 872        let file_path = variables.get(&VariableName::RelativeFile)?;
 873
 874        let pytest_class_name =
 875            variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name")));
 876
 877        let pytest_method_name =
 878            variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name")));
 879
 880        let pytest_target_str = match (pytest_class_name, pytest_method_name) {
 881            (Some(class_name), Some(method_name)) => {
 882                format!("{file_path}::{class_name}::{method_name}")
 883            }
 884            (Some(class_name), None) => {
 885                format!("{file_path}::{class_name}")
 886            }
 887            (None, Some(method_name)) => {
 888                format!("{file_path}::{method_name}")
 889            }
 890            (None, None) => file_path.to_string(),
 891        };
 892
 893        Some((PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str))
 894    }
 895
 896    fn build_module_target(
 897        &self,
 898        variables: &task::TaskVariables,
 899    ) -> Result<(VariableName, String)> {
 900        let python_module_name = python_module_name_from_relative_path(
 901            variables.get(&VariableName::RelativeFile).unwrap_or(""),
 902        );
 903
 904        let module_target = (PYTHON_MODULE_NAME_TASK_VARIABLE.clone(), python_module_name);
 905
 906        Ok(module_target)
 907    }
 908}
 909
 910fn python_module_name_from_relative_path(relative_path: &str) -> String {
 911    let path_with_dots = relative_path.replace('/', ".");
 912    path_with_dots
 913        .strip_suffix(".py")
 914        .unwrap_or(&path_with_dots)
 915        .to_string()
 916}
 917
 918fn is_python_env_global(k: &PythonEnvironmentKind) -> bool {
 919    matches!(
 920        k,
 921        PythonEnvironmentKind::Homebrew
 922            | PythonEnvironmentKind::Pyenv
 923            | PythonEnvironmentKind::GlobalPaths
 924            | PythonEnvironmentKind::MacPythonOrg
 925            | PythonEnvironmentKind::MacCommandLineTools
 926            | PythonEnvironmentKind::LinuxGlobal
 927            | PythonEnvironmentKind::MacXCode
 928            | PythonEnvironmentKind::WindowsStore
 929            | PythonEnvironmentKind::WindowsRegistry
 930    )
 931}
 932
 933fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
 934    match k {
 935        PythonEnvironmentKind::Conda => "Conda",
 936        PythonEnvironmentKind::Pixi => "pixi",
 937        PythonEnvironmentKind::Homebrew => "Homebrew",
 938        PythonEnvironmentKind::Pyenv => "global (Pyenv)",
 939        PythonEnvironmentKind::GlobalPaths => "global",
 940        PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
 941        PythonEnvironmentKind::Pipenv => "Pipenv",
 942        PythonEnvironmentKind::Poetry => "Poetry",
 943        PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
 944        PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
 945        PythonEnvironmentKind::LinuxGlobal => "global",
 946        PythonEnvironmentKind::MacXCode => "global (Xcode)",
 947        PythonEnvironmentKind::Venv => "venv",
 948        PythonEnvironmentKind::VirtualEnv => "virtualenv",
 949        PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
 950        PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
 951        PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
 952    }
 953}
 954
 955pub(crate) struct PythonToolchainProvider;
 956
 957static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
 958    // Prioritize non-Conda environments.
 959    PythonEnvironmentKind::Poetry,
 960    PythonEnvironmentKind::Pipenv,
 961    PythonEnvironmentKind::VirtualEnvWrapper,
 962    PythonEnvironmentKind::Venv,
 963    PythonEnvironmentKind::VirtualEnv,
 964    PythonEnvironmentKind::PyenvVirtualEnv,
 965    PythonEnvironmentKind::Pixi,
 966    PythonEnvironmentKind::Conda,
 967    PythonEnvironmentKind::Pyenv,
 968    PythonEnvironmentKind::GlobalPaths,
 969    PythonEnvironmentKind::Homebrew,
 970];
 971
 972fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
 973    if let Some(kind) = kind {
 974        ENV_PRIORITY_LIST
 975            .iter()
 976            .position(|blessed_env| blessed_env == &kind)
 977            .unwrap_or(ENV_PRIORITY_LIST.len())
 978    } else {
 979        // Unknown toolchains are less useful than non-blessed ones.
 980        ENV_PRIORITY_LIST.len() + 1
 981    }
 982}
 983
 984/// Return the name of environment declared in <worktree-root/.venv.
 985///
 986/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
 987async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
 988    let file = async_fs::File::open(worktree_root.join(".venv"))
 989        .await
 990        .ok()?;
 991    let mut venv_name = String::new();
 992    smol::io::BufReader::new(file)
 993        .read_line(&mut venv_name)
 994        .await
 995        .ok()?;
 996    Some(venv_name.trim().to_string())
 997}
 998
 999fn get_venv_parent_dir(env: &PythonEnvironment) -> Option<PathBuf> {
1000    // If global, we aren't a virtual environment
1001    if let Some(kind) = env.kind
1002        && is_python_env_global(&kind)
1003    {
1004        return None;
1005    }
1006
1007    // Check to be sure we are a virtual environment using pet's most generic
1008    // virtual environment type, VirtualEnv
1009    let venv = env
1010        .executable
1011        .as_ref()
1012        .and_then(|p| p.parent())
1013        .and_then(|p| p.parent())
1014        .filter(|p| is_virtualenv_dir(p))?;
1015
1016    venv.parent().map(|parent| parent.to_path_buf())
1017}
1018
1019fn wr_distance(wr: &PathBuf, venv: Option<&PathBuf>) -> usize {
1020    if let Some(venv) = venv
1021        && let Ok(p) = venv.strip_prefix(wr)
1022    {
1023        p.components().count()
1024    } else {
1025        usize::MAX
1026    }
1027}
1028
1029#[async_trait]
1030impl ToolchainLister for PythonToolchainProvider {
1031    async fn list(
1032        &self,
1033        worktree_root: PathBuf,
1034        subroot_relative_path: Arc<RelPath>,
1035        project_env: Option<HashMap<String, String>>,
1036        fs: &dyn Fs,
1037    ) -> ToolchainList {
1038        let env = project_env.unwrap_or_default();
1039        let environment = EnvironmentApi::from_env(&env);
1040        let locators = pet::locators::create_locators(
1041            Arc::new(pet_conda::Conda::from(&environment)),
1042            Arc::new(pet_poetry::Poetry::from(&environment)),
1043            &environment,
1044        );
1045        let mut config = Configuration::default();
1046
1047        // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
1048        // worktree root as the workspace directory.
1049        config.workspace_directories = Some(
1050            subroot_relative_path
1051                .ancestors()
1052                .map(|ancestor| worktree_root.join(ancestor.as_std_path()))
1053                .collect(),
1054        );
1055        for locator in locators.iter() {
1056            locator.configure(&config);
1057        }
1058
1059        let reporter = pet_reporter::collect::create_reporter();
1060        pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
1061
1062        let mut toolchains = reporter
1063            .environments
1064            .lock()
1065            .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
1066
1067        let wr = worktree_root;
1068        let wr_venv = get_worktree_venv_declaration(&wr).await;
1069        // Sort detected environments by:
1070        //     environment name matching activation file (<workdir>/.venv)
1071        //     environment project dir matching worktree_root
1072        //     general env priority
1073        //     environment path matching the CONDA_PREFIX env var
1074        //     executable path
1075        toolchains.sort_by(|lhs, rhs| {
1076            // Compare venv names against worktree .venv file
1077            let venv_ordering =
1078                wr_venv
1079                    .as_ref()
1080                    .map_or(Ordering::Equal, |venv| match (&lhs.name, &rhs.name) {
1081                        (Some(l), Some(r)) => (r == venv).cmp(&(l == venv)),
1082                        (Some(l), None) if l == venv => Ordering::Less,
1083                        (None, Some(r)) if r == venv => Ordering::Greater,
1084                        _ => Ordering::Equal,
1085                    });
1086
1087            // Compare project paths against worktree root
1088            let proj_ordering = || {
1089                let lhs_project = lhs.project.clone().or_else(|| get_venv_parent_dir(lhs));
1090                let rhs_project = rhs.project.clone().or_else(|| get_venv_parent_dir(rhs));
1091                wr_distance(&wr, lhs_project.as_ref()).cmp(&wr_distance(&wr, rhs_project.as_ref()))
1092            };
1093
1094            // Compare environment priorities
1095            let priority_ordering = || env_priority(lhs.kind).cmp(&env_priority(rhs.kind));
1096
1097            // Compare conda prefixes
1098            let conda_ordering = || {
1099                if lhs.kind == Some(PythonEnvironmentKind::Conda) {
1100                    environment
1101                        .get_env_var("CONDA_PREFIX".to_string())
1102                        .map(|conda_prefix| {
1103                            let is_match = |exe: &Option<PathBuf>| {
1104                                exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
1105                            };
1106                            match (is_match(&lhs.executable), is_match(&rhs.executable)) {
1107                                (true, false) => Ordering::Less,
1108                                (false, true) => Ordering::Greater,
1109                                _ => Ordering::Equal,
1110                            }
1111                        })
1112                        .unwrap_or(Ordering::Equal)
1113                } else {
1114                    Ordering::Equal
1115                }
1116            };
1117
1118            // Compare Python executables
1119            let exe_ordering = || lhs.executable.cmp(&rhs.executable);
1120
1121            venv_ordering
1122                .then_with(proj_ordering)
1123                .then_with(priority_ordering)
1124                .then_with(conda_ordering)
1125                .then_with(exe_ordering)
1126        });
1127
1128        let mut out_toolchains = Vec::new();
1129        for toolchain in toolchains {
1130            let Some(toolchain) = venv_to_toolchain(toolchain, fs).await else {
1131                continue;
1132            };
1133            out_toolchains.push(toolchain);
1134        }
1135        out_toolchains.dedup();
1136        ToolchainList {
1137            toolchains: out_toolchains,
1138            default: None,
1139            groups: Default::default(),
1140        }
1141    }
1142    fn meta(&self) -> ToolchainMetadata {
1143        ToolchainMetadata {
1144            term: SharedString::new_static("Virtual Environment"),
1145            new_toolchain_placeholder: SharedString::new_static(
1146                "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
1147            ),
1148            manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
1149        }
1150    }
1151
1152    async fn resolve(
1153        &self,
1154        path: PathBuf,
1155        env: Option<HashMap<String, String>>,
1156        fs: &dyn Fs,
1157    ) -> anyhow::Result<Toolchain> {
1158        let env = env.unwrap_or_default();
1159        let environment = EnvironmentApi::from_env(&env);
1160        let locators = pet::locators::create_locators(
1161            Arc::new(pet_conda::Conda::from(&environment)),
1162            Arc::new(pet_poetry::Poetry::from(&environment)),
1163            &environment,
1164        );
1165        let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
1166            .context("Could not find a virtual environment in provided path")?;
1167        let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
1168        venv_to_toolchain(venv, fs)
1169            .await
1170            .context("Could not convert a venv into a toolchain")
1171    }
1172
1173    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec<String> {
1174        let Ok(toolchain) =
1175            serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
1176        else {
1177            return vec![];
1178        };
1179
1180        log::debug!("(Python) Composing activation script for toolchain {toolchain:?}");
1181
1182        let mut activation_script = vec![];
1183
1184        match toolchain.environment.kind {
1185            Some(PythonEnvironmentKind::Conda) => {
1186                if let Some(name) = &toolchain.environment.name {
1187                    activation_script.push(format!("conda activate {name}"));
1188                } else {
1189                    activation_script.push("conda activate".to_string());
1190                }
1191            }
1192            Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
1193                if let Some(activation_scripts) = &toolchain.activation_scripts {
1194                    if let Some(activate_script_path) = activation_scripts.get(&shell) {
1195                        let activate_keyword = shell.activate_keyword();
1196                        if let Some(quoted) =
1197                            shell.try_quote(&activate_script_path.to_string_lossy())
1198                        {
1199                            activation_script.push(format!("{activate_keyword} {quoted}"));
1200                        }
1201                    }
1202                }
1203            }
1204            Some(PythonEnvironmentKind::Pyenv) => {
1205                let Some(manager) = &toolchain.environment.manager else {
1206                    return vec![];
1207                };
1208                let version = toolchain.environment.version.as_deref().unwrap_or("system");
1209                let pyenv = &manager.executable;
1210                let pyenv = pyenv.display();
1211                activation_script.extend(match shell {
1212                    ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
1213                    ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
1214                    ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
1215                    ShellKind::PowerShell => None,
1216                    ShellKind::Csh => None,
1217                    ShellKind::Tcsh => None,
1218                    ShellKind::Cmd => None,
1219                    ShellKind::Rc => None,
1220                    ShellKind::Xonsh => None,
1221                })
1222            }
1223            _ => {}
1224        }
1225        activation_script
1226    }
1227}
1228
1229async fn venv_to_toolchain(venv: PythonEnvironment, fs: &dyn Fs) -> Option<Toolchain> {
1230    let mut name = String::from("Python");
1231    if let Some(ref version) = venv.version {
1232        _ = write!(name, " {version}");
1233    }
1234
1235    let name_and_kind = match (&venv.name, &venv.kind) {
1236        (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
1237        (Some(name), None) => Some(format!("({name})")),
1238        (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
1239        (None, None) => None,
1240    };
1241
1242    if let Some(nk) = name_and_kind {
1243        _ = write!(name, " {nk}");
1244    }
1245
1246    let mut activation_scripts = HashMap::default();
1247    match venv.kind {
1248        Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
1249            resolve_venv_activation_scripts(&venv, fs, &mut activation_scripts).await
1250        }
1251        _ => {}
1252    }
1253    let data = PythonToolchainData {
1254        environment: venv,
1255        activation_scripts: Some(activation_scripts),
1256    };
1257
1258    Some(Toolchain {
1259        name: name.into(),
1260        path: data
1261            .environment
1262            .executable
1263            .as_ref()?
1264            .to_str()?
1265            .to_owned()
1266            .into(),
1267        language_name: LanguageName::new("Python"),
1268        as_json: serde_json::to_value(data).ok()?,
1269    })
1270}
1271
1272async fn resolve_venv_activation_scripts(
1273    venv: &PythonEnvironment,
1274    fs: &dyn Fs,
1275    activation_scripts: &mut HashMap<ShellKind, PathBuf>,
1276) {
1277    log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}");
1278    if let Some(prefix) = &venv.prefix {
1279        for (shell_kind, script_name) in &[
1280            (ShellKind::Posix, "activate"),
1281            (ShellKind::Rc, "activate"),
1282            (ShellKind::Csh, "activate.csh"),
1283            (ShellKind::Tcsh, "activate.csh"),
1284            (ShellKind::Fish, "activate.fish"),
1285            (ShellKind::Nushell, "activate.nu"),
1286            (ShellKind::PowerShell, "activate.ps1"),
1287            (ShellKind::Cmd, "activate.bat"),
1288            (ShellKind::Xonsh, "activate.xsh"),
1289        ] {
1290            let path = prefix.join(BINARY_DIR).join(script_name);
1291
1292            log::debug!("Trying path: {}", path.display());
1293
1294            if fs.is_file(&path).await {
1295                activation_scripts.insert(*shell_kind, path);
1296            }
1297        }
1298    }
1299}
1300
1301pub struct EnvironmentApi<'a> {
1302    global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
1303    project_env: &'a HashMap<String, String>,
1304    pet_env: pet_core::os_environment::EnvironmentApi,
1305}
1306
1307impl<'a> EnvironmentApi<'a> {
1308    pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
1309        let paths = project_env
1310            .get("PATH")
1311            .map(|p| std::env::split_paths(p).collect())
1312            .unwrap_or_default();
1313
1314        EnvironmentApi {
1315            global_search_locations: Arc::new(Mutex::new(paths)),
1316            project_env,
1317            pet_env: pet_core::os_environment::EnvironmentApi::new(),
1318        }
1319    }
1320
1321    fn user_home(&self) -> Option<PathBuf> {
1322        self.project_env
1323            .get("HOME")
1324            .or_else(|| self.project_env.get("USERPROFILE"))
1325            .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
1326            .or_else(|| self.pet_env.get_user_home())
1327    }
1328}
1329
1330impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
1331    fn get_user_home(&self) -> Option<PathBuf> {
1332        self.user_home()
1333    }
1334
1335    fn get_root(&self) -> Option<PathBuf> {
1336        None
1337    }
1338
1339    fn get_env_var(&self, key: String) -> Option<String> {
1340        self.project_env
1341            .get(&key)
1342            .cloned()
1343            .or_else(|| self.pet_env.get_env_var(key))
1344    }
1345
1346    fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
1347        if self.global_search_locations.lock().is_empty() {
1348            let mut paths = std::env::split_paths(
1349                &self
1350                    .get_env_var("PATH".to_string())
1351                    .or_else(|| self.get_env_var("Path".to_string()))
1352                    .unwrap_or_default(),
1353            )
1354            .collect::<Vec<PathBuf>>();
1355
1356            log::trace!("Env PATH: {:?}", paths);
1357            for p in self.pet_env.get_know_global_search_locations() {
1358                if !paths.contains(&p) {
1359                    paths.push(p);
1360                }
1361            }
1362
1363            let mut paths = paths
1364                .into_iter()
1365                .filter(|p| p.exists())
1366                .collect::<Vec<PathBuf>>();
1367
1368            self.global_search_locations.lock().append(&mut paths);
1369        }
1370        self.global_search_locations.lock().clone()
1371    }
1372}
1373
1374pub(crate) struct PyLspAdapter {
1375    python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1376}
1377impl PyLspAdapter {
1378    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
1379    pub(crate) fn new() -> Self {
1380        Self {
1381            python_venv_base: OnceCell::new(),
1382        }
1383    }
1384    async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1385        let python_path = Self::find_base_python(delegate)
1386            .await
1387            .with_context(|| {
1388                let mut message = "Could not find Python installation for PyLSP".to_owned();
1389                if cfg!(windows){
1390                    message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
1391                }
1392                message
1393            })?;
1394        let work_dir = delegate
1395            .language_server_download_dir(&Self::SERVER_NAME)
1396            .await
1397            .context("Could not get working directory for PyLSP")?;
1398        let mut path = PathBuf::from(work_dir.as_ref());
1399        path.push("pylsp-venv");
1400        if !path.exists() {
1401            util::command::new_smol_command(python_path)
1402                .arg("-m")
1403                .arg("venv")
1404                .arg("pylsp-venv")
1405                .current_dir(work_dir)
1406                .spawn()?
1407                .output()
1408                .await?;
1409        }
1410
1411        Ok(path.into())
1412    }
1413    // Find "baseline", user python version from which we'll create our own venv.
1414    async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1415        for path in ["python3", "python"] {
1416            let Some(path) = delegate.which(path.as_ref()).await else {
1417                continue;
1418            };
1419            // Try to detect situations where `python3` exists but is not a real Python interpreter.
1420            // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
1421            // when run with no arguments, and just fails otherwise.
1422            let Some(output) = new_smol_command(&path)
1423                .args(["-c", "print(1 + 2)"])
1424                .output()
1425                .await
1426                .ok()
1427            else {
1428                continue;
1429            };
1430            if output.stdout.trim_ascii() != b"3" {
1431                continue;
1432            }
1433            return Some(path);
1434        }
1435        None
1436    }
1437
1438    async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1439        self.python_venv_base
1440            .get_or_init(move || async move {
1441                Self::ensure_venv(delegate)
1442                    .await
1443                    .map_err(|e| format!("{e}"))
1444            })
1445            .await
1446            .clone()
1447    }
1448}
1449
1450const BINARY_DIR: &str = if cfg!(target_os = "windows") {
1451    "Scripts"
1452} else {
1453    "bin"
1454};
1455
1456#[async_trait(?Send)]
1457impl LspAdapter for PyLspAdapter {
1458    fn name(&self) -> LanguageServerName {
1459        Self::SERVER_NAME
1460    }
1461
1462    async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
1463
1464    async fn label_for_completion(
1465        &self,
1466        item: &lsp::CompletionItem,
1467        language: &Arc<language::Language>,
1468    ) -> Option<language::CodeLabel> {
1469        let label = &item.label;
1470        let grammar = language.grammar()?;
1471        let highlight_id = match item.kind? {
1472            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1473            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1474            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1475            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1476            _ => return None,
1477        };
1478        Some(language::CodeLabel::filtered(
1479            label.clone(),
1480            item.filter_text.as_deref(),
1481            vec![(0..label.len(), highlight_id)],
1482        ))
1483    }
1484
1485    async fn label_for_symbol(
1486        &self,
1487        name: &str,
1488        kind: lsp::SymbolKind,
1489        language: &Arc<language::Language>,
1490    ) -> Option<language::CodeLabel> {
1491        let (text, filter_range, display_range) = match kind {
1492            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1493                let text = format!("def {}():\n", name);
1494                let filter_range = 4..4 + name.len();
1495                let display_range = 0..filter_range.end;
1496                (text, filter_range, display_range)
1497            }
1498            lsp::SymbolKind::CLASS => {
1499                let text = format!("class {}:", name);
1500                let filter_range = 6..6 + name.len();
1501                let display_range = 0..filter_range.end;
1502                (text, filter_range, display_range)
1503            }
1504            lsp::SymbolKind::CONSTANT => {
1505                let text = format!("{} = 0", name);
1506                let filter_range = 0..name.len();
1507                let display_range = 0..filter_range.end;
1508                (text, filter_range, display_range)
1509            }
1510            _ => return None,
1511        };
1512        Some(language::CodeLabel::new(
1513            text[display_range.clone()].to_string(),
1514            filter_range,
1515            language.highlight_text(&Rope::from_str_small(text.as_str()), display_range),
1516        ))
1517    }
1518
1519    async fn workspace_configuration(
1520        self: Arc<Self>,
1521        adapter: &Arc<dyn LspAdapterDelegate>,
1522        toolchain: Option<Toolchain>,
1523        cx: &mut AsyncApp,
1524    ) -> Result<Value> {
1525        cx.update(move |cx| {
1526            let mut user_settings =
1527                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1528                    .and_then(|s| s.settings.clone())
1529                    .unwrap_or_else(|| {
1530                        json!({
1531                            "plugins": {
1532                                "pycodestyle": {"enabled": false},
1533                                "rope_autoimport": {"enabled": true, "memory": true},
1534                                "pylsp_mypy": {"enabled": false}
1535                            },
1536                            "rope": {
1537                                "ropeFolder": null
1538                            },
1539                        })
1540                    });
1541
1542            // If user did not explicitly modify their python venv, use one from picker.
1543            if let Some(toolchain) = toolchain {
1544                if !user_settings.is_object() {
1545                    user_settings = Value::Object(serde_json::Map::default());
1546                }
1547                let object = user_settings.as_object_mut().unwrap();
1548                if let Some(python) = object
1549                    .entry("plugins")
1550                    .or_insert(Value::Object(serde_json::Map::default()))
1551                    .as_object_mut()
1552                {
1553                    if let Some(jedi) = python
1554                        .entry("jedi")
1555                        .or_insert(Value::Object(serde_json::Map::default()))
1556                        .as_object_mut()
1557                    {
1558                        jedi.entry("environment".to_string())
1559                            .or_insert_with(|| Value::String(toolchain.path.clone().into()));
1560                    }
1561                    if let Some(pylint) = python
1562                        .entry("pylsp_mypy")
1563                        .or_insert(Value::Object(serde_json::Map::default()))
1564                        .as_object_mut()
1565                    {
1566                        pylint.entry("overrides".to_string()).or_insert_with(|| {
1567                            Value::Array(vec![
1568                                Value::String("--python-executable".into()),
1569                                Value::String(toolchain.path.into()),
1570                                Value::String("--cache-dir=/dev/null".into()),
1571                                Value::Bool(true),
1572                            ])
1573                        });
1574                    }
1575                }
1576            }
1577            user_settings = Value::Object(serde_json::Map::from_iter([(
1578                "pylsp".to_string(),
1579                user_settings,
1580            )]));
1581
1582            user_settings
1583        })
1584    }
1585}
1586
1587impl LspInstaller for PyLspAdapter {
1588    type BinaryVersion = ();
1589    async fn check_if_user_installed(
1590        &self,
1591        delegate: &dyn LspAdapterDelegate,
1592        toolchain: Option<Toolchain>,
1593        _: &AsyncApp,
1594    ) -> Option<LanguageServerBinary> {
1595        if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
1596            let env = delegate.shell_env().await;
1597            Some(LanguageServerBinary {
1598                path: pylsp_bin,
1599                env: Some(env),
1600                arguments: vec![],
1601            })
1602        } else {
1603            let toolchain = toolchain?;
1604            let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
1605            pylsp_path.exists().then(|| LanguageServerBinary {
1606                path: toolchain.path.to_string().into(),
1607                arguments: vec![pylsp_path.into()],
1608                env: None,
1609            })
1610        }
1611    }
1612
1613    async fn fetch_latest_server_version(
1614        &self,
1615        _: &dyn LspAdapterDelegate,
1616        _: bool,
1617        _: &mut AsyncApp,
1618    ) -> Result<()> {
1619        Ok(())
1620    }
1621
1622    async fn fetch_server_binary(
1623        &self,
1624        _: (),
1625        _: PathBuf,
1626        delegate: &dyn LspAdapterDelegate,
1627    ) -> Result<LanguageServerBinary> {
1628        let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1629        let pip_path = venv.join(BINARY_DIR).join("pip3");
1630        ensure!(
1631            util::command::new_smol_command(pip_path.as_path())
1632                .arg("install")
1633                .arg("python-lsp-server[all]")
1634                .arg("--upgrade")
1635                .output()
1636                .await?
1637                .status
1638                .success(),
1639            "python-lsp-server[all] installation failed"
1640        );
1641        ensure!(
1642            util::command::new_smol_command(pip_path)
1643                .arg("install")
1644                .arg("pylsp-mypy")
1645                .arg("--upgrade")
1646                .output()
1647                .await?
1648                .status
1649                .success(),
1650            "pylsp-mypy installation failed"
1651        );
1652        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1653        ensure!(
1654            delegate.which(pylsp.as_os_str()).await.is_some(),
1655            "pylsp installation was incomplete"
1656        );
1657        Ok(LanguageServerBinary {
1658            path: pylsp,
1659            env: None,
1660            arguments: vec![],
1661        })
1662    }
1663
1664    async fn cached_server_binary(
1665        &self,
1666        _: PathBuf,
1667        delegate: &dyn LspAdapterDelegate,
1668    ) -> Option<LanguageServerBinary> {
1669        let venv = self.base_venv(delegate).await.ok()?;
1670        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1671        delegate.which(pylsp.as_os_str()).await?;
1672        Some(LanguageServerBinary {
1673            path: pylsp,
1674            env: None,
1675            arguments: vec![],
1676        })
1677    }
1678}
1679
1680pub(crate) struct BasedPyrightLspAdapter {
1681    node: NodeRuntime,
1682}
1683
1684impl BasedPyrightLspAdapter {
1685    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
1686    const BINARY_NAME: &'static str = "basedpyright-langserver";
1687    const SERVER_PATH: &str = "node_modules/basedpyright/langserver.index.js";
1688    const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "basedpyright/langserver.index.js";
1689
1690    pub(crate) fn new(node: NodeRuntime) -> Self {
1691        BasedPyrightLspAdapter { node }
1692    }
1693
1694    async fn get_cached_server_binary(
1695        container_dir: PathBuf,
1696        node: &NodeRuntime,
1697    ) -> Option<LanguageServerBinary> {
1698        let server_path = container_dir.join(Self::SERVER_PATH);
1699        if server_path.exists() {
1700            Some(LanguageServerBinary {
1701                path: node.binary_path().await.log_err()?,
1702                env: None,
1703                arguments: vec![server_path.into(), "--stdio".into()],
1704            })
1705        } else {
1706            log::error!("missing executable in directory {:?}", server_path);
1707            None
1708        }
1709    }
1710}
1711
1712#[async_trait(?Send)]
1713impl LspAdapter for BasedPyrightLspAdapter {
1714    fn name(&self) -> LanguageServerName {
1715        Self::SERVER_NAME
1716    }
1717
1718    async fn initialization_options(
1719        self: Arc<Self>,
1720        _: &Arc<dyn LspAdapterDelegate>,
1721    ) -> Result<Option<Value>> {
1722        // Provide minimal initialization options
1723        // Virtual environment configuration will be handled through workspace configuration
1724        Ok(Some(json!({
1725            "python": {
1726                "analysis": {
1727                    "autoSearchPaths": true,
1728                    "useLibraryCodeForTypes": true,
1729                    "autoImportCompletions": true
1730                }
1731            }
1732        })))
1733    }
1734
1735    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
1736        process_pyright_completions(items);
1737    }
1738
1739    async fn label_for_completion(
1740        &self,
1741        item: &lsp::CompletionItem,
1742        language: &Arc<language::Language>,
1743    ) -> Option<language::CodeLabel> {
1744        let label = &item.label;
1745        let grammar = language.grammar()?;
1746        let highlight_id = match item.kind? {
1747            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
1748            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
1749            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
1750            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
1751            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
1752            _ => {
1753                return None;
1754            }
1755        };
1756        let mut text = label.clone();
1757        if let Some(completion_details) = item
1758            .label_details
1759            .as_ref()
1760            .and_then(|details| details.description.as_ref())
1761        {
1762            write!(&mut text, " {}", completion_details).ok();
1763        }
1764        Some(language::CodeLabel::filtered(
1765            text,
1766            item.filter_text.as_deref(),
1767            highlight_id
1768                .map(|id| (0..label.len(), id))
1769                .into_iter()
1770                .collect(),
1771        ))
1772    }
1773
1774    async fn label_for_symbol(
1775        &self,
1776        name: &str,
1777        kind: lsp::SymbolKind,
1778        language: &Arc<language::Language>,
1779    ) -> Option<language::CodeLabel> {
1780        let (text, filter_range, display_range) = match kind {
1781            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1782                let text = format!("def {}():\n", name);
1783                let filter_range = 4..4 + name.len();
1784                let display_range = 0..filter_range.end;
1785                (text, filter_range, display_range)
1786            }
1787            lsp::SymbolKind::CLASS => {
1788                let text = format!("class {}:", name);
1789                let filter_range = 6..6 + name.len();
1790                let display_range = 0..filter_range.end;
1791                (text, filter_range, display_range)
1792            }
1793            lsp::SymbolKind::CONSTANT => {
1794                let text = format!("{} = 0", name);
1795                let filter_range = 0..name.len();
1796                let display_range = 0..filter_range.end;
1797                (text, filter_range, display_range)
1798            }
1799            _ => return None,
1800        };
1801        Some(language::CodeLabel::new(
1802            text[display_range.clone()].to_string(),
1803            filter_range,
1804            language.highlight_text(&Rope::from_str_small(text.as_str()), display_range),
1805        ))
1806    }
1807
1808    async fn workspace_configuration(
1809        self: Arc<Self>,
1810        adapter: &Arc<dyn LspAdapterDelegate>,
1811        toolchain: Option<Toolchain>,
1812        cx: &mut AsyncApp,
1813    ) -> Result<Value> {
1814        cx.update(move |cx| {
1815            let mut user_settings =
1816                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1817                    .and_then(|s| s.settings.clone())
1818                    .unwrap_or_default();
1819
1820            // If we have a detected toolchain, configure Pyright to use it
1821            if let Some(toolchain) = toolchain
1822                && let Ok(env) = serde_json::from_value::<
1823                    pet_core::python_environment::PythonEnvironment,
1824                >(toolchain.as_json.clone())
1825            {
1826                if !user_settings.is_object() {
1827                    user_settings = Value::Object(serde_json::Map::default());
1828                }
1829                let object = user_settings.as_object_mut().unwrap();
1830
1831                let interpreter_path = toolchain.path.to_string();
1832                if let Some(venv_dir) = env.prefix {
1833                    // Set venvPath and venv at the root level
1834                    // This matches the format of a pyrightconfig.json file
1835                    if let Some(parent) = venv_dir.parent() {
1836                        // Use relative path if the venv is inside the workspace
1837                        let venv_path = if parent == adapter.worktree_root_path() {
1838                            ".".to_string()
1839                        } else {
1840                            parent.to_string_lossy().into_owned()
1841                        };
1842                        object.insert("venvPath".to_string(), Value::String(venv_path));
1843                    }
1844
1845                    if let Some(venv_name) = venv_dir.file_name() {
1846                        object.insert(
1847                            "venv".to_owned(),
1848                            Value::String(venv_name.to_string_lossy().into_owned()),
1849                        );
1850                    }
1851                }
1852
1853                // Set both pythonPath and defaultInterpreterPath for compatibility
1854                if let Some(python) = object
1855                    .entry("python")
1856                    .or_insert(Value::Object(serde_json::Map::default()))
1857                    .as_object_mut()
1858                {
1859                    python.insert(
1860                        "pythonPath".to_owned(),
1861                        Value::String(interpreter_path.clone()),
1862                    );
1863                    python.insert(
1864                        "defaultInterpreterPath".to_owned(),
1865                        Value::String(interpreter_path),
1866                    );
1867                }
1868                // Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
1869                maybe!({
1870                    let analysis = object
1871                        .entry("basedpyright.analysis")
1872                        .or_insert(Value::Object(serde_json::Map::default()));
1873                    if let serde_json::map::Entry::Vacant(v) =
1874                        analysis.as_object_mut()?.entry("typeCheckingMode")
1875                    {
1876                        v.insert(Value::String("standard".to_owned()));
1877                    }
1878                    Some(())
1879                });
1880            }
1881
1882            user_settings
1883        })
1884    }
1885}
1886
1887impl LspInstaller for BasedPyrightLspAdapter {
1888    type BinaryVersion = String;
1889
1890    async fn fetch_latest_server_version(
1891        &self,
1892        _: &dyn LspAdapterDelegate,
1893        _: bool,
1894        _: &mut AsyncApp,
1895    ) -> Result<String> {
1896        self.node
1897            .npm_package_latest_version(Self::SERVER_NAME.as_ref())
1898            .await
1899    }
1900
1901    async fn check_if_user_installed(
1902        &self,
1903        delegate: &dyn LspAdapterDelegate,
1904        _: Option<Toolchain>,
1905        _: &AsyncApp,
1906    ) -> Option<LanguageServerBinary> {
1907        if let Some(path) = delegate.which(Self::BINARY_NAME.as_ref()).await {
1908            let env = delegate.shell_env().await;
1909            Some(LanguageServerBinary {
1910                path,
1911                env: Some(env),
1912                arguments: vec!["--stdio".into()],
1913            })
1914        } else {
1915            // TODO shouldn't this be self.node.binary_path()?
1916            let node = delegate.which("node".as_ref()).await?;
1917            let (node_modules_path, _) = delegate
1918                .npm_package_installed_version(Self::SERVER_NAME.as_ref())
1919                .await
1920                .log_err()??;
1921
1922            let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
1923
1924            let env = delegate.shell_env().await;
1925            Some(LanguageServerBinary {
1926                path: node,
1927                env: Some(env),
1928                arguments: vec![path.into(), "--stdio".into()],
1929            })
1930        }
1931    }
1932
1933    async fn fetch_server_binary(
1934        &self,
1935        latest_version: Self::BinaryVersion,
1936        container_dir: PathBuf,
1937        delegate: &dyn LspAdapterDelegate,
1938    ) -> Result<LanguageServerBinary> {
1939        let server_path = container_dir.join(Self::SERVER_PATH);
1940
1941        self.node
1942            .npm_install_packages(
1943                &container_dir,
1944                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
1945            )
1946            .await?;
1947
1948        let env = delegate.shell_env().await;
1949        Ok(LanguageServerBinary {
1950            path: self.node.binary_path().await?,
1951            env: Some(env),
1952            arguments: vec![server_path.into(), "--stdio".into()],
1953        })
1954    }
1955
1956    async fn check_if_version_installed(
1957        &self,
1958        version: &Self::BinaryVersion,
1959        container_dir: &PathBuf,
1960        delegate: &dyn LspAdapterDelegate,
1961    ) -> Option<LanguageServerBinary> {
1962        let server_path = container_dir.join(Self::SERVER_PATH);
1963
1964        let should_install_language_server = self
1965            .node
1966            .should_install_npm_package(
1967                Self::SERVER_NAME.as_ref(),
1968                &server_path,
1969                container_dir,
1970                VersionStrategy::Latest(version),
1971            )
1972            .await;
1973
1974        if should_install_language_server {
1975            None
1976        } else {
1977            let env = delegate.shell_env().await;
1978            Some(LanguageServerBinary {
1979                path: self.node.binary_path().await.ok()?,
1980                env: Some(env),
1981                arguments: vec![server_path.into(), "--stdio".into()],
1982            })
1983        }
1984    }
1985
1986    async fn cached_server_binary(
1987        &self,
1988        container_dir: PathBuf,
1989        delegate: &dyn LspAdapterDelegate,
1990    ) -> Option<LanguageServerBinary> {
1991        let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
1992        binary.env = Some(delegate.shell_env().await);
1993        Some(binary)
1994    }
1995}
1996
1997pub(crate) struct RuffLspAdapter {
1998    fs: Arc<dyn Fs>,
1999}
2000
2001#[cfg(target_os = "macos")]
2002impl RuffLspAdapter {
2003    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2004    const ARCH_SERVER_NAME: &str = "apple-darwin";
2005}
2006
2007#[cfg(target_os = "linux")]
2008impl RuffLspAdapter {
2009    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2010    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
2011}
2012
2013#[cfg(target_os = "freebsd")]
2014impl RuffLspAdapter {
2015    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2016    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
2017}
2018
2019#[cfg(target_os = "windows")]
2020impl RuffLspAdapter {
2021    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
2022    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
2023}
2024
2025impl RuffLspAdapter {
2026    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ruff");
2027
2028    pub fn new(fs: Arc<dyn Fs>) -> RuffLspAdapter {
2029        RuffLspAdapter { fs }
2030    }
2031
2032    fn build_asset_name() -> Result<(String, String)> {
2033        let arch = match consts::ARCH {
2034            "x86" => "i686",
2035            _ => consts::ARCH,
2036        };
2037        let os = Self::ARCH_SERVER_NAME;
2038        let suffix = match consts::OS {
2039            "windows" => "zip",
2040            _ => "tar.gz",
2041        };
2042        let asset_name = format!("ruff-{arch}-{os}.{suffix}");
2043        let asset_stem = format!("ruff-{arch}-{os}");
2044        Ok((asset_stem, asset_name))
2045    }
2046}
2047
2048#[async_trait(?Send)]
2049impl LspAdapter for RuffLspAdapter {
2050    fn name(&self) -> LanguageServerName {
2051        Self::SERVER_NAME
2052    }
2053}
2054
2055impl LspInstaller for RuffLspAdapter {
2056    type BinaryVersion = GitHubLspBinaryVersion;
2057    async fn check_if_user_installed(
2058        &self,
2059        delegate: &dyn LspAdapterDelegate,
2060        toolchain: Option<Toolchain>,
2061        _: &AsyncApp,
2062    ) -> Option<LanguageServerBinary> {
2063        let ruff_in_venv = if let Some(toolchain) = toolchain
2064            && toolchain.language_name.as_ref() == "Python"
2065        {
2066            Path::new(toolchain.path.as_str())
2067                .parent()
2068                .map(|path| path.join("ruff"))
2069        } else {
2070            None
2071        };
2072
2073        for path in ruff_in_venv.into_iter().chain(["ruff".into()]) {
2074            if let Some(ruff_bin) = delegate.which(path.as_os_str()).await {
2075                let env = delegate.shell_env().await;
2076                return Some(LanguageServerBinary {
2077                    path: ruff_bin,
2078                    env: Some(env),
2079                    arguments: vec!["server".into()],
2080                });
2081            }
2082        }
2083
2084        None
2085    }
2086
2087    async fn fetch_latest_server_version(
2088        &self,
2089        delegate: &dyn LspAdapterDelegate,
2090        _: bool,
2091        _: &mut AsyncApp,
2092    ) -> Result<GitHubLspBinaryVersion> {
2093        let release =
2094            latest_github_release("astral-sh/ruff", true, false, delegate.http_client()).await?;
2095        let (_, asset_name) = Self::build_asset_name()?;
2096        let asset = release
2097            .assets
2098            .into_iter()
2099            .find(|asset| asset.name == asset_name)
2100            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
2101        Ok(GitHubLspBinaryVersion {
2102            name: release.tag_name,
2103            url: asset.browser_download_url,
2104            digest: asset.digest,
2105        })
2106    }
2107
2108    async fn fetch_server_binary(
2109        &self,
2110        latest_version: GitHubLspBinaryVersion,
2111        container_dir: PathBuf,
2112        delegate: &dyn LspAdapterDelegate,
2113    ) -> Result<LanguageServerBinary> {
2114        let GitHubLspBinaryVersion {
2115            name,
2116            url,
2117            digest: expected_digest,
2118        } = latest_version;
2119        let destination_path = container_dir.join(format!("ruff-{name}"));
2120        let server_path = match Self::GITHUB_ASSET_KIND {
2121            AssetKind::TarGz | AssetKind::Gz => destination_path
2122                .join(Self::build_asset_name()?.0)
2123                .join("ruff"),
2124            AssetKind::Zip => destination_path.clone().join("ruff.exe"),
2125        };
2126
2127        let binary = LanguageServerBinary {
2128            path: server_path.clone(),
2129            env: None,
2130            arguments: vec!["server".into()],
2131        };
2132
2133        let metadata_path = destination_path.with_extension("metadata");
2134        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
2135            .await
2136            .ok();
2137        if let Some(metadata) = metadata {
2138            let validity_check = async || {
2139                delegate
2140                    .try_exec(LanguageServerBinary {
2141                        path: server_path.clone(),
2142                        arguments: vec!["--version".into()],
2143                        env: None,
2144                    })
2145                    .await
2146                    .inspect_err(|err| {
2147                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
2148                    })
2149            };
2150            if let (Some(actual_digest), Some(expected_digest)) =
2151                (&metadata.digest, &expected_digest)
2152            {
2153                if actual_digest == expected_digest {
2154                    if validity_check().await.is_ok() {
2155                        return Ok(binary);
2156                    }
2157                } else {
2158                    log::info!(
2159                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
2160                    );
2161                }
2162            } else if validity_check().await.is_ok() {
2163                return Ok(binary);
2164            }
2165        }
2166
2167        download_server_binary(
2168            &*delegate.http_client(),
2169            &url,
2170            expected_digest.as_deref(),
2171            &destination_path,
2172            Self::GITHUB_ASSET_KIND,
2173        )
2174        .await?;
2175        make_file_executable(&server_path).await?;
2176        remove_matching(&container_dir, |path| path != destination_path).await;
2177        GithubBinaryMetadata::write_to_file(
2178            &GithubBinaryMetadata {
2179                metadata_version: 1,
2180                digest: expected_digest,
2181            },
2182            &metadata_path,
2183        )
2184        .await?;
2185
2186        Ok(LanguageServerBinary {
2187            path: server_path,
2188            env: None,
2189            arguments: vec!["server".into()],
2190        })
2191    }
2192
2193    async fn cached_server_binary(
2194        &self,
2195        container_dir: PathBuf,
2196        _: &dyn LspAdapterDelegate,
2197    ) -> Option<LanguageServerBinary> {
2198        maybe!(async {
2199            let mut last = None;
2200            let mut entries = self.fs.read_dir(&container_dir).await?;
2201            while let Some(entry) = entries.next().await {
2202                let path = entry?;
2203                if path.extension().is_some_and(|ext| ext == "metadata") {
2204                    continue;
2205                }
2206                last = Some(path);
2207            }
2208
2209            let path = last.context("no cached binary")?;
2210            let path = match Self::GITHUB_ASSET_KIND {
2211                AssetKind::TarGz | AssetKind::Gz => {
2212                    path.join(Self::build_asset_name()?.0).join("ruff")
2213                }
2214                AssetKind::Zip => path.join("ruff.exe"),
2215            };
2216
2217            anyhow::Ok(LanguageServerBinary {
2218                path,
2219                env: None,
2220                arguments: vec!["server".into()],
2221            })
2222        })
2223        .await
2224        .log_err()
2225    }
2226}
2227
2228#[cfg(test)]
2229mod tests {
2230    use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
2231    use language::{AutoindentMode, Buffer};
2232    use settings::SettingsStore;
2233    use std::num::NonZeroU32;
2234
2235    #[gpui::test]
2236    async fn test_python_autoindent(cx: &mut TestAppContext) {
2237        cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
2238        let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
2239        cx.update(|cx| {
2240            let test_settings = SettingsStore::test(cx);
2241            cx.set_global(test_settings);
2242            language::init(cx);
2243            cx.update_global::<SettingsStore, _>(|store, cx| {
2244                store.update_user_settings(cx, |s| {
2245                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
2246                });
2247            });
2248        });
2249
2250        cx.new(|cx| {
2251            let mut buffer = Buffer::local("", cx).with_language(language, cx);
2252            let append = |buffer: &mut Buffer, text: &str, cx: &mut Context<Buffer>| {
2253                let ix = buffer.len();
2254                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
2255            };
2256
2257            // indent after "def():"
2258            append(&mut buffer, "def a():\n", cx);
2259            assert_eq!(buffer.text(), "def a():\n  ");
2260
2261            // preserve indent after blank line
2262            append(&mut buffer, "\n  ", cx);
2263            assert_eq!(buffer.text(), "def a():\n  \n  ");
2264
2265            // indent after "if"
2266            append(&mut buffer, "if a:\n  ", cx);
2267            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
2268
2269            // preserve indent after statement
2270            append(&mut buffer, "b()\n", cx);
2271            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
2272
2273            // preserve indent after statement
2274            append(&mut buffer, "else", cx);
2275            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
2276
2277            // dedent "else""
2278            append(&mut buffer, ":", cx);
2279            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
2280
2281            // indent lines after else
2282            append(&mut buffer, "\n", cx);
2283            assert_eq!(
2284                buffer.text(),
2285                "def a():\n  \n  if a:\n    b()\n  else:\n    "
2286            );
2287
2288            // indent after an open paren. the closing paren is not indented
2289            // because there is another token before it on the same line.
2290            append(&mut buffer, "foo(\n1)", cx);
2291            assert_eq!(
2292                buffer.text(),
2293                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
2294            );
2295
2296            // dedent the closing paren if it is shifted to the beginning of the line
2297            let argument_ix = buffer.text().find('1').unwrap();
2298            buffer.edit(
2299                [(argument_ix..argument_ix + 1, "")],
2300                Some(AutoindentMode::EachLine),
2301                cx,
2302            );
2303            assert_eq!(
2304                buffer.text(),
2305                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
2306            );
2307
2308            // preserve indent after the close paren
2309            append(&mut buffer, "\n", cx);
2310            assert_eq!(
2311                buffer.text(),
2312                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
2313            );
2314
2315            // manually outdent the last line
2316            let end_whitespace_ix = buffer.len() - 4;
2317            buffer.edit(
2318                [(end_whitespace_ix..buffer.len(), "")],
2319                Some(AutoindentMode::EachLine),
2320                cx,
2321            );
2322            assert_eq!(
2323                buffer.text(),
2324                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
2325            );
2326
2327            // preserve the newly reduced indentation on the next newline
2328            append(&mut buffer, "\n", cx);
2329            assert_eq!(
2330                buffer.text(),
2331                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
2332            );
2333
2334            // reset to a for loop statement
2335            let statement = "for i in range(10):\n  print(i)\n";
2336            buffer.edit([(0..buffer.len(), statement)], None, cx);
2337
2338            // insert single line comment after each line
2339            let eol_ixs = statement
2340                .char_indices()
2341                .filter_map(|(ix, c)| if c == '\n' { Some(ix) } else { None })
2342                .collect::<Vec<usize>>();
2343            let editions = eol_ixs
2344                .iter()
2345                .enumerate()
2346                .map(|(i, &eol_ix)| (eol_ix..eol_ix, format!(" # comment {}", i + 1)))
2347                .collect::<Vec<(std::ops::Range<usize>, String)>>();
2348            buffer.edit(editions, Some(AutoindentMode::EachLine), cx);
2349            assert_eq!(
2350                buffer.text(),
2351                "for i in range(10): # comment 1\n  print(i) # comment 2\n"
2352            );
2353
2354            // reset to a simple if statement
2355            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
2356
2357            // dedent "else" on the line after a closing paren
2358            append(&mut buffer, "\n  else:\n", cx);
2359            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
2360
2361            buffer
2362        });
2363    }
2364}