lsp: Implement support for the textDocument/diagnostic
command (#19230)
Closes [#13107](https://github.com/zed-industries/zed/issues/13107) Enabled pull diagnostics by default, for the language servers that declare support in the corresponding capabilities. ``` "diagnostics": { "lsp_pull_diagnostics_debounce_ms": null } ``` settings can be used to disable the pulling. Release Notes: - Added support for the LSP `textDocument/diagnostic` command. # Brief This is draft PR that implements the LSP `textDocument/diagnostic` command. The goal is to receive your feedback and establish further steps towards fully implementing this command. I tried to re-use existing method and structures to ensure: 1. The existing functionality works as before 2. There is no interference between the diagnostics sent by a server and the diagnostics requested by a client. The current implementation is done via a new LSP command `GetDocumentDiagnostics` that is sent when a buffer is saved and when a buffer is edited. There is a new method called `pull_diagnostic` that is called for such events. It has debounce to ensure we don't spam a server with commands every time the buffer is edited. Probably, we don't need the debounce when the buffer is saved. All in all, the goal is basically to get your feedback and ensure I am on the right track. Thanks! ## References 1. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics ## In action You can clone any Ruby repo since the `ruby-lsp` supports the pull diagnostics only. Steps to reproduce: 1. Clone this repo https://github.com/vitallium/stimulus-lsp-error-zed 2. Install Ruby (via `asdf` or `mise). 4. Install Ruby gems via `bundle install` 5. Install Ruby LSP with `gem install ruby-lsp` 6. Check out this PR and build Zed 7. Open any file and start editing to see diagnostics in realtime. https://github.com/user-attachments/assets/0ef6ec41-e4fa-4539-8f2c-6be0d8be4129 --------- Co-authored-by: Kirill Bulatov <mail4score@gmail.com> Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
04cd3fcd23
commit
7aa70a4858
24 changed files with 1408 additions and 124 deletions
|
@ -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
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue