Detailed changes
@@ -1034,6 +1034,9 @@
"button": true,
// Whether to show warnings or not by default.
"include_warnings": true,
+ // Minimum time to wait before pulling diagnostics from the language server(s).
+ // 0 turns the debounce off, `null` disables the feature.
+ "lsp_pull_diagnostics_debounce_ms": 50,
// Settings for inline diagnostics
"inline": {
// Whether to show diagnostics inline or not
@@ -312,6 +312,7 @@ impl Server {
.add_request_handler(
forward_read_only_project_request::<proto::LanguageServerIdForName>,
)
+ .add_request_handler(forward_read_only_project_request::<proto::GetDocumentDiagnostics>)
.add_request_handler(
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
)
@@ -354,6 +355,9 @@ impl Server {
.add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
.add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBases>)
+ .add_message_handler(
+ broadcast_project_message_from_host::<proto::RefreshDocumentsDiagnostics>,
+ )
.add_request_handler(get_users)
.add_request_handler(fuzzy_search_users)
.add_request_handler(request_contact)
@@ -20,8 +20,8 @@ use gpui::{
UpdateGlobal, px, size,
};
use language::{
- Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
- LineEnding, OffsetRangeExt, Point, Rope,
+ Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
+ LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
},
@@ -4237,7 +4237,8 @@ async fn test_collaborating_with_diagnostics(
message: "message 1".to_string(),
severity: lsp::DiagnosticSeverity::ERROR,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4247,7 +4248,8 @@ async fn test_collaborating_with_diagnostics(
severity: lsp::DiagnosticSeverity::WARNING,
message: "message 2".to_string(),
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
}
]
@@ -4259,7 +4261,7 @@ async fn test_collaborating_with_diagnostics(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
- diagnostics: vec![],
+ diagnostics: Vec::new(),
},
);
executor.run_until_parked();
@@ -520,7 +520,7 @@ impl Copilot {
let server = cx
.update(|cx| {
- let mut params = server.default_initialize_params(cx);
+ let mut params = server.default_initialize_params(false, cx);
params.initialization_options = Some(editor_info_json);
server.initialize(params, configuration.into(), cx)
})?
@@ -11,7 +11,7 @@ use editor::{
};
use gpui::{TestAppContext, VisualTestContext};
use indoc::indoc;
-use language::Rope;
+use language::{DiagnosticSourceKind, Rope};
use lsp::LanguageServerId;
use pretty_assertions::assert_eq;
use project::FakeFs;
@@ -105,7 +105,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
}
],
version: None
- }, &[], cx).unwrap();
+ }, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
});
// Open the project diagnostics view while there are already diagnostics.
@@ -176,6 +176,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
}],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -261,6 +262,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -368,6 +370,7 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
}],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -465,6 +468,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -507,6 +511,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -548,6 +553,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -560,6 +566,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
diagnostics: vec![],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -600,6 +607,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -732,6 +740,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
diagnostics: diagnostics.clone(),
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -919,6 +928,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
diagnostics: diagnostics.clone(),
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -974,6 +984,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1007,6 +1018,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
version: None,
diagnostics: Vec::new(),
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1088,6 +1100,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
},
],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1226,6 +1239,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1277,6 +1291,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1378,6 +1393,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
],
version: None,
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -74,8 +74,9 @@ pub use element::{
};
use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt};
use futures::{
- FutureExt,
+ FutureExt, StreamExt as _,
future::{self, Shared, join},
+ stream::FuturesUnordered,
};
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -108,9 +109,10 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
- CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
- EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
- Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
+ CursorShape, DiagnosticEntry, DiagnosticSourceKind, DiffOptions, DocumentationConfig,
+ EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language,
+ OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
+ WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -123,7 +125,7 @@ use markdown::Markdown;
use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
- BreakpointWithPosition, CompletionResponse, ProjectPath,
+ BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
@@ -1072,6 +1074,7 @@ pub struct Editor {
tasks_update_task: Option<Task<()>>,
breakpoint_store: Option<Entity<BreakpointStore>>,
gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
+ pull_diagnostics_task: Task<()>,
in_project_search: bool,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
breadcrumb_header: Option<String>,
@@ -1690,6 +1693,10 @@ impl Editor {
editor.tasks_update_task =
Some(editor.refresh_runnables(window, cx));
}
+ editor.pull_diagnostics(window, cx);
+ }
+ project::Event::RefreshDocumentsDiagnostics => {
+ editor.pull_diagnostics(window, cx);
}
project::Event::SnippetEdit(id, snippet_edits) => {
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
@@ -1792,7 +1799,7 @@ impl Editor {
code_action_providers.push(Rc::new(project) as Rc<_>);
}
- let mut this = Self {
+ let mut editor = Self {
focus_handle,
show_cursor_when_unfocused: false,
last_focused_descendant: None,
@@ -1954,6 +1961,7 @@ impl Editor {
}),
],
tasks_update_task: None,
+ pull_diagnostics_task: Task::ready(()),
linked_edit_ranges: Default::default(),
in_project_search: false,
previous_search_ranges: None,
@@ -1978,16 +1986,17 @@ impl Editor {
change_list: ChangeList::new(),
mode,
};
- if let Some(breakpoints) = this.breakpoint_store.as_ref() {
- this._subscriptions
+ if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
+ editor
+ ._subscriptions
.push(cx.observe(breakpoints, |_, _, cx| {
cx.notify();
}));
}
- this.tasks_update_task = Some(this.refresh_runnables(window, cx));
- this._subscriptions.extend(project_subscriptions);
+ editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
+ editor._subscriptions.extend(project_subscriptions);
- this._subscriptions.push(cx.subscribe_in(
+ editor._subscriptions.push(cx.subscribe_in(
&cx.entity(),
window,
|editor, _, e: &EditorEvent, window, cx| match e {
@@ -2032,14 +2041,15 @@ impl Editor {
},
));
- if let Some(dap_store) = this
+ if let Some(dap_store) = editor
.project
.as_ref()
.map(|project| project.read(cx).dap_store())
{
let weak_editor = cx.weak_entity();
- this._subscriptions
+ editor
+ ._subscriptions
.push(
cx.observe_new::<project::debugger::session::Session>(move |_, _, cx| {
let session_entity = cx.entity();
@@ -2054,40 +2064,44 @@ impl Editor {
);
for session in dap_store.read(cx).sessions().cloned().collect::<Vec<_>>() {
- this._subscriptions
+ editor
+ ._subscriptions
.push(cx.subscribe(&session, Self::on_debug_session_event));
}
}
- this.end_selection(window, cx);
- this.scroll_manager.show_scrollbars(window, cx);
- jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx);
+ editor.end_selection(window, cx);
+ editor.scroll_manager.show_scrollbars(window, cx);
+ jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
if full_mode {
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
- if this.git_blame_inline_enabled {
- this.start_git_blame_inline(false, window, cx);
+ if editor.git_blame_inline_enabled {
+ editor.start_git_blame_inline(false, window, cx);
}
- this.go_to_active_debug_line(window, cx);
+ editor.go_to_active_debug_line(window, cx);
if let Some(buffer) = buffer.read(cx).as_singleton() {
- if let Some(project) = this.project.as_ref() {
+ if let Some(project) = editor.project.as_ref() {
let handle = project.update(cx, |project, cx| {
project.register_buffer_with_language_servers(&buffer, cx)
});
- this.registered_buffers
+ editor
+ .registered_buffers
.insert(buffer.read(cx).remote_id(), handle);
}
}
- this.minimap = this.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
+ editor.minimap =
+ editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
+ editor.pull_diagnostics(window, cx);
}
- this.report_editor_event("Editor Opened", None, cx);
- this
+ editor.report_editor_event("Editor Opened", None, cx);
+ editor
}
pub fn deploy_mouse_context_menu(
@@ -15890,6 +15904,49 @@ impl Editor {
});
}
+ fn pull_diagnostics(&mut self, window: &Window, cx: &mut Context<Self>) -> Option<()> {
+ let project = self.project.as_ref()?.downgrade();
+ let debounce = Duration::from_millis(
+ ProjectSettings::get_global(cx)
+ .diagnostics
+ .lsp_pull_diagnostics_debounce_ms?,
+ );
+ let buffers = self.buffer.read(cx).all_buffers();
+
+ self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
+ cx.background_executor().timer(debounce).await;
+
+ let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
+ buffers
+ .into_iter()
+ .flat_map(|buffer| {
+ Some(project.upgrade()?.pull_diagnostics_for_buffer(buffer, cx))
+ })
+ .collect::<FuturesUnordered<_>>()
+ }) else {
+ return;
+ };
+
+ while let Some(pull_task) = pull_diagnostics_tasks.next().await {
+ match pull_task {
+ Ok(()) => {
+ if editor
+ .update_in(cx, |editor, window, cx| {
+ editor.update_diagnostics_state(window, cx);
+ })
+ .is_err()
+ {
+ return;
+ }
+ }
+ Err(e) => log::error!("Failed to update project diagnostics: {e:#}"),
+ }
+ }
+ });
+
+ Some(())
+ }
+
pub fn set_selections_from_remote(
&mut self,
selections: Vec<Selection<Anchor>>,
@@ -18603,7 +18660,7 @@ impl Editor {
match event {
multi_buffer::Event::Edited {
singleton_buffer_edited,
- edited_buffer: buffer_edited,
+ edited_buffer,
} => {
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
@@ -18614,18 +18671,25 @@ impl Editor {
if self.has_active_inline_completion() {
self.update_visible_inline_completion(window, cx);
}
- if let Some(buffer) = buffer_edited {
- let buffer_id = buffer.read(cx).remote_id();
- if !self.registered_buffers.contains_key(&buffer_id) {
- if let Some(project) = self.project.as_ref() {
- project.update(cx, |project, cx| {
- self.registered_buffers.insert(
- buffer_id,
- project.register_buffer_with_language_servers(&buffer, cx),
- );
- })
+ if let Some(project) = self.project.as_ref() {
+ project.update(cx, |project, cx| {
+ // Diagnostics are not local: an edit within one file (`pub mod foo()` -> `pub mod bar()`), may cause errors in another files with `foo()`.
+ // Hence, emit a project-wide event to pull for every buffer's diagnostics that has an open editor.
+ if edited_buffer
+ .as_ref()
+ .is_some_and(|buffer| buffer.read(cx).file().is_some())
+ {
+ cx.emit(project::Event::RefreshDocumentsDiagnostics);
}
- }
+
+ if let Some(buffer) = edited_buffer {
+ self.registered_buffers
+ .entry(buffer.read(cx).remote_id())
+ .or_insert_with(|| {
+ project.register_buffer_with_language_servers(&buffer, cx)
+ });
+ }
+ });
}
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
@@ -18744,15 +18808,19 @@ impl Editor {
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
- self.refresh_active_diagnostics(cx);
- self.refresh_inline_diagnostics(true, window, cx);
- self.scrollbar_marker_state.dirty = true;
- cx.notify();
+ self.update_diagnostics_state(window, cx);
}
_ => {}
};
}
+ fn update_diagnostics_state(&mut self, window: &mut Window, cx: &mut Context<'_, Editor>) {
+ self.refresh_active_diagnostics(cx);
+ self.refresh_inline_diagnostics(true, window, cx);
+ self.scrollbar_marker_state.dirty = true;
+ cx.notify();
+ }
+
pub fn start_temporary_diff_override(&mut self) {
self.load_diff_task.take();
self.temporary_diff_override = true;
@@ -20319,6 +20387,12 @@ pub trait SemanticsProvider {
new_name: String,
cx: &mut App,
) -> Option<Task<Result<ProjectTransaction>>>;
+
+ fn pull_diagnostics_for_buffer(
+ &self,
+ buffer: Entity<Buffer>,
+ cx: &mut App,
+ ) -> Task<anyhow::Result<()>>;
}
pub trait CompletionProvider {
@@ -20836,6 +20910,61 @@ impl SemanticsProvider for Entity<Project> {
project.perform_rename(buffer.clone(), position, new_name, cx)
}))
}
+
+ fn pull_diagnostics_for_buffer(
+ &self,
+ buffer: Entity<Buffer>,
+ cx: &mut App,
+ ) -> Task<anyhow::Result<()>> {
+ let diagnostics = self.update(cx, |project, cx| {
+ project
+ .lsp_store()
+ .update(cx, |lsp_store, cx| lsp_store.pull_diagnostics(buffer, cx))
+ });
+ let project = self.clone();
+ cx.spawn(async move |cx| {
+ let diagnostics = diagnostics.await.context("pulling diagnostics")?;
+ project.update(cx, |project, cx| {
+ project.lsp_store().update(cx, |lsp_store, cx| {
+ for diagnostics_set in diagnostics {
+ let LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics: project::PulledDiagnostics::Changed { diagnostics, .. },
+ } = diagnostics_set
+ else {
+ continue;
+ };
+
+ let adapter = lsp_store.language_server_adapter_for_id(server_id);
+ let disk_based_sources = adapter
+ .as_ref()
+ .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
+ .unwrap_or(&[]);
+ lsp_store
+ .merge_diagnostics(
+ server_id,
+ lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics,
+ version: None,
+ },
+ DiagnosticSourceKind::Pulled,
+ disk_based_sources,
+ |old_diagnostic, _| match old_diagnostic.source_kind {
+ DiagnosticSourceKind::Pulled => false,
+ DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => {
+ true
+ }
+ },
+ cx,
+ )
+ .log_err();
+ }
+ })
+ })
+ })
+ }
}
fn inlay_hint_settings(
@@ -13650,6 +13650,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
},
],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -21562,3 +21563,134 @@ fn assert_hunk_revert(
cx.assert_editor_state(expected_reverted_text_with_selections);
assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
}
+
+#[gpui::test]
+async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let diagnostic_requests = Arc::new(AtomicUsize::new(0));
+ let counter = diagnostic_requests.clone();
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/a"),
+ json!({
+ "first.rs": "fn main() { let a = 5; }",
+ "second.rs": "// Test file",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
+ let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+ let mut fake_servers = language_registry.register_fake_lsp(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
+ lsp::DiagnosticOptions {
+ identifier: None,
+ inter_file_dependencies: true,
+ workspace_diagnostics: true,
+ work_done_progress_options: Default::default(),
+ },
+ )),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
+ let editor = workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.open_abs_path(
+ PathBuf::from(path!("/a/first.rs")),
+ OpenOptions::default(),
+ window,
+ cx,
+ )
+ })
+ .unwrap()
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let fake_server = fake_servers.next().await.unwrap();
+ let mut first_request = fake_server
+ .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
+ counter.fetch_add(1, atomic::Ordering::Release);
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
+ );
+ async move {
+ Ok(lsp::DocumentDiagnosticReportResult::Report(
+ lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
+ related_documents: None,
+ full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
+ items: Vec::new(),
+ result_id: None,
+ },
+ }),
+ ))
+ }
+ });
+
+ cx.executor().advance_clock(Duration::from_millis(60));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ diagnostic_requests.load(atomic::Ordering::Acquire),
+ 1,
+ "Opening file should trigger diagnostic request"
+ );
+ first_request
+ .next()
+ .await
+ .expect("should have sent the first diagnostics pull request");
+
+ // Editing should trigger diagnostics
+ editor.update_in(cx, |editor, window, cx| {
+ editor.handle_input("2", window, cx)
+ });
+ cx.executor().advance_clock(Duration::from_millis(60));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ diagnostic_requests.load(atomic::Ordering::Acquire),
+ 2,
+ "Editing should trigger diagnostic request"
+ );
+
+ // Moving cursor should not trigger diagnostic request
+ editor.update_in(cx, |editor, window, cx| {
+ editor.change_selections(None, window, cx, |s| {
+ s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
+ });
+ });
+ cx.executor().advance_clock(Duration::from_millis(60));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ diagnostic_requests.load(atomic::Ordering::Acquire),
+ 2,
+ "Cursor movement should not trigger diagnostic request"
+ );
+
+ // Multiple rapid edits should be debounced
+ for _ in 0..5 {
+ editor.update_in(cx, |editor, window, cx| {
+ editor.handle_input("x", window, cx)
+ });
+ }
+ cx.executor().advance_clock(Duration::from_millis(60));
+ cx.executor().run_until_parked();
+
+ let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
+ assert!(
+ final_requests <= 4,
+ "Multiple rapid edits should be debounced (got {} requests)",
+ final_requests
+ );
+}
@@ -522,4 +522,12 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
None
}
+
+ fn pull_diagnostics_for_buffer(
+ &self,
+ _: Entity<Buffer>,
+ _: &mut App,
+ ) -> Task<anyhow::Result<()>> {
+ Task::ready(Ok(()))
+ }
}
@@ -229,12 +229,21 @@ pub struct Diagnostic {
pub is_disk_based: bool,
/// Whether this diagnostic marks unnecessary code.
pub is_unnecessary: bool,
+ /// Quick separation of diagnostics groups based by their source.
+ pub source_kind: DiagnosticSourceKind,
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
pub data: Option<Value>,
/// Whether to underline the corresponding text range in the editor.
pub underline: bool,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum DiagnosticSourceKind {
+ Pulled,
+ Pushed,
+ Other,
+}
+
/// An operation used to synchronize this buffer with its other replicas.
#[derive(Clone, Debug, PartialEq)]
pub enum Operation {
@@ -4636,6 +4645,7 @@ impl Default for Diagnostic {
fn default() -> Self {
Self {
source: Default::default(),
+ source_kind: DiagnosticSourceKind::Other,
code: None,
code_description: None,
severity: DiagnosticSeverity::ERROR,
@@ -1,6 +1,6 @@
//! Handles conversions of `language` items to and from the [`rpc`] protocol.
-use crate::{CursorShape, Diagnostic, diagnostic_set::DiagnosticEntry};
+use crate::{CursorShape, Diagnostic, DiagnosticSourceKind, diagnostic_set::DiagnosticEntry};
use anyhow::{Context as _, Result};
use clock::ReplicaId;
use lsp::{DiagnosticSeverity, LanguageServerId};
@@ -200,6 +200,11 @@ pub fn serialize_diagnostics<'a>(
.into_iter()
.map(|entry| proto::Diagnostic {
source: entry.diagnostic.source.clone(),
+ source_kind: match entry.diagnostic.source_kind {
+ DiagnosticSourceKind::Pulled => proto::diagnostic::SourceKind::Pulled,
+ DiagnosticSourceKind::Pushed => proto::diagnostic::SourceKind::Pushed,
+ DiagnosticSourceKind::Other => proto::diagnostic::SourceKind::Other,
+ } as i32,
start: Some(serialize_anchor(&entry.range.start)),
end: Some(serialize_anchor(&entry.range.end)),
message: entry.diagnostic.message.clone(),
@@ -431,6 +436,13 @@ pub fn deserialize_diagnostics(
is_disk_based: diagnostic.is_disk_based,
is_unnecessary: diagnostic.is_unnecessary,
underline: diagnostic.underline,
+ source_kind: match proto::diagnostic::SourceKind::from_i32(
+ diagnostic.source_kind,
+ )? {
+ proto::diagnostic::SourceKind::Pulled => DiagnosticSourceKind::Pulled,
+ proto::diagnostic::SourceKind::Pushed => DiagnosticSourceKind::Pushed,
+ proto::diagnostic::SourceKind::Other => DiagnosticSourceKind::Other,
+ },
data,
},
})
@@ -603,7 +603,7 @@ impl LanguageServer {
Ok(())
}
- pub fn default_initialize_params(&self, cx: &App) -> InitializeParams {
+ pub fn default_initialize_params(&self, pull_diagnostics: bool, cx: &App) -> InitializeParams {
let workspace_folders = self
.workspace_folders
.lock()
@@ -643,8 +643,9 @@ impl LanguageServer {
refresh_support: Some(true),
}),
diagnostic: Some(DiagnosticWorkspaceClientCapabilities {
- refresh_support: None,
- }),
+ refresh_support: Some(true),
+ })
+ .filter(|_| pull_diagnostics),
code_lens: Some(CodeLensWorkspaceClientCapabilities {
refresh_support: Some(true),
}),
@@ -793,6 +794,11 @@ impl LanguageServer {
hierarchical_document_symbol_support: Some(true),
..DocumentSymbolClientCapabilities::default()
}),
+ diagnostic: Some(DiagnosticClientCapabilities {
+ dynamic_registration: Some(false),
+ related_document_support: Some(true),
+ })
+ .filter(|_| pull_diagnostics),
..TextDocumentClientCapabilities::default()
}),
experimental: Some(json!({
@@ -1703,7 +1709,7 @@ mod tests {
let server = cx
.update(|cx| {
- let params = server.default_initialize_params(cx);
+ let params = server.default_initialize_params(false, cx);
let configuration = DidChangeConfigurationParams {
settings: Default::default(),
};
@@ -292,7 +292,7 @@ impl Prettier {
let server = cx
.update(|cx| {
- let params = server.default_initialize_params(cx);
+ let params = server.default_initialize_params(false, cx);
let configuration = lsp::DidChangeConfigurationParams {
settings: Default::default(),
};
@@ -4,14 +4,15 @@ use crate::{
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
- LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState,
+ LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectTransaction,
+ PulledDiagnostics, ResolveState,
lsp_store::{LocalLspStore, LspStore},
};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use client::proto::{self, PeerId};
use clock::Global;
-use collections::HashSet;
+use collections::{HashMap, HashSet};
use futures::future;
use gpui::{App, AsyncApp, Entity, Task};
use language::{
@@ -23,14 +24,18 @@ use language::{
range_from_lsp, range_to_lsp,
};
use lsp::{
- AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CompletionContext,
- CompletionListItemDefaultsEditRange, CompletionTriggerKind, DocumentHighlightKind,
- LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions,
- ServerCapabilities,
+ AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
+ CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
+ DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
+ OneOf, RenameOptions, ServerCapabilities,
};
+use serde_json::Value;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
-use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
+use std::{
+ cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
+};
use text::{BufferId, LineEnding};
+use util::{ResultExt as _, debug_panic};
pub use signature_help::SignatureHelp;
@@ -45,7 +50,7 @@ pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOpt
}
}
-pub(crate) fn file_path_to_lsp_url(path: &Path) -> Result<lsp::Url> {
+pub fn file_path_to_lsp_url(path: &Path) -> Result<lsp::Url> {
match lsp::Url::from_file_path(path) {
Ok(url) => Ok(url),
Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"),
@@ -254,6 +259,9 @@ pub(crate) struct LinkedEditingRange {
pub position: Anchor,
}
+#[derive(Clone, Debug)]
+pub(crate) struct GetDocumentDiagnostics {}
+
#[async_trait(?Send)]
impl LspCommand for PrepareRename {
type Response = PrepareRenameResponse;
@@ -3656,3 +3664,627 @@ impl LspCommand for LinkedEditingRange {
BufferId::new(message.buffer_id)
}
}
+
+impl GetDocumentDiagnostics {
+ fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result<lsp::Diagnostic> {
+ let start = diagnostic.start.context("invalid start range")?;
+ let end = diagnostic.end.context("invalid end range")?;
+
+ let range = Range::<PointUtf16> {
+ start: PointUtf16 {
+ row: start.row,
+ column: start.column,
+ },
+ end: PointUtf16 {
+ row: end.row,
+ column: end.column,
+ },
+ };
+
+ let data = diagnostic.data.and_then(|data| Value::from_str(&data).ok());
+ let code = diagnostic.code.map(lsp::NumberOrString::String);
+
+ let related_information = diagnostic
+ .related_information
+ .into_iter()
+ .map(|info| {
+ let start = info.location_range_start.unwrap();
+ let end = info.location_range_end.unwrap();
+
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location {
+ range: lsp::Range {
+ start: point_to_lsp(PointUtf16::new(start.row, start.column)),
+ end: point_to_lsp(PointUtf16::new(end.row, end.column)),
+ },
+ uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(),
+ },
+ message: info.message.clone(),
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let tags = diagnostic
+ .tags
+ .into_iter()
+ .filter_map(|tag| match proto::LspDiagnosticTag::from_i32(tag) {
+ Some(proto::LspDiagnosticTag::Unnecessary) => Some(lsp::DiagnosticTag::UNNECESSARY),
+ Some(proto::LspDiagnosticTag::Deprecated) => Some(lsp::DiagnosticTag::DEPRECATED),
+ _ => None,
+ })
+ .collect::<Vec<_>>();
+
+ Ok(lsp::Diagnostic {
+ range: language::range_to_lsp(range)?,
+ severity: match proto::lsp_diagnostic::Severity::from_i32(diagnostic.severity).unwrap()
+ {
+ proto::lsp_diagnostic::Severity::Error => Some(lsp::DiagnosticSeverity::ERROR),
+ proto::lsp_diagnostic::Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
+ proto::lsp_diagnostic::Severity::Information => {
+ Some(lsp::DiagnosticSeverity::INFORMATION)
+ }
+ proto::lsp_diagnostic::Severity::Hint => Some(lsp::DiagnosticSeverity::HINT),
+ _ => None,
+ },
+ code,
+ code_description: match diagnostic.code_description {
+ Some(code_description) => Some(CodeDescription {
+ href: lsp::Url::parse(&code_description).unwrap(),
+ }),
+ None => None,
+ },
+ related_information: Some(related_information),
+ tags: Some(tags),
+ source: diagnostic.source.clone(),
+ message: diagnostic.message,
+ data,
+ })
+ }
+
+ fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result<proto::LspDiagnostic> {
+ let range = language::range_from_lsp(diagnostic.range);
+ let related_information = diagnostic
+ .related_information
+ .unwrap_or_default()
+ .into_iter()
+ .map(|related_information| {
+ let location_range_start =
+ point_from_lsp(related_information.location.range.start).0;
+ let location_range_end = point_from_lsp(related_information.location.range.end).0;
+
+ Ok(proto::LspDiagnosticRelatedInformation {
+ location_url: Some(related_information.location.uri.to_string()),
+ location_range_start: Some(proto::PointUtf16 {
+ row: location_range_start.row,
+ column: location_range_start.column,
+ }),
+ location_range_end: Some(proto::PointUtf16 {
+ row: location_range_end.row,
+ column: location_range_end.column,
+ }),
+ message: related_information.message,
+ })
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ let tags = diagnostic
+ .tags
+ .unwrap_or_default()
+ .into_iter()
+ .map(|tag| match tag {
+ lsp::DiagnosticTag::UNNECESSARY => proto::LspDiagnosticTag::Unnecessary,
+ lsp::DiagnosticTag::DEPRECATED => proto::LspDiagnosticTag::Deprecated,
+ _ => proto::LspDiagnosticTag::None,
+ } as i32)
+ .collect();
+
+ Ok(proto::LspDiagnostic {
+ start: Some(proto::PointUtf16 {
+ row: range.start.0.row,
+ column: range.start.0.column,
+ }),
+ end: Some(proto::PointUtf16 {
+ row: range.end.0.row,
+ column: range.end.0.column,
+ }),
+ severity: match diagnostic.severity {
+ Some(lsp::DiagnosticSeverity::ERROR) => proto::lsp_diagnostic::Severity::Error,
+ Some(lsp::DiagnosticSeverity::WARNING) => proto::lsp_diagnostic::Severity::Warning,
+ Some(lsp::DiagnosticSeverity::INFORMATION) => {
+ proto::lsp_diagnostic::Severity::Information
+ }
+ Some(lsp::DiagnosticSeverity::HINT) => proto::lsp_diagnostic::Severity::Hint,
+ _ => proto::lsp_diagnostic::Severity::None,
+ } as i32,
+ code: diagnostic.code.as_ref().map(|code| match code {
+ lsp::NumberOrString::Number(code) => code.to_string(),
+ lsp::NumberOrString::String(code) => code.clone(),
+ }),
+ source: diagnostic.source.clone(),
+ related_information,
+ tags,
+ code_description: diagnostic
+ .code_description
+ .map(|desc| desc.href.to_string()),
+ message: diagnostic.message,
+ data: diagnostic.data.as_ref().map(|data| data.to_string()),
+ })
+ }
+}
+
+#[async_trait(?Send)]
+impl LspCommand for GetDocumentDiagnostics {
+ type Response = Vec<LspPullDiagnostics>;
+ type LspRequest = lsp::request::DocumentDiagnosticRequest;
+ type ProtoRequest = proto::GetDocumentDiagnostics;
+
+ fn display_name(&self) -> &str {
+ "Get diagnostics"
+ }
+
+ fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
+ server_capabilities
+ .server_capabilities
+ .diagnostic_provider
+ .is_some()
+ }
+
+ fn to_lsp(
+ &self,
+ path: &Path,
+ _: &Buffer,
+ language_server: &Arc<LanguageServer>,
+ _: &App,
+ ) -> Result<lsp::DocumentDiagnosticParams> {
+ let identifier = match language_server.capabilities().diagnostic_provider {
+ Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier,
+ Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => {
+ options.diagnostic_options.identifier
+ }
+ None => None,
+ };
+
+ Ok(lsp::DocumentDiagnosticParams {
+ text_document: lsp::TextDocumentIdentifier {
+ uri: file_path_to_lsp_url(path)?,
+ },
+ identifier,
+ previous_result_id: None,
+ partial_result_params: Default::default(),
+ work_done_progress_params: Default::default(),
+ })
+ }
+
+ async fn response_from_lsp(
+ self,
+ message: lsp::DocumentDiagnosticReportResult,
+ _: Entity<LspStore>,
+ buffer: Entity<Buffer>,
+ server_id: LanguageServerId,
+ cx: AsyncApp,
+ ) -> Result<Self::Response> {
+ let url = buffer.read_with(&cx, |buffer, cx| {
+ buffer
+ .file()
+ .and_then(|file| file.as_local())
+ .map(|file| {
+ let abs_path = file.abs_path(cx);
+ file_path_to_lsp_url(&abs_path)
+ })
+ .transpose()?
+ .with_context(|| format!("missing url on buffer {}", buffer.remote_id()))
+ })??;
+
+ let mut pulled_diagnostics = HashMap::default();
+ match message {
+ lsp::DocumentDiagnosticReportResult::Report(report) => match report {
+ lsp::DocumentDiagnosticReport::Full(report) => {
+ if let Some(related_documents) = report.related_documents {
+ process_related_documents(
+ &mut pulled_diagnostics,
+ server_id,
+ related_documents,
+ );
+ }
+ process_full_diagnostics_report(
+ &mut pulled_diagnostics,
+ server_id,
+ url,
+ report.full_document_diagnostic_report,
+ );
+ }
+ lsp::DocumentDiagnosticReport::Unchanged(report) => {
+ if let Some(related_documents) = report.related_documents {
+ process_related_documents(
+ &mut pulled_diagnostics,
+ server_id,
+ related_documents,
+ );
+ }
+ process_unchanged_diagnostics_report(
+ &mut pulled_diagnostics,
+ server_id,
+ url,
+ report.unchanged_document_diagnostic_report,
+ );
+ }
+ },
+ lsp::DocumentDiagnosticReportResult::Partial(report) => {
+ if let Some(related_documents) = report.related_documents {
+ process_related_documents(
+ &mut pulled_diagnostics,
+ server_id,
+ related_documents,
+ );
+ }
+ }
+ }
+
+ Ok(pulled_diagnostics.into_values().collect())
+ }
+
+ fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentDiagnostics {
+ proto::GetDocumentDiagnostics {
+ project_id,
+ buffer_id: buffer.remote_id().into(),
+ version: serialize_version(&buffer.version()),
+ }
+ }
+
+ async fn from_proto(
+ message: proto::GetDocumentDiagnostics,
+ _: Entity<LspStore>,
+ buffer: Entity<Buffer>,
+ mut cx: AsyncApp,
+ ) -> Result<Self> {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })?
+ .await?;
+ Ok(Self {})
+ }
+
+ fn response_to_proto(
+ response: Self::Response,
+ _: &mut LspStore,
+ _: PeerId,
+ _: &clock::Global,
+ _: &mut App,
+ ) -> proto::GetDocumentDiagnosticsResponse {
+ let pulled_diagnostics = response
+ .into_iter()
+ .filter_map(|diagnostics| match diagnostics {
+ LspPullDiagnostics::Default => None,
+ LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics,
+ } => {
+ let mut changed = false;
+ let (diagnostics, result_id) = match diagnostics {
+ PulledDiagnostics::Unchanged { result_id } => (Vec::new(), Some(result_id)),
+ PulledDiagnostics::Changed {
+ result_id,
+ diagnostics,
+ } => {
+ changed = true;
+ (diagnostics, result_id)
+ }
+ };
+ Some(proto::PulledDiagnostics {
+ changed,
+ result_id,
+ uri: uri.to_string(),
+ server_id: server_id.to_proto(),
+ diagnostics: diagnostics
+ .into_iter()
+ .filter_map(|diagnostic| {
+ GetDocumentDiagnostics::serialize_lsp_diagnostic(diagnostic)
+ .context("serializing diagnostics")
+ .log_err()
+ })
+ .collect(),
+ })
+ }
+ })
+ .collect();
+
+ proto::GetDocumentDiagnosticsResponse { pulled_diagnostics }
+ }
+
+ async fn response_from_proto(
+ self,
+ response: proto::GetDocumentDiagnosticsResponse,
+ _: Entity<LspStore>,
+ _: Entity<Buffer>,
+ _: AsyncApp,
+ ) -> Result<Self::Response> {
+ let pulled_diagnostics = response
+ .pulled_diagnostics
+ .into_iter()
+ .filter_map(|diagnostics| {
+ Some(LspPullDiagnostics::Response {
+ server_id: LanguageServerId::from_proto(diagnostics.server_id),
+ uri: lsp::Url::from_str(diagnostics.uri.as_str()).log_err()?,
+ diagnostics: if diagnostics.changed {
+ PulledDiagnostics::Unchanged {
+ result_id: diagnostics.result_id?,
+ }
+ } else {
+ PulledDiagnostics::Changed {
+ result_id: diagnostics.result_id,
+ diagnostics: diagnostics
+ .diagnostics
+ .into_iter()
+ .filter_map(|diagnostic| {
+ GetDocumentDiagnostics::deserialize_lsp_diagnostic(diagnostic)
+ .context("deserializing diagnostics")
+ .log_err()
+ })
+ .collect(),
+ }
+ },
+ })
+ })
+ .collect();
+
+ Ok(pulled_diagnostics)
+ }
+
+ fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result<BufferId> {
+ BufferId::new(message.buffer_id)
+ }
+}
+
+fn process_related_documents(
+ diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
+ server_id: LanguageServerId,
+ documents: impl IntoIterator<Item = (lsp::Url, lsp::DocumentDiagnosticReportKind)>,
+) {
+ for (url, report_kind) in documents {
+ match report_kind {
+ lsp::DocumentDiagnosticReportKind::Full(report) => {
+ process_full_diagnostics_report(diagnostics, server_id, url, report)
+ }
+ lsp::DocumentDiagnosticReportKind::Unchanged(report) => {
+ process_unchanged_diagnostics_report(diagnostics, server_id, url, report)
+ }
+ }
+ }
+}
+
+fn process_unchanged_diagnostics_report(
+ diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
+ server_id: LanguageServerId,
+ uri: lsp::Url,
+ report: lsp::UnchangedDocumentDiagnosticReport,
+) {
+ let result_id = report.result_id;
+ match diagnostics.entry(uri.clone()) {
+ hash_map::Entry::Occupied(mut o) => match o.get_mut() {
+ LspPullDiagnostics::Default => {
+ o.insert(LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics: PulledDiagnostics::Unchanged { result_id },
+ });
+ }
+ LspPullDiagnostics::Response {
+ server_id: existing_server_id,
+ uri: existing_uri,
+ diagnostics: existing_diagnostics,
+ } => {
+ if server_id != *existing_server_id || &uri != existing_uri {
+ debug_panic!(
+ "Unexpected state: file {uri} has two different sets of diagnostics reported"
+ );
+ }
+ match existing_diagnostics {
+ PulledDiagnostics::Unchanged { .. } => {
+ *existing_diagnostics = PulledDiagnostics::Unchanged { result_id };
+ }
+ PulledDiagnostics::Changed { .. } => {}
+ }
+ }
+ },
+ hash_map::Entry::Vacant(v) => {
+ v.insert(LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics: PulledDiagnostics::Unchanged { result_id },
+ });
+ }
+ }
+}
+
+fn process_full_diagnostics_report(
+ diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
+ server_id: LanguageServerId,
+ uri: lsp::Url,
+ report: lsp::FullDocumentDiagnosticReport,
+) {
+ let result_id = report.result_id;
+ match diagnostics.entry(uri.clone()) {
+ hash_map::Entry::Occupied(mut o) => match o.get_mut() {
+ LspPullDiagnostics::Default => {
+ o.insert(LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics: PulledDiagnostics::Changed {
+ result_id,
+ diagnostics: report.items,
+ },
+ });
+ }
+ LspPullDiagnostics::Response {
+ server_id: existing_server_id,
+ uri: existing_uri,
+ diagnostics: existing_diagnostics,
+ } => {
+ if server_id != *existing_server_id || &uri != existing_uri {
+ debug_panic!(
+ "Unexpected state: file {uri} has two different sets of diagnostics reported"
+ );
+ }
+ match existing_diagnostics {
+ PulledDiagnostics::Unchanged { .. } => {
+ *existing_diagnostics = PulledDiagnostics::Changed {
+ result_id,
+ diagnostics: report.items,
+ };
+ }
+ PulledDiagnostics::Changed {
+ result_id: existing_result_id,
+ diagnostics: existing_diagnostics,
+ } => {
+ if result_id.is_some() {
+ *existing_result_id = result_id;
+ }
+ existing_diagnostics.extend(report.items);
+ }
+ }
+ }
+ },
+ hash_map::Entry::Vacant(v) => {
+ v.insert(LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics: PulledDiagnostics::Changed {
+ result_id,
+ diagnostics: report.items,
+ },
+ });
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lsp::{DiagnosticSeverity, DiagnosticTag};
+ use serde_json::json;
+
+ #[test]
+ fn test_serialize_lsp_diagnostic() {
+ let lsp_diagnostic = lsp::Diagnostic {
+ range: lsp::Range {
+ start: lsp::Position::new(0, 1),
+ end: lsp::Position::new(2, 3),
+ },
+ severity: Some(DiagnosticSeverity::ERROR),
+ code: Some(lsp::NumberOrString::String("E001".to_string())),
+ source: Some("test-source".to_string()),
+ message: "Test error message".to_string(),
+ related_information: None,
+ tags: Some(vec![DiagnosticTag::DEPRECATED]),
+ code_description: None,
+ data: Some(json!({"detail": "test detail"})),
+ };
+
+ let proto_diagnostic =
+ GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone())
+ .expect("Failed to serialize diagnostic");
+
+ let start = proto_diagnostic.start.unwrap();
+ let end = proto_diagnostic.end.unwrap();
+ assert_eq!(start.row, 0);
+ assert_eq!(start.column, 1);
+ assert_eq!(end.row, 2);
+ assert_eq!(end.column, 3);
+ assert_eq!(
+ proto_diagnostic.severity,
+ proto::lsp_diagnostic::Severity::Error as i32
+ );
+ assert_eq!(proto_diagnostic.code, Some("E001".to_string()));
+ assert_eq!(proto_diagnostic.source, Some("test-source".to_string()));
+ assert_eq!(proto_diagnostic.message, "Test error message");
+ }
+
+ #[test]
+ fn test_deserialize_lsp_diagnostic() {
+ let proto_diagnostic = proto::LspDiagnostic {
+ start: Some(proto::PointUtf16 { row: 0, column: 1 }),
+ end: Some(proto::PointUtf16 { row: 2, column: 3 }),
+ severity: proto::lsp_diagnostic::Severity::Warning as i32,
+ code: Some("ERR".to_string()),
+ source: Some("Prism".to_string()),
+ message: "assigned but unused variable - a".to_string(),
+ related_information: vec![],
+ tags: vec![],
+ code_description: None,
+ data: None,
+ };
+
+ let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic)
+ .expect("Failed to deserialize diagnostic");
+
+ assert_eq!(lsp_diagnostic.range.start.line, 0);
+ assert_eq!(lsp_diagnostic.range.start.character, 1);
+ assert_eq!(lsp_diagnostic.range.end.line, 2);
+ assert_eq!(lsp_diagnostic.range.end.character, 3);
+ assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING));
+ assert_eq!(
+ lsp_diagnostic.code,
+ Some(lsp::NumberOrString::String("ERR".to_string()))
+ );
+ assert_eq!(lsp_diagnostic.source, Some("Prism".to_string()));
+ assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a");
+ }
+
+ #[test]
+ fn test_related_information() {
+ let related_info = lsp::DiagnosticRelatedInformation {
+ location: lsp::Location {
+ uri: lsp::Url::parse("file:///test.rs").unwrap(),
+ range: lsp::Range {
+ start: lsp::Position::new(1, 1),
+ end: lsp::Position::new(1, 5),
+ },
+ },
+ message: "Related info message".to_string(),
+ };
+
+ let lsp_diagnostic = lsp::Diagnostic {
+ range: lsp::Range {
+ start: lsp::Position::new(0, 0),
+ end: lsp::Position::new(0, 1),
+ },
+ severity: Some(DiagnosticSeverity::INFORMATION),
+ code: None,
+ source: Some("Prism".to_string()),
+ message: "assigned but unused variable - a".to_string(),
+ related_information: Some(vec![related_info]),
+ tags: None,
+ code_description: None,
+ data: None,
+ };
+
+ let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic)
+ .expect("Failed to serialize diagnostic");
+
+ assert_eq!(proto_diagnostic.related_information.len(), 1);
+ let related = &proto_diagnostic.related_information[0];
+ assert_eq!(related.location_url, Some("file:///test.rs".to_string()));
+ assert_eq!(related.message, "Related info message");
+ }
+
+ #[test]
+ fn test_invalid_ranges() {
+ let proto_diagnostic = proto::LspDiagnostic {
+ start: None,
+ end: Some(proto::PointUtf16 { row: 2, column: 3 }),
+ severity: proto::lsp_diagnostic::Severity::Error as i32,
+ code: None,
+ source: None,
+ message: "Test message".to_string(),
+ related_information: vec![],
+ tags: vec![],
+ code_description: None,
+ data: None,
+ };
+
+ let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic);
+ assert!(result.is_err());
+ }
+}
@@ -4,7 +4,8 @@ pub mod rust_analyzer_ext;
use crate::{
CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
- LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
+ LspAction, LspPullDiagnostics, ProjectItem, ProjectPath, ProjectTransaction, ResolveState,
+ Symbol, ToolchainStore,
buffer_store::{BufferStore, BufferStoreEvent},
environment::ProjectEnvironment,
lsp_command::{self, *},
@@ -39,9 +40,9 @@ use http_client::HttpClient;
use itertools::Itertools as _;
use language::{
Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
- DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, LanguageName, LanguageRegistry,
- LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
- TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+ DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName,
+ LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch,
+ PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
},
@@ -252,6 +253,10 @@ impl LocalLspStore {
let this = self.weak.clone();
let pending_workspace_folders = pending_workspace_folders.clone();
let fs = self.fs.clone();
+ let pull_diagnostics = ProjectSettings::get_global(cx)
+ .diagnostics
+ .lsp_pull_diagnostics_debounce_ms
+ .is_some();
cx.spawn(async move |cx| {
let result = async {
let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?;
@@ -282,7 +287,8 @@ impl LocalLspStore {
}
let initialization_params = cx.update(|cx| {
- let mut params = language_server.default_initialize_params(cx);
+ let mut params =
+ language_server.default_initialize_params(pull_diagnostics, cx);
params.initialization_options = initialization_options;
adapter.adapter.prepare_initialize_params(params, cx)
})??;
@@ -474,8 +480,14 @@ impl LocalLspStore {
this.merge_diagnostics(
server_id,
params,
+ DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources,
- |diagnostic, cx| adapter.retain_old_diagnostic(diagnostic, cx),
+ |diagnostic, cx| match diagnostic.source_kind {
+ DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => {
+ adapter.retain_old_diagnostic(diagnostic, cx)
+ }
+ DiagnosticSourceKind::Pulled => true,
+ },
cx,
)
.log_err();
@@ -851,6 +863,28 @@ impl LocalLspStore {
})
.detach();
+ language_server
+ .on_request::<lsp::request::WorkspaceDiagnosticRefresh, _, _>({
+ let this = this.clone();
+ move |(), cx| {
+ let this = this.clone();
+ let mut cx = cx.clone();
+ async move {
+ this.update(&mut cx, |this, cx| {
+ cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
+ this.downstream_client.as_ref().map(|(client, project_id)| {
+ client.send(proto::RefreshDocumentsDiagnostics {
+ project_id: *project_id,
+ })
+ })
+ })?
+ .transpose()?;
+ Ok(())
+ }
+ }
+ })
+ .detach();
+
language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = this.clone();
@@ -1869,8 +1903,7 @@ impl LocalLspStore {
);
}
- let uri = lsp::Url::from_file_path(abs_path)
- .map_err(|()| anyhow!("failed to convert abs path to uri"))?;
+ let uri = file_path_to_lsp_url(abs_path)?;
let text_document = lsp::TextDocumentIdentifier::new(uri);
let lsp_edits = {
@@ -1934,8 +1967,7 @@ impl LocalLspStore {
let logger = zlog::scoped!("lsp_format");
zlog::info!(logger => "Formatting via LSP");
- let uri = lsp::Url::from_file_path(abs_path)
- .map_err(|()| anyhow!("failed to convert abs path to uri"))?;
+ let uri = file_path_to_lsp_url(abs_path)?;
let text_document = lsp::TextDocumentIdentifier::new(uri);
let capabilities = &language_server.capabilities();
@@ -2262,7 +2294,7 @@ impl LocalLspStore {
}
let abs_path = file.abs_path(cx);
- let Some(uri) = lsp::Url::from_file_path(&abs_path).log_err() else {
+ let Some(uri) = file_path_to_lsp_url(&abs_path).log_err() else {
return;
};
let initial_snapshot = buffer.text_snapshot();
@@ -3447,6 +3479,7 @@ pub enum LspStoreEvent {
edits: Vec<(lsp::Range, Snippet)>,
most_recent_edit: clock::Lamport,
},
+ RefreshDocumentsDiagnostics,
}
#[derive(Clone, Debug, Serialize)]
@@ -3494,6 +3527,7 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers);
client.add_entity_request_handler(Self::handle_rename_project_entry);
client.add_entity_request_handler(Self::handle_language_server_id_for_name);
+ client.add_entity_request_handler(Self::handle_refresh_documents_diagnostics);
client.add_entity_request_handler(Self::handle_lsp_command::<GetCodeActions>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetCompletions>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetHover>);
@@ -3521,6 +3555,7 @@ impl LspStore {
client.add_entity_request_handler(
Self::handle_lsp_command::<lsp_ext_command::SwitchSourceHeader>,
);
+ client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentDiagnostics>);
}
pub fn as_remote(&self) -> Option<&RemoteLspStore> {
@@ -4043,8 +4078,7 @@ impl LspStore {
.contains_key(&buffer.read(cx).remote_id())
{
if let Some(file_url) =
- lsp::Url::from_file_path(&f.abs_path(cx))
- .log_err()
+ file_path_to_lsp_url(&f.abs_path(cx)).log_err()
{
local.unregister_buffer_from_language_servers(
&buffer, &file_url, cx,
@@ -4148,7 +4182,7 @@ impl LspStore {
if let Some(abs_path) =
File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx))
{
- if let Some(file_url) = lsp::Url::from_file_path(&abs_path).log_err() {
+ if let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() {
local_store.unregister_buffer_from_language_servers(
buffer_entity,
&file_url,
@@ -5674,6 +5708,73 @@ impl LspStore {
}
}
+ pub fn pull_diagnostics(
+ &mut self,
+ buffer_handle: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Vec<LspPullDiagnostics>>> {
+ let buffer = buffer_handle.read(cx);
+ let buffer_id = buffer.remote_id();
+
+ if let Some((client, upstream_project_id)) = self.upstream_client() {
+ let request_task = client.request(proto::MultiLspQuery {
+ buffer_id: buffer_id.into(),
+ version: serialize_version(&buffer_handle.read(cx).version()),
+ project_id: upstream_project_id,
+ strategy: Some(proto::multi_lsp_query::Strategy::All(
+ proto::AllLanguageServers {},
+ )),
+ request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
+ GetDocumentDiagnostics {}.to_proto(upstream_project_id, buffer_handle.read(cx)),
+ )),
+ });
+ let buffer = buffer_handle.clone();
+ cx.spawn(async move |weak_project, cx| {
+ let Some(project) = weak_project.upgrade() else {
+ return Ok(Vec::new());
+ };
+ let responses = request_task.await?.responses;
+ let diagnostics = join_all(
+ responses
+ .into_iter()
+ .filter_map(|lsp_response| match lsp_response.response? {
+ proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
+ response,
+ ) => Some(response),
+ unexpected => {
+ debug_panic!("Unexpected response: {unexpected:?}");
+ None
+ }
+ })
+ .map(|diagnostics_response| {
+ GetDocumentDiagnostics {}.response_from_proto(
+ diagnostics_response,
+ project.clone(),
+ buffer.clone(),
+ cx.clone(),
+ )
+ }),
+ )
+ .await;
+
+ Ok(diagnostics
+ .into_iter()
+ .collect::<Result<Vec<_>>>()?
+ .into_iter()
+ .flatten()
+ .collect())
+ })
+ } else {
+ let all_actions_task = self.request_multiple_lsp_locally(
+ &buffer_handle,
+ None::<PointUtf16>,
+ GetDocumentDiagnostics {},
+ cx,
+ );
+ cx.spawn(async move |_, _| Ok(all_actions_task.await.into_iter().flatten().collect()))
+ }
+ }
+
pub fn inlay_hints(
&mut self,
buffer_handle: Entity<Buffer>,
@@ -6218,7 +6319,7 @@ impl LspStore {
let worktree_id = file.worktree_id(cx);
let abs_path = file.as_local()?.abs_path(cx);
let text_document = lsp::TextDocumentIdentifier {
- uri: lsp::Url::from_file_path(abs_path).log_err()?,
+ uri: file_path_to_lsp_url(&abs_path).log_err()?,
};
let local = self.as_local()?;
@@ -6525,15 +6626,15 @@ impl LspStore {
path: relative_path.into(),
};
- if let Some(buffer) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
+ if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
let snapshot = self
.as_local_mut()
.unwrap()
- .buffer_snapshot_for_lsp_version(&buffer, server_id, version, cx)?;
+ .buffer_snapshot_for_lsp_version(&buffer_handle, server_id, version, cx)?;
+ let buffer = buffer_handle.read(cx);
diagnostics.extend(
buffer
- .read(cx)
.get_diagnostics(server_id)
.into_iter()
.flat_map(|diag| {
@@ -6549,7 +6650,7 @@ impl LspStore {
);
self.as_local_mut().unwrap().update_buffer_diagnostics(
- &buffer,
+ &buffer_handle,
server_id,
version,
diagnostics.clone(),
@@ -7071,6 +7172,47 @@ impl LspStore {
.collect(),
})
}
+ Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
+ get_document_diagnostics,
+ )) => {
+ let get_document_diagnostics = GetDocumentDiagnostics::from_proto(
+ get_document_diagnostics,
+ this.clone(),
+ buffer.clone(),
+ cx.clone(),
+ )
+ .await?;
+
+ let all_diagnostics = this
+ .update(&mut cx, |project, cx| {
+ project.request_multiple_lsp_locally(
+ &buffer,
+ None::<PointUtf16>,
+ get_document_diagnostics,
+ cx,
+ )
+ })?
+ .await
+ .into_iter();
+
+ this.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
+ responses: all_diagnostics
+ .map(|lsp_diagnostic| proto::LspResponse {
+ response: Some(
+ proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
+ GetDocumentDiagnostics::response_to_proto(
+ lsp_diagnostic,
+ project,
+ sender_id,
+ &buffer_version,
+ cx,
+ ),
+ ),
+ ),
+ })
+ .collect(),
+ })
+ }
None => anyhow::bail!("empty multi lsp query request"),
}
}
@@ -7671,7 +7813,7 @@ impl LspStore {
PathEventKind::Changed => lsp::FileChangeType::CHANGED,
};
Some(lsp::FileEvent {
- uri: lsp::Url::from_file_path(&event.path).ok()?,
+ uri: file_path_to_lsp_url(&event.path).log_err()?,
typ,
})
})
@@ -7997,6 +8139,17 @@ impl LspStore {
Ok(proto::Ack {})
}
+ async fn handle_refresh_documents_diagnostics(
+ this: Entity<Self>,
+ _: TypedEnvelope<proto::RefreshDocumentsDiagnostics>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ this.update(&mut cx, |_, cx| {
+ cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
+ })?;
+ Ok(proto::Ack {})
+ }
+
async fn handle_inlay_hints(
this: Entity<Self>,
envelope: TypedEnvelope<proto::InlayHints>,
@@ -8719,12 +8872,14 @@ impl LspStore {
&mut self,
language_server_id: LanguageServerId,
params: lsp::PublishDiagnosticsParams,
+ source_kind: DiagnosticSourceKind,
disk_based_sources: &[String],
cx: &mut Context<Self>,
) -> Result<()> {
self.merge_diagnostics(
language_server_id,
params,
+ source_kind,
disk_based_sources,
|_, _| false,
cx,
@@ -8735,6 +8890,7 @@ impl LspStore {
&mut self,
language_server_id: LanguageServerId,
mut params: lsp::PublishDiagnosticsParams,
+ source_kind: DiagnosticSourceKind,
disk_based_sources: &[String],
filter: F,
cx: &mut Context<Self>,
@@ -8799,6 +8955,7 @@ impl LspStore {
range,
diagnostic: Diagnostic {
source: diagnostic.source.clone(),
+ source_kind,
code: diagnostic.code.clone(),
code_description: diagnostic
.code_description
@@ -8825,6 +8982,7 @@ impl LspStore {
range,
diagnostic: Diagnostic {
source: diagnostic.source.clone(),
+ source_kind,
code: diagnostic.code.clone(),
code_description: diagnostic
.code_description
@@ -2,7 +2,7 @@ use std::sync::Arc;
use ::serde::{Deserialize, Serialize};
use gpui::WeakEntity;
-use language::{CachedLspAdapter, Diagnostic};
+use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind};
use lsp::LanguageServer;
use util::ResultExt as _;
@@ -84,6 +84,7 @@ pub fn register_notifications(
this.merge_diagnostics(
server_id,
mapped_diagnostics,
+ DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources,
|diag, _| !is_inactive_region(diag),
cx,
@@ -1,8 +1,9 @@
use crate::{
LocationLink,
lsp_command::{
- LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
- location_links_from_lsp, location_links_from_proto, location_links_to_proto,
+ LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto,
+ location_link_to_proto, location_links_from_lsp, location_links_from_proto,
+ location_links_to_proto,
},
lsp_store::LspStore,
make_lsp_text_document_position, make_text_document_identifier,
@@ -584,10 +585,7 @@ impl LspCommand for GetLspRunnables {
_: &Arc<LanguageServer>,
_: &App,
) -> Result<RunnablesParams> {
- let url = match lsp::Url::from_file_path(path) {
- Ok(url) => url,
- Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
- };
+ let url = file_path_to_lsp_url(path)?;
Ok(RunnablesParams {
text_document: lsp::TextDocumentIdentifier::new(url),
position: self
@@ -72,9 +72,9 @@ use gpui::{
};
use itertools::Itertools;
use language::{
- Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
- LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
- Unclipped, language_settings::InlayHintKind, proto::split_operations,
+ Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiagnosticSourceKind, Language,
+ LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList,
+ Transaction, Unclipped, language_settings::InlayHintKind, proto::split_operations,
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
@@ -317,6 +317,7 @@ pub enum Event {
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
AgentLocationChanged,
+ RefreshDocumentsDiagnostics,
}
pub struct AgentLocationChanged;
@@ -861,6 +862,34 @@ pub const DEFAULT_COMPLETION_CONTEXT: CompletionContext = CompletionContext {
trigger_character: None,
};
+/// An LSP diagnostics associated with a certain language server.
+#[derive(Clone, Debug, Default)]
+pub enum LspPullDiagnostics {
+ #[default]
+ Default,
+ Response {
+ /// The id of the language server that produced diagnostics.
+ server_id: LanguageServerId,
+ /// URI of the resource,
+ uri: lsp::Url,
+ /// The diagnostics produced by this language server.
+ diagnostics: PulledDiagnostics,
+ },
+}
+
+#[derive(Clone, Debug)]
+pub enum PulledDiagnostics {
+ Unchanged {
+ /// An ID the current pulled batch for this file.
+ /// If given, can be used to query workspace diagnostics partially.
+ result_id: String,
+ },
+ Changed {
+ result_id: Option<String>,
+ diagnostics: Vec<lsp::Diagnostic>,
+ },
+}
+
impl Project {
pub fn init_settings(cx: &mut App) {
WorktreeSettings::register(cx);
@@ -2785,6 +2814,9 @@ impl Project {
}
LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
+ LspStoreEvent::RefreshDocumentsDiagnostics => {
+ cx.emit(Event::RefreshDocumentsDiagnostics)
+ }
LspStoreEvent::LanguageServerPrompt(prompt) => {
cx.emit(Event::LanguageServerPrompt(prompt.clone()))
}
@@ -3686,6 +3718,35 @@ impl Project {
})
}
+ pub fn document_diagnostics(
+ &mut self,
+ buffer_handle: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Vec<LspPullDiagnostics>>> {
+ self.lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store.pull_diagnostics(buffer_handle, cx)
+ })
+ }
+
+ pub fn update_diagnostics(
+ &mut self,
+ language_server_id: LanguageServerId,
+ source_kind: DiagnosticSourceKind,
+ params: lsp::PublishDiagnosticsParams,
+ disk_based_sources: &[String],
+ cx: &mut Context<Self>,
+ ) -> Result<(), anyhow::Error> {
+ self.lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store.update_diagnostics(
+ language_server_id,
+ params,
+ source_kind,
+ disk_based_sources,
+ cx,
+ )
+ })
+ }
+
pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
let (result_tx, result_rx) = smol::channel::unbounded();
@@ -127,6 +127,10 @@ pub struct DiagnosticsSettings {
/// Whether or not to include warning diagnostics.
pub include_warnings: bool,
+ /// Minimum time to wait before pulling diagnostics from the language server(s).
+ /// 0 turns the debounce off, None disables the feature.
+ pub lsp_pull_diagnostics_debounce_ms: Option<u64>,
+
/// Settings for showing inline diagnostics.
pub inline: InlineDiagnosticsSettings,
@@ -209,8 +213,9 @@ impl Default for DiagnosticsSettings {
Self {
button: true,
include_warnings: true,
- inline: Default::default(),
- cargo: Default::default(),
+ lsp_pull_diagnostics_debounce_ms: Some(30),
+ inline: InlineDiagnosticsSettings::default(),
+ cargo: None,
}
}
}
@@ -1332,6 +1332,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1349,6 +1350,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1439,6 +1441,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1456,6 +1459,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
+ DiagnosticSourceKind::Pushed,
&[],
cx,
)
@@ -1633,7 +1637,8 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
message: "undefined variable 'A'".to_string(),
group_id: 0,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
}]
)
@@ -2149,7 +2154,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 1,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
},
DiagnosticEntry {
@@ -2161,7 +2167,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 2,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
}
]
@@ -2227,7 +2234,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 4,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -2239,7 +2247,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 3,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
}
]
@@ -2319,7 +2328,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 6,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -2331,7 +2341,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
is_disk_based: true,
group_id: 5,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
}
]
@@ -2372,7 +2383,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 1".to_string(),
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
},
DiagnosticEntry {
@@ -2381,7 +2393,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 2".to_string(),
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
},
],
@@ -2435,7 +2448,8 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
severity: DiagnosticSeverity::ERROR,
is_primary: true,
message: "syntax error a1".to_string(),
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
}],
cx,
@@ -2452,7 +2466,8 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
severity: DiagnosticSeverity::ERROR,
is_primary: true,
message: "syntax error b1".to_string(),
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
},
}],
cx,
@@ -4578,7 +4593,13 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
lsp_store
.update(cx, |lsp_store, cx| {
- lsp_store.update_diagnostics(LanguageServerId(0), message, &[], cx)
+ lsp_store.update_diagnostics(
+ LanguageServerId(0),
+ message,
+ DiagnosticSourceKind::Pushed,
+ &[],
+ cx,
+ )
})
.unwrap();
let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
@@ -4595,7 +4616,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1".to_string(),
group_id: 1,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4605,7 +4627,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1 hint 1".to_string(),
group_id: 1,
is_primary: false,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4615,7 +4638,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 1".to_string(),
group_id: 0,
is_primary: false,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4625,7 +4649,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 2".to_string(),
group_id: 0,
is_primary: false,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4635,7 +4660,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2".to_string(),
group_id: 0,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
}
]
@@ -4651,7 +4677,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 1".to_string(),
group_id: 0,
is_primary: false,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4661,7 +4688,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2 hint 2".to_string(),
group_id: 0,
is_primary: false,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4671,7 +4699,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 2".to_string(),
group_id: 0,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
}
]
@@ -4687,7 +4716,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1".to_string(),
group_id: 1,
is_primary: true,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
DiagnosticEntry {
@@ -4697,7 +4727,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
message: "error 1 hint 1".to_string(),
group_id: 1,
is_primary: false,
- ..Default::default()
+ source_kind: DiagnosticSourceKind::Pushed,
+ ..Diagnostic::default()
}
},
]
@@ -251,6 +251,14 @@ message Diagnostic {
Anchor start = 1;
Anchor end = 2;
optional string source = 3;
+
+ enum SourceKind {
+ Pulled = 0;
+ Pushed = 1;
+ Other = 2;
+ }
+
+ SourceKind source_kind = 16;
Severity severity = 4;
string message = 5;
optional string code = 6;
@@ -678,6 +678,7 @@ message MultiLspQuery {
GetCodeActions get_code_actions = 6;
GetSignatureHelp get_signature_help = 7;
GetCodeLens get_code_lens = 8;
+ GetDocumentDiagnostics get_document_diagnostics = 9;
}
}
@@ -703,6 +704,7 @@ message LspResponse {
GetCodeActionsResponse get_code_actions_response = 2;
GetSignatureHelpResponse get_signature_help_response = 3;
GetCodeLensResponse get_code_lens_response = 4;
+ GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
}
}
@@ -749,3 +751,59 @@ message LspExtClearFlycheck {
uint64 buffer_id = 2;
uint64 language_server_id = 3;
}
+
+message LspDiagnosticRelatedInformation {
+ optional string location_url = 1;
+ PointUtf16 location_range_start = 2;
+ PointUtf16 location_range_end = 3;
+ string message = 4;
+}
+
+enum LspDiagnosticTag {
+ None = 0;
+ Unnecessary = 1;
+ Deprecated = 2;
+}
+
+message LspDiagnostic {
+ PointUtf16 start = 1;
+ PointUtf16 end = 2;
+ Severity severity = 3;
+ optional string code = 4;
+ optional string code_description = 5;
+ optional string source = 6;
+ string message = 7;
+ repeated LspDiagnosticRelatedInformation related_information = 8;
+ repeated LspDiagnosticTag tags = 9;
+ optional string data = 10;
+
+ enum Severity {
+ None = 0;
+ Error = 1;
+ Warning = 2;
+ Information = 3;
+ Hint = 4;
+ }
+}
+
+message GetDocumentDiagnostics {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ repeated VectorClockEntry version = 3;
+}
+
+message GetDocumentDiagnosticsResponse {
+ repeated PulledDiagnostics pulled_diagnostics = 1;
+}
+
+message PulledDiagnostics {
+ uint64 server_id = 1;
+ string uri = 2;
+ optional string result_id = 3;
+ bool changed = 4;
+ repeated LspDiagnostic diagnostics = 5;
+}
+
+message RefreshDocumentsDiagnostics {
+ uint64 project_id = 1;
+}
@@ -387,7 +387,12 @@ message Envelope {
LspExtRunFlycheck lsp_ext_run_flycheck = 346;
LspExtClearFlycheck lsp_ext_clear_flycheck = 347;
- LogToDebugConsole log_to_debug_console = 348; // current max
+ LogToDebugConsole log_to_debug_console = 348;
+
+ GetDocumentDiagnostics get_document_diagnostics = 350;
+ GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351;
+ RefreshDocumentsDiagnostics refresh_documents_diagnostics = 352; // current max
+
}
reserved 87 to 88;
@@ -307,6 +307,9 @@ messages!(
(RunDebugLocators, Background),
(DebugRequest, Background),
(LogToDebugConsole, Background),
+ (GetDocumentDiagnostics, Background),
+ (GetDocumentDiagnosticsResponse, Background),
+ (RefreshDocumentsDiagnostics, Background)
);
request_messages!(
@@ -469,6 +472,8 @@ request_messages!(
(ToggleBreakpoint, Ack),
(GetDebugAdapterBinary, DebugAdapterBinary),
(RunDebugLocators, DebugRequest),
+ (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
+ (RefreshDocumentsDiagnostics, Ack)
);
entity_messages!(
@@ -595,6 +600,8 @@ entity_messages!(
RunDebugLocators,
GetDebugAdapterBinary,
LogToDebugConsole,
+ GetDocumentDiagnostics,
+ RefreshDocumentsDiagnostics
);
entity_messages!(
@@ -2401,7 +2401,7 @@ impl Workspace {
})
}
})
- .log_err()?;
+ .ok()?;
None
} else {
Some(
@@ -2414,7 +2414,7 @@ impl Workspace {
cx,
)
})
- .log_err()?
+ .ok()?
.await,
)
}
@@ -3111,7 +3111,7 @@ impl Workspace {
window.spawn(cx, async move |cx| {
let (project_entry_id, build_item) = task.await?;
let result = pane.update_in(cx, |pane, window, cx| {
- let result = pane.open_item(
+ pane.open_item(
project_entry_id,
project_path,
focus_item,
@@ -3121,9 +3121,7 @@ impl Workspace {
window,
cx,
build_item,
- );
-
- result
+ )
});
result
})