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