diff --git a/Cargo.lock b/Cargo.lock
index 80c510d491..1682b80a8c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9226,6 +9226,7 @@ dependencies = [
"chrono",
"collections",
"dap",
+ "feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg
new file mode 100644
index 0000000000..bc7a8376d1
--- /dev/null
+++ b/assets/icons/cloud_download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 8f57fb1a20..3c877873a0 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -1774,7 +1774,7 @@ impl Editor {
) -> Self {
debug_assert!(
display_map.is_none() || mode.is_minimap(),
- "Providing a display map for a new editor is only intended for the minimap and might have unindended side effects otherwise!"
+ "Providing a display map for a new editor is only intended for the minimap and might have unintended side effects otherwise!"
);
let full_mode = mode.is_full();
@@ -8235,8 +8235,7 @@ impl Editor {
return;
};
- // Try to find a closest, enclosing node using tree-sitter that has a
- // task
+ // Try to find a closest, enclosing node using tree-sitter that has a task
let Some((buffer, buffer_row, tasks)) = self
.find_enclosing_node_task(cx)
// Or find the task that's closest in row-distance.
@@ -21835,11 +21834,11 @@ impl CodeActionProvider for Entity {
cx: &mut App,
) -> Task>> {
self.update(cx, |project, cx| {
- let code_lens = project.code_lens(buffer, range.clone(), cx);
+ let code_lens_actions = project.code_lens_actions(buffer, range.clone(), cx);
let code_actions = project.code_actions(buffer, range, None, cx);
cx.background_spawn(async move {
- let (code_lens, code_actions) = join(code_lens, code_actions).await;
- Ok(code_lens
+ let (code_lens_actions, code_actions) = join(code_lens_actions, code_actions).await;
+ Ok(code_lens_actions
.context("code lens fetch")?
.into_iter()
.chain(code_actions.context("code action fetch")?)
diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs
index a0333bb494..a13708c580 100644
--- a/crates/editor/src/editor_tests.rs
+++ b/crates/editor/src/editor_tests.rs
@@ -10072,8 +10072,14 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
);
}
-#[gpui::test]
-async fn test_range_format_during_save(cx: &mut TestAppContext) {
+async fn setup_range_format_test(
+ cx: &mut TestAppContext,
+) -> (
+ Entity,
+ Entity,
+ &mut gpui::VisualTestContext,
+ lsp::FakeLanguageServer,
+) {
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
@@ -10088,9 +10094,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
- ..Default::default()
+ ..lsp::ServerCapabilities::default()
},
- ..Default::default()
+ ..FakeLspAdapter::default()
},
);
@@ -10105,14 +10111,22 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
let (editor, cx) = cx.add_window_view(|window, cx| {
build_editor_with_project(project.clone(), buffer, window, cx)
});
+
+ cx.executor().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+
+ (project, editor, cx, fake_server)
+}
+
+#[gpui::test]
+async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
+ let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
+
editor.update_in(cx, |editor, window, cx| {
editor.set_text("one\ntwo\nthree\n", window, cx)
});
assert!(cx.read(|cx| editor.is_dirty(cx)));
- cx.executor().start_waiting();
- let fake_server = fake_servers.next().await.unwrap();
-
let save = editor
.update_in(cx, |editor, window, cx| {
editor.save(
@@ -10147,13 +10161,18 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
"one, two\nthree\n"
);
assert!(!cx.read(|cx| editor.is_dirty(cx)));
+}
+
+#[gpui::test]
+async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
+ let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
editor.update_in(cx, |editor, window, cx| {
editor.set_text("one\ntwo\nthree\n", window, cx)
});
assert!(cx.read(|cx| editor.is_dirty(cx)));
- // Ensure we can still save even if formatting hangs.
+ // Test that save still works when formatting hangs
fake_server.set_request_handler::(
move |params, _| async move {
assert_eq!(
@@ -10185,8 +10204,13 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
"one\ntwo\nthree\n"
);
assert!(!cx.read(|cx| editor.is_dirty(cx)));
+}
- // For non-dirty buffer, no formatting request should be sent
+#[gpui::test]
+async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
+ let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
+
+ // Buffer starts clean, no formatting should be requested
let save = editor
.update_in(cx, |editor, window, cx| {
editor.save(
@@ -10207,6 +10231,12 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
.next();
cx.executor().start_waiting();
save.await;
+ cx.run_until_parked();
+}
+
+#[gpui::test]
+async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
+ let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
// Set Rust language override and assert overridden tabsize is sent to language server
update_test_language_settings(cx, |settings| {
@@ -10220,7 +10250,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
});
editor.update_in(cx, |editor, window, cx| {
- editor.set_text("somehting_new\n", window, cx)
+ editor.set_text("something_new\n", window, cx)
});
assert!(cx.read(|cx| editor.is_dirty(cx)));
let save = editor
@@ -21310,16 +21340,32 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
},
);
- let (buffer, _handle) = project
- .update(cx, |p, cx| {
- p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
+ let editor = workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.open_abs_path(
+ PathBuf::from(path!("/dir/a.ts")),
+ OpenOptions::default(),
+ window,
+ cx,
+ )
})
+ .unwrap()
.await
+ .unwrap()
+ .downcast::()
.unwrap();
cx.executor().run_until_parked();
let fake_server = fake_language_servers.next().await.unwrap();
+ let buffer = editor.update(cx, |editor, cx| {
+ editor
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .expect("have opened a single file by path")
+ });
+
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
drop(buffer_snapshot);
@@ -21377,7 +21423,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
assert_eq!(
actions.len(),
1,
- "Should have only one valid action for the 0..0 range"
+ "Should have only one valid action for the 0..0 range, got: {actions:#?}"
);
let action = actions[0].clone();
let apply = project.update(cx, |project, cx| {
@@ -21423,7 +21469,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
.into_iter()
.collect(),
),
- ..Default::default()
+ ..lsp::WorkspaceEdit::default()
},
},
)
@@ -21446,6 +21492,38 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
buffer.undo(cx);
assert_eq!(buffer.text(), "a");
});
+
+ let actions_after_edits = cx
+ .update_window(*workspace, |_, window, cx| {
+ project.code_actions(&buffer, anchor..anchor, window, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ actions, actions_after_edits,
+ "For the same selection, same code lens actions should be returned"
+ );
+
+ let _responses =
+ fake_server.set_request_handler::(|_, _| async move {
+ panic!("No more code lens requests are expected");
+ });
+ editor.update_in(cx, |editor, window, cx| {
+ editor.select_all(&SelectAll, window, cx);
+ });
+ cx.executor().run_until_parked();
+ let new_actions = cx
+ .update_window(*workspace, |_, window, cx| {
+ project.code_actions(&buffer, anchor..anchor, window, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ actions, new_actions,
+ "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
+ );
}
#[gpui::test]
diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs
index ce07dd43fe..08cf9078f2 100644
--- a/crates/editor/src/lsp_colors.rs
+++ b/crates/editor/src/lsp_colors.rs
@@ -6,7 +6,7 @@ use gpui::{Hsla, Rgba};
use itertools::Itertools;
use language::point_from_lsp;
use multi_buffer::Anchor;
-use project::{DocumentColor, lsp_store::ColorFetchStrategy};
+use project::{DocumentColor, lsp_store::LspFetchStrategy};
use settings::Settings as _;
use text::{Bias, BufferId, OffsetRangeExt as _};
use ui::{App, Context, Window};
@@ -180,9 +180,9 @@ impl Editor {
.filter_map(|buffer| {
let buffer_id = buffer.read(cx).remote_id();
let fetch_strategy = if ignore_cache {
- ColorFetchStrategy::IgnoreCache
+ LspFetchStrategy::IgnoreCache
} else {
- ColorFetchStrategy::UseCache {
+ LspFetchStrategy::UseCache {
known_cache_version: self.colors.as_ref().and_then(|colors| {
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
}),
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index e7066ae151..7552060be4 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -71,6 +71,7 @@ pub enum IconName {
CircleHelp,
Close,
Cloud,
+ CloudDownload,
Code,
Cog,
Command,
diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs
index 1df33286ee..7cda2b4b5a 100644
--- a/crates/language/src/language.rs
+++ b/crates/language/src/language.rs
@@ -313,6 +313,15 @@ impl Attach {
}
}
+/// Determines what gets sent out as a workspace folders content
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum WorkspaceFoldersContent {
+ /// Send out a single entry with the root of the workspace.
+ WorktreeRoot,
+ /// Send out a list of subproject roots.
+ SubprojectRoots,
+}
+
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
// e.g. to display a notification or fetch data from the web.
#[async_trait]
@@ -606,6 +615,13 @@ pub trait LspAdapter: 'static + Send + Sync {
Attach::Shared
}
+ /// Determines whether a language server supports workspace folders.
+ ///
+ /// And does not trip over itself in the process.
+ fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+ WorkspaceFoldersContent::SubprojectRoots
+ }
+
fn manifest_name(&self) -> Option {
None
}
diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs
index d1a90d7dbb..2b0e13f4be 100644
--- a/crates/language_tools/src/lsp_log.rs
+++ b/crates/language_tools/src/lsp_log.rs
@@ -867,7 +867,7 @@ impl LspLogView {
BINARY = server.binary(),
WORKSPACE_FOLDERS = server
.workspace_folders()
- .iter()
+ .into_iter()
.filter_map(|path| path
.to_file_path()
.ok()
diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml
index 2e8f007cff..260126da63 100644
--- a/crates/languages/Cargo.toml
+++ b/crates/languages/Cargo.toml
@@ -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
diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs
index a224111002..001fd15200 100644
--- a/crates/languages/src/lib.rs
+++ b/crates/languages/src/lib.rs
@@ -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> =
))
});
+struct BasedPyrightFeatureFlag;
+
+impl FeatureFlag for BasedPyrightFeatureFlag {
+ const NAME: &'static str = "basedpyright";
+}
+
pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) {
#[cfg(feature = "load-grammars")]
languages.register_native_grammars([
@@ -88,6 +95,7 @@ pub fn init(languages: Arc, 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, node: NodeRuntime, cx: &mut App) {
);
}
+ let mut basedpyright_lsp_adapter = Some(basedpyright_lsp_adapter);
+ cx.observe_flag::({
+ 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)
diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs
index dc6996d399..4a0cc7078b 100644
--- a/crates/languages/src/python.rs
+++ b/crates/languages/src/python.rs
@@ -4,13 +4,13 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
-use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::language_settings::language_settings;
use language::{ContextLocation, LanguageToolchainStore};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
+use language::{Toolchain, WorkspaceFoldersContent};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
@@ -400,6 +400,9 @@ impl LspAdapter for PythonLspAdapter {
fn manifest_name(&self) -> Option {
Some(SharedString::new_static("pyproject.toml").into())
}
+ fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+ WorkspaceFoldersContent::WorktreeRoot
+ }
}
async fn get_cached_server_binary(
@@ -1282,6 +1285,346 @@ impl LspAdapter for PyLspAdapter {
fn manifest_name(&self) -> Option {
Some(SharedString::new_static("pyproject.toml").into())
}
+ fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+ WorkspaceFoldersContent::WorktreeRoot
+ }
+}
+
+pub(crate) struct BasedPyrightLspAdapter {
+ python_venv_base: OnceCell, 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> {
+ 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 {
+ 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 BasedPyrightLspAdapter {
+ fn name(&self) -> LanguageServerName {
+ Self::SERVER_NAME.clone()
+ }
+
+ async fn initialization_options(
+ self: Arc,
+ _: &dyn Fs,
+ _: &Arc,
+ ) -> Result