diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 77c8531f3e..776d47a5f7 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -175,9 +175,10 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu language!("markdown-inline"); language!( "python", - vec![Arc::new(python::PythonLspAdapter::new( - node_runtime.clone(), - ))], + vec![ + Arc::new(python::PythonLspAdapter::new(node_runtime.clone(),)), + Arc::new(python::PyLspAdapter::new()) + ], PythonContextProvider, Arc::new(PythonToolchainProvider::default()) as Arc ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 60b514d3af..941c7da6d3 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use anyhow::ensure; +use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; use gpui::AppContext; @@ -16,7 +17,8 @@ use pet_core::os_environment::Environment; use pet_core::python_environment::PythonEnvironmentKind; use pet_core::Configuration; use project::lsp_store::language_server_settings; -use serde_json::Value; +use serde_json::{json, Value}; +use smol::{lock::OnceCell, process::Command}; use std::sync::Mutex; use std::{ @@ -507,6 +509,285 @@ impl<'a> pet_core::os_environment::Environment for EnvironmentApi<'a> { } } +pub(crate) struct PyLspAdapter { + python_venv_base: OnceCell, String>>, +} +impl PyLspAdapter { + const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp"); + pub(crate) fn new() -> Self { + Self { + python_venv_base: OnceCell::new(), + } + } + async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result> { + let python_path = Self::find_base_python(delegate) + .await + .ok_or_else(|| anyhow!("Could not find Python installation for PyLSP"))?; + let work_dir = delegate + .language_server_download_dir(&Self::SERVER_NAME) + .await + .ok_or_else(|| anyhow!("Could not get working directory for PyLSP"))?; + let mut path = PathBuf::from(work_dir.as_ref()); + path.push("pylsp-venv"); + if !path.exists() { + Command::new(python_path) + .arg("-m") + .arg("venv") + .arg("pylsp-venv") + .current_dir(work_dir) + .spawn()? + .output() + .await?; + } + + Ok(path.into()) + } + // Find "baseline", user python version from which we'll create our own venv. + async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option { + for path in ["python3", "python"] { + if let Some(path) = delegate.which(path.as_ref()).await { + return Some(path); + } + } + None + } + + async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result, String> { + self.python_venv_base + .get_or_init(move || async move { + Self::ensure_venv(delegate) + .await + .map_err(|e| format!("{e}")) + }) + .await + .clone() + } +} + +#[async_trait(?Send)] +impl LspAdapter for PyLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME.clone() + } + + async fn check_if_user_installed( + &self, + _: &dyn LspAdapterDelegate, + _: &AsyncAppContext, + ) -> Option { + // We don't support user-provided pylsp, as global packages are discouraged in Python ecosystem. + None + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + // let uri = "https://pypi.org/pypi/python-lsp-server/json"; + // let mut root_manifest = delegate + // .http_client() + // .get(&uri, Default::default(), true) + // .await?; + // let mut body = Vec::new(); + // root_manifest.body_mut().read_to_end(&mut body).await?; + // let as_str = String::from_utf8(body)?; + // let json = serde_json::Value::from_str(&as_str)?; + // let latest_version = json + // .get("info") + // .and_then(|info| info.get("version")) + // .and_then(|version| version.as_str().map(ToOwned::to_owned)) + // .ok_or_else(|| { + // anyhow!("PyPI response did not contain version info for python-language-server") + // })?; + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?; + let pip_path = venv.join("bin").join("pip3"); + ensure!( + Command::new(pip_path.as_path()) + .arg("install") + .arg("python-lsp-server") + .output() + .await? + .status + .success(), + "python-lsp-server installation failed" + ); + ensure!( + Command::new(pip_path.as_path()) + .arg("install") + .arg("python-lsp-server[all]") + .output() + .await? + .status + .success(), + "python-lsp-server[all] installation failed" + ); + ensure!( + Command::new(pip_path) + .arg("install") + .arg("pylsp-mypy") + .output() + .await? + .status + .success(), + "pylsp-mypy installation failed" + ); + let pylsp = venv.join("bin").join("pylsp"); + Ok(LanguageServerBinary { + path: pylsp, + env: None, + arguments: vec![], + }) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Option { + let venv = self.base_venv(delegate).await.ok()?; + let pylsp = venv.join("bin").join("pylsp"); + Some(LanguageServerBinary { + path: pylsp, + env: None, + arguments: vec![], + }) + } + + async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {} + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + let label = &item.label; + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?, + lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?, + lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + _ => return None, + }; + Some(language::CodeLabel { + text: label.clone(), + runs: vec![(0..label.len(), highlight_id)], + filter_range: 0..label.len(), + }) + } + + async fn label_for_symbol( + &self, + name: &str, + kind: lsp::SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp::SymbolKind::METHOD | lsp::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) + } + lsp::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) + } + lsp::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 { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } + + async fn workspace_configuration( + self: Arc, + adapter: &Arc, + toolchains: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + let toolchain = toolchains + .active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx) + .await; + cx.update(move |cx| { + let mut user_settings = + language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) + .and_then(|s| s.settings.clone()) + .unwrap_or_else(|| { + json!({ + "plugins": { + "rope_autoimport": {"enabled": true}, + "mypy": {"enabled": true} + } + }) + }); + + // If python.pythonPath is not set in user config, do so using our toolchain picker. + if let Some(toolchain) = toolchain { + if user_settings.is_null() { + user_settings = Value::Object(serde_json::Map::default()); + } + let object = user_settings.as_object_mut().unwrap(); + if let Some(python) = object + .entry("plugins") + .or_insert(Value::Object(serde_json::Map::default())) + .as_object_mut() + { + if let Some(jedi) = python + .entry("jedi") + .or_insert(Value::Object(serde_json::Map::default())) + .as_object_mut() + { + jedi.insert( + "environment".to_string(), + Value::String(toolchain.path.clone().into()), + ); + } + if let Some(pylint) = python + .entry("mypy") + .or_insert(Value::Object(serde_json::Map::default())) + .as_object_mut() + { + pylint.insert( + "overrides".to_string(), + Value::Array(vec![ + Value::String("--python-executable".into()), + Value::String(toolchain.path.into()), + ]), + ); + } + } + } + user_settings = Value::Object(serde_json::Map::from_iter([( + "pylsp".to_string(), + user_settings, + )])); + + user_settings + }) + } +} + #[cfg(test)] mod tests { use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};