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