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    }
 995}
 996
 997pub(crate) struct PythonToolchainProvider;
 998
 999static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
1000    // Prioritize non-Conda environments.
1001    PythonEnvironmentKind::Poetry,
1002    PythonEnvironmentKind::Pipenv,
1003    PythonEnvironmentKind::VirtualEnvWrapper,
1004    PythonEnvironmentKind::Venv,
1005    PythonEnvironmentKind::VirtualEnv,
1006    PythonEnvironmentKind::PyenvVirtualEnv,
1007    PythonEnvironmentKind::Pixi,
1008    PythonEnvironmentKind::Conda,
1009    PythonEnvironmentKind::Pyenv,
1010    PythonEnvironmentKind::GlobalPaths,
1011    PythonEnvironmentKind::Homebrew,
1012];
1013
1014fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
1015    if let Some(kind) = kind {
1016        ENV_PRIORITY_LIST
1017            .iter()
1018            .position(|blessed_env| blessed_env == &kind)
1019            .unwrap_or(ENV_PRIORITY_LIST.len())
1020    } else {
1021        // Unknown toolchains are less useful than non-blessed ones.
1022        ENV_PRIORITY_LIST.len() + 1
1023    }
1024}
1025
1026/// Return the name of environment declared in <worktree-root/.venv.
1027///
1028/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
1029async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
1030    let file = async_fs::File::open(worktree_root.join(".venv"))
1031        .await
1032        .ok()?;
1033    let mut venv_name = String::new();
1034    smol::io::BufReader::new(file)
1035        .read_line(&mut venv_name)
1036        .await
1037        .ok()?;
1038    Some(venv_name.trim().to_string())
1039}
1040
1041fn get_venv_parent_dir(env: &PythonEnvironment) -> Option<PathBuf> {
1042    // If global, we aren't a virtual environment
1043    if let Some(kind) = env.kind
1044        && is_python_env_global(&kind)
1045    {
1046        return None;
1047    }
1048
1049    // Check to be sure we are a virtual environment using pet's most generic
1050    // virtual environment type, VirtualEnv
1051    let venv = env
1052        .executable
1053        .as_ref()
1054        .and_then(|p| p.parent())
1055        .and_then(|p| p.parent())
1056        .filter(|p| is_virtualenv_dir(p))?;
1057
1058    venv.parent().map(|parent| parent.to_path_buf())
1059}
1060
1061fn wr_distance(wr: &PathBuf, venv: Option<&PathBuf>) -> usize {
1062    if let Some(venv) = venv
1063        && let Ok(p) = venv.strip_prefix(wr)
1064    {
1065        p.components().count()
1066    } else {
1067        usize::MAX
1068    }
1069}
1070
1071#[async_trait]
1072impl ToolchainLister for PythonToolchainProvider {
1073    async fn list(
1074        &self,
1075        worktree_root: PathBuf,
1076        subroot_relative_path: Arc<RelPath>,
1077        project_env: Option<HashMap<String, String>>,
1078        fs: &dyn Fs,
1079    ) -> ToolchainList {
1080        let env = project_env.unwrap_or_default();
1081        let environment = EnvironmentApi::from_env(&env);
1082        let locators = pet::locators::create_locators(
1083            Arc::new(pet_conda::Conda::from(&environment)),
1084            Arc::new(pet_poetry::Poetry::from(&environment)),
1085            &environment,
1086        );
1087        let mut config = Configuration::default();
1088
1089        // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
1090        // worktree root as the workspace directory.
1091        config.workspace_directories = Some(
1092            subroot_relative_path
1093                .ancestors()
1094                .map(|ancestor| worktree_root.join(ancestor.as_std_path()))
1095                .collect(),
1096        );
1097        for locator in locators.iter() {
1098            locator.configure(&config);
1099        }
1100
1101        let reporter = pet_reporter::collect::create_reporter();
1102        pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
1103
1104        let mut toolchains = reporter
1105            .environments
1106            .lock()
1107            .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
1108
1109        let wr = worktree_root;
1110        let wr_venv = get_worktree_venv_declaration(&wr).await;
1111        // Sort detected environments by:
1112        //     environment name matching activation file (<workdir>/.venv)
1113        //     environment project dir matching worktree_root
1114        //     general env priority
1115        //     environment path matching the CONDA_PREFIX env var
1116        //     executable path
1117        toolchains.sort_by(|lhs, rhs| {
1118            // Compare venv names against worktree .venv file
1119            let venv_ordering =
1120                wr_venv
1121                    .as_ref()
1122                    .map_or(Ordering::Equal, |venv| match (&lhs.name, &rhs.name) {
1123                        (Some(l), Some(r)) => (r == venv).cmp(&(l == venv)),
1124                        (Some(l), None) if l == venv => Ordering::Less,
1125                        (None, Some(r)) if r == venv => Ordering::Greater,
1126                        _ => Ordering::Equal,
1127                    });
1128
1129            // Compare project paths against worktree root
1130            let proj_ordering = || {
1131                let lhs_project = lhs.project.clone().or_else(|| get_venv_parent_dir(lhs));
1132                let rhs_project = rhs.project.clone().or_else(|| get_venv_parent_dir(rhs));
1133                wr_distance(&wr, lhs_project.as_ref()).cmp(&wr_distance(&wr, rhs_project.as_ref()))
1134            };
1135
1136            // Compare environment priorities
1137            let priority_ordering = || env_priority(lhs.kind).cmp(&env_priority(rhs.kind));
1138
1139            // Compare conda prefixes
1140            let conda_ordering = || {
1141                if lhs.kind == Some(PythonEnvironmentKind::Conda) {
1142                    environment
1143                        .get_env_var("CONDA_PREFIX".to_string())
1144                        .map(|conda_prefix| {
1145                            let is_match = |exe: &Option<PathBuf>| {
1146                                exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
1147                            };
1148                            match (is_match(&lhs.executable), is_match(&rhs.executable)) {
1149                                (true, false) => Ordering::Less,
1150                                (false, true) => Ordering::Greater,
1151                                _ => Ordering::Equal,
1152                            }
1153                        })
1154                        .unwrap_or(Ordering::Equal)
1155                } else {
1156                    Ordering::Equal
1157                }
1158            };
1159
1160            // Compare Python executables
1161            let exe_ordering = || lhs.executable.cmp(&rhs.executable);
1162
1163            venv_ordering
1164                .then_with(proj_ordering)
1165                .then_with(priority_ordering)
1166                .then_with(conda_ordering)
1167                .then_with(exe_ordering)
1168        });
1169
1170        let mut out_toolchains = Vec::new();
1171        for toolchain in toolchains {
1172            let Some(toolchain) = venv_to_toolchain(toolchain, fs).await else {
1173                continue;
1174            };
1175            out_toolchains.push(toolchain);
1176        }
1177        out_toolchains.dedup();
1178        ToolchainList {
1179            toolchains: out_toolchains,
1180            default: None,
1181            groups: Default::default(),
1182        }
1183    }
1184    fn meta(&self) -> ToolchainMetadata {
1185        ToolchainMetadata {
1186            term: SharedString::new_static("Virtual Environment"),
1187            new_toolchain_placeholder: SharedString::new_static(
1188                "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
1189            ),
1190            manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
1191        }
1192    }
1193
1194    async fn resolve(
1195        &self,
1196        path: PathBuf,
1197        env: Option<HashMap<String, String>>,
1198        fs: &dyn Fs,
1199    ) -> anyhow::Result<Toolchain> {
1200        let env = env.unwrap_or_default();
1201        let environment = EnvironmentApi::from_env(&env);
1202        let locators = pet::locators::create_locators(
1203            Arc::new(pet_conda::Conda::from(&environment)),
1204            Arc::new(pet_poetry::Poetry::from(&environment)),
1205            &environment,
1206        );
1207        let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
1208            .context("Could not find a virtual environment in provided path")?;
1209        let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
1210        venv_to_toolchain(venv, fs)
1211            .await
1212            .context("Could not convert a venv into a toolchain")
1213    }
1214
1215    fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind, cx: &App) -> Vec<String> {
1216        let Ok(toolchain) =
1217            serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
1218        else {
1219            return vec![];
1220        };
1221
1222        log::debug!("(Python) Composing activation script for toolchain {toolchain:?}");
1223
1224        let mut activation_script = vec![];
1225
1226        match toolchain.environment.kind {
1227            Some(PythonEnvironmentKind::Conda) => {
1228                let settings = TerminalSettings::get_global(cx);
1229                let conda_manager = settings
1230                    .detect_venv
1231                    .as_option()
1232                    .map(|venv| venv.conda_manager)
1233                    .unwrap_or(settings::CondaManager::Auto);
1234
1235                let manager = match conda_manager {
1236                    settings::CondaManager::Conda => "conda",
1237                    settings::CondaManager::Mamba => "mamba",
1238                    settings::CondaManager::Micromamba => "micromamba",
1239                    settings::CondaManager::Auto => {
1240                        // When auto, prefer the detected manager or fall back to conda
1241                        toolchain
1242                            .environment
1243                            .manager
1244                            .as_ref()
1245                            .and_then(|m| m.executable.file_name())
1246                            .and_then(|name| name.to_str())
1247                            .filter(|name| matches!(*name, "conda" | "mamba" | "micromamba"))
1248                            .unwrap_or("conda")
1249                    }
1250                };
1251
1252                if let Some(name) = &toolchain.environment.name {
1253                    activation_script.push(format!("{manager} activate {name}"));
1254                } else {
1255                    activation_script.push(format!("{manager} activate base"));
1256                }
1257            }
1258            Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
1259                if let Some(activation_scripts) = &toolchain.activation_scripts {
1260                    if let Some(activate_script_path) = activation_scripts.get(&shell) {
1261                        let activate_keyword = shell.activate_keyword();
1262                        if let Some(quoted) =
1263                            shell.try_quote(&activate_script_path.to_string_lossy())
1264                        {
1265                            activation_script.push(format!("{activate_keyword} {quoted}"));
1266                        }
1267                    }
1268                }
1269            }
1270            Some(PythonEnvironmentKind::Pyenv) => {
1271                let Some(manager) = &toolchain.environment.manager else {
1272                    return vec![];
1273                };
1274                let version = toolchain.environment.version.as_deref().unwrap_or("system");
1275                let pyenv = &manager.executable;
1276                let pyenv = pyenv.display();
1277                activation_script.extend(match shell {
1278                    ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
1279                    ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
1280                    ShellKind::Nushell => Some(format!("^\"{pyenv}\" shell - nu {version}")),
1281                    ShellKind::PowerShell => None,
1282                    ShellKind::Csh => None,
1283                    ShellKind::Tcsh => None,
1284                    ShellKind::Cmd => None,
1285                    ShellKind::Rc => None,
1286                    ShellKind::Xonsh => None,
1287                    ShellKind::Elvish => None,
1288                })
1289            }
1290            _ => {}
1291        }
1292        activation_script
1293    }
1294}
1295
1296async fn venv_to_toolchain(venv: PythonEnvironment, fs: &dyn Fs) -> Option<Toolchain> {
1297    let mut name = String::from("Python");
1298    if let Some(ref version) = venv.version {
1299        _ = write!(name, " {version}");
1300    }
1301
1302    let name_and_kind = match (&venv.name, &venv.kind) {
1303        (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
1304        (Some(name), None) => Some(format!("({name})")),
1305        (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
1306        (None, None) => None,
1307    };
1308
1309    if let Some(nk) = name_and_kind {
1310        _ = write!(name, " {nk}");
1311    }
1312
1313    let mut activation_scripts = HashMap::default();
1314    match venv.kind {
1315        Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
1316            resolve_venv_activation_scripts(&venv, fs, &mut activation_scripts).await
1317        }
1318        _ => {}
1319    }
1320    let data = PythonToolchainData {
1321        environment: venv,
1322        activation_scripts: Some(activation_scripts),
1323    };
1324
1325    Some(Toolchain {
1326        name: name.into(),
1327        path: data
1328            .environment
1329            .executable
1330            .as_ref()?
1331            .to_str()?
1332            .to_owned()
1333            .into(),
1334        language_name: LanguageName::new("Python"),
1335        as_json: serde_json::to_value(data).ok()?,
1336    })
1337}
1338
1339async fn resolve_venv_activation_scripts(
1340    venv: &PythonEnvironment,
1341    fs: &dyn Fs,
1342    activation_scripts: &mut HashMap<ShellKind, PathBuf>,
1343) {
1344    log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}");
1345    if let Some(prefix) = &venv.prefix {
1346        for (shell_kind, script_name) in &[
1347            (ShellKind::Posix, "activate"),
1348            (ShellKind::Rc, "activate"),
1349            (ShellKind::Csh, "activate.csh"),
1350            (ShellKind::Tcsh, "activate.csh"),
1351            (ShellKind::Fish, "activate.fish"),
1352            (ShellKind::Nushell, "activate.nu"),
1353            (ShellKind::PowerShell, "activate.ps1"),
1354            (ShellKind::Cmd, "activate.bat"),
1355            (ShellKind::Xonsh, "activate.xsh"),
1356        ] {
1357            let path = prefix.join(BINARY_DIR).join(script_name);
1358
1359            log::debug!("Trying path: {}", path.display());
1360
1361            if fs.is_file(&path).await {
1362                activation_scripts.insert(*shell_kind, path);
1363            }
1364        }
1365    }
1366}
1367
1368pub struct EnvironmentApi<'a> {
1369    global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
1370    project_env: &'a HashMap<String, String>,
1371    pet_env: pet_core::os_environment::EnvironmentApi,
1372}
1373
1374impl<'a> EnvironmentApi<'a> {
1375    pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
1376        let paths = project_env
1377            .get("PATH")
1378            .map(|p| std::env::split_paths(p).collect())
1379            .unwrap_or_default();
1380
1381        EnvironmentApi {
1382            global_search_locations: Arc::new(Mutex::new(paths)),
1383            project_env,
1384            pet_env: pet_core::os_environment::EnvironmentApi::new(),
1385        }
1386    }
1387
1388    fn user_home(&self) -> Option<PathBuf> {
1389        self.project_env
1390            .get("HOME")
1391            .or_else(|| self.project_env.get("USERPROFILE"))
1392            .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
1393            .or_else(|| self.pet_env.get_user_home())
1394    }
1395}
1396
1397impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
1398    fn get_user_home(&self) -> Option<PathBuf> {
1399        self.user_home()
1400    }
1401
1402    fn get_root(&self) -> Option<PathBuf> {
1403        None
1404    }
1405
1406    fn get_env_var(&self, key: String) -> Option<String> {
1407        self.project_env
1408            .get(&key)
1409            .cloned()
1410            .or_else(|| self.pet_env.get_env_var(key))
1411    }
1412
1413    fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
1414        if self.global_search_locations.lock().is_empty() {
1415            let mut paths = std::env::split_paths(
1416                &self
1417                    .get_env_var("PATH".to_string())
1418                    .or_else(|| self.get_env_var("Path".to_string()))
1419                    .unwrap_or_default(),
1420            )
1421            .collect::<Vec<PathBuf>>();
1422
1423            log::trace!("Env PATH: {:?}", paths);
1424            for p in self.pet_env.get_know_global_search_locations() {
1425                if !paths.contains(&p) {
1426                    paths.push(p);
1427                }
1428            }
1429
1430            let mut paths = paths
1431                .into_iter()
1432                .filter(|p| p.exists())
1433                .collect::<Vec<PathBuf>>();
1434
1435            self.global_search_locations.lock().append(&mut paths);
1436        }
1437        self.global_search_locations.lock().clone()
1438    }
1439}
1440
1441pub(crate) struct PyLspAdapter {
1442    python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1443}
1444impl PyLspAdapter {
1445    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
1446    pub(crate) fn new() -> Self {
1447        Self {
1448            python_venv_base: OnceCell::new(),
1449        }
1450    }
1451    async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1452        let python_path = Self::find_base_python(delegate)
1453            .await
1454            .with_context(|| {
1455                let mut message = "Could not find Python installation for PyLSP".to_owned();
1456                if cfg!(windows){
1457                    message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
1458                }
1459                message
1460            })?;
1461        let work_dir = delegate
1462            .language_server_download_dir(&Self::SERVER_NAME)
1463            .await
1464            .context("Could not get working directory for PyLSP")?;
1465        let mut path = PathBuf::from(work_dir.as_ref());
1466        path.push("pylsp-venv");
1467        if !path.exists() {
1468            util::command::new_smol_command(python_path)
1469                .arg("-m")
1470                .arg("venv")
1471                .arg("pylsp-venv")
1472                .current_dir(work_dir)
1473                .spawn()?
1474                .output()
1475                .await?;
1476        }
1477
1478        Ok(path.into())
1479    }
1480    // Find "baseline", user python version from which we'll create our own venv.
1481    async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1482        for path in ["python3", "python"] {
1483            let Some(path) = delegate.which(path.as_ref()).await else {
1484                continue;
1485            };
1486            // Try to detect situations where `python3` exists but is not a real Python interpreter.
1487            // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
1488            // when run with no arguments, and just fails otherwise.
1489            let Some(output) = new_smol_command(&path)
1490                .args(["-c", "print(1 + 2)"])
1491                .output()
1492                .await
1493                .ok()
1494            else {
1495                continue;
1496            };
1497            if output.stdout.trim_ascii() != b"3" {
1498                continue;
1499            }
1500            return Some(path);
1501        }
1502        None
1503    }
1504
1505    async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1506        self.python_venv_base
1507            .get_or_init(move || async move {
1508                Self::ensure_venv(delegate)
1509                    .await
1510                    .map_err(|e| format!("{e}"))
1511            })
1512            .await
1513            .clone()
1514    }
1515}
1516
1517const BINARY_DIR: &str = if cfg!(target_os = "windows") {
1518    "Scripts"
1519} else {
1520    "bin"
1521};
1522
1523#[async_trait(?Send)]
1524impl LspAdapter for PyLspAdapter {
1525    fn name(&self) -> LanguageServerName {
1526        Self::SERVER_NAME
1527    }
1528
1529    async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
1530
1531    async fn label_for_completion(
1532        &self,
1533        item: &lsp::CompletionItem,
1534        language: &Arc<language::Language>,
1535    ) -> Option<language::CodeLabel> {
1536        let label = &item.label;
1537        let label_len = label.len();
1538        let grammar = language.grammar()?;
1539        let highlight_id = match item.kind? {
1540            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1541            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1542            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1543            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1544            _ => return None,
1545        };
1546        Some(language::CodeLabel::filtered(
1547            label.clone(),
1548            label_len,
1549            item.filter_text.as_deref(),
1550            vec![(0..label.len(), highlight_id)],
1551        ))
1552    }
1553
1554    async fn label_for_symbol(
1555        &self,
1556        name: &str,
1557        kind: lsp::SymbolKind,
1558        language: &Arc<language::Language>,
1559    ) -> Option<language::CodeLabel> {
1560        let (text, filter_range, display_range) = match kind {
1561            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1562                let text = format!("def {}():\n", name);
1563                let filter_range = 4..4 + name.len();
1564                let display_range = 0..filter_range.end;
1565                (text, filter_range, display_range)
1566            }
1567            lsp::SymbolKind::CLASS => {
1568                let text = format!("class {}:", name);
1569                let filter_range = 6..6 + name.len();
1570                let display_range = 0..filter_range.end;
1571                (text, filter_range, display_range)
1572            }
1573            lsp::SymbolKind::CONSTANT => {
1574                let text = format!("{} = 0", name);
1575                let filter_range = 0..name.len();
1576                let display_range = 0..filter_range.end;
1577                (text, filter_range, display_range)
1578            }
1579            _ => return None,
1580        };
1581        Some(language::CodeLabel::new(
1582            text[display_range.clone()].to_string(),
1583            filter_range,
1584            language.highlight_text(&text.as_str().into(), display_range),
1585        ))
1586    }
1587
1588    async fn workspace_configuration(
1589        self: Arc<Self>,
1590        adapter: &Arc<dyn LspAdapterDelegate>,
1591        toolchain: Option<Toolchain>,
1592        cx: &mut AsyncApp,
1593    ) -> Result<Value> {
1594        cx.update(move |cx| {
1595            let mut user_settings =
1596                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1597                    .and_then(|s| s.settings.clone())
1598                    .unwrap_or_else(|| {
1599                        json!({
1600                            "plugins": {
1601                                "pycodestyle": {"enabled": false},
1602                                "rope_autoimport": {"enabled": true, "memory": true},
1603                                "pylsp_mypy": {"enabled": false}
1604                            },
1605                            "rope": {
1606                                "ropeFolder": null
1607                            },
1608                        })
1609                    });
1610
1611            // If user did not explicitly modify their python venv, use one from picker.
1612            if let Some(toolchain) = toolchain {
1613                if !user_settings.is_object() {
1614                    user_settings = Value::Object(serde_json::Map::default());
1615                }
1616                let object = user_settings.as_object_mut().unwrap();
1617                if let Some(python) = object
1618                    .entry("plugins")
1619                    .or_insert(Value::Object(serde_json::Map::default()))
1620                    .as_object_mut()
1621                {
1622                    if let Some(jedi) = python
1623                        .entry("jedi")
1624                        .or_insert(Value::Object(serde_json::Map::default()))
1625                        .as_object_mut()
1626                    {
1627                        jedi.entry("environment".to_string())
1628                            .or_insert_with(|| Value::String(toolchain.path.clone().into()));
1629                    }
1630                    if let Some(pylint) = python
1631                        .entry("pylsp_mypy")
1632                        .or_insert(Value::Object(serde_json::Map::default()))
1633                        .as_object_mut()
1634                    {
1635                        pylint.entry("overrides".to_string()).or_insert_with(|| {
1636                            Value::Array(vec![
1637                                Value::String("--python-executable".into()),
1638                                Value::String(toolchain.path.into()),
1639                                Value::String("--cache-dir=/dev/null".into()),
1640                                Value::Bool(true),
1641                            ])
1642                        });
1643                    }
1644                }
1645            }
1646            user_settings = Value::Object(serde_json::Map::from_iter([(
1647                "pylsp".to_string(),
1648                user_settings,
1649            )]));
1650
1651            user_settings
1652        })
1653    }
1654}
1655
1656impl LspInstaller for PyLspAdapter {
1657    type BinaryVersion = ();
1658    async fn check_if_user_installed(
1659        &self,
1660        delegate: &dyn LspAdapterDelegate,
1661        toolchain: Option<Toolchain>,
1662        _: &AsyncApp,
1663    ) -> Option<LanguageServerBinary> {
1664        if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
1665            let env = delegate.shell_env().await;
1666            Some(LanguageServerBinary {
1667                path: pylsp_bin,
1668                env: Some(env),
1669                arguments: vec![],
1670            })
1671        } else {
1672            let toolchain = toolchain?;
1673            let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
1674            pylsp_path.exists().then(|| LanguageServerBinary {
1675                path: toolchain.path.to_string().into(),
1676                arguments: vec![pylsp_path.into()],
1677                env: None,
1678            })
1679        }
1680    }
1681
1682    async fn fetch_latest_server_version(
1683        &self,
1684        _: &dyn LspAdapterDelegate,
1685        _: bool,
1686        _: &mut AsyncApp,
1687    ) -> Result<()> {
1688        Ok(())
1689    }
1690
1691    async fn fetch_server_binary(
1692        &self,
1693        _: (),
1694        _: PathBuf,
1695        delegate: &dyn LspAdapterDelegate,
1696    ) -> Result<LanguageServerBinary> {
1697        let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1698        let pip_path = venv.join(BINARY_DIR).join("pip3");
1699        ensure!(
1700            util::command::new_smol_command(pip_path.as_path())
1701                .arg("install")
1702                .arg("python-lsp-server[all]")
1703                .arg("--upgrade")
1704                .output()
1705                .await?
1706                .status
1707                .success(),
1708            "python-lsp-server[all] installation failed"
1709        );
1710        ensure!(
1711            util::command::new_smol_command(pip_path)
1712                .arg("install")
1713                .arg("pylsp-mypy")
1714                .arg("--upgrade")
1715                .output()
1716                .await?
1717                .status
1718                .success(),
1719            "pylsp-mypy installation failed"
1720        );
1721        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1722        ensure!(
1723            delegate.which(pylsp.as_os_str()).await.is_some(),
1724            "pylsp installation was incomplete"
1725        );
1726        Ok(LanguageServerBinary {
1727            path: pylsp,
1728            env: None,
1729            arguments: vec![],
1730        })
1731    }
1732
1733    async fn cached_server_binary(
1734        &self,
1735        _: PathBuf,
1736        delegate: &dyn LspAdapterDelegate,
1737    ) -> Option<LanguageServerBinary> {
1738        let venv = self.base_venv(delegate).await.ok()?;
1739        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1740        delegate.which(pylsp.as_os_str()).await?;
1741        Some(LanguageServerBinary {
1742            path: pylsp,
1743            env: None,
1744            arguments: vec![],
1745        })
1746    }
1747}
1748
1749pub(crate) struct BasedPyrightLspAdapter {
1750    node: NodeRuntime,
1751}
1752
1753impl BasedPyrightLspAdapter {
1754    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
1755    const BINARY_NAME: &'static str = "basedpyright-langserver";
1756    const SERVER_PATH: &str = "node_modules/basedpyright/langserver.index.js";
1757    const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "basedpyright/langserver.index.js";
1758
1759    pub(crate) fn new(node: NodeRuntime) -> Self {
1760        BasedPyrightLspAdapter { node }
1761    }
1762
1763    async fn get_cached_server_binary(
1764        container_dir: PathBuf,
1765        node: &NodeRuntime,
1766    ) -> Option<LanguageServerBinary> {
1767        let server_path = container_dir.join(Self::SERVER_PATH);
1768        if server_path.exists() {
1769            Some(LanguageServerBinary {
1770                path: node.binary_path().await.log_err()?,
1771                env: None,
1772                arguments: vec![server_path.into(), "--stdio".into()],
1773            })
1774        } else {
1775            log::error!("missing executable in directory {:?}", server_path);
1776            None
1777        }
1778    }
1779}
1780
1781#[async_trait(?Send)]
1782impl LspAdapter for BasedPyrightLspAdapter {
1783    fn name(&self) -> LanguageServerName {
1784        Self::SERVER_NAME
1785    }
1786
1787    async fn initialization_options(
1788        self: Arc<Self>,
1789        _: &Arc<dyn LspAdapterDelegate>,
1790    ) -> Result<Option<Value>> {
1791        // Provide minimal initialization options
1792        // Virtual environment configuration will be handled through workspace configuration
1793        Ok(Some(json!({
1794            "python": {
1795                "analysis": {
1796                    "autoSearchPaths": true,
1797                    "useLibraryCodeForTypes": true,
1798                    "autoImportCompletions": true
1799                }
1800            }
1801        })))
1802    }
1803
1804    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
1805        process_pyright_completions(items);
1806    }
1807
1808    async fn label_for_completion(
1809        &self,
1810        item: &lsp::CompletionItem,
1811        language: &Arc<language::Language>,
1812    ) -> Option<language::CodeLabel> {
1813        let label = &item.label;
1814        let label_len = label.len();
1815        let grammar = language.grammar()?;
1816        let highlight_id = match item.kind? {
1817            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
1818            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
1819            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
1820            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
1821            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
1822            _ => {
1823                return None;
1824            }
1825        };
1826        let mut text = label.clone();
1827        if let Some(completion_details) = item
1828            .label_details
1829            .as_ref()
1830            .and_then(|details| details.description.as_ref())
1831        {
1832            write!(&mut text, " {}", completion_details).ok();
1833        }
1834        Some(language::CodeLabel::filtered(
1835            text,
1836            label_len,
1837            item.filter_text.as_deref(),
1838            highlight_id
1839                .map(|id| (0..label.len(), id))
1840                .into_iter()
1841                .collect(),
1842        ))
1843    }
1844
1845    async fn label_for_symbol(
1846        &self,
1847        name: &str,
1848        kind: lsp::SymbolKind,
1849        language: &Arc<language::Language>,
1850    ) -> Option<language::CodeLabel> {
1851        let (text, filter_range, display_range) = match kind {
1852            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1853                let text = format!("def {}():\n", name);
1854                let filter_range = 4..4 + name.len();
1855                let display_range = 0..filter_range.end;
1856                (text, filter_range, display_range)
1857            }
1858            lsp::SymbolKind::CLASS => {
1859                let text = format!("class {}:", name);
1860                let filter_range = 6..6 + name.len();
1861                let display_range = 0..filter_range.end;
1862                (text, filter_range, display_range)
1863            }
1864            lsp::SymbolKind::CONSTANT => {
1865                let text = format!("{} = 0", name);
1866                let filter_range = 0..name.len();
1867                let display_range = 0..filter_range.end;
1868                (text, filter_range, display_range)
1869            }
1870            _ => return None,
1871        };
1872        Some(language::CodeLabel::new(
1873            text[display_range.clone()].to_string(),
1874            filter_range,
1875            language.highlight_text(&text.as_str().into(), display_range),
1876        ))
1877    }
1878
1879    async fn workspace_configuration(
1880        self: Arc<Self>,
1881        adapter: &Arc<dyn LspAdapterDelegate>,
1882        toolchain: Option<Toolchain>,
1883        cx: &mut AsyncApp,
1884    ) -> Result<Value> {
1885        cx.update(move |cx| {
1886            let mut user_settings =
1887                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1888                    .and_then(|s| s.settings.clone())
1889                    .unwrap_or_default();
1890
1891            // If we have a detected toolchain, configure Pyright to use it
1892            if let Some(toolchain) = toolchain
1893                && let Ok(env) = serde_json::from_value::<
1894                    pet_core::python_environment::PythonEnvironment,
1895                >(toolchain.as_json.clone())
1896            {
1897                if !user_settings.is_object() {
1898                    user_settings = Value::Object(serde_json::Map::default());
1899                }
1900                let object = user_settings.as_object_mut().unwrap();
1901
1902                let interpreter_path = toolchain.path.to_string();
1903                if let Some(venv_dir) = env.prefix {
1904                    // Set venvPath and venv at the root level
1905                    // This matches the format of a pyrightconfig.json file
1906                    if let Some(parent) = venv_dir.parent() {
1907                        // Use relative path if the venv is inside the workspace
1908                        let venv_path = if parent == adapter.worktree_root_path() {
1909                            ".".to_string()
1910                        } else {
1911                            parent.to_string_lossy().into_owned()
1912                        };
1913                        object.insert("venvPath".to_string(), Value::String(venv_path));
1914                    }
1915
1916                    if let Some(venv_name) = venv_dir.file_name() {
1917                        object.insert(
1918                            "venv".to_owned(),
1919                            Value::String(venv_name.to_string_lossy().into_owned()),
1920                        );
1921                    }
1922                }
1923
1924                // Set both pythonPath and defaultInterpreterPath for compatibility
1925                if let Some(python) = object
1926                    .entry("python")
1927                    .or_insert(Value::Object(serde_json::Map::default()))
1928                    .as_object_mut()
1929                {
1930                    python.insert(
1931                        "pythonPath".to_owned(),
1932                        Value::String(interpreter_path.clone()),
1933                    );
1934                    python.insert(
1935                        "defaultInterpreterPath".to_owned(),
1936                        Value::String(interpreter_path),
1937                    );
1938                }
1939                // Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
1940                maybe!({
1941                    let analysis = object
1942                        .entry("basedpyright.analysis")
1943                        .or_insert(Value::Object(serde_json::Map::default()));
1944                    if let serde_json::map::Entry::Vacant(v) =
1945                        analysis.as_object_mut()?.entry("typeCheckingMode")
1946                    {
1947                        v.insert(Value::String("standard".to_owned()));
1948                    }
1949                    Some(())
1950                });
1951            }
1952
1953            user_settings
1954        })
1955    }
1956}
1957
1958impl LspInstaller for BasedPyrightLspAdapter {
1959    type BinaryVersion = String;
1960
1961    async fn fetch_latest_server_version(
1962        &self,
1963        _: &dyn LspAdapterDelegate,
1964        _: bool,
1965        _: &mut AsyncApp,
1966    ) -> Result<String> {
1967        self.node
1968            .npm_package_latest_version(Self::SERVER_NAME.as_ref())
1969            .await
1970    }
1971
1972    async fn check_if_user_installed(
1973        &self,
1974        delegate: &dyn LspAdapterDelegate,
1975        _: Option<Toolchain>,
1976        _: &AsyncApp,
1977    ) -> Option<LanguageServerBinary> {
1978        if let Some(path) = delegate.which(Self::BINARY_NAME.as_ref()).await {
1979            let env = delegate.shell_env().await;
1980            Some(LanguageServerBinary {
1981                path,
1982                env: Some(env),
1983                arguments: vec!["--stdio".into()],
1984            })
1985        } else {
1986            // TODO shouldn't this be self.node.binary_path()?
1987            let node = delegate.which("node".as_ref()).await?;
1988            let (node_modules_path, _) = delegate
1989                .npm_package_installed_version(Self::SERVER_NAME.as_ref())
1990                .await
1991                .log_err()??;
1992
1993            let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
1994
1995            let env = delegate.shell_env().await;
1996            Some(LanguageServerBinary {
1997                path: node,
1998                env: Some(env),
1999                arguments: vec![path.into(), "--stdio".into()],
2000            })
2001        }
2002    }
2003
2004    async fn fetch_server_binary(
2005        &self,
2006        latest_version: Self::BinaryVersion,
2007        container_dir: PathBuf,
2008        delegate: &dyn LspAdapterDelegate,
2009    ) -> Result<LanguageServerBinary> {
2010        let server_path = container_dir.join(Self::SERVER_PATH);
2011
2012        self.node
2013            .npm_install_packages(
2014                &container_dir,
2015                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
2016            )
2017            .await?;
2018
2019        let env = delegate.shell_env().await;
2020        Ok(LanguageServerBinary {
2021            path: self.node.binary_path().await?,
2022            env: Some(env),
2023            arguments: vec![server_path.into(), "--stdio".into()],
2024        })
2025    }
2026
2027    async fn check_if_version_installed(
2028        &self,
2029        version: &Self::BinaryVersion,
2030        container_dir: &PathBuf,
2031        delegate: &dyn LspAdapterDelegate,
2032    ) -> Option<LanguageServerBinary> {
2033        let server_path = container_dir.join(Self::SERVER_PATH);
2034
2035        let should_install_language_server = self
2036            .node
2037            .should_install_npm_package(
2038                Self::SERVER_NAME.as_ref(),
2039                &server_path,
2040                container_dir,
2041                VersionStrategy::Latest(version),
2042            )
2043            .await;
2044
2045        if should_install_language_server {
2046            None
2047        } else {
2048            let env = delegate.shell_env().await;
2049            Some(LanguageServerBinary {
2050                path: self.node.binary_path().await.ok()?,
2051                env: Some(env),
2052                arguments: vec![server_path.into(), "--stdio".into()],
2053            })
2054        }
2055    }
2056
2057    async fn cached_server_binary(
2058        &self,
2059        container_dir: PathBuf,
2060        delegate: &dyn LspAdapterDelegate,
2061    ) -> Option<LanguageServerBinary> {
2062        let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
2063        binary.env = Some(delegate.shell_env().await);
2064        Some(binary)
2065    }
2066}
2067
2068pub(crate) struct RuffLspAdapter {
2069    fs: Arc<dyn Fs>,
2070}
2071
2072#[cfg(target_os = "macos")]
2073impl RuffLspAdapter {
2074    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2075    const ARCH_SERVER_NAME: &str = "apple-darwin";
2076}
2077
2078#[cfg(target_os = "linux")]
2079impl RuffLspAdapter {
2080    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2081    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
2082}
2083
2084#[cfg(target_os = "freebsd")]
2085impl RuffLspAdapter {
2086    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2087    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
2088}
2089
2090#[cfg(target_os = "windows")]
2091impl RuffLspAdapter {
2092    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
2093    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
2094}
2095
2096impl RuffLspAdapter {
2097    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ruff");
2098
2099    pub fn new(fs: Arc<dyn Fs>) -> RuffLspAdapter {
2100        RuffLspAdapter { fs }
2101    }
2102
2103    fn build_asset_name() -> Result<(String, String)> {
2104        let arch = match consts::ARCH {
2105            "x86" => "i686",
2106            _ => consts::ARCH,
2107        };
2108        let os = Self::ARCH_SERVER_NAME;
2109        let suffix = match consts::OS {
2110            "windows" => "zip",
2111            _ => "tar.gz",
2112        };
2113        let asset_name = format!("ruff-{arch}-{os}.{suffix}");
2114        let asset_stem = format!("ruff-{arch}-{os}");
2115        Ok((asset_stem, asset_name))
2116    }
2117}
2118
2119#[async_trait(?Send)]
2120impl LspAdapter for RuffLspAdapter {
2121    fn name(&self) -> LanguageServerName {
2122        Self::SERVER_NAME
2123    }
2124}
2125
2126impl LspInstaller for RuffLspAdapter {
2127    type BinaryVersion = GitHubLspBinaryVersion;
2128    async fn check_if_user_installed(
2129        &self,
2130        delegate: &dyn LspAdapterDelegate,
2131        toolchain: Option<Toolchain>,
2132        _: &AsyncApp,
2133    ) -> Option<LanguageServerBinary> {
2134        let ruff_in_venv = if let Some(toolchain) = toolchain
2135            && toolchain.language_name.as_ref() == "Python"
2136        {
2137            Path::new(toolchain.path.as_str())
2138                .parent()
2139                .map(|path| path.join("ruff"))
2140        } else {
2141            None
2142        };
2143
2144        for path in ruff_in_venv.into_iter().chain(["ruff".into()]) {
2145            if let Some(ruff_bin) = delegate.which(path.as_os_str()).await {
2146                let env = delegate.shell_env().await;
2147                return Some(LanguageServerBinary {
2148                    path: ruff_bin,
2149                    env: Some(env),
2150                    arguments: vec!["server".into()],
2151                });
2152            }
2153        }
2154
2155        None
2156    }
2157
2158    async fn fetch_latest_server_version(
2159        &self,
2160        delegate: &dyn LspAdapterDelegate,
2161        _: bool,
2162        _: &mut AsyncApp,
2163    ) -> Result<GitHubLspBinaryVersion> {
2164        let release =
2165            latest_github_release("astral-sh/ruff", true, false, delegate.http_client()).await?;
2166        let (_, asset_name) = Self::build_asset_name()?;
2167        let asset = release
2168            .assets
2169            .into_iter()
2170            .find(|asset| asset.name == asset_name)
2171            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
2172        Ok(GitHubLspBinaryVersion {
2173            name: release.tag_name,
2174            url: asset.browser_download_url,
2175            digest: asset.digest,
2176        })
2177    }
2178
2179    async fn fetch_server_binary(
2180        &self,
2181        latest_version: GitHubLspBinaryVersion,
2182        container_dir: PathBuf,
2183        delegate: &dyn LspAdapterDelegate,
2184    ) -> Result<LanguageServerBinary> {
2185        let GitHubLspBinaryVersion {
2186            name,
2187            url,
2188            digest: expected_digest,
2189        } = latest_version;
2190        let destination_path = container_dir.join(format!("ruff-{name}"));
2191        let server_path = match Self::GITHUB_ASSET_KIND {
2192            AssetKind::TarGz | AssetKind::Gz => destination_path
2193                .join(Self::build_asset_name()?.0)
2194                .join("ruff"),
2195            AssetKind::Zip => destination_path.clone().join("ruff.exe"),
2196        };
2197
2198        let binary = LanguageServerBinary {
2199            path: server_path.clone(),
2200            env: None,
2201            arguments: vec!["server".into()],
2202        };
2203
2204        let metadata_path = destination_path.with_extension("metadata");
2205        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
2206            .await
2207            .ok();
2208        if let Some(metadata) = metadata {
2209            let validity_check = async || {
2210                delegate
2211                    .try_exec(LanguageServerBinary {
2212                        path: server_path.clone(),
2213                        arguments: vec!["--version".into()],
2214                        env: None,
2215                    })
2216                    .await
2217                    .inspect_err(|err| {
2218                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
2219                    })
2220            };
2221            if let (Some(actual_digest), Some(expected_digest)) =
2222                (&metadata.digest, &expected_digest)
2223            {
2224                if actual_digest == expected_digest {
2225                    if validity_check().await.is_ok() {
2226                        return Ok(binary);
2227                    }
2228                } else {
2229                    log::info!(
2230                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
2231                    );
2232                }
2233            } else if validity_check().await.is_ok() {
2234                return Ok(binary);
2235            }
2236        }
2237
2238        download_server_binary(
2239            &*delegate.http_client(),
2240            &url,
2241            expected_digest.as_deref(),
2242            &destination_path,
2243            Self::GITHUB_ASSET_KIND,
2244        )
2245        .await?;
2246        make_file_executable(&server_path).await?;
2247        remove_matching(&container_dir, |path| path != destination_path).await;
2248        GithubBinaryMetadata::write_to_file(
2249            &GithubBinaryMetadata {
2250                metadata_version: 1,
2251                digest: expected_digest,
2252            },
2253            &metadata_path,
2254        )
2255        .await?;
2256
2257        Ok(LanguageServerBinary {
2258            path: server_path,
2259            env: None,
2260            arguments: vec!["server".into()],
2261        })
2262    }
2263
2264    async fn cached_server_binary(
2265        &self,
2266        container_dir: PathBuf,
2267        _: &dyn LspAdapterDelegate,
2268    ) -> Option<LanguageServerBinary> {
2269        maybe!(async {
2270            let mut last = None;
2271            let mut entries = self.fs.read_dir(&container_dir).await?;
2272            while let Some(entry) = entries.next().await {
2273                let path = entry?;
2274                if path.extension().is_some_and(|ext| ext == "metadata") {
2275                    continue;
2276                }
2277                last = Some(path);
2278            }
2279
2280            let path = last.context("no cached binary")?;
2281            let path = match Self::GITHUB_ASSET_KIND {
2282                AssetKind::TarGz | AssetKind::Gz => {
2283                    path.join(Self::build_asset_name()?.0).join("ruff")
2284                }
2285                AssetKind::Zip => path.join("ruff.exe"),
2286            };
2287
2288            anyhow::Ok(LanguageServerBinary {
2289                path,
2290                env: None,
2291                arguments: vec!["server".into()],
2292            })
2293        })
2294        .await
2295        .log_err()
2296    }
2297}
2298
2299#[cfg(test)]
2300mod tests {
2301    use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
2302    use language::{AutoindentMode, Buffer};
2303    use settings::SettingsStore;
2304    use std::num::NonZeroU32;
2305
2306    #[gpui::test]
2307    async fn test_python_autoindent(cx: &mut TestAppContext) {
2308        cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
2309        let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
2310        cx.update(|cx| {
2311            let test_settings = SettingsStore::test(cx);
2312            cx.set_global(test_settings);
2313            cx.update_global::<SettingsStore, _>(|store, cx| {
2314                store.update_user_settings(cx, |s| {
2315                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
2316                });
2317            });
2318        });
2319
2320        cx.new(|cx| {
2321            let mut buffer = Buffer::local("", cx).with_language(language, cx);
2322            let append = |buffer: &mut Buffer, text: &str, cx: &mut Context<Buffer>| {
2323                let ix = buffer.len();
2324                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
2325            };
2326
2327            // indent after "def():"
2328            append(&mut buffer, "def a():\n", cx);
2329            assert_eq!(buffer.text(), "def a():\n  ");
2330
2331            // preserve indent after blank line
2332            append(&mut buffer, "\n  ", cx);
2333            assert_eq!(buffer.text(), "def a():\n  \n  ");
2334
2335            // indent after "if"
2336            append(&mut buffer, "if a:\n  ", cx);
2337            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
2338
2339            // preserve indent after statement
2340            append(&mut buffer, "b()\n", cx);
2341            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
2342
2343            // preserve indent after statement
2344            append(&mut buffer, "else", cx);
2345            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
2346
2347            // dedent "else""
2348            append(&mut buffer, ":", cx);
2349            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
2350
2351            // indent lines after else
2352            append(&mut buffer, "\n", cx);
2353            assert_eq!(
2354                buffer.text(),
2355                "def a():\n  \n  if a:\n    b()\n  else:\n    "
2356            );
2357
2358            // indent after an open paren. the closing paren is not indented
2359            // because there is another token before it on the same line.
2360            append(&mut buffer, "foo(\n1)", cx);
2361            assert_eq!(
2362                buffer.text(),
2363                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
2364            );
2365
2366            // dedent the closing paren if it is shifted to the beginning of the line
2367            let argument_ix = buffer.text().find('1').unwrap();
2368            buffer.edit(
2369                [(argument_ix..argument_ix + 1, "")],
2370                Some(AutoindentMode::EachLine),
2371                cx,
2372            );
2373            assert_eq!(
2374                buffer.text(),
2375                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
2376            );
2377
2378            // preserve indent after the close paren
2379            append(&mut buffer, "\n", cx);
2380            assert_eq!(
2381                buffer.text(),
2382                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
2383            );
2384
2385            // manually outdent the last line
2386            let end_whitespace_ix = buffer.len() - 4;
2387            buffer.edit(
2388                [(end_whitespace_ix..buffer.len(), "")],
2389                Some(AutoindentMode::EachLine),
2390                cx,
2391            );
2392            assert_eq!(
2393                buffer.text(),
2394                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
2395            );
2396
2397            // preserve the newly reduced indentation on the next newline
2398            append(&mut buffer, "\n", cx);
2399            assert_eq!(
2400                buffer.text(),
2401                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
2402            );
2403
2404            // reset to a for loop statement
2405            let statement = "for i in range(10):\n  print(i)\n";
2406            buffer.edit([(0..buffer.len(), statement)], None, cx);
2407
2408            // insert single line comment after each line
2409            let eol_ixs = statement
2410                .char_indices()
2411                .filter_map(|(ix, c)| if c == '\n' { Some(ix) } else { None })
2412                .collect::<Vec<usize>>();
2413            let editions = eol_ixs
2414                .iter()
2415                .enumerate()
2416                .map(|(i, &eol_ix)| (eol_ix..eol_ix, format!(" # comment {}", i + 1)))
2417                .collect::<Vec<(std::ops::Range<usize>, String)>>();
2418            buffer.edit(editions, Some(AutoindentMode::EachLine), cx);
2419            assert_eq!(
2420                buffer.text(),
2421                "for i in range(10): # comment 1\n  print(i) # comment 2\n"
2422            );
2423
2424            // reset to a simple if statement
2425            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
2426
2427            // dedent "else" on the line after a closing paren
2428            append(&mut buffer, "\n  else:\n", cx);
2429            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
2430
2431            buffer
2432        });
2433    }
2434}