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