python: Add pylsp as the secondary language server (#20358)
Closes #ISSUE Release Notes: - Added python-lsp-server as a secondary built-in language server.
This commit is contained in:
parent
181c3724aa
commit
d1e2c6e657
2 changed files with 287 additions and 5 deletions
|
@ -175,9 +175,10 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
|
||||||
language!("markdown-inline");
|
language!("markdown-inline");
|
||||||
language!(
|
language!(
|
||||||
"python",
|
"python",
|
||||||
vec![Arc::new(python::PythonLspAdapter::new(
|
vec![
|
||||||
node_runtime.clone(),
|
Arc::new(python::PythonLspAdapter::new(node_runtime.clone(),)),
|
||||||
))],
|
Arc::new(python::PyLspAdapter::new())
|
||||||
|
],
|
||||||
PythonContextProvider,
|
PythonContextProvider,
|
||||||
Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
|
Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::ensure;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
|
@ -16,7 +17,8 @@ use pet_core::os_environment::Environment;
|
||||||
use pet_core::python_environment::PythonEnvironmentKind;
|
use pet_core::python_environment::PythonEnvironmentKind;
|
||||||
use pet_core::Configuration;
|
use pet_core::Configuration;
|
||||||
use project::lsp_store::language_server_settings;
|
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::sync::Mutex;
|
||||||
use std::{
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
|
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue