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