python.rs

   1use anyhow::{Context as _, ensure};
   2use anyhow::{Result, anyhow};
   3use async_trait::async_trait;
   4use collections::HashMap;
   5use futures::future::BoxFuture;
   6use futures::lock::OwnedMutexGuard;
   7use futures::{AsyncBufReadExt, StreamExt as _};
   8use gpui::{App, AsyncApp, Entity, SharedString, Task};
   9use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
  10use language::language_settings::LanguageSettings;
  11use language::{
  12    Buffer, ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol,
  13};
  14use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
  15use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
  16use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
  17use lsp::{LanguageServerBinary, Uri};
  18use lsp::{LanguageServerBinaryOptions, LanguageServerName};
  19use node_runtime::{NodeRuntime, VersionStrategy};
  20use pet_core::Configuration;
  21use pet_core::os_environment::Environment;
  22use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
  23use pet_virtualenv::is_virtualenv_dir;
  24use project::Fs;
  25use project::lsp_store::language_server_settings;
  26use semver::Version;
  27use serde::{Deserialize, Serialize};
  28use serde_json::{Value, json};
  29use settings::{SemanticTokenRules, Settings};
  30use terminal::terminal_settings::TerminalSettings;
  31
  32use smol::lock::OnceCell;
  33use std::cmp::{Ordering, Reverse};
  34use std::env::consts;
  35use util::command::Stdio;
  36
  37use util::command::new_command;
  38use util::fs::{make_file_executable, remove_matching};
  39use util::paths::PathStyle;
  40use util::rel_path::RelPath;
  41
  42use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
  43use parking_lot::Mutex;
  44use std::str::FromStr;
  45use std::{
  46    borrow::Cow,
  47    fmt::Write,
  48    path::{Path, PathBuf},
  49    sync::Arc,
  50};
  51use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
  52use util::{ResultExt, maybe};
  53
  54pub(crate) fn semantic_token_rules() -> SemanticTokenRules {
  55    let content = grammars::get_file("python/semantic_token_rules.json")
  56        .expect("missing python/semantic_token_rules.json");
  57    let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");
  58    settings::parse_json_with_comments::<SemanticTokenRules>(json)
  59        .expect("failed to parse python semantic_token_rules.json")
  60}
  61
  62#[derive(Debug, Serialize, Deserialize)]
  63pub(crate) struct PythonToolchainData {
  64    #[serde(flatten)]
  65    environment: PythonEnvironment,
  66    #[serde(skip_serializing_if = "Option::is_none")]
  67    activation_scripts: Option<HashMap<ShellKind, PathBuf>>,
  68}
  69
  70pub(crate) struct PyprojectTomlManifestProvider;
  71
  72impl ManifestProvider for PyprojectTomlManifestProvider {
  73    fn name(&self) -> ManifestName {
  74        SharedString::new_static("pyproject.toml").into()
  75    }
  76
  77    fn search(
  78        &self,
  79        ManifestQuery {
  80            path,
  81            depth,
  82            delegate,
  83        }: ManifestQuery,
  84    ) -> Option<Arc<RelPath>> {
  85        for path in path.ancestors().take(depth) {
  86            let p = path.join(RelPath::unix("pyproject.toml").unwrap());
  87            if delegate.exists(&p, Some(false)) {
  88                return Some(path.into());
  89            }
  90        }
  91
  92        None
  93    }
  94}
  95
  96enum TestRunner {
  97    UNITTEST,
  98    PYTEST,
  99}
 100
 101impl FromStr for TestRunner {
 102    type Err = ();
 103
 104    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
 105        match s {
 106            "unittest" => Ok(Self::UNITTEST),
 107            "pytest" => Ok(Self::PYTEST),
 108            _ => Err(()),
 109        }
 110    }
 111}
 112
 113/// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
 114/// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
 115/// and `name` is the symbol name itself.
 116///
 117/// 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),
 118/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail.
 119/// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
 120///
 121/// upd 02.12.25:
 122/// Decided to ignore Pyright's sortText() completely and to manually sort all entries
 123fn process_pyright_completions(items: &mut [lsp::CompletionItem]) {
 124    for item in items {
 125        let is_named_argument = item.label.ends_with('=');
 126
 127        let is_dunder = item.label.starts_with("__") && item.label.ends_with("__");
 128
 129        let visibility_priority = if is_dunder {
 130            '3'
 131        } else if item.label.starts_with("__") {
 132            '2' // private non-dunder
 133        } else if item.label.starts_with('_') {
 134            '1' // protected
 135        } else {
 136            '0' // public
 137        };
 138
 139        let is_external = item
 140            .detail
 141            .as_ref()
 142            .is_some_and(|detail| detail == "Auto-import");
 143
 144        let source_priority = if is_external { '1' } else { '0' };
 145
 146        // Kind priority within same visibility level
 147        let kind_priority = match item.kind {
 148            Some(lsp::CompletionItemKind::KEYWORD) => '0',
 149            Some(lsp::CompletionItemKind::ENUM_MEMBER) => '1',
 150            Some(lsp::CompletionItemKind::FIELD) => '2',
 151            Some(lsp::CompletionItemKind::PROPERTY) => '3',
 152            Some(lsp::CompletionItemKind::VARIABLE) => '4',
 153            Some(lsp::CompletionItemKind::CONSTANT) => '5',
 154            Some(lsp::CompletionItemKind::METHOD) => '6',
 155            Some(lsp::CompletionItemKind::FUNCTION) => '6',
 156            Some(lsp::CompletionItemKind::CLASS) => '7',
 157            Some(lsp::CompletionItemKind::MODULE) => '8',
 158
 159            _ => 'z',
 160        };
 161
 162        // Named arguments get higher priority
 163        let argument_priority = if is_named_argument { '0' } else { '1' };
 164
 165        item.sort_text = Some(format!(
 166            "{}{}{}{}{}",
 167            argument_priority, source_priority, visibility_priority, kind_priority, item.label
 168        ));
 169    }
 170}
 171
 172fn label_for_pyright_completion(
 173    item: &lsp::CompletionItem,
 174    language: &Arc<language::Language>,
 175) -> Option<language::CodeLabel> {
 176    let label = &item.label;
 177    let label_len = label.len();
 178    let grammar = language.grammar()?;
 179    let highlight_id = match item.kind? {
 180        lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
 181        lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
 182        lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
 183        lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
 184        lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
 185        _ => {
 186            return None;
 187        }
 188    };
 189    let mut text = label.clone();
 190    if let Some(completion_details) = item
 191        .label_details
 192        .as_ref()
 193        .and_then(|details| details.description.as_ref())
 194    {
 195        write!(&mut text, " {}", completion_details).ok();
 196    }
 197    Some(language::CodeLabel::filtered(
 198        text,
 199        label_len,
 200        item.filter_text.as_deref(),
 201        highlight_id
 202            .map(|id| (0..label_len, id))
 203            .into_iter()
 204            .collect(),
 205    ))
 206}
 207
 208fn label_for_python_symbol(
 209    symbol: &Symbol,
 210    language: &Arc<language::Language>,
 211) -> Option<language::CodeLabel> {
 212    let name = &symbol.name;
 213    let (text, filter_range, display_range) = match symbol.kind {
 214        lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 215            let text = format!("def {}():\n", name);
 216            let filter_range = 4..4 + name.len();
 217            let display_range = 0..filter_range.end;
 218            (text, filter_range, display_range)
 219        }
 220        lsp::SymbolKind::CLASS => {
 221            let text = format!("class {}:", name);
 222            let filter_range = 6..6 + name.len();
 223            let display_range = 0..filter_range.end;
 224            (text, filter_range, display_range)
 225        }
 226        lsp::SymbolKind::CONSTANT => {
 227            let text = format!("{} = 0", name);
 228            let filter_range = 0..name.len();
 229            let display_range = 0..filter_range.end;
 230            (text, filter_range, display_range)
 231        }
 232        _ => return None,
 233    };
 234    Some(language::CodeLabel::new(
 235        text[display_range.clone()].to_string(),
 236        filter_range,
 237        language.highlight_text(&text.as_str().into(), display_range),
 238    ))
 239}
 240
 241pub struct TyLspAdapter {
 242    fs: Arc<dyn Fs>,
 243}
 244
 245#[cfg(target_os = "macos")]
 246impl TyLspAdapter {
 247    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 248    const ARCH_SERVER_NAME: &str = "apple-darwin";
 249}
 250
 251#[cfg(target_os = "linux")]
 252impl TyLspAdapter {
 253    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 254    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
 255}
 256
 257#[cfg(target_os = "freebsd")]
 258impl TyLspAdapter {
 259    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 260    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
 261}
 262
 263#[cfg(target_os = "windows")]
 264impl TyLspAdapter {
 265    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
 266    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
 267}
 268
 269impl TyLspAdapter {
 270    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty");
 271
 272    pub fn new(fs: Arc<dyn Fs>) -> TyLspAdapter {
 273        TyLspAdapter { fs }
 274    }
 275
 276    fn build_asset_name() -> Result<(String, String)> {
 277        let arch = match consts::ARCH {
 278            "x86" => "i686",
 279            _ => consts::ARCH,
 280        };
 281        let os = Self::ARCH_SERVER_NAME;
 282        let suffix = match consts::OS {
 283            "windows" => "zip",
 284            _ => "tar.gz",
 285        };
 286        let asset_name = format!("ty-{arch}-{os}.{suffix}");
 287        let asset_stem = format!("ty-{arch}-{os}");
 288        Ok((asset_stem, asset_name))
 289    }
 290}
 291
 292#[async_trait(?Send)]
 293impl LspAdapter for TyLspAdapter {
 294    fn name(&self) -> LanguageServerName {
 295        Self::SERVER_NAME
 296    }
 297
 298    async fn label_for_completion(
 299        &self,
 300        item: &lsp::CompletionItem,
 301        language: &Arc<language::Language>,
 302    ) -> Option<language::CodeLabel> {
 303        let label = &item.label;
 304        let label_len = label.len();
 305        let grammar = language.grammar()?;
 306        let highlight_id = match item.kind? {
 307            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
 308            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
 309            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
 310            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
 311            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
 312            _ => {
 313                return None;
 314            }
 315        };
 316
 317        let mut text = label.clone();
 318        if let Some(completion_details) = item
 319            .label_details
 320            .as_ref()
 321            .and_then(|details| details.detail.as_ref())
 322        {
 323            write!(&mut text, " {}", completion_details).ok();
 324        }
 325
 326        Some(language::CodeLabel::filtered(
 327            text,
 328            label_len,
 329            item.filter_text.as_deref(),
 330            highlight_id
 331                .map(|id| (0..label_len, id))
 332                .into_iter()
 333                .collect(),
 334        ))
 335    }
 336
 337    async fn label_for_symbol(
 338        &self,
 339        symbol: &language::Symbol,
 340        language: &Arc<language::Language>,
 341    ) -> Option<language::CodeLabel> {
 342        label_for_python_symbol(symbol, language)
 343    }
 344
 345    async fn workspace_configuration(
 346        self: Arc<Self>,
 347        delegate: &Arc<dyn LspAdapterDelegate>,
 348        toolchain: Option<Toolchain>,
 349        _: Option<Uri>,
 350        cx: &mut AsyncApp,
 351    ) -> Result<Value> {
 352        let mut ret = cx
 353            .update(|cx| {
 354                language_server_settings(delegate.as_ref(), &self.name(), cx)
 355                    .and_then(|s| s.settings.clone())
 356            })
 357            .unwrap_or_else(|| json!({}));
 358        if let Some(toolchain) = toolchain.and_then(|toolchain| {
 359            serde_json::from_value::<PythonToolchainData>(toolchain.as_json).ok()
 360        }) {
 361            _ = maybe!({
 362                let uri =
 363                    url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?;
 364                let sys_prefix = toolchain.environment.prefix.clone()?;
 365                let environment = json!({
 366                    "executable": {
 367                        "uri": uri,
 368                        "sysPrefix": sys_prefix
 369                    }
 370                });
 371                ret.as_object_mut()?
 372                    .entry("pythonExtension")
 373                    .or_insert_with(|| json!({ "activeEnvironment": environment }));
 374                Some(())
 375            });
 376        }
 377        Ok(json!({"ty": ret}))
 378    }
 379}
 380
 381impl LspInstaller for TyLspAdapter {
 382    type BinaryVersion = GitHubLspBinaryVersion;
 383    async fn fetch_latest_server_version(
 384        &self,
 385        delegate: &dyn LspAdapterDelegate,
 386        _: bool,
 387        _: &mut AsyncApp,
 388    ) -> Result<Self::BinaryVersion> {
 389        let release =
 390            latest_github_release("astral-sh/ty", true, false, delegate.http_client()).await?;
 391        let (_, asset_name) = Self::build_asset_name()?;
 392        let asset = release
 393            .assets
 394            .into_iter()
 395            .find(|asset| asset.name == asset_name)
 396            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 397        Ok(GitHubLspBinaryVersion {
 398            name: release.tag_name,
 399            url: asset.browser_download_url,
 400            digest: asset.digest,
 401        })
 402    }
 403
 404    async fn check_if_user_installed(
 405        &self,
 406        delegate: &dyn LspAdapterDelegate,
 407        toolchain: Option<Toolchain>,
 408        _: &AsyncApp,
 409    ) -> Option<LanguageServerBinary> {
 410        let ty_in_venv = if let Some(toolchain) = toolchain
 411            && toolchain.language_name.as_ref() == "Python"
 412        {
 413            Path::new(toolchain.path.as_str())
 414                .parent()
 415                .map(|path| path.join("ty"))
 416        } else {
 417            None
 418        };
 419
 420        for path in ty_in_venv.into_iter().chain(["ty".into()]) {
 421            if let Some(ty_bin) = delegate.which(path.as_os_str()).await {
 422                let env = delegate.shell_env().await;
 423                return Some(LanguageServerBinary {
 424                    path: ty_bin,
 425                    env: Some(env),
 426                    arguments: vec!["server".into()],
 427                });
 428            }
 429        }
 430
 431        None
 432    }
 433
 434    async fn fetch_server_binary(
 435        &self,
 436        latest_version: Self::BinaryVersion,
 437        container_dir: PathBuf,
 438        delegate: &dyn LspAdapterDelegate,
 439    ) -> Result<LanguageServerBinary> {
 440        let GitHubLspBinaryVersion {
 441            name,
 442            url,
 443            digest: expected_digest,
 444        } = latest_version;
 445        let destination_path = container_dir.join(format!("ty-{name}"));
 446
 447        async_fs::create_dir_all(&destination_path).await?;
 448
 449        let server_path = match Self::GITHUB_ASSET_KIND {
 450            AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path
 451                .join(Self::build_asset_name()?.0)
 452                .join("ty"),
 453            AssetKind::Zip => destination_path.clone().join("ty.exe"),
 454        };
 455
 456        let binary = LanguageServerBinary {
 457            path: server_path.clone(),
 458            env: None,
 459            arguments: vec!["server".into()],
 460        };
 461
 462        let metadata_path = destination_path.with_extension("metadata");
 463        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
 464            .await
 465            .ok();
 466        if let Some(metadata) = metadata {
 467            let validity_check = async || {
 468                delegate
 469                    .try_exec(LanguageServerBinary {
 470                        path: server_path.clone(),
 471                        arguments: vec!["--version".into()],
 472                        env: None,
 473                    })
 474                    .await
 475                    .inspect_err(|err| {
 476                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
 477                    })
 478            };
 479            if let (Some(actual_digest), Some(expected_digest)) =
 480                (&metadata.digest, &expected_digest)
 481            {
 482                if actual_digest == expected_digest {
 483                    if validity_check().await.is_ok() {
 484                        return Ok(binary);
 485                    }
 486                } else {
 487                    log::info!(
 488                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
 489                    );
 490                }
 491            } else if validity_check().await.is_ok() {
 492                return Ok(binary);
 493            }
 494        }
 495
 496        download_server_binary(
 497            &*delegate.http_client(),
 498            &url,
 499            expected_digest.as_deref(),
 500            &destination_path,
 501            Self::GITHUB_ASSET_KIND,
 502        )
 503        .await?;
 504        make_file_executable(&server_path).await?;
 505        remove_matching(&container_dir, |path| path != destination_path).await;
 506        GithubBinaryMetadata::write_to_file(
 507            &GithubBinaryMetadata {
 508                metadata_version: 1,
 509                digest: expected_digest,
 510            },
 511            &metadata_path,
 512        )
 513        .await?;
 514
 515        Ok(LanguageServerBinary {
 516            path: server_path,
 517            env: None,
 518            arguments: vec!["server".into()],
 519        })
 520    }
 521
 522    async fn cached_server_binary(
 523        &self,
 524        container_dir: PathBuf,
 525        _: &dyn LspAdapterDelegate,
 526    ) -> Option<LanguageServerBinary> {
 527        maybe!(async {
 528            let mut last = None;
 529            let mut entries = self.fs.read_dir(&container_dir).await?;
 530            while let Some(entry) = entries.next().await {
 531                let path = entry?;
 532                if path.extension().is_some_and(|ext| ext == "metadata") {
 533                    continue;
 534                }
 535                last = Some(path);
 536            }
 537
 538            let path = last.context("no cached binary")?;
 539            let path = match TyLspAdapter::GITHUB_ASSET_KIND {
 540                AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => {
 541                    path.join(Self::build_asset_name()?.0).join("ty")
 542                }
 543                AssetKind::Zip => path.join("ty.exe"),
 544            };
 545
 546            anyhow::Ok(LanguageServerBinary {
 547                path,
 548                env: None,
 549                arguments: vec!["server".into()],
 550            })
 551        })
 552        .await
 553        .log_err()
 554    }
 555}
 556
 557pub struct PyrightLspAdapter {
 558    node: NodeRuntime,
 559}
 560
 561impl PyrightLspAdapter {
 562    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
 563    const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
 564    const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
 565
 566    pub fn new(node: NodeRuntime) -> Self {
 567        PyrightLspAdapter { node }
 568    }
 569
 570    async fn get_cached_server_binary(
 571        container_dir: PathBuf,
 572        node: &NodeRuntime,
 573    ) -> Option<LanguageServerBinary> {
 574        let server_path = container_dir.join(Self::SERVER_PATH);
 575        if server_path.exists() {
 576            Some(LanguageServerBinary {
 577                path: node.binary_path().await.log_err()?,
 578                env: None,
 579                arguments: vec![server_path.into(), "--stdio".into()],
 580            })
 581        } else {
 582            log::error!("missing executable in directory {:?}", server_path);
 583            None
 584        }
 585    }
 586}
 587
 588#[async_trait(?Send)]
 589impl LspAdapter for PyrightLspAdapter {
 590    fn name(&self) -> LanguageServerName {
 591        Self::SERVER_NAME
 592    }
 593
 594    async fn initialization_options(
 595        self: Arc<Self>,
 596        _: &Arc<dyn LspAdapterDelegate>,
 597        _: &mut AsyncApp,
 598    ) -> Result<Option<Value>> {
 599        // Provide minimal initialization options
 600        // Virtual environment configuration will be handled through workspace configuration
 601        Ok(Some(json!({
 602            "python": {
 603                "analysis": {
 604                    "autoSearchPaths": true,
 605                    "useLibraryCodeForTypes": true,
 606                    "autoImportCompletions": true
 607                }
 608            }
 609        })))
 610    }
 611
 612    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
 613        process_pyright_completions(items);
 614    }
 615
 616    async fn label_for_completion(
 617        &self,
 618        item: &lsp::CompletionItem,
 619        language: &Arc<language::Language>,
 620    ) -> Option<language::CodeLabel> {
 621        label_for_pyright_completion(item, language)
 622    }
 623
 624    async fn label_for_symbol(
 625        &self,
 626        symbol: &language::Symbol,
 627        language: &Arc<language::Language>,
 628    ) -> Option<language::CodeLabel> {
 629        label_for_python_symbol(symbol, language)
 630    }
 631
 632    async fn workspace_configuration(
 633        self: Arc<Self>,
 634        adapter: &Arc<dyn LspAdapterDelegate>,
 635        toolchain: Option<Toolchain>,
 636        _: Option<Uri>,
 637        cx: &mut AsyncApp,
 638    ) -> Result<Value> {
 639        Ok(cx.update(move |cx| {
 640            let mut user_settings =
 641                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
 642                    .and_then(|s| s.settings.clone())
 643                    .unwrap_or_default();
 644
 645            // If we have a detected toolchain, configure Pyright to use it
 646            if let Some(toolchain) = toolchain
 647                && let Ok(env) =
 648                    serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
 649            {
 650                if !user_settings.is_object() {
 651                    user_settings = Value::Object(serde_json::Map::default());
 652                }
 653                let object = user_settings.as_object_mut().unwrap();
 654
 655                let interpreter_path = toolchain.path.to_string();
 656                if let Some(venv_dir) = &env.environment.prefix {
 657                    // Set venvPath and venv at the root level
 658                    // This matches the format of a pyrightconfig.json file
 659                    if let Some(parent) = venv_dir.parent() {
 660                        // Use relative path if the venv is inside the workspace
 661                        let venv_path = if parent == adapter.worktree_root_path() {
 662                            ".".to_string()
 663                        } else {
 664                            parent.to_string_lossy().into_owned()
 665                        };
 666                        object.insert("venvPath".to_string(), Value::String(venv_path));
 667                    }
 668
 669                    if let Some(venv_name) = venv_dir.file_name() {
 670                        object.insert(
 671                            "venv".to_owned(),
 672                            Value::String(venv_name.to_string_lossy().into_owned()),
 673                        );
 674                    }
 675                }
 676
 677                // Always set the python interpreter path
 678                // Get or create the python section
 679                let python = object
 680                    .entry("python")
 681                    .and_modify(|v| {
 682                        if !v.is_object() {
 683                            *v = Value::Object(serde_json::Map::default());
 684                        }
 685                    })
 686                    .or_insert(Value::Object(serde_json::Map::default()));
 687                let python = python.as_object_mut().unwrap();
 688
 689                // Set both pythonPath and defaultInterpreterPath for compatibility
 690                python.insert(
 691                    "pythonPath".to_owned(),
 692                    Value::String(interpreter_path.clone()),
 693                );
 694                python.insert(
 695                    "defaultInterpreterPath".to_owned(),
 696                    Value::String(interpreter_path),
 697                );
 698            }
 699
 700            user_settings
 701        }))
 702    }
 703}
 704
 705impl LspInstaller for PyrightLspAdapter {
 706    type BinaryVersion = Version;
 707
 708    async fn fetch_latest_server_version(
 709        &self,
 710        _: &dyn LspAdapterDelegate,
 711        _: bool,
 712        _: &mut AsyncApp,
 713    ) -> Result<Self::BinaryVersion> {
 714        self.node
 715            .npm_package_latest_version(Self::SERVER_NAME.as_ref())
 716            .await
 717    }
 718
 719    async fn check_if_user_installed(
 720        &self,
 721        delegate: &dyn LspAdapterDelegate,
 722        _: Option<Toolchain>,
 723        _: &AsyncApp,
 724    ) -> Option<LanguageServerBinary> {
 725        if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
 726            let env = delegate.shell_env().await;
 727            Some(LanguageServerBinary {
 728                path: pyright_bin,
 729                env: Some(env),
 730                arguments: vec!["--stdio".into()],
 731            })
 732        } else {
 733            let node = delegate.which("node".as_ref()).await?;
 734            let (node_modules_path, _) = delegate
 735                .npm_package_installed_version(Self::SERVER_NAME.as_ref())
 736                .await
 737                .log_err()??;
 738
 739            let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
 740
 741            let env = delegate.shell_env().await;
 742            Some(LanguageServerBinary {
 743                path: node,
 744                env: Some(env),
 745                arguments: vec![path.into(), "--stdio".into()],
 746            })
 747        }
 748    }
 749
 750    async fn fetch_server_binary(
 751        &self,
 752        latest_version: Self::BinaryVersion,
 753        container_dir: PathBuf,
 754        delegate: &dyn LspAdapterDelegate,
 755    ) -> Result<LanguageServerBinary> {
 756        let server_path = container_dir.join(Self::SERVER_PATH);
 757        let latest_version = latest_version.to_string();
 758
 759        self.node
 760            .npm_install_packages(
 761                &container_dir,
 762                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
 763            )
 764            .await?;
 765
 766        let env = delegate.shell_env().await;
 767        Ok(LanguageServerBinary {
 768            path: self.node.binary_path().await?,
 769            env: Some(env),
 770            arguments: vec![server_path.into(), "--stdio".into()],
 771        })
 772    }
 773
 774    async fn check_if_version_installed(
 775        &self,
 776        version: &Self::BinaryVersion,
 777        container_dir: &PathBuf,
 778        delegate: &dyn LspAdapterDelegate,
 779    ) -> Option<LanguageServerBinary> {
 780        let server_path = container_dir.join(Self::SERVER_PATH);
 781
 782        let should_install_language_server = self
 783            .node
 784            .should_install_npm_package(
 785                Self::SERVER_NAME.as_ref(),
 786                &server_path,
 787                container_dir,
 788                VersionStrategy::Latest(version),
 789            )
 790            .await;
 791
 792        if should_install_language_server {
 793            None
 794        } else {
 795            let env = delegate.shell_env().await;
 796            Some(LanguageServerBinary {
 797                path: self.node.binary_path().await.ok()?,
 798                env: Some(env),
 799                arguments: vec![server_path.into(), "--stdio".into()],
 800            })
 801        }
 802    }
 803
 804    async fn cached_server_binary(
 805        &self,
 806        container_dir: PathBuf,
 807        delegate: &dyn LspAdapterDelegate,
 808    ) -> Option<LanguageServerBinary> {
 809        let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
 810        binary.env = Some(delegate.shell_env().await);
 811        Some(binary)
 812    }
 813}
 814
 815pub(crate) struct PythonContextProvider;
 816
 817const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
 818    VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET"));
 819
 820const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
 821    VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
 822
 823const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
 824    VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
 825
 826impl ContextProvider for PythonContextProvider {
 827    fn build_context(
 828        &self,
 829        variables: &task::TaskVariables,
 830        location: ContextLocation<'_>,
 831        _: Option<HashMap<String, String>>,
 832        toolchains: Arc<dyn LanguageToolchainStore>,
 833        cx: &mut gpui::App,
 834    ) -> Task<Result<task::TaskVariables>> {
 835        let test_target = match selected_test_runner(Some(&location.file_location.buffer), cx) {
 836            TestRunner::UNITTEST => self.build_unittest_target(variables),
 837            TestRunner::PYTEST => self.build_pytest_target(variables),
 838        };
 839
 840        let module_target = self.build_module_target(variables);
 841        let location_file = location.file_location.buffer.read(cx).file().cloned();
 842        let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
 843
 844        cx.spawn(async move |cx| {
 845            let active_toolchain = if let Some(worktree_id) = worktree_id {
 846                let file_path = location_file
 847                    .as_ref()
 848                    .and_then(|f| f.path().parent())
 849                    .map(Arc::from)
 850                    .unwrap_or_else(|| RelPath::empty().into());
 851
 852                toolchains
 853                    .active_toolchain(worktree_id, file_path, "Python".into(), cx)
 854                    .await
 855                    .map_or_else(
 856                        || String::from("python3"),
 857                        |toolchain| toolchain.path.to_string(),
 858                    )
 859            } else {
 860                String::from("python3")
 861            };
 862
 863            let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
 864
 865            Ok(task::TaskVariables::from_iter(
 866                test_target
 867                    .into_iter()
 868                    .chain(module_target.into_iter())
 869                    .chain([toolchain]),
 870            ))
 871        })
 872    }
 873
 874    fn associated_tasks(
 875        &self,
 876        buffer: Option<Entity<Buffer>>,
 877        cx: &App,
 878    ) -> Task<Option<TaskTemplates>> {
 879        let test_runner = selected_test_runner(buffer.as_ref(), cx);
 880
 881        let mut tasks = vec![
 882            // Execute a selection
 883            TaskTemplate {
 884                label: "execute selection".to_owned(),
 885                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 886                args: vec![
 887                    "-c".to_owned(),
 888                    VariableName::SelectedText.template_value_with_whitespace(),
 889                ],
 890                cwd: Some(VariableName::WorktreeRoot.template_value()),
 891                ..TaskTemplate::default()
 892            },
 893            // Execute an entire file
 894            TaskTemplate {
 895                label: format!("run '{}'", VariableName::File.template_value()),
 896                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 897                args: vec![VariableName::File.template_value_with_whitespace()],
 898                cwd: Some(VariableName::WorktreeRoot.template_value()),
 899                ..TaskTemplate::default()
 900            },
 901            // Execute a file as module
 902            TaskTemplate {
 903                label: format!("run module '{}'", VariableName::File.template_value()),
 904                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 905                args: vec![
 906                    "-m".to_owned(),
 907                    PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
 908                ],
 909                cwd: Some(VariableName::WorktreeRoot.template_value()),
 910                tags: vec!["python-module-main-method".to_owned()],
 911                ..TaskTemplate::default()
 912            },
 913        ];
 914
 915        tasks.extend(match test_runner {
 916            TestRunner::UNITTEST => {
 917                [
 918                    // Run tests for an entire file
 919                    TaskTemplate {
 920                        label: format!("unittest '{}'", VariableName::File.template_value()),
 921                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 922                        args: vec![
 923                            "-m".to_owned(),
 924                            "unittest".to_owned(),
 925                            VariableName::File.template_value_with_whitespace(),
 926                        ],
 927                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 928                        ..TaskTemplate::default()
 929                    },
 930                    // Run test(s) for a specific target within a file
 931                    TaskTemplate {
 932                        label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
 933                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 934                        args: vec![
 935                            "-m".to_owned(),
 936                            "unittest".to_owned(),
 937                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
 938                        ],
 939                        tags: vec![
 940                            "python-unittest-class".to_owned(),
 941                            "python-unittest-method".to_owned(),
 942                        ],
 943                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 944                        ..TaskTemplate::default()
 945                    },
 946                ]
 947            }
 948            TestRunner::PYTEST => {
 949                [
 950                    // Run tests for an entire file
 951                    TaskTemplate {
 952                        label: format!("pytest '{}'", VariableName::File.template_value()),
 953                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 954                        args: vec![
 955                            "-m".to_owned(),
 956                            "pytest".to_owned(),
 957                            VariableName::File.template_value_with_whitespace(),
 958                        ],
 959                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 960                        ..TaskTemplate::default()
 961                    },
 962                    // Run test(s) for a specific target within a file
 963                    TaskTemplate {
 964                        label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
 965                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 966                        args: vec![
 967                            "-m".to_owned(),
 968                            "pytest".to_owned(),
 969                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
 970                        ],
 971                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 972                        tags: vec![
 973                            "python-pytest-class".to_owned(),
 974                            "python-pytest-method".to_owned(),
 975                        ],
 976                        ..TaskTemplate::default()
 977                    },
 978                ]
 979            }
 980        });
 981
 982        Task::ready(Some(TaskTemplates(tasks)))
 983    }
 984}
 985
 986fn selected_test_runner(location: Option<&Entity<Buffer>>, cx: &App) -> TestRunner {
 987    const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";
 988    let language = LanguageName::new_static("Python");
 989    let settings = LanguageSettings::resolve(location.map(|b| b.read(cx)), Some(&language), cx);
 990    settings
 991        .tasks
 992        .variables
 993        .get(TEST_RUNNER_VARIABLE)
 994        .and_then(|val| TestRunner::from_str(val).ok())
 995        .unwrap_or(TestRunner::PYTEST)
 996}
 997
 998impl PythonContextProvider {
 999    fn build_unittest_target(
1000        &self,
1001        variables: &task::TaskVariables,
1002    ) -> Option<(VariableName, String)> {
1003        let python_module_name =
1004            python_module_name_from_relative_path(variables.get(&VariableName::RelativeFile)?)?;
1005
1006        let unittest_class_name =
1007            variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
1008
1009        let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
1010            "_unittest_method_name",
1011        )));
1012
1013        let unittest_target_str = match (unittest_class_name, unittest_method_name) {
1014            (Some(class_name), Some(method_name)) => {
1015                format!("{python_module_name}.{class_name}.{method_name}")
1016            }
1017            (Some(class_name), None) => format!("{python_module_name}.{class_name}"),
1018            (None, None) => python_module_name,
1019            // should never happen, a TestCase class is the unit of testing
1020            (None, Some(_)) => return None,
1021        };
1022
1023        Some((
1024            PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),
1025            unittest_target_str,
1026        ))
1027    }
1028
1029    fn build_pytest_target(
1030        &self,
1031        variables: &task::TaskVariables,
1032    ) -> Option<(VariableName, String)> {
1033        let file_path = variables.get(&VariableName::RelativeFile)?;
1034
1035        let pytest_class_name =
1036            variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name")));
1037
1038        let pytest_method_name =
1039            variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name")));
1040
1041        let pytest_target_str = match (pytest_class_name, pytest_method_name) {
1042            (Some(class_name), Some(method_name)) => {
1043                format!("{file_path}::{class_name}::{method_name}")
1044            }
1045            (Some(class_name), None) => {
1046                format!("{file_path}::{class_name}")
1047            }
1048            (None, Some(method_name)) => {
1049                format!("{file_path}::{method_name}")
1050            }
1051            (None, None) => file_path.to_string(),
1052        };
1053
1054        Some((PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str))
1055    }
1056
1057    fn build_module_target(
1058        &self,
1059        variables: &task::TaskVariables,
1060    ) -> Result<(VariableName, String)> {
1061        let python_module_name = variables
1062            .get(&VariableName::RelativeFile)
1063            .and_then(|module| python_module_name_from_relative_path(module))
1064            .unwrap_or_default();
1065
1066        let module_target = (PYTHON_MODULE_NAME_TASK_VARIABLE.clone(), python_module_name);
1067
1068        Ok(module_target)
1069    }
1070}
1071
1072fn python_module_name_from_relative_path(relative_path: &str) -> Option<String> {
1073    let rel_path = RelPath::new(relative_path.as_ref(), PathStyle::local()).ok()?;
1074    let path_with_dots = rel_path.display(PathStyle::Posix).replace('/', ".");
1075    Some(
1076        path_with_dots
1077            .strip_suffix(".py")
1078            .map(ToOwned::to_owned)
1079            .unwrap_or(path_with_dots),
1080    )
1081}
1082
1083fn is_python_env_global(k: &PythonEnvironmentKind) -> bool {
1084    matches!(
1085        k,
1086        PythonEnvironmentKind::Homebrew
1087            | PythonEnvironmentKind::Pyenv
1088            | PythonEnvironmentKind::GlobalPaths
1089            | PythonEnvironmentKind::MacPythonOrg
1090            | PythonEnvironmentKind::MacCommandLineTools
1091            | PythonEnvironmentKind::LinuxGlobal
1092            | PythonEnvironmentKind::MacXCode
1093            | PythonEnvironmentKind::WindowsStore
1094            | PythonEnvironmentKind::WindowsRegistry
1095    )
1096}
1097
1098fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
1099    match k {
1100        PythonEnvironmentKind::Conda => "Conda",
1101        PythonEnvironmentKind::Pixi => "pixi",
1102        PythonEnvironmentKind::Homebrew => "Homebrew",
1103        PythonEnvironmentKind::Pyenv => "global (Pyenv)",
1104        PythonEnvironmentKind::GlobalPaths => "global",
1105        PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
1106        PythonEnvironmentKind::Pipenv => "Pipenv",
1107        PythonEnvironmentKind::Poetry => "Poetry",
1108        PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
1109        PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
1110        PythonEnvironmentKind::LinuxGlobal => "global",
1111        PythonEnvironmentKind::MacXCode => "global (Xcode)",
1112        PythonEnvironmentKind::Venv => "venv",
1113        PythonEnvironmentKind::VirtualEnv => "virtualenv",
1114        PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
1115        PythonEnvironmentKind::WinPython => "WinPython",
1116        PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
1117        PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
1118        PythonEnvironmentKind::Uv => "uv",
1119        PythonEnvironmentKind::UvWorkspace => "uv (Workspace)",
1120    }
1121}
1122
1123pub(crate) struct PythonToolchainProvider {
1124    fs: Arc<dyn Fs>,
1125}
1126
1127impl PythonToolchainProvider {
1128    pub fn new(fs: Arc<dyn Fs>) -> Self {
1129        Self { fs }
1130    }
1131}
1132
1133static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
1134    // Prioritize non-Conda environments.
1135    PythonEnvironmentKind::UvWorkspace,
1136    PythonEnvironmentKind::Uv,
1137    PythonEnvironmentKind::Poetry,
1138    PythonEnvironmentKind::Pipenv,
1139    PythonEnvironmentKind::VirtualEnvWrapper,
1140    PythonEnvironmentKind::Venv,
1141    PythonEnvironmentKind::VirtualEnv,
1142    PythonEnvironmentKind::PyenvVirtualEnv,
1143    PythonEnvironmentKind::Pixi,
1144    PythonEnvironmentKind::Conda,
1145    PythonEnvironmentKind::Pyenv,
1146    PythonEnvironmentKind::GlobalPaths,
1147    PythonEnvironmentKind::Homebrew,
1148];
1149
1150fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
1151    if let Some(kind) = kind {
1152        ENV_PRIORITY_LIST
1153            .iter()
1154            .position(|blessed_env| blessed_env == &kind)
1155            .unwrap_or(ENV_PRIORITY_LIST.len())
1156    } else {
1157        // Unknown toolchains are less useful than non-blessed ones.
1158        ENV_PRIORITY_LIST.len() + 1
1159    }
1160}
1161
1162/// Return the name of environment declared in <worktree-root/.venv.
1163///
1164/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
1165async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
1166    let file = async_fs::File::open(worktree_root.join(".venv"))
1167        .await
1168        .ok()?;
1169    let mut venv_name = String::new();
1170    smol::io::BufReader::new(file)
1171        .read_line(&mut venv_name)
1172        .await
1173        .ok()?;
1174    Some(venv_name.trim().to_string())
1175}
1176
1177fn get_venv_parent_dir(env: &PythonEnvironment) -> Option<PathBuf> {
1178    // If global, we aren't a virtual environment
1179    if let Some(kind) = env.kind
1180        && is_python_env_global(&kind)
1181    {
1182        return None;
1183    }
1184
1185    // Check to be sure we are a virtual environment using pet's most generic
1186    // virtual environment type, VirtualEnv
1187    let venv = env
1188        .executable
1189        .as_ref()
1190        .and_then(|p| p.parent())
1191        .and_then(|p| p.parent())
1192        .filter(|p| is_virtualenv_dir(p))?;
1193
1194    venv.parent().map(|parent| parent.to_path_buf())
1195}
1196
1197// How far is this venv from the root of our current project?
1198#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1199enum SubprojectDistance {
1200    WithinSubproject(Reverse<usize>),
1201    WithinWorktree(Reverse<usize>),
1202    NotInWorktree,
1203}
1204
1205fn wr_distance(
1206    wr: &PathBuf,
1207    subroot_relative_path: &RelPath,
1208    venv: Option<&PathBuf>,
1209) -> SubprojectDistance {
1210    if let Some(venv) = venv
1211        && let Ok(p) = venv.strip_prefix(wr)
1212    {
1213        if subroot_relative_path.components().next().is_some()
1214            && let Ok(distance) = p
1215                .strip_prefix(subroot_relative_path.as_std_path())
1216                .map(|p| p.components().count())
1217        {
1218            SubprojectDistance::WithinSubproject(Reverse(distance))
1219        } else {
1220            SubprojectDistance::WithinWorktree(Reverse(p.components().count()))
1221        }
1222    } else {
1223        SubprojectDistance::NotInWorktree
1224    }
1225}
1226
1227fn micromamba_shell_name(kind: ShellKind) -> &'static str {
1228    match kind {
1229        ShellKind::Csh => "csh",
1230        ShellKind::Fish => "fish",
1231        ShellKind::Nushell => "nu",
1232        ShellKind::PowerShell => "powershell",
1233        ShellKind::Cmd => "cmd.exe",
1234        // default / catch-all:
1235        _ => "posix",
1236    }
1237}
1238
1239#[async_trait]
1240impl ToolchainLister for PythonToolchainProvider {
1241    async fn list(
1242        &self,
1243        worktree_root: PathBuf,
1244        subroot_relative_path: Arc<RelPath>,
1245        project_env: Option<HashMap<String, String>>,
1246    ) -> ToolchainList {
1247        let fs = &*self.fs;
1248        let env = project_env.unwrap_or_default();
1249        let environment = EnvironmentApi::from_env(&env);
1250        let locators = pet::locators::create_locators(
1251            Arc::new(pet_conda::Conda::from(&environment)),
1252            Arc::new(pet_poetry::Poetry::from(&environment)),
1253            &environment,
1254        );
1255        let mut config = Configuration::default();
1256
1257        // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
1258        // worktree root as the workspace directory.
1259        config.workspace_directories = Some(
1260            subroot_relative_path
1261                .ancestors()
1262                .map(|ancestor| {
1263                    // remove trailing separator as it alters the environment name hash used by Poetry.
1264                    let path = worktree_root.join(ancestor.as_std_path());
1265                    let path_str = path.to_string_lossy();
1266                    if path_str.ends_with(std::path::MAIN_SEPARATOR) && path_str.len() > 1 {
1267                        PathBuf::from(path_str.trim_end_matches(std::path::MAIN_SEPARATOR))
1268                    } else {
1269                        path
1270                    }
1271                })
1272                .collect(),
1273        );
1274        for locator in locators.iter() {
1275            locator.configure(&config);
1276        }
1277
1278        let reporter = pet_reporter::collect::create_reporter();
1279        pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
1280
1281        let mut toolchains = reporter
1282            .environments
1283            .lock()
1284            .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
1285
1286        let wr = worktree_root;
1287        let wr_venv = get_worktree_venv_declaration(&wr).await;
1288        // Sort detected environments by:
1289        //     environment name matching activation file (<workdir>/.venv)
1290        //     environment project dir matching worktree_root
1291        //     general env priority
1292        //     environment path matching the CONDA_PREFIX env var
1293        //     executable path
1294        toolchains.sort_by(|lhs, rhs| {
1295            // Compare venv names against worktree .venv file
1296            let venv_ordering =
1297                wr_venv
1298                    .as_ref()
1299                    .map_or(Ordering::Equal, |venv| match (&lhs.name, &rhs.name) {
1300                        (Some(l), Some(r)) => (r == venv).cmp(&(l == venv)),
1301                        (Some(l), None) if l == venv => Ordering::Less,
1302                        (None, Some(r)) if r == venv => Ordering::Greater,
1303                        _ => Ordering::Equal,
1304                    });
1305
1306            // Compare project paths against worktree root
1307            let proj_ordering =
1308                || {
1309                    let lhs_project = lhs.project.clone().or_else(|| get_venv_parent_dir(lhs));
1310                    let rhs_project = rhs.project.clone().or_else(|| get_venv_parent_dir(rhs));
1311                    wr_distance(&wr, &subroot_relative_path, lhs_project.as_ref()).cmp(
1312                        &wr_distance(&wr, &subroot_relative_path, rhs_project.as_ref()),
1313                    )
1314                };
1315
1316            // Compare environment priorities
1317            let priority_ordering = || env_priority(lhs.kind).cmp(&env_priority(rhs.kind));
1318
1319            // Compare conda prefixes
1320            let conda_ordering = || {
1321                if lhs.kind == Some(PythonEnvironmentKind::Conda) {
1322                    environment
1323                        .get_env_var("CONDA_PREFIX".to_string())
1324                        .map(|conda_prefix| {
1325                            let is_match = |exe: &Option<PathBuf>| {
1326                                exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
1327                            };
1328                            match (is_match(&lhs.executable), is_match(&rhs.executable)) {
1329                                (true, false) => Ordering::Less,
1330                                (false, true) => Ordering::Greater,
1331                                _ => Ordering::Equal,
1332                            }
1333                        })
1334                        .unwrap_or(Ordering::Equal)
1335                } else {
1336                    Ordering::Equal
1337                }
1338            };
1339
1340            // Compare Python executables
1341            let exe_ordering = || lhs.executable.cmp(&rhs.executable);
1342
1343            venv_ordering
1344                .then_with(proj_ordering)
1345                .then_with(priority_ordering)
1346                .then_with(conda_ordering)
1347                .then_with(exe_ordering)
1348        });
1349
1350        let mut out_toolchains = Vec::new();
1351        for toolchain in toolchains {
1352            let Some(toolchain) = venv_to_toolchain(toolchain, fs).await else {
1353                continue;
1354            };
1355            out_toolchains.push(toolchain);
1356        }
1357        out_toolchains.dedup();
1358        ToolchainList {
1359            toolchains: out_toolchains,
1360            default: None,
1361            groups: Default::default(),
1362        }
1363    }
1364    fn meta(&self) -> ToolchainMetadata {
1365        ToolchainMetadata {
1366            term: SharedString::new_static("Virtual Environment"),
1367            new_toolchain_placeholder: SharedString::new_static(
1368                "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
1369            ),
1370            manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
1371        }
1372    }
1373
1374    async fn resolve(
1375        &self,
1376        path: PathBuf,
1377        env: Option<HashMap<String, String>>,
1378    ) -> anyhow::Result<Toolchain> {
1379        let fs = &*self.fs;
1380        let env = env.unwrap_or_default();
1381        let environment = EnvironmentApi::from_env(&env);
1382        let locators = pet::locators::create_locators(
1383            Arc::new(pet_conda::Conda::from(&environment)),
1384            Arc::new(pet_poetry::Poetry::from(&environment)),
1385            &environment,
1386        );
1387        let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
1388            .context("Could not find a virtual environment in provided path")?;
1389        let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
1390        venv_to_toolchain(venv, fs)
1391            .await
1392            .context("Could not convert a venv into a toolchain")
1393    }
1394
1395    fn activation_script(
1396        &self,
1397        toolchain: &Toolchain,
1398        shell: ShellKind,
1399        cx: &App,
1400    ) -> BoxFuture<'static, Vec<String>> {
1401        let settings = TerminalSettings::get_global(cx);
1402        let conda_manager = settings
1403            .detect_venv
1404            .as_option()
1405            .map(|venv| venv.conda_manager)
1406            .unwrap_or(settings::CondaManager::Auto);
1407
1408        let toolchain_clone = toolchain.clone();
1409        Box::pin(async move {
1410            let Ok(toolchain) =
1411                serde_json::from_value::<PythonToolchainData>(toolchain_clone.as_json.clone())
1412            else {
1413                return vec![];
1414            };
1415
1416            log::debug!("(Python) Composing activation script for toolchain {toolchain:?}");
1417
1418            let mut activation_script = vec![];
1419
1420            match toolchain.environment.kind {
1421                Some(PythonEnvironmentKind::Conda) => {
1422                    if toolchain.environment.manager.is_none() {
1423                        return vec![];
1424                    };
1425
1426                    let manager = match conda_manager {
1427                        settings::CondaManager::Conda => "conda",
1428                        settings::CondaManager::Mamba => "mamba",
1429                        settings::CondaManager::Micromamba => "micromamba",
1430                        settings::CondaManager::Auto => toolchain
1431                            .environment
1432                            .manager
1433                            .as_ref()
1434                            .and_then(|m| m.executable.file_name())
1435                            .and_then(|name| name.to_str())
1436                            .filter(|name| matches!(*name, "conda" | "mamba" | "micromamba"))
1437                            .unwrap_or("conda"),
1438                    };
1439
1440                    // Activate micromamba shell in the child shell
1441                    // [required for micromamba]
1442                    if manager == "micromamba" {
1443                        let shell = micromamba_shell_name(shell);
1444                        activation_script
1445                            .push(format!(r#"eval "$({manager} shell hook --shell {shell})""#));
1446                    }
1447
1448                    if let Some(name) = &toolchain.environment.name {
1449                        if let Some(quoted_name) = shell.try_quote(name) {
1450                            activation_script.push(format!("{manager} activate {quoted_name}"));
1451                        } else {
1452                            log::warn!(
1453                                "Could not safely quote environment name {:?}, falling back to base",
1454                                name
1455                            );
1456                            activation_script.push(format!("{manager} activate base"));
1457                        }
1458                    } else {
1459                        activation_script.push(format!("{manager} activate base"));
1460                    }
1461                }
1462                Some(
1463                    PythonEnvironmentKind::Venv
1464                    | PythonEnvironmentKind::VirtualEnv
1465                    | PythonEnvironmentKind::Uv
1466                    | PythonEnvironmentKind::UvWorkspace
1467                    | PythonEnvironmentKind::Poetry,
1468                ) => {
1469                    if let Some(activation_scripts) = &toolchain.activation_scripts {
1470                        if let Some(activate_script_path) = activation_scripts.get(&shell) {
1471                            let activate_keyword = shell.activate_keyword();
1472                            if let Some(quoted) =
1473                                shell.try_quote(&activate_script_path.to_string_lossy())
1474                            {
1475                                activation_script.push(format!("{activate_keyword} {quoted}"));
1476                            }
1477                        }
1478                    }
1479                }
1480                Some(PythonEnvironmentKind::Pyenv) => {
1481                    let Some(manager) = &toolchain.environment.manager else {
1482                        return vec![];
1483                    };
1484                    let version = toolchain.environment.version.as_deref().unwrap_or("system");
1485                    let pyenv = &manager.executable;
1486                    let pyenv = pyenv.display();
1487                    activation_script.extend(match shell {
1488                        ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
1489                        ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
1490                        ShellKind::Nushell => Some(format!("^\"{pyenv}\" shell - nu {version}")),
1491                        ShellKind::PowerShell | ShellKind::Pwsh => None,
1492                        ShellKind::Csh => None,
1493                        ShellKind::Tcsh => None,
1494                        ShellKind::Cmd => None,
1495                        ShellKind::Rc => None,
1496                        ShellKind::Xonsh => None,
1497                        ShellKind::Elvish => None,
1498                    })
1499                }
1500                _ => {}
1501            }
1502            activation_script
1503        })
1504    }
1505}
1506
1507async fn venv_to_toolchain(venv: PythonEnvironment, fs: &dyn Fs) -> Option<Toolchain> {
1508    let mut name = String::from("Python");
1509    if let Some(ref version) = venv.version {
1510        _ = write!(name, " {version}");
1511    }
1512
1513    let name_and_kind = match (&venv.name, &venv.kind) {
1514        (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
1515        (Some(name), None) => Some(format!("({name})")),
1516        (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
1517        (None, None) => None,
1518    };
1519
1520    if let Some(nk) = name_and_kind {
1521        _ = write!(name, " {nk}");
1522    }
1523
1524    let mut activation_scripts = HashMap::default();
1525    match venv.kind {
1526        Some(
1527            PythonEnvironmentKind::Venv
1528            | PythonEnvironmentKind::VirtualEnv
1529            | PythonEnvironmentKind::Uv
1530            | PythonEnvironmentKind::UvWorkspace
1531            | PythonEnvironmentKind::Poetry,
1532        ) => resolve_venv_activation_scripts(&venv, fs, &mut activation_scripts).await,
1533        _ => {}
1534    }
1535    let data = PythonToolchainData {
1536        environment: venv,
1537        activation_scripts: Some(activation_scripts),
1538    };
1539
1540    Some(Toolchain {
1541        name: name.into(),
1542        path: data
1543            .environment
1544            .executable
1545            .as_ref()?
1546            .to_str()?
1547            .to_owned()
1548            .into(),
1549        language_name: LanguageName::new_static("Python"),
1550        as_json: serde_json::to_value(data).ok()?,
1551    })
1552}
1553
1554async fn resolve_venv_activation_scripts(
1555    venv: &PythonEnvironment,
1556    fs: &dyn Fs,
1557    activation_scripts: &mut HashMap<ShellKind, PathBuf>,
1558) {
1559    log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}");
1560    if let Some(prefix) = &venv.prefix {
1561        for (shell_kind, script_name) in &[
1562            (ShellKind::Posix, "activate"),
1563            (ShellKind::Rc, "activate"),
1564            (ShellKind::Csh, "activate.csh"),
1565            (ShellKind::Tcsh, "activate.csh"),
1566            (ShellKind::Fish, "activate.fish"),
1567            (ShellKind::Nushell, "activate.nu"),
1568            (ShellKind::PowerShell, "activate.ps1"),
1569            (ShellKind::Pwsh, "activate.ps1"),
1570            (ShellKind::Cmd, "activate.bat"),
1571            (ShellKind::Xonsh, "activate.xsh"),
1572        ] {
1573            let path = prefix.join(BINARY_DIR).join(script_name);
1574
1575            log::debug!("Trying path: {}", path.display());
1576
1577            if fs.is_file(&path).await {
1578                activation_scripts.insert(*shell_kind, path);
1579            }
1580        }
1581    }
1582}
1583
1584pub struct EnvironmentApi<'a> {
1585    global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
1586    project_env: &'a HashMap<String, String>,
1587    pet_env: pet_core::os_environment::EnvironmentApi,
1588}
1589
1590impl<'a> EnvironmentApi<'a> {
1591    pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
1592        let paths = project_env
1593            .get("PATH")
1594            .map(|p| std::env::split_paths(p).collect())
1595            .unwrap_or_default();
1596
1597        EnvironmentApi {
1598            global_search_locations: Arc::new(Mutex::new(paths)),
1599            project_env,
1600            pet_env: pet_core::os_environment::EnvironmentApi::new(),
1601        }
1602    }
1603
1604    fn user_home(&self) -> Option<PathBuf> {
1605        self.project_env
1606            .get("HOME")
1607            .or_else(|| self.project_env.get("USERPROFILE"))
1608            .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
1609            .or_else(|| self.pet_env.get_user_home())
1610    }
1611}
1612
1613impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
1614    fn get_user_home(&self) -> Option<PathBuf> {
1615        self.user_home()
1616    }
1617
1618    fn get_root(&self) -> Option<PathBuf> {
1619        None
1620    }
1621
1622    fn get_env_var(&self, key: String) -> Option<String> {
1623        self.project_env
1624            .get(&key)
1625            .cloned()
1626            .or_else(|| self.pet_env.get_env_var(key))
1627    }
1628
1629    fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
1630        if self.global_search_locations.lock().is_empty() {
1631            let mut paths = std::env::split_paths(
1632                &self
1633                    .get_env_var("PATH".to_string())
1634                    .or_else(|| self.get_env_var("Path".to_string()))
1635                    .unwrap_or_default(),
1636            )
1637            .collect::<Vec<PathBuf>>();
1638
1639            log::trace!("Env PATH: {:?}", paths);
1640            for p in self.pet_env.get_know_global_search_locations() {
1641                if !paths.contains(&p) {
1642                    paths.push(p);
1643                }
1644            }
1645
1646            let mut paths = paths
1647                .into_iter()
1648                .filter(|p| p.exists())
1649                .collect::<Vec<PathBuf>>();
1650
1651            self.global_search_locations.lock().append(&mut paths);
1652        }
1653        self.global_search_locations.lock().clone()
1654    }
1655}
1656
1657pub(crate) struct PyLspAdapter {
1658    python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1659}
1660impl PyLspAdapter {
1661    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
1662    pub(crate) fn new() -> Self {
1663        Self {
1664            python_venv_base: OnceCell::new(),
1665        }
1666    }
1667    async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1668        let python_path = Self::find_base_python(delegate)
1669            .await
1670            .with_context(|| {
1671                let mut message = "Could not find Python installation for PyLSP".to_owned();
1672                if cfg!(windows){
1673                    message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
1674                }
1675                message
1676            })?;
1677        let work_dir = delegate
1678            .language_server_download_dir(&Self::SERVER_NAME)
1679            .await
1680            .context("Could not get working directory for PyLSP")?;
1681        let mut path = PathBuf::from(work_dir.as_ref());
1682        path.push("pylsp-venv");
1683        if !path.exists() {
1684            util::command::new_command(python_path)
1685                .arg("-m")
1686                .arg("venv")
1687                .arg("pylsp-venv")
1688                .current_dir(work_dir)
1689                .spawn()?
1690                .output()
1691                .await?;
1692        }
1693
1694        Ok(path.into())
1695    }
1696    // Find "baseline", user python version from which we'll create our own venv.
1697    async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1698        for path in ["python3", "python"] {
1699            let Some(path) = delegate.which(path.as_ref()).await else {
1700                continue;
1701            };
1702            // Try to detect situations where `python3` exists but is not a real Python interpreter.
1703            // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
1704            // when run with no arguments, and just fails otherwise.
1705            let Some(output) = new_command(&path)
1706                .args(["-c", "print(1 + 2)"])
1707                .output()
1708                .await
1709                .ok()
1710            else {
1711                continue;
1712            };
1713            if output.stdout.trim_ascii() != b"3" {
1714                continue;
1715            }
1716            return Some(path);
1717        }
1718        None
1719    }
1720
1721    async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1722        self.python_venv_base
1723            .get_or_init(move || async move {
1724                Self::ensure_venv(delegate)
1725                    .await
1726                    .map_err(|e| format!("{e}"))
1727            })
1728            .await
1729            .clone()
1730    }
1731}
1732
1733const BINARY_DIR: &str = if cfg!(target_os = "windows") {
1734    "Scripts"
1735} else {
1736    "bin"
1737};
1738
1739#[async_trait(?Send)]
1740impl LspAdapter for PyLspAdapter {
1741    fn name(&self) -> LanguageServerName {
1742        Self::SERVER_NAME
1743    }
1744
1745    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
1746        for item in items {
1747            let is_named_argument = item.label.ends_with('=');
1748            let priority = if is_named_argument { '0' } else { '1' };
1749            let sort_text = item.sort_text.take().unwrap_or_else(|| item.label.clone());
1750            item.sort_text = Some(format!("{}{}", priority, sort_text));
1751        }
1752    }
1753
1754    async fn label_for_completion(
1755        &self,
1756        item: &lsp::CompletionItem,
1757        language: &Arc<language::Language>,
1758    ) -> Option<language::CodeLabel> {
1759        let label = &item.label;
1760        let label_len = label.len();
1761        let grammar = language.grammar()?;
1762        let highlight_id = match item.kind? {
1763            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1764            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1765            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1766            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1767            _ => return None,
1768        };
1769        Some(language::CodeLabel::filtered(
1770            label.clone(),
1771            label_len,
1772            item.filter_text.as_deref(),
1773            vec![(0..label.len(), highlight_id)],
1774        ))
1775    }
1776
1777    async fn label_for_symbol(
1778        &self,
1779        symbol: &language::Symbol,
1780        language: &Arc<language::Language>,
1781    ) -> Option<language::CodeLabel> {
1782        label_for_python_symbol(symbol, language)
1783    }
1784
1785    async fn workspace_configuration(
1786        self: Arc<Self>,
1787        adapter: &Arc<dyn LspAdapterDelegate>,
1788        toolchain: Option<Toolchain>,
1789        _: Option<Uri>,
1790        cx: &mut AsyncApp,
1791    ) -> Result<Value> {
1792        Ok(cx.update(move |cx| {
1793            let mut user_settings =
1794                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1795                    .and_then(|s| s.settings.clone())
1796                    .unwrap_or_else(|| {
1797                        json!({
1798                            "plugins": {
1799                                "pycodestyle": {"enabled": false},
1800                                "rope_autoimport": {"enabled": true, "memory": true},
1801                                "pylsp_mypy": {"enabled": false}
1802                            },
1803                            "rope": {
1804                                "ropeFolder": null
1805                            },
1806                        })
1807                    });
1808
1809            // If user did not explicitly modify their python venv, use one from picker.
1810            if let Some(toolchain) = toolchain {
1811                if !user_settings.is_object() {
1812                    user_settings = Value::Object(serde_json::Map::default());
1813                }
1814                let object = user_settings.as_object_mut().unwrap();
1815                if let Some(python) = object
1816                    .entry("plugins")
1817                    .or_insert(Value::Object(serde_json::Map::default()))
1818                    .as_object_mut()
1819                {
1820                    if let Some(jedi) = python
1821                        .entry("jedi")
1822                        .or_insert(Value::Object(serde_json::Map::default()))
1823                        .as_object_mut()
1824                    {
1825                        jedi.entry("environment".to_string())
1826                            .or_insert_with(|| Value::String(toolchain.path.clone().into()));
1827                    }
1828                    if let Some(pylint) = python
1829                        .entry("pylsp_mypy")
1830                        .or_insert(Value::Object(serde_json::Map::default()))
1831                        .as_object_mut()
1832                    {
1833                        pylint.entry("overrides".to_string()).or_insert_with(|| {
1834                            Value::Array(vec![
1835                                Value::String("--python-executable".into()),
1836                                Value::String(toolchain.path.into()),
1837                                Value::String("--cache-dir=/dev/null".into()),
1838                                Value::Bool(true),
1839                            ])
1840                        });
1841                    }
1842                }
1843            }
1844            user_settings = Value::Object(serde_json::Map::from_iter([(
1845                "pylsp".to_string(),
1846                user_settings,
1847            )]));
1848
1849            user_settings
1850        }))
1851    }
1852}
1853
1854impl LspInstaller for PyLspAdapter {
1855    type BinaryVersion = ();
1856    async fn check_if_user_installed(
1857        &self,
1858        delegate: &dyn LspAdapterDelegate,
1859        toolchain: Option<Toolchain>,
1860        _: &AsyncApp,
1861    ) -> Option<LanguageServerBinary> {
1862        if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
1863            let env = delegate.shell_env().await;
1864            delegate
1865                .try_exec(LanguageServerBinary {
1866                    path: pylsp_bin.clone(),
1867                    arguments: vec!["--version".into()],
1868                    env: Some(env.clone()),
1869                })
1870                .await
1871                .inspect_err(|err| {
1872                    log::warn!("failed to validate user-installed pylsp at {pylsp_bin:?}: {err:#}")
1873                })
1874                .ok()?;
1875            Some(LanguageServerBinary {
1876                path: pylsp_bin,
1877                env: Some(env),
1878                arguments: vec![],
1879            })
1880        } else {
1881            let toolchain = toolchain?;
1882            let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
1883            if !pylsp_path.exists() {
1884                return None;
1885            }
1886            delegate
1887                .try_exec(LanguageServerBinary {
1888                    path: toolchain.path.to_string().into(),
1889                    arguments: vec![pylsp_path.clone().into(), "--version".into()],
1890                    env: None,
1891                })
1892                .await
1893                .inspect_err(|err| {
1894                    log::warn!("failed to validate toolchain pylsp at {pylsp_path:?}: {err:#}")
1895                })
1896                .ok()?;
1897            Some(LanguageServerBinary {
1898                path: toolchain.path.to_string().into(),
1899                arguments: vec![pylsp_path.into()],
1900                env: None,
1901            })
1902        }
1903    }
1904
1905    async fn fetch_latest_server_version(
1906        &self,
1907        _: &dyn LspAdapterDelegate,
1908        _: bool,
1909        _: &mut AsyncApp,
1910    ) -> Result<()> {
1911        Ok(())
1912    }
1913
1914    async fn fetch_server_binary(
1915        &self,
1916        _: (),
1917        _: PathBuf,
1918        delegate: &dyn LspAdapterDelegate,
1919    ) -> Result<LanguageServerBinary> {
1920        let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1921        let pip_path = venv.join(BINARY_DIR).join("pip3");
1922        ensure!(
1923            util::command::new_command(pip_path.as_path())
1924                .arg("install")
1925                .arg("python-lsp-server[all]")
1926                .arg("--upgrade")
1927                .output()
1928                .await?
1929                .status
1930                .success(),
1931            "python-lsp-server[all] installation failed"
1932        );
1933        ensure!(
1934            util::command::new_command(pip_path)
1935                .arg("install")
1936                .arg("pylsp-mypy")
1937                .arg("--upgrade")
1938                .output()
1939                .await?
1940                .status
1941                .success(),
1942            "pylsp-mypy installation failed"
1943        );
1944        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1945        ensure!(
1946            delegate.which(pylsp.as_os_str()).await.is_some(),
1947            "pylsp installation was incomplete"
1948        );
1949        Ok(LanguageServerBinary {
1950            path: pylsp,
1951            env: None,
1952            arguments: vec![],
1953        })
1954    }
1955
1956    async fn cached_server_binary(
1957        &self,
1958        _: PathBuf,
1959        delegate: &dyn LspAdapterDelegate,
1960    ) -> Option<LanguageServerBinary> {
1961        let venv = self.base_venv(delegate).await.ok()?;
1962        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1963        delegate.which(pylsp.as_os_str()).await?;
1964        Some(LanguageServerBinary {
1965            path: pylsp,
1966            env: None,
1967            arguments: vec![],
1968        })
1969    }
1970}
1971
1972pub(crate) struct BasedPyrightLspAdapter {
1973    node: NodeRuntime,
1974}
1975
1976impl BasedPyrightLspAdapter {
1977    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
1978    const BINARY_NAME: &'static str = "basedpyright-langserver";
1979    const SERVER_PATH: &str = "node_modules/basedpyright/langserver.index.js";
1980    const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "basedpyright/langserver.index.js";
1981
1982    pub(crate) fn new(node: NodeRuntime) -> Self {
1983        BasedPyrightLspAdapter { node }
1984    }
1985
1986    async fn get_cached_server_binary(
1987        container_dir: PathBuf,
1988        node: &NodeRuntime,
1989    ) -> Option<LanguageServerBinary> {
1990        let server_path = container_dir.join(Self::SERVER_PATH);
1991        if server_path.exists() {
1992            Some(LanguageServerBinary {
1993                path: node.binary_path().await.log_err()?,
1994                env: None,
1995                arguments: vec![server_path.into(), "--stdio".into()],
1996            })
1997        } else {
1998            log::error!("missing executable in directory {:?}", server_path);
1999            None
2000        }
2001    }
2002}
2003
2004#[async_trait(?Send)]
2005impl LspAdapter for BasedPyrightLspAdapter {
2006    fn name(&self) -> LanguageServerName {
2007        Self::SERVER_NAME
2008    }
2009
2010    async fn initialization_options(
2011        self: Arc<Self>,
2012        _: &Arc<dyn LspAdapterDelegate>,
2013        _: &mut AsyncApp,
2014    ) -> Result<Option<Value>> {
2015        // Provide minimal initialization options
2016        // Virtual environment configuration will be handled through workspace configuration
2017        Ok(Some(json!({
2018            "python": {
2019                "analysis": {
2020                    "autoSearchPaths": true,
2021                    "useLibraryCodeForTypes": true,
2022                    "autoImportCompletions": true
2023                }
2024            }
2025        })))
2026    }
2027
2028    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
2029        process_pyright_completions(items);
2030    }
2031
2032    async fn label_for_completion(
2033        &self,
2034        item: &lsp::CompletionItem,
2035        language: &Arc<language::Language>,
2036    ) -> Option<language::CodeLabel> {
2037        label_for_pyright_completion(item, language)
2038    }
2039
2040    async fn label_for_symbol(
2041        &self,
2042        symbol: &Symbol,
2043        language: &Arc<language::Language>,
2044    ) -> Option<language::CodeLabel> {
2045        label_for_python_symbol(symbol, language)
2046    }
2047
2048    async fn workspace_configuration(
2049        self: Arc<Self>,
2050        adapter: &Arc<dyn LspAdapterDelegate>,
2051        toolchain: Option<Toolchain>,
2052        _: Option<Uri>,
2053        cx: &mut AsyncApp,
2054    ) -> Result<Value> {
2055        Ok(cx.update(move |cx| {
2056            let mut user_settings =
2057                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
2058                    .and_then(|s| s.settings.clone())
2059                    .unwrap_or_default();
2060
2061            // If we have a detected toolchain, configure Pyright to use it
2062            if let Some(toolchain) = toolchain
2063                && let Ok(env) = serde_json::from_value::<
2064                    pet_core::python_environment::PythonEnvironment,
2065                >(toolchain.as_json.clone())
2066            {
2067                if !user_settings.is_object() {
2068                    user_settings = Value::Object(serde_json::Map::default());
2069                }
2070                let object = user_settings.as_object_mut().unwrap();
2071
2072                let interpreter_path = toolchain.path.to_string();
2073                if let Some(venv_dir) = env.prefix {
2074                    // Set venvPath and venv at the root level
2075                    // This matches the format of a pyrightconfig.json file
2076                    if let Some(parent) = venv_dir.parent() {
2077                        // Use relative path if the venv is inside the workspace
2078                        let venv_path = if parent == adapter.worktree_root_path() {
2079                            ".".to_string()
2080                        } else {
2081                            parent.to_string_lossy().into_owned()
2082                        };
2083                        object.insert("venvPath".to_string(), Value::String(venv_path));
2084                    }
2085
2086                    if let Some(venv_name) = venv_dir.file_name() {
2087                        object.insert(
2088                            "venv".to_owned(),
2089                            Value::String(venv_name.to_string_lossy().into_owned()),
2090                        );
2091                    }
2092                }
2093
2094                // Set both pythonPath and defaultInterpreterPath for compatibility
2095                if let Some(python) = object
2096                    .entry("python")
2097                    .or_insert(Value::Object(serde_json::Map::default()))
2098                    .as_object_mut()
2099                {
2100                    python.insert(
2101                        "pythonPath".to_owned(),
2102                        Value::String(interpreter_path.clone()),
2103                    );
2104                    python.insert(
2105                        "defaultInterpreterPath".to_owned(),
2106                        Value::String(interpreter_path),
2107                    );
2108                }
2109                // Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
2110                maybe!({
2111                    let analysis = object
2112                        .entry("basedpyright.analysis")
2113                        .or_insert(Value::Object(serde_json::Map::default()));
2114                    if let serde_json::map::Entry::Vacant(v) =
2115                        analysis.as_object_mut()?.entry("typeCheckingMode")
2116                    {
2117                        v.insert(Value::String("standard".to_owned()));
2118                    }
2119                    Some(())
2120                });
2121                // Disable basedpyright's organizeImports so ruff handles it instead
2122                if let serde_json::map::Entry::Vacant(v) =
2123                    object.entry("basedpyright.disableOrganizeImports")
2124                {
2125                    v.insert(Value::Bool(true));
2126                }
2127            }
2128
2129            user_settings
2130        }))
2131    }
2132}
2133
2134impl LspInstaller for BasedPyrightLspAdapter {
2135    type BinaryVersion = Version;
2136
2137    async fn fetch_latest_server_version(
2138        &self,
2139        _: &dyn LspAdapterDelegate,
2140        _: bool,
2141        _: &mut AsyncApp,
2142    ) -> Result<Self::BinaryVersion> {
2143        self.node
2144            .npm_package_latest_version(Self::SERVER_NAME.as_ref())
2145            .await
2146    }
2147
2148    async fn check_if_user_installed(
2149        &self,
2150        delegate: &dyn LspAdapterDelegate,
2151        _: Option<Toolchain>,
2152        _: &AsyncApp,
2153    ) -> Option<LanguageServerBinary> {
2154        if let Some(path) = delegate.which(Self::BINARY_NAME.as_ref()).await {
2155            let env = delegate.shell_env().await;
2156            Some(LanguageServerBinary {
2157                path,
2158                env: Some(env),
2159                arguments: vec!["--stdio".into()],
2160            })
2161        } else {
2162            // TODO shouldn't this be self.node.binary_path()?
2163            let node = delegate.which("node".as_ref()).await?;
2164            let (node_modules_path, _) = delegate
2165                .npm_package_installed_version(Self::SERVER_NAME.as_ref())
2166                .await
2167                .log_err()??;
2168
2169            let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
2170
2171            let env = delegate.shell_env().await;
2172            Some(LanguageServerBinary {
2173                path: node,
2174                env: Some(env),
2175                arguments: vec![path.into(), "--stdio".into()],
2176            })
2177        }
2178    }
2179
2180    async fn fetch_server_binary(
2181        &self,
2182        latest_version: Self::BinaryVersion,
2183        container_dir: PathBuf,
2184        delegate: &dyn LspAdapterDelegate,
2185    ) -> Result<LanguageServerBinary> {
2186        let server_path = container_dir.join(Self::SERVER_PATH);
2187        let latest_version = latest_version.to_string();
2188
2189        self.node
2190            .npm_install_packages(
2191                &container_dir,
2192                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
2193            )
2194            .await?;
2195
2196        let env = delegate.shell_env().await;
2197        Ok(LanguageServerBinary {
2198            path: self.node.binary_path().await?,
2199            env: Some(env),
2200            arguments: vec![server_path.into(), "--stdio".into()],
2201        })
2202    }
2203
2204    async fn check_if_version_installed(
2205        &self,
2206        version: &Self::BinaryVersion,
2207        container_dir: &PathBuf,
2208        delegate: &dyn LspAdapterDelegate,
2209    ) -> Option<LanguageServerBinary> {
2210        let server_path = container_dir.join(Self::SERVER_PATH);
2211
2212        let should_install_language_server = self
2213            .node
2214            .should_install_npm_package(
2215                Self::SERVER_NAME.as_ref(),
2216                &server_path,
2217                container_dir,
2218                VersionStrategy::Latest(version),
2219            )
2220            .await;
2221
2222        if should_install_language_server {
2223            None
2224        } else {
2225            let env = delegate.shell_env().await;
2226            Some(LanguageServerBinary {
2227                path: self.node.binary_path().await.ok()?,
2228                env: Some(env),
2229                arguments: vec![server_path.into(), "--stdio".into()],
2230            })
2231        }
2232    }
2233
2234    async fn cached_server_binary(
2235        &self,
2236        container_dir: PathBuf,
2237        delegate: &dyn LspAdapterDelegate,
2238    ) -> Option<LanguageServerBinary> {
2239        let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
2240        binary.env = Some(delegate.shell_env().await);
2241        Some(binary)
2242    }
2243}
2244
2245pub(crate) struct RuffLspAdapter {
2246    fs: Arc<dyn Fs>,
2247}
2248
2249impl RuffLspAdapter {
2250    fn convert_ruff_schema(raw_schema: &serde_json::Value) -> serde_json::Value {
2251        let Some(schema_object) = raw_schema.as_object() else {
2252            return raw_schema.clone();
2253        };
2254
2255        let mut root_properties = serde_json::Map::new();
2256
2257        for (key, value) in schema_object {
2258            let parts: Vec<&str> = key.split('.').collect();
2259
2260            if parts.is_empty() {
2261                continue;
2262            }
2263
2264            let mut current = &mut root_properties;
2265
2266            for (i, part) in parts.iter().enumerate() {
2267                let is_last = i == parts.len() - 1;
2268
2269                if is_last {
2270                    let mut schema_entry = serde_json::Map::new();
2271
2272                    if let Some(doc) = value.get("doc").and_then(|d| d.as_str()) {
2273                        schema_entry.insert(
2274                            "markdownDescription".to_string(),
2275                            serde_json::Value::String(doc.to_string()),
2276                        );
2277                    }
2278
2279                    if let Some(default_val) = value.get("default") {
2280                        schema_entry.insert("default".to_string(), default_val.clone());
2281                    }
2282
2283                    if let Some(value_type) = value.get("value_type").and_then(|v| v.as_str()) {
2284                        if value_type.contains('|') {
2285                            let enum_values: Vec<serde_json::Value> = value_type
2286                                .split('|')
2287                                .map(|s| s.trim().trim_matches('"'))
2288                                .filter(|s| !s.is_empty())
2289                                .map(|s| serde_json::Value::String(s.to_string()))
2290                                .collect();
2291
2292                            if !enum_values.is_empty() {
2293                                schema_entry
2294                                    .insert("type".to_string(), serde_json::json!("string"));
2295                                schema_entry.insert(
2296                                    "enum".to_string(),
2297                                    serde_json::Value::Array(enum_values),
2298                                );
2299                            }
2300                        } else if value_type.starts_with("list[") {
2301                            schema_entry.insert("type".to_string(), serde_json::json!("array"));
2302                            if let Some(item_type) = value_type
2303                                .strip_prefix("list[")
2304                                .and_then(|s| s.strip_suffix(']'))
2305                            {
2306                                let json_type = match item_type {
2307                                    "str" => "string",
2308                                    "int" => "integer",
2309                                    "bool" => "boolean",
2310                                    _ => "string",
2311                                };
2312                                schema_entry.insert(
2313                                    "items".to_string(),
2314                                    serde_json::json!({"type": json_type}),
2315                                );
2316                            }
2317                        } else if value_type.starts_with("dict[") {
2318                            schema_entry.insert("type".to_string(), serde_json::json!("object"));
2319                        } else {
2320                            let json_type = match value_type {
2321                                "bool" => "boolean",
2322                                "int" | "usize" => "integer",
2323                                "str" => "string",
2324                                _ => "string",
2325                            };
2326                            schema_entry.insert(
2327                                "type".to_string(),
2328                                serde_json::Value::String(json_type.to_string()),
2329                            );
2330                        }
2331                    }
2332
2333                    current.insert(part.to_string(), serde_json::Value::Object(schema_entry));
2334                } else {
2335                    let next_current = current
2336                        .entry(part.to_string())
2337                        .or_insert_with(|| {
2338                            serde_json::json!({
2339                                "type": "object",
2340                                "properties": {}
2341                            })
2342                        })
2343                        .as_object_mut()
2344                        .expect("should be an object")
2345                        .entry("properties")
2346                        .or_insert_with(|| serde_json::json!({}))
2347                        .as_object_mut()
2348                        .expect("properties should be an object");
2349
2350                    current = next_current;
2351                }
2352            }
2353        }
2354
2355        serde_json::json!({
2356            "type": "object",
2357            "properties": root_properties
2358        })
2359    }
2360}
2361
2362#[cfg(target_os = "macos")]
2363impl RuffLspAdapter {
2364    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2365    const ARCH_SERVER_NAME: &str = "apple-darwin";
2366}
2367
2368#[cfg(target_os = "linux")]
2369impl RuffLspAdapter {
2370    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2371    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
2372}
2373
2374#[cfg(target_os = "freebsd")]
2375impl RuffLspAdapter {
2376    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2377    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
2378}
2379
2380#[cfg(target_os = "windows")]
2381impl RuffLspAdapter {
2382    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
2383    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
2384}
2385
2386impl RuffLspAdapter {
2387    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ruff");
2388
2389    pub fn new(fs: Arc<dyn Fs>) -> RuffLspAdapter {
2390        RuffLspAdapter { fs }
2391    }
2392
2393    fn build_asset_name() -> Result<(String, String)> {
2394        let arch = match consts::ARCH {
2395            "x86" => "i686",
2396            _ => consts::ARCH,
2397        };
2398        let os = Self::ARCH_SERVER_NAME;
2399        let suffix = match consts::OS {
2400            "windows" => "zip",
2401            _ => "tar.gz",
2402        };
2403        let asset_name = format!("ruff-{arch}-{os}.{suffix}");
2404        let asset_stem = format!("ruff-{arch}-{os}");
2405        Ok((asset_stem, asset_name))
2406    }
2407}
2408
2409#[async_trait(?Send)]
2410impl LspAdapter for RuffLspAdapter {
2411    fn name(&self) -> LanguageServerName {
2412        Self::SERVER_NAME
2413    }
2414
2415    async fn initialization_options_schema(
2416        self: Arc<Self>,
2417        delegate: &Arc<dyn LspAdapterDelegate>,
2418        cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
2419        cx: &mut AsyncApp,
2420    ) -> Option<serde_json::Value> {
2421        let binary = self
2422            .get_language_server_command(
2423                delegate.clone(),
2424                None,
2425                LanguageServerBinaryOptions {
2426                    allow_path_lookup: true,
2427                    allow_binary_download: false,
2428                    pre_release: false,
2429                },
2430                cached_binary,
2431                cx.clone(),
2432            )
2433            .await
2434            .0
2435            .ok()?;
2436
2437        let mut command = util::command::new_command(&binary.path);
2438        command
2439            .args(&["config", "--output-format", "json"])
2440            .stdout(Stdio::piped())
2441            .stderr(Stdio::piped());
2442        let cmd = command
2443            .spawn()
2444            .map_err(|e| log::debug!("failed to spawn command {command:?}: {e}"))
2445            .ok()?;
2446        let output = cmd
2447            .output()
2448            .await
2449            .map_err(|e| log::debug!("failed to execute command {command:?}: {e}"))
2450            .ok()?;
2451        if !output.status.success() {
2452            return None;
2453        }
2454
2455        let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice())
2456            .map_err(|e| log::debug!("failed to parse ruff's JSON schema output: {e}"))
2457            .ok()?;
2458
2459        let converted_schema = Self::convert_ruff_schema(&raw_schema);
2460        Some(converted_schema)
2461    }
2462}
2463
2464impl LspInstaller for RuffLspAdapter {
2465    type BinaryVersion = GitHubLspBinaryVersion;
2466    async fn check_if_user_installed(
2467        &self,
2468        delegate: &dyn LspAdapterDelegate,
2469        toolchain: Option<Toolchain>,
2470        _: &AsyncApp,
2471    ) -> Option<LanguageServerBinary> {
2472        let ruff_in_venv = if let Some(toolchain) = toolchain
2473            && toolchain.language_name.as_ref() == "Python"
2474        {
2475            Path::new(toolchain.path.as_str())
2476                .parent()
2477                .map(|path| path.join("ruff"))
2478        } else {
2479            None
2480        };
2481
2482        for path in ruff_in_venv.into_iter().chain(["ruff".into()]) {
2483            if let Some(ruff_bin) = delegate.which(path.as_os_str()).await {
2484                let env = delegate.shell_env().await;
2485                return Some(LanguageServerBinary {
2486                    path: ruff_bin,
2487                    env: Some(env),
2488                    arguments: vec!["server".into()],
2489                });
2490            }
2491        }
2492
2493        None
2494    }
2495
2496    async fn fetch_latest_server_version(
2497        &self,
2498        delegate: &dyn LspAdapterDelegate,
2499        _: bool,
2500        _: &mut AsyncApp,
2501    ) -> Result<GitHubLspBinaryVersion> {
2502        let release =
2503            latest_github_release("astral-sh/ruff", true, false, delegate.http_client()).await?;
2504        let (_, asset_name) = Self::build_asset_name()?;
2505        let asset = release
2506            .assets
2507            .into_iter()
2508            .find(|asset| asset.name == asset_name)
2509            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
2510        Ok(GitHubLspBinaryVersion {
2511            name: release.tag_name,
2512            url: asset.browser_download_url,
2513            digest: asset.digest,
2514        })
2515    }
2516
2517    async fn fetch_server_binary(
2518        &self,
2519        latest_version: GitHubLspBinaryVersion,
2520        container_dir: PathBuf,
2521        delegate: &dyn LspAdapterDelegate,
2522    ) -> Result<LanguageServerBinary> {
2523        let GitHubLspBinaryVersion {
2524            name,
2525            url,
2526            digest: expected_digest,
2527        } = latest_version;
2528        let destination_path = container_dir.join(format!("ruff-{name}"));
2529        let server_path = match Self::GITHUB_ASSET_KIND {
2530            AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path
2531                .join(Self::build_asset_name()?.0)
2532                .join("ruff"),
2533            AssetKind::Zip => destination_path.clone().join("ruff.exe"),
2534        };
2535
2536        let binary = LanguageServerBinary {
2537            path: server_path.clone(),
2538            env: None,
2539            arguments: vec!["server".into()],
2540        };
2541
2542        let metadata_path = destination_path.with_extension("metadata");
2543        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
2544            .await
2545            .ok();
2546        if let Some(metadata) = metadata {
2547            let validity_check = async || {
2548                delegate
2549                    .try_exec(LanguageServerBinary {
2550                        path: server_path.clone(),
2551                        arguments: vec!["--version".into()],
2552                        env: None,
2553                    })
2554                    .await
2555                    .inspect_err(|err| {
2556                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
2557                    })
2558            };
2559            if let (Some(actual_digest), Some(expected_digest)) =
2560                (&metadata.digest, &expected_digest)
2561            {
2562                if actual_digest == expected_digest {
2563                    if validity_check().await.is_ok() {
2564                        return Ok(binary);
2565                    }
2566                } else {
2567                    log::info!(
2568                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
2569                    );
2570                }
2571            } else if validity_check().await.is_ok() {
2572                return Ok(binary);
2573            }
2574        }
2575
2576        download_server_binary(
2577            &*delegate.http_client(),
2578            &url,
2579            expected_digest.as_deref(),
2580            &destination_path,
2581            Self::GITHUB_ASSET_KIND,
2582        )
2583        .await?;
2584        make_file_executable(&server_path).await?;
2585        remove_matching(&container_dir, |path| path != destination_path).await;
2586        GithubBinaryMetadata::write_to_file(
2587            &GithubBinaryMetadata {
2588                metadata_version: 1,
2589                digest: expected_digest,
2590            },
2591            &metadata_path,
2592        )
2593        .await?;
2594
2595        Ok(LanguageServerBinary {
2596            path: server_path,
2597            env: None,
2598            arguments: vec!["server".into()],
2599        })
2600    }
2601
2602    async fn cached_server_binary(
2603        &self,
2604        container_dir: PathBuf,
2605        _: &dyn LspAdapterDelegate,
2606    ) -> Option<LanguageServerBinary> {
2607        maybe!(async {
2608            let mut last = None;
2609            let mut entries = self.fs.read_dir(&container_dir).await?;
2610            while let Some(entry) = entries.next().await {
2611                let path = entry?;
2612                if path.extension().is_some_and(|ext| ext == "metadata") {
2613                    continue;
2614                }
2615                last = Some(path);
2616            }
2617
2618            let path = last.context("no cached binary")?;
2619            let path = match Self::GITHUB_ASSET_KIND {
2620                AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => {
2621                    path.join(Self::build_asset_name()?.0).join("ruff")
2622                }
2623                AssetKind::Zip => path.join("ruff.exe"),
2624            };
2625
2626            anyhow::Ok(LanguageServerBinary {
2627                path,
2628                env: None,
2629                arguments: vec!["server".into()],
2630            })
2631        })
2632        .await
2633        .log_err()
2634    }
2635}
2636
2637#[cfg(test)]
2638mod tests {
2639    use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
2640    use language::{AutoindentMode, Buffer};
2641    use settings::SettingsStore;
2642    use std::num::NonZeroU32;
2643
2644    use crate::python::python_module_name_from_relative_path;
2645
2646    #[gpui::test]
2647    async fn test_conda_activation_script_injection(cx: &mut TestAppContext) {
2648        use language::{LanguageName, Toolchain, ToolchainLister};
2649        use settings::{CondaManager, VenvSettings};
2650        use task::ShellKind;
2651
2652        use crate::python::PythonToolchainProvider;
2653
2654        cx.executor().allow_parking();
2655
2656        cx.update(|cx| {
2657            let test_settings = SettingsStore::test(cx);
2658            cx.set_global(test_settings);
2659            cx.update_global::<SettingsStore, _>(|store, cx| {
2660                store.update_user_settings(cx, |s| {
2661                    s.terminal
2662                        .get_or_insert_with(Default::default)
2663                        .project
2664                        .detect_venv = Some(VenvSettings::On {
2665                        activate_script: None,
2666                        venv_name: None,
2667                        directories: None,
2668                        conda_manager: Some(CondaManager::Conda),
2669                    });
2670                });
2671            });
2672        });
2673
2674        let fs = project::FakeFs::new(cx.executor());
2675        let provider = PythonToolchainProvider::new(fs);
2676        let malicious_name = "foo; rm -rf /";
2677
2678        let manager_executable = std::env::current_exe().unwrap();
2679
2680        let data = serde_json::json!({
2681            "name": malicious_name,
2682            "kind": "Conda",
2683            "executable": "/tmp/conda/bin/python",
2684            "version": serde_json::Value::Null,
2685            "prefix": serde_json::Value::Null,
2686            "arch": serde_json::Value::Null,
2687            "displayName": serde_json::Value::Null,
2688            "project": serde_json::Value::Null,
2689            "symlinks": serde_json::Value::Null,
2690            "manager": {
2691                "executable": manager_executable,
2692                "version": serde_json::Value::Null,
2693                "tool": "Conda",
2694            },
2695        });
2696
2697        let toolchain = Toolchain {
2698            name: "test".into(),
2699            path: "/tmp/conda".into(),
2700            language_name: LanguageName::new_static("Python"),
2701            as_json: data,
2702        };
2703
2704        let script = cx
2705            .update(|cx| provider.activation_script(&toolchain, ShellKind::Posix, cx))
2706            .await;
2707
2708        assert!(
2709            script
2710                .iter()
2711                .any(|s| s.contains("conda activate 'foo; rm -rf /'")),
2712            "Script should contain quoted malicious name, actual: {:?}",
2713            script
2714        );
2715    }
2716
2717    #[gpui::test]
2718    async fn test_python_autoindent(cx: &mut TestAppContext) {
2719        cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
2720        let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
2721        cx.update(|cx| {
2722            let test_settings = SettingsStore::test(cx);
2723            cx.set_global(test_settings);
2724            cx.update_global::<SettingsStore, _>(|store, cx| {
2725                store.update_user_settings(cx, |s| {
2726                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
2727                });
2728            });
2729        });
2730
2731        cx.new(|cx| {
2732            let mut buffer = Buffer::local("", cx).with_language(language, cx);
2733            let append = |buffer: &mut Buffer, text: &str, cx: &mut Context<Buffer>| {
2734                let ix = buffer.len();
2735                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
2736            };
2737
2738            // indent after "def():"
2739            append(&mut buffer, "def a():\n", cx);
2740            assert_eq!(buffer.text(), "def a():\n  ");
2741
2742            // preserve indent after blank line
2743            append(&mut buffer, "\n  ", cx);
2744            assert_eq!(buffer.text(), "def a():\n  \n  ");
2745
2746            // indent after "if"
2747            append(&mut buffer, "if a:\n  ", cx);
2748            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
2749
2750            // preserve indent after statement
2751            append(&mut buffer, "b()\n", cx);
2752            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
2753
2754            // preserve indent after statement
2755            append(&mut buffer, "else", cx);
2756            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
2757
2758            // dedent "else""
2759            append(&mut buffer, ":", cx);
2760            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
2761
2762            // indent lines after else
2763            append(&mut buffer, "\n", cx);
2764            assert_eq!(
2765                buffer.text(),
2766                "def a():\n  \n  if a:\n    b()\n  else:\n    "
2767            );
2768
2769            // indent after an open paren. the closing paren is not indented
2770            // because there is another token before it on the same line.
2771            append(&mut buffer, "foo(\n1)", cx);
2772            assert_eq!(
2773                buffer.text(),
2774                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
2775            );
2776
2777            // dedent the closing paren if it is shifted to the beginning of the line
2778            let argument_ix = buffer.text().find('1').unwrap();
2779            buffer.edit(
2780                [(argument_ix..argument_ix + 1, "")],
2781                Some(AutoindentMode::EachLine),
2782                cx,
2783            );
2784            assert_eq!(
2785                buffer.text(),
2786                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
2787            );
2788
2789            // preserve indent after the close paren
2790            append(&mut buffer, "\n", cx);
2791            assert_eq!(
2792                buffer.text(),
2793                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
2794            );
2795
2796            // manually outdent the last line
2797            let end_whitespace_ix = buffer.len() - 4;
2798            buffer.edit(
2799                [(end_whitespace_ix..buffer.len(), "")],
2800                Some(AutoindentMode::EachLine),
2801                cx,
2802            );
2803            assert_eq!(
2804                buffer.text(),
2805                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
2806            );
2807
2808            // preserve the newly reduced indentation on the next newline
2809            append(&mut buffer, "\n", cx);
2810            assert_eq!(
2811                buffer.text(),
2812                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
2813            );
2814
2815            // reset to a for loop statement
2816            let statement = "for i in range(10):\n  print(i)\n";
2817            buffer.edit([(0..buffer.len(), statement)], None, cx);
2818
2819            // insert single line comment after each line
2820            let eol_ixs = statement
2821                .char_indices()
2822                .filter_map(|(ix, c)| if c == '\n' { Some(ix) } else { None })
2823                .collect::<Vec<usize>>();
2824            let editions = eol_ixs
2825                .iter()
2826                .enumerate()
2827                .map(|(i, &eol_ix)| (eol_ix..eol_ix, format!(" # comment {}", i + 1)))
2828                .collect::<Vec<(std::ops::Range<usize>, String)>>();
2829            buffer.edit(editions, Some(AutoindentMode::EachLine), cx);
2830            assert_eq!(
2831                buffer.text(),
2832                "for i in range(10): # comment 1\n  print(i) # comment 2\n"
2833            );
2834
2835            // reset to a simple if statement
2836            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
2837
2838            // dedent "else" on the line after a closing paren
2839            append(&mut buffer, "\n  else:\n", cx);
2840            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
2841
2842            buffer
2843        });
2844    }
2845
2846    #[test]
2847    fn test_python_module_name_from_relative_path() {
2848        assert_eq!(
2849            python_module_name_from_relative_path("foo/bar.py"),
2850            Some("foo.bar".to_string())
2851        );
2852        assert_eq!(
2853            python_module_name_from_relative_path("foo/bar"),
2854            Some("foo.bar".to_string())
2855        );
2856        if cfg!(windows) {
2857            assert_eq!(
2858                python_module_name_from_relative_path("foo\\bar.py"),
2859                Some("foo.bar".to_string())
2860            );
2861            assert_eq!(
2862                python_module_name_from_relative_path("foo\\bar"),
2863                Some("foo.bar".to_string())
2864            );
2865        } else {
2866            assert_eq!(
2867                python_module_name_from_relative_path("foo\\bar.py"),
2868                Some("foo\\bar".to_string())
2869            );
2870            assert_eq!(
2871                python_module_name_from_relative_path("foo\\bar"),
2872                Some("foo\\bar".to_string())
2873            );
2874        }
2875    }
2876
2877    #[test]
2878    fn test_convert_ruff_schema() {
2879        use super::RuffLspAdapter;
2880
2881        let raw_schema = serde_json::json!({
2882            "line-length": {
2883                "doc": "The line length to use when enforcing long-lines violations",
2884                "default": "88",
2885                "value_type": "int",
2886                "scope": null,
2887                "example": "line-length = 120",
2888                "deprecated": null
2889            },
2890            "lint.select": {
2891                "doc": "A list of rule codes or prefixes to enable",
2892                "default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
2893                "value_type": "list[RuleSelector]",
2894                "scope": null,
2895                "example": "select = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
2896                "deprecated": null
2897            },
2898            "lint.isort.case-sensitive": {
2899                "doc": "Sort imports taking into account case sensitivity.",
2900                "default": "false",
2901                "value_type": "bool",
2902                "scope": null,
2903                "example": "case-sensitive = true",
2904                "deprecated": null
2905            },
2906            "format.quote-style": {
2907                "doc": "Configures the preferred quote character for strings.",
2908                "default": "\"double\"",
2909                "value_type": "\"double\" | \"single\" | \"preserve\"",
2910                "scope": null,
2911                "example": "quote-style = \"single\"",
2912                "deprecated": null
2913            }
2914        });
2915
2916        let converted = RuffLspAdapter::convert_ruff_schema(&raw_schema);
2917
2918        assert!(converted.is_object());
2919        assert_eq!(
2920            converted.get("type").and_then(|v| v.as_str()),
2921            Some("object")
2922        );
2923
2924        let properties = converted
2925            .get("properties")
2926            .expect("should have properties")
2927            .as_object()
2928            .expect("properties should be an object");
2929
2930        assert!(properties.contains_key("line-length"));
2931        assert!(properties.contains_key("lint"));
2932        assert!(properties.contains_key("format"));
2933
2934        let line_length = properties
2935            .get("line-length")
2936            .expect("should have line-length")
2937            .as_object()
2938            .expect("line-length should be an object");
2939
2940        assert_eq!(
2941            line_length.get("type").and_then(|v| v.as_str()),
2942            Some("integer")
2943        );
2944        assert_eq!(
2945            line_length.get("default").and_then(|v| v.as_str()),
2946            Some("88")
2947        );
2948
2949        let lint = properties
2950            .get("lint")
2951            .expect("should have lint")
2952            .as_object()
2953            .expect("lint should be an object");
2954
2955        let lint_props = lint
2956            .get("properties")
2957            .expect("lint should have properties")
2958            .as_object()
2959            .expect("lint properties should be an object");
2960
2961        assert!(lint_props.contains_key("select"));
2962        assert!(lint_props.contains_key("isort"));
2963
2964        let select = lint_props.get("select").expect("should have select");
2965        assert_eq!(select.get("type").and_then(|v| v.as_str()), Some("array"));
2966
2967        let isort = lint_props
2968            .get("isort")
2969            .expect("should have isort")
2970            .as_object()
2971            .expect("isort should be an object");
2972
2973        let isort_props = isort
2974            .get("properties")
2975            .expect("isort should have properties")
2976            .as_object()
2977            .expect("isort properties should be an object");
2978
2979        let case_sensitive = isort_props
2980            .get("case-sensitive")
2981            .expect("should have case-sensitive");
2982
2983        assert_eq!(
2984            case_sensitive.get("type").and_then(|v| v.as_str()),
2985            Some("boolean")
2986        );
2987        assert!(case_sensitive.get("markdownDescription").is_some());
2988
2989        let format = properties
2990            .get("format")
2991            .expect("should have format")
2992            .as_object()
2993            .expect("format should be an object");
2994
2995        let format_props = format
2996            .get("properties")
2997            .expect("format should have properties")
2998            .as_object()
2999            .expect("format properties should be an object");
3000
3001        let quote_style = format_props
3002            .get("quote-style")
3003            .expect("should have quote-style");
3004
3005        assert_eq!(
3006            quote_style.get("type").and_then(|v| v.as_str()),
3007            Some("string")
3008        );
3009
3010        let enum_values = quote_style
3011            .get("enum")
3012            .expect("should have enum")
3013            .as_array()
3014            .expect("enum should be an array");
3015
3016        assert_eq!(enum_values.len(), 3);
3017        assert!(enum_values.contains(&serde_json::json!("double")));
3018        assert!(enum_values.contains(&serde_json::json!("single")));
3019        assert!(enum_values.contains(&serde_json::json!("preserve")));
3020    }
3021}