python: Replace pyright with basedpyright (#35362)

Follow-up to #35250. Let's experiment with having this by default on
nightly.

Release Notes:

- Added built-in support for the basedpyright language server for Python
code. basedpyright is now enabled by default, and pyright (previously
the primary Python language server) remains available but is disabled by
default. This supersedes the basedpyright extension, which can be
uninstalled. Advantages of basedpyright over pyright include support for
inlay hints, semantic highlighting, auto-import code actions, and
stricter type checking. To switch back to pyright, add the following
configuration to settings.json:

```json
{
  "languages": {
    "Python": {
      "language_servers": ["pyright", "pylsp", "!basedpyright"]
    }
  }
}
```

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
This commit is contained in:
Cole Miller
2025-09-08 19:15:17 +00:00
committed by GitHub
co-authored by Piotr Osiewicz Lukas Wirth
parent 99102a84fa
commit fa0df6da1c
14 changed files with 233 additions and 67 deletions
Generated
+14 -1
View File
@@ -9181,6 +9181,19 @@ dependencies = [
"x_ai",
]
[[package]]
name = "language_onboarding"
version = "0.1.0"
dependencies = [
"db",
"editor",
"gpui",
"project",
"ui",
"workspace",
"workspace-hack",
]
[[package]]
name = "language_selector"
version = "0.1.0"
@@ -9243,7 +9256,6 @@ dependencies = [
"chrono",
"collections",
"dap",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -20428,6 +20440,7 @@ dependencies = [
"language_extension",
"language_model",
"language_models",
"language_onboarding",
"language_selector",
"language_tools",
"languages",
+2
View File
@@ -94,6 +94,7 @@ members = [
"crates/language_extension",
"crates/language_model",
"crates/language_models",
"crates/language_onboarding",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
@@ -320,6 +321,7 @@ language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_onboarding = { path = "crates/language_onboarding" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
+9
View File
@@ -590,6 +590,11 @@ pub trait LspAdapter: 'static + Send + Sync {
"Not implemented for this adapter. This method should only be called on the default JSON language server adapter"
);
}
/// True for the extension adapter and false otherwise.
fn is_extension(&self) -> bool {
false
}
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
@@ -2270,6 +2275,10 @@ impl LspAdapter for FakeLspAdapter {
let label_for_completion = self.label_for_completion.as_ref()?;
label_for_completion(item, language)
}
fn is_extension(&self) -> bool {
false
}
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
+29 -14
View File
@@ -374,14 +374,23 @@ impl LanguageRegistry {
pub fn register_available_lsp_adapter(
&self,
name: LanguageServerName,
load: impl Fn() -> Arc<dyn LspAdapter> + 'static + Send + Sync,
adapter: Arc<dyn LspAdapter>,
) {
self.state.write().available_lsp_adapters.insert(
let mut state = self.state.write();
if adapter.is_extension()
&& let Some(existing_adapter) = state.all_lsp_adapters.get(&name)
&& !existing_adapter.adapter.is_extension()
{
log::warn!(
"not registering extension-provided language server {name:?}, since a builtin language server exists with that name",
);
return;
}
state.available_lsp_adapters.insert(
name,
Arc::new(move || {
let lsp_adapter = load();
CachedLspAdapter::new(lsp_adapter)
}),
Arc::new(move || CachedLspAdapter::new(adapter.clone())),
);
}
@@ -396,13 +405,21 @@ impl LanguageRegistry {
Some(load_lsp_adapter())
}
pub fn register_lsp_adapter(
&self,
language_name: LanguageName,
adapter: Arc<dyn LspAdapter>,
) -> Arc<CachedLspAdapter> {
let cached = CachedLspAdapter::new(adapter);
pub fn register_lsp_adapter(&self, language_name: LanguageName, adapter: Arc<dyn LspAdapter>) {
let mut state = self.state.write();
if adapter.is_extension()
&& let Some(existing_adapter) = state.all_lsp_adapters.get(&adapter.name())
&& !existing_adapter.adapter.is_extension()
{
log::warn!(
"not registering extension-provided language server {:?} for language {language_name:?}, since a builtin language server exists with that name",
adapter.name(),
);
return;
}
let cached = CachedLspAdapter::new(adapter);
state
.lsp_adapters
.entry(language_name)
@@ -411,8 +428,6 @@ impl LanguageRegistry {
state
.all_lsp_adapters
.insert(cached.name.clone(), cached.clone());
cached
}
/// Register a fake language server and adapter
@@ -398,6 +398,10 @@ impl LspAdapter for ExtensionLspAdapter {
Ok(labels_from_extension(labels, language))
}
fn is_extension(&self) -> bool {
true
}
}
fn labels_from_extension(
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "language_onboarding"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/python.rs"
[features]
default = []
[dependencies]
db.workspace = true
editor.workspace = true
gpui.workspace = true
project.workspace = true
ui.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
# Uncomment other workspace dependencies as needed
# assistant.workspace = true
# client.workspace = true
# project.workspace = true
# settings.workspace = true
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
+95
View File
@@ -0,0 +1,95 @@
use db::kvp::Dismissable;
use editor::Editor;
use gpui::{Context, EventEmitter, Subscription};
use ui::{
Banner, Button, Clickable, Color, FluentBuilder as _, IconButton, IconName,
InteractiveElement as _, IntoElement, Label, LabelCommon, LabelSize, ParentElement as _,
Render, Styled as _, Window, div, h_flex, v_flex,
};
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace};
pub struct BasedPyrightBanner {
dismissed: bool,
have_basedpyright: bool,
_subscriptions: [Subscription; 1],
}
impl Dismissable for BasedPyrightBanner {
const KEY: &str = "basedpyright-banner";
}
impl BasedPyrightBanner {
pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
let subscription = cx.subscribe(workspace.project(), |this, _, event, _| {
if let project::Event::LanguageServerAdded(_, name, _) = event
&& name == "basedpyright"
{
this.have_basedpyright = true;
}
});
let dismissed = Self::dismissed();
Self {
dismissed,
have_basedpyright: false,
_subscriptions: [subscription],
}
}
}
impl EventEmitter<ToolbarItemEvent> for BasedPyrightBanner {}
impl Render for BasedPyrightBanner {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("basedpyright-banner")
.when(!self.dismissed && self.have_basedpyright, |el| {
el.child(
Banner::new()
.severity(ui::Severity::Info)
.child(
h_flex()
.gap_2()
.child(v_flex()
.child("Basedpyright is now the only default language server for Python")
.child(Label::new("We have disabled PyRight and pylsp by default. They can be re-enabled in your settings.").size(LabelSize::XSmall).color(Color::Muted))
)
.child(
Button::new("learn-more", "Learn More")
.icon(IconName::ArrowUpRight)
.on_click(|_, _, cx| {
cx.open_url("https://zed.dev/docs/languages/python")
}),
),
)
.action_slot(IconButton::new("dismiss", IconName::Close).on_click(
cx.listener(|this, _, _, cx| {
this.dismissed = true;
Self::set_dismissed(true, cx);
cx.notify();
}),
))
.into_any_element(),
)
})
}
}
impl ToolbarItemView for BasedPyrightBanner {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
_window: &mut ui::Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item
&& let Some(editor) = item.act_as::<Editor>(cx)
&& let Some(path) = editor.update(cx, |editor, cx| editor.target_file_abs_path(cx))
&& let Some(file_name) = path.file_name()
&& file_name.as_encoded_bytes().ends_with(".py".as_bytes())
{
return ToolbarItemLocation::Secondary;
}
ToolbarItemLocation::Hidden
}
}
-1
View File
@@ -42,7 +42,6 @@ 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
+10 -36
View File
@@ -1,5 +1,4 @@
use anyhow::Context as _;
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use gpui::{App, SharedString, UpdateGlobal};
use node_runtime::NodeRuntime;
use python::PyprojectTomlManifestProvider;
@@ -54,12 +53,6 @@ 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([
@@ -174,7 +167,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
},
LanguageInfo {
name: "python",
adapters: vec![python_lsp_adapter, py_lsp_adapter],
adapters: vec![basedpyright_lsp_adapter],
context: Some(python_context_provider),
toolchain: Some(python_toolchain_provider),
manifest_name: Some(SharedString::new_static("pyproject.toml").into()),
@@ -240,17 +233,6 @@ 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 && 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)
@@ -267,27 +249,19 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
// ```
languages.register_available_lsp_adapter(
LanguageServerName("tailwindcss-language-server".into()),
{
let adapter = tailwind_adapter.clone();
move || adapter.clone()
},
tailwind_adapter.clone(),
);
languages.register_available_lsp_adapter(LanguageServerName("eslint".into()), {
let adapter = eslint_adapter.clone();
move || adapter.clone()
});
languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), {
let adapter = vtsls_adapter;
move || adapter.clone()
});
languages.register_available_lsp_adapter(
LanguageServerName("eslint".into()),
eslint_adapter.clone(),
);
languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), vtsls_adapter);
languages.register_available_lsp_adapter(
LanguageServerName("typescript-language-server".into()),
{
let adapter = typescript_lsp_adapter;
move || adapter.clone()
},
typescript_lsp_adapter,
);
languages.register_available_lsp_adapter(python_lsp_adapter.name(), python_lsp_adapter);
languages.register_available_lsp_adapter(py_lsp_adapter.name(), py_lsp_adapter);
// Register Tailwind for the existing languages that should have it by default.
//
// This can be driven by the `language_servers` setting once we have a way for
+29 -15
View File
@@ -35,7 +35,7 @@ use std::{
sync::Arc,
};
use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
use util::ResultExt;
use util::{ResultExt, maybe};
pub(crate) struct PyprojectTomlManifestProvider;
@@ -1619,23 +1619,37 @@ impl LspAdapter for BasedPyrightLspAdapter {
}
}
// Always set the python interpreter path
// Get or create the python section
let python = object
// Set both pythonPath and defaultInterpreterPath for compatibility
if let Some(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),
);
{
python.insert(
"pythonPath".to_owned(),
Value::String(interpreter_path.clone()),
);
python.insert(
"defaultInterpreterPath".to_owned(),
Value::String(interpreter_path),
);
}
// Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
maybe!({
let basedpyright = object
.entry("basedpyright")
.or_insert(Value::Object(serde_json::Map::default()));
let analysis = basedpyright
.as_object_mut()?
.entry("analysis")
.or_insert(Value::Object(serde_json::Map::default()));
if let serde_json::map::Entry::Vacant(v) =
analysis.as_object_mut()?.entry("typeCheckingMode")
{
v.insert(Value::String("standard".to_owned()));
}
Some(())
});
}
user_settings
+6
View File
@@ -166,6 +166,12 @@ impl<'a> From<&'a str> for LanguageServerName {
}
}
impl PartialEq<str> for LanguageServerName {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
/// Handle to a language server RPC activity subscription.
pub enum Subscription {
Notification {
+1
View File
@@ -89,6 +89,7 @@ language.workspace = true
language_extension.workspace = true
language_model.workspace = true
language_models.workspace = true
language_onboarding.workspace = true
language_selector.workspace = true
language_tools.workspace = true
languages = { workspace = true, features = ["load-grammars"] }
+3
View File
@@ -32,6 +32,7 @@ use gpui::{
};
use image_viewer::ImageInfo;
use language::Capability;
use language_onboarding::BasedPyrightBanner;
use language_tools::lsp_button::{self, LspButton};
use language_tools::lsp_log_view::LspLogToolbarItemView;
use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
@@ -1001,6 +1002,8 @@ fn initialize_pane(
toolbar.add_item(project_diff_toolbar, window, cx);
let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
toolbar.add_item(agent_diff_toolbar, window, cx);
let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
toolbar.add_item(basedpyright_banner, window, cx);
})
});
}