3381 lines · 120.5 KB · Rust
RawDownload
use anyhow::Result;use anyhow::{Context as _, ensure};use async_trait::async_trait;use collections::HashMap;use futures::future::BoxFuture;use futures::lock::OwnedMutexGuard;use futures::{AsyncBufReadExt, StreamExt as _};use gpui::{App, AsyncApp, Entity, SharedString, Task};use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};use language::language_settings::LanguageSettings;use language::{    Buffer, ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol,};use language::{ContextProvider, LspAdapter, LspAdapterDelegate};use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};use lsp::{CompletionItemKind, LanguageServerBinary, Uri};use lsp::{LanguageServerBinaryOptions, LanguageServerName};use node_runtime::{NodeRuntime, VersionStrategy};use pet_core::Configuration;use pet_core::os_environment::Environment;use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};use pet_virtualenv::is_virtualenv_dir;use project::Fs;use project::lsp_store::language_server_settings;use semver::Version;use serde::{Deserialize, Serialize};use serde_json::{Value, json};use settings::{SemanticTokenRules, Settings};use terminal::terminal_settings::TerminalSettings; use smol::lock::OnceCell;use std::cmp::{Ordering, Reverse};use std::env::consts;use util::command::Stdio; use util::command::new_command;use util::fs::{make_file_executable, remove_matching};use util::paths::PathStyle;use util::rel_path::RelPath; use http_client::github_download::{GithubBinaryMetadata, download_server_binary};use parking_lot::Mutex;use std::str::FromStr;use std::{    borrow::Cow,    fmt::Write,    future::Future,    path::{Path, PathBuf},    sync::Arc,};use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};use util::{ResultExt, maybe}; pub(crate) fn semantic_token_rules() -> SemanticTokenRules {    let content = grammars::get_file("python/semantic_token_rules.json")        .expect("missing python/semantic_token_rules.json");    let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");    settings::parse_json_with_comments::<SemanticTokenRules>(json)        .expect("failed to parse python semantic_token_rules.json")} #[derive(Debug, Serialize, Deserialize)]pub(crate) struct PythonToolchainData {    #[serde(flatten)]    environment: PythonEnvironment,    #[serde(skip_serializing_if = "Option::is_none")]    activation_scripts: Option<HashMap<ShellKind, PathBuf>>,} pub(crate) struct PyprojectTomlManifestProvider; impl ManifestProvider for PyprojectTomlManifestProvider {    fn name(&self) -> ManifestName {        SharedString::new_static("pyproject.toml").into()    }     fn search(        &self,        ManifestQuery {            path,            depth,            delegate,        }: ManifestQuery,    ) -> Option<Arc<RelPath>> {        const WORKSPACE_LOCKFILES: &[&str] =            &["uv.lock", "poetry.lock", "pdm.lock", "Pipfile.lock"];         let mut innermost_pyproject = None;        let mut outermost_workspace_root = None;         for path in path.ancestors().take(depth) {            let pyproject_path = path.join(RelPath::from_unix_str("pyproject.toml").unwrap());            if delegate.exists(&pyproject_path, Some(false)) {                if innermost_pyproject.is_none() {                    innermost_pyproject = Some(Arc::from(path));                }                 let has_lockfile = WORKSPACE_LOCKFILES.iter().any(|lockfile| {                    let lockfile_path = path.join(RelPath::from_unix_str(lockfile).unwrap());                    delegate.exists(&lockfile_path, Some(false))                });                if has_lockfile {                    outermost_workspace_root = Some(Arc::from(path));                }            }        }         outermost_workspace_root.or(innermost_pyproject)    }} enum TestRunner {    UNITTEST,    PYTEST,} impl FromStr for TestRunner {    type Err = ();     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {        match s {            "unittest" => Ok(Self::UNITTEST),            "pytest" => Ok(Self::PYTEST),            _ => Err(()),        }    }} /// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`./// Where `XX` is the sorting category, `YYYY` is based on most recent usage,/// and `name` is the symbol name itself.////// 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),/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail./// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873////// upd 02.12.25:/// Decided to ignore Pyright's sortText() completely and to manually sort all entriesfn process_pyright_completions(items: &mut [lsp::CompletionItem]) {    for item in items {        let is_named_argument = item.label.ends_with('=');         let is_dunder = item.label.starts_with("__") && item.label.ends_with("__");         let visibility_priority = if is_dunder {            '3'        } else if item.label.starts_with("__") {            '2' // private non-dunder        } else if item.label.starts_with('_') {            '1' // protected        } else {            '0' // public        };         let is_external = item            .detail            .as_ref()            .is_some_and(|detail| detail == "Auto-import");         let source_priority = if is_external { '1' } else { '0' };         // Kind priority within same visibility level        let kind_priority = match item.kind {            Some(lsp::CompletionItemKind::KEYWORD) => '0',            Some(lsp::CompletionItemKind::ENUM_MEMBER) => '1',            Some(lsp::CompletionItemKind::FIELD) => '2',            Some(lsp::CompletionItemKind::PROPERTY) => '3',            Some(lsp::CompletionItemKind::VARIABLE) => '4',            Some(lsp::CompletionItemKind::CONSTANT) => '5',            Some(lsp::CompletionItemKind::METHOD) => '6',            Some(lsp::CompletionItemKind::FUNCTION) => '6',            Some(lsp::CompletionItemKind::CLASS) => '7',            Some(lsp::CompletionItemKind::MODULE) => '8',             _ => 'z',        };         // Named arguments get higher priority        let argument_priority = if is_named_argument { '0' } else { '1' };         item.sort_text = Some(format!(            "{}{}{}{}{}",            argument_priority, source_priority, visibility_priority, kind_priority, item.label        ));    }} fn label_for_pyright_completion(    item: &lsp::CompletionItem,    language: &Arc<language::Language>,) -> Option<language::CodeLabel> {    let label = &item.label;    let label_len = label.len();    let grammar = language.grammar()?;    let highlight_id = highlight_id_for_completion(item.kind?, grammar)?;     let mut text = label.clone();    if let Some(completion_details) = item        .label_details        .as_ref()        .and_then(|details| details.description.as_ref())    {        write!(&mut text, " {}", completion_details).ok();    }    Some(language::CodeLabel::filtered(        text,        label_len,        item.filter_text.as_deref(),        highlight_id            .map(|id| (0..label_len, id))            .into_iter()            .collect(),    ))} fn label_for_python_symbol(    symbol: &Symbol,    language: &Arc<language::Language>,) -> Option<language::CodeLabel> {    let name = &symbol.name;    let (text, filter_range, display_range) = match symbol.kind {        language::SymbolKind::Method | language::SymbolKind::Function => {            let text = format!("def {}():\n", name);            let filter_range = 4..4 + name.len();            let display_range = 0..filter_range.end;            (text, filter_range, display_range)        }        language::SymbolKind::Class => {            let text = format!("class {}:", name);            let filter_range = 6..6 + name.len();            let display_range = 0..filter_range.end;            (text, filter_range, display_range)        }        language::SymbolKind::Constant => {            let text = format!("{} = 0", name);            let filter_range = 0..name.len();            let display_range = 0..filter_range.end;            (text, filter_range, display_range)        }        _ => return None,    };    Some(language::CodeLabel::new(        text[display_range.clone()].to_string(),        filter_range,        language.highlight_text(&text.as_str().into(), display_range),    ))} /// Returns the highlight ID for the given completion item kind, if it is supported.////// The outer `Option` is `None` if the item kind returned by the language server is not covered./// The inner `Option` is `None` if the item kind is covered, but the highlight name is not present in the grammar.fn highlight_id_for_completion(    item_kind: CompletionItemKind,    grammar: &Arc<language::Grammar>,) -> Option<Option<language::HighlightId>> {    match item_kind {        CompletionItemKind::METHOD => Some(grammar.highlight_id_for_name("function.method.call")),        CompletionItemKind::FUNCTION => Some(grammar.highlight_id_for_name("function.call")),        CompletionItemKind::CLASS => Some(grammar.highlight_id_for_name("type")),        CompletionItemKind::CONSTANT => Some(grammar.highlight_id_for_name("constant")),        CompletionItemKind::VARIABLE => Some(grammar.highlight_id_for_name("variable")),        _ => None,    }} pub struct TyLspAdapter {    fs: Arc<dyn Fs>,} #[cfg(target_os = "macos")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;    const ARCH_SERVER_NAME: &str = "apple-darwin";} #[cfg(target_os = "linux")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";} #[cfg(target_os = "freebsd")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;    const ARCH_SERVER_NAME: &str = "unknown-freebsd";} #[cfg(target_os = "windows")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";} impl TyLspAdapter {    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty");     pub fn new(fs: Arc<dyn Fs>) -> TyLspAdapter {        TyLspAdapter { fs }    }     fn build_asset_name() -> Result<(String, String)> {        let arch = match consts::ARCH {            "x86" => "i686",            _ => consts::ARCH,        };        let os = Self::ARCH_SERVER_NAME;        let suffix = match consts::OS {            "windows" => "zip",            _ => "tar.gz",        };        let asset_name = format!("ty-{arch}-{os}.{suffix}");        let asset_stem = format!("ty-{arch}-{os}");        Ok((asset_stem, asset_name))    }} #[async_trait(?Send)]impl LspAdapter for TyLspAdapter {    fn name(&self) -> LanguageServerName {        Self::SERVER_NAME    }     async fn label_for_completion(        &self,        item: &lsp::CompletionItem,        language: &Arc<language::Language>,    ) -> Option<language::CodeLabel> {        let label = &item.label;        let label_len = label.len();        let grammar = language.grammar()?;        let highlight_id = highlight_id_for_completion(item.kind?, grammar)?;         let mut text = label.clone();        if let Some(completion_details) = item            .label_details            .as_ref()            .and_then(|details| details.detail.as_ref())        {            write!(&mut text, " {}", completion_details).ok();        }         Some(language::CodeLabel::filtered(            text,            label_len,            item.filter_text.as_deref(),            highlight_id                .map(|id| (0..label_len, id))                .into_iter()                .collect(),        ))    }     async fn label_for_symbol(        &self,        symbol: &language::Symbol,        language: &Arc<language::Language>,    ) -> Option<language::CodeLabel> {        label_for_python_symbol(symbol, language)    }     async fn workspace_configuration(        self: Arc<Self>,        delegate: &Arc<dyn LspAdapterDelegate>,        toolchain: Option<Toolchain>,        _: Option<Uri>,        cx: &mut AsyncApp,    ) -> Result<Value> {        let mut ret = cx            .update(|cx| {                language_server_settings(delegate.as_ref(), &self.name(), cx)                    .and_then(|s| s.settings.clone())            })            .unwrap_or_else(|| json!({}));        if let Some(toolchain) = toolchain.and_then(|toolchain| {            serde_json::from_value::<PythonToolchainData>(toolchain.as_json).ok()        }) {            _ = maybe!({                let uri =                    url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?;                let sys_prefix = toolchain.environment.prefix.clone()?;                let environment = json!({                    "executable": {                        "uri": uri,                        "sysPrefix": sys_prefix                    }                });                ret.as_object_mut()?                    .entry("pythonExtension")                    .or_insert_with(|| json!({ "activeEnvironment": environment }));                Some(())            });        }        Ok(json!({"ty": ret}))    }} impl LspInstaller for TyLspAdapter {    type BinaryVersion = GitHubLspBinaryVersion;    async fn fetch_latest_server_version(        &self,        delegate: &Arc<dyn LspAdapterDelegate>,        _: bool,        _: &mut AsyncApp,    ) -> Result<Self::BinaryVersion> {        let release =            latest_github_release("astral-sh/ty", true, false, delegate.http_client()).await?;        let (_, asset_name) = Self::build_asset_name()?;        let asset = release            .assets            .into_iter()            .find(|asset| asset.name == asset_name)            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;        Ok(GitHubLspBinaryVersion {            name: release.tag_name,            url: asset.browser_download_url,            digest: asset.digest,        })    }     async fn check_if_user_installed(        &self,        delegate: &Arc<dyn LspAdapterDelegate>,        toolchain: Option<Toolchain>,        _: &AsyncApp,    ) -> Option<LanguageServerBinary> {        let ty_in_venv = if let Some(toolchain) = toolchain            && toolchain.language_name.as_ref() == "Python"        {            Path::new(toolchain.path.as_str())                .parent()                .map(|path| path.join("ty"))        } else {            None        };         for path in ty_in_venv.into_iter().chain(["ty".into()]) {            if let Some(ty_bin) = delegate.which(path.as_os_str()).await {                let env = delegate.shell_env().await;                return Some(LanguageServerBinary {                    path: ty_bin,                    env: Some(env),                    arguments: vec!["server".into()],                });            }        }         None    }     fn fetch_server_binary(        &self,        latest_version: Self::BinaryVersion,        container_dir: PathBuf,        delegate: &Arc<dyn LspAdapterDelegate>,    ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {        let delegate = delegate.clone();         async move {            let GitHubLspBinaryVersion {                name,                url,                digest: expected_digest,            } = latest_version;            let destination_path = container_dir.join(format!("ty-{name}"));             async_fs::create_dir_all(&destination_path).await?;             let server_path = match Self::GITHUB_ASSET_KIND {                AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path                    .join(Self::build_asset_name()?.0)                    .join("ty"),                AssetKind::Zip => destination_path.clone().join("ty.exe"),            };             let binary = LanguageServerBinary {                path: server_path.clone(),                env: None,                arguments: vec!["server".into()],            };             let metadata_path = destination_path.with_extension("metadata");            let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)                .await                .ok();            if let Some(metadata) = metadata {                let validity_check = async || {                    delegate                        .try_exec(LanguageServerBinary {                            path: server_path.clone(),                            arguments: vec!["--version".into()],                            env: None,                        })                        .await                        .inspect_err(|err| {                            log::warn!(                                "Unable to run {server_path:?} asset, redownloading: {err:#}",                            )                        })

Lines 1–500 of 3381.