python.rs

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