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