python.rs

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