Cargo.lock 🔗
@@ -9226,6 +9226,7 @@ dependencies = [
"chrono",
"collections",
"dap",
+ "feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
Cole Miller created
This PR adds a built-in adapter for the basedpyright language server.
For now, it's behind the `basedpyright` feature flag, and needs to be
requested explicitly like this for staff:
```
"languages": {
"Python": {
"language_servers": ["basedpyright", "!pylsp", "!pyright"]
}
}
```
(After uninstalling the basedpyright extension.)
Release Notes:
- N/A
Cargo.lock | 1
crates/languages/Cargo.toml | 1
crates/languages/src/lib.rs | 24 ++
crates/languages/src/python.rs | 337 ++++++++++++++++++++++++++++++++++++
4 files changed, 362 insertions(+), 1 deletion(-)
@@ -9226,6 +9226,7 @@ dependencies = [
"chrono",
"collections",
"dap",
+ "feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -41,6 +41,7 @@ async-trait.workspace = true
chrono.workspace = true
collections.workspace = true
dap.workspace = true
+feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
@@ -1,4 +1,5 @@
use anyhow::Context as _;
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use gpui::{App, UpdateGlobal};
use node_runtime::NodeRuntime;
use python::PyprojectTomlManifestProvider;
@@ -11,7 +12,7 @@ use util::{ResultExt, asset_str};
pub use language::*;
-use crate::json::JsonTaskProvider;
+use crate::{json::JsonTaskProvider, python::BasedPyrightLspAdapter};
mod bash;
mod c;
@@ -52,6 +53,12 @@ pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
))
});
+struct BasedPyrightFeatureFlag;
+
+impl FeatureFlag for BasedPyrightFeatureFlag {
+ const NAME: &'static str = "basedpyright";
+}
+
pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
#[cfg(feature = "load-grammars")]
languages.register_native_grammars([
@@ -88,6 +95,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let py_lsp_adapter = Arc::new(python::PyLspAdapter::new());
let python_context_provider = Arc::new(python::PythonContextProvider);
let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone()));
+ let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new());
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default());
let rust_context_provider = Arc::new(rust::RustContextProvider);
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
@@ -228,6 +236,20 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
);
}
+ let mut basedpyright_lsp_adapter = Some(basedpyright_lsp_adapter);
+ cx.observe_flag::<BasedPyrightFeatureFlag, _>({
+ let languages = languages.clone();
+ move |enabled, _| {
+ if enabled {
+ if let Some(adapter) = basedpyright_lsp_adapter.take() {
+ languages
+ .register_available_lsp_adapter(adapter.name(), move || adapter.clone());
+ }
+ }
+ }
+ })
+ .detach();
+
// Register globally available language servers.
//
// This will allow users to add support for a built-in language server (e.g., Tailwind)
@@ -1290,6 +1290,343 @@ impl LspAdapter for PyLspAdapter {
}
}
+pub(crate) struct BasedPyrightLspAdapter {
+ python_venv_base: OnceCell<Result<Arc<Path>, String>>,
+}
+
+impl BasedPyrightLspAdapter {
+ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
+ const BINARY_NAME: &'static str = "basedpyright-langserver";
+
+ 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
+ .context("Could not find Python installation for basedpyright")?;
+ let work_dir = delegate
+ .language_server_download_dir(&Self::SERVER_NAME)
+ .await
+ .context("Could not get working directory for basedpyright")?;
+ let mut path = PathBuf::from(work_dir.as_ref());
+ path.push("basedpyright-venv");
+ if !path.exists() {
+ util::command::new_smol_command(python_path)
+ .arg("-m")
+ .arg("venv")
+ .arg("basedpyright-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 BasedPyrightLspAdapter {
+ fn name(&self) -> LanguageServerName {
+ Self::SERVER_NAME.clone()
+ }
+
+ async fn initialization_options(
+ self: Arc<Self>,
+ _: &dyn Fs,
+ _: &Arc<dyn LspAdapterDelegate>,
+ ) -> Result<Option<Value>> {
+ // Provide minimal initialization options
+ // Virtual environment configuration will be handled through workspace configuration
+ Ok(Some(json!({
+ "python": {
+ "analysis": {
+ "autoSearchPaths": true,
+ "useLibraryCodeForTypes": true,
+ "autoImportCompletions": true
+ }
+ }
+ })))
+ }
+
+ async fn check_if_user_installed(
+ &self,
+ delegate: &dyn LspAdapterDelegate,
+ toolchains: Arc<dyn LanguageToolchainStore>,
+ cx: &AsyncApp,
+ ) -> Option<LanguageServerBinary> {
+ if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
+ let env = delegate.shell_env().await;
+ Some(LanguageServerBinary {
+ path: bin,
+ env: Some(env),
+ arguments: vec!["--stdio".into()],
+ })
+ } else {
+ let venv = toolchains
+ .active_toolchain(
+ delegate.worktree_id(),
+ Arc::from("".as_ref()),
+ LanguageName::new("Python"),
+ &mut cx.clone(),
+ )
+ .await?;
+ let path = Path::new(venv.path.as_ref())
+ .parent()?
+ .join(Self::BINARY_NAME);
+ path.exists().then(|| LanguageServerBinary {
+ path,
+ arguments: vec!["--stdio".into()],
+ env: None,
+ })
+ }
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(()) as Box<_>)
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _latest_version: Box<dyn 'static + Send + Any>,
+ _container_dir: PathBuf,
+ delegate: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
+ let pip_path = venv.join(BINARY_DIR).join("pip3");
+ ensure!(
+ util::command::new_smol_command(pip_path.as_path())
+ .arg("install")
+ .arg("basedpyright")
+ .arg("-U")
+ .output()
+ .await?
+ .status
+ .success(),
+ "basedpyright installation failed"
+ );
+ let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
+ Ok(LanguageServerBinary {
+ path: pylsp,
+ env: None,
+ arguments: vec!["--stdio".into()],
+ })
+ }
+
+ async fn cached_server_binary(
+ &self,
+ _container_dir: PathBuf,
+ delegate: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ let venv = self.base_venv(delegate).await.ok()?;
+ let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
+ Some(LanguageServerBinary {
+ path: pylsp,
+ env: None,
+ arguments: vec!["--stdio".into()],
+ })
+ }
+
+ async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
+ // 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.
+ //
+ // Because the symbol name is included, there generally are not ties when
+ // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
+ // into account. Here, we remove the symbol name from the sortText in order
+ // to allow our own fuzzy score to be used to break ties.
+ //
+ // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
+ for item in items {
+ let Some(sort_text) = &mut item.sort_text else {
+ continue;
+ };
+ let mut parts = sort_text.split('.');
+ let Some(first) = parts.next() else { continue };
+ let Some(second) = parts.next() else { continue };
+ let Some(_) = parts.next() else { continue };
+ sort_text.replace_range(first.len() + second.len() + 1.., "");
+ }
+ }
+
+ 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,
+ };
+ let filter_range = item
+ .filter_text
+ .as_deref()
+ .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
+ .unwrap_or(0..label.len());
+ Some(language::CodeLabel {
+ text: label.clone(),
+ runs: vec![(0..label.len(), highlight_id)],
+ filter_range,
+ })
+ }
+
+ 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>,
+ _: &dyn Fs,
+ adapter: &Arc<dyn LspAdapterDelegate>,
+ toolchains: Arc<dyn LanguageToolchainStore>,
+ cx: &mut AsyncApp,
+ ) -> Result<Value> {
+ let toolchain = toolchains
+ .active_toolchain(
+ adapter.worktree_id(),
+ Arc::from("".as_ref()),
+ 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_default();
+
+ // If we have a detected toolchain, configure Pyright to use it
+ 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();
+
+ let interpreter_path = toolchain.path.to_string();
+
+ // Detect if this is a virtual environment
+ if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() {
+ if let Some(venv_dir) = interpreter_dir.parent() {
+ // Check if this looks like a virtual environment
+ if venv_dir.join("pyvenv.cfg").exists()
+ || venv_dir.join("bin/activate").exists()
+ || venv_dir.join("Scripts/activate.bat").exists()
+ {
+ // Set venvPath and venv at the root level
+ // This matches the format of a pyrightconfig.json file
+ if let Some(parent) = venv_dir.parent() {
+ // Use relative path if the venv is inside the workspace
+ let venv_path = if parent == adapter.worktree_root_path() {
+ ".".to_string()
+ } else {
+ parent.to_string_lossy().into_owned()
+ };
+ object.insert("venvPath".to_string(), Value::String(venv_path));
+ }
+
+ if let Some(venv_name) = venv_dir.file_name() {
+ object.insert(
+ "venv".to_owned(),
+ Value::String(venv_name.to_string_lossy().into_owned()),
+ );
+ }
+ }
+ }
+ }
+
+ // Always set the python interpreter path
+ // Get or create the python section
+ let python = object
+ .entry("python")
+ .or_insert(Value::Object(serde_json::Map::default()))
+ .as_object_mut()
+ .unwrap();
+
+ // Set both pythonPath and defaultInterpreterPath for compatibility
+ python.insert(
+ "pythonPath".to_owned(),
+ Value::String(interpreter_path.clone()),
+ );
+ python.insert(
+ "defaultInterpreterPath".to_owned(),
+ Value::String(interpreter_path),
+ );
+ }
+
+ user_settings
+ })
+ }
+
+ fn manifest_name(&self) -> Option<ManifestName> {
+ Some(SharedString::new_static("pyproject.toml").into())
+ }
+}
+
#[cfg(test)]
mod tests {
use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};