python: Add pylsp as the secondary language server (#20358)

Piotr Osiewicz created

Closes #ISSUE

Release Notes:

- Added python-lsp-server as a secondary built-in language server.

Change summary

crates/languages/src/lib.rs    |   7 
crates/languages/src/python.rs | 285 +++++++++++++++++++++++++++++++++++
2 files changed, 287 insertions(+), 5 deletions(-)

Detailed changes

crates/languages/src/lib.rs 🔗

@@ -175,9 +175,10 @@ pub fn init(languages: Arc<LanguageRegistry>, 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<dyn ToolchainLister>
     );

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<Result<Arc<Path>, 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<Arc<Path>> {
+        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<PathBuf> {
+        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<Arc<Path>, 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<LanguageServerBinary> {
+        // 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<Box<dyn 'static + Any + Send>> {
+        // 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<dyn 'static + Send + Any>,
+        _: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        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<LanguageServerBinary> {
+        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<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        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<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        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<Self>,
+        adapter: &Arc<dyn LspAdapterDelegate>,
+        toolchains: Arc<dyn LanguageToolchainStore>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Value> {
+        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};