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