diff --git a/Cargo.lock b/Cargo.lock index 75e353144d..5c9408f013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,6 +1410,17 @@ dependencies = [ "const-oid", ] +[[package]] +name = "diagnostics" +version = "0.1.0" +dependencies = [ + "editor", + "gpui", + "postage", + "project", + "workspace", +] + [[package]] name = "digest" version = "0.8.1" diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml new file mode 100644 index 0000000000..6f9979a22e --- /dev/null +++ b/crates/diagnostics/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "diagnostics" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/diagnostics.rs" + +[dependencies] +editor = { path = "../editor" } +gpui = { path = "../gpui" } +project = { path = "../project" } +workspace = { path = "../workspace" } +postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs new file mode 100644 index 0000000000..83bc074f18 --- /dev/null +++ b/crates/diagnostics/src/diagnostics.rs @@ -0,0 +1,45 @@ +use editor::{Editor, MultiBuffer}; +use gpui::{elements::*, Entity, ModelHandle, RenderContext, View, ViewContext, ViewHandle}; +use postage::watch; +use project::Project; + +struct ProjectDiagnostics { + editor: ViewHandle, + project: ModelHandle, +} + +impl ProjectDiagnostics { + fn new( + project: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let mut buffer = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id(cx))); + for (path, diagnostics) in project.read(cx).diagnostics(cx) {} + + Self { + editor: cx.add_view(|cx| { + Editor::for_buffer( + buffer.clone(), + editor::settings_builder(buffer.downgrade(), settings), + cx, + ) + }), + project, + } + } +} + +impl Entity for ProjectDiagnostics { + type Event = (); +} + +impl View for ProjectDiagnostics { + fn ui_name() -> &'static str { + "ProjectDiagnostics" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + ChildView::new(self.editor.id()).boxed() + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 211585b9ad..52bc739d07 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15,10 +15,11 @@ pub use element::*; use gpui::{ action, elements::Text, + fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, WeakViewHandle, + MutableAppContext, RenderContext, View, ViewContext, WeakModelHandle, WeakViewHandle, }; use items::BufferItemHandle; use language::{ @@ -29,6 +30,7 @@ pub use multi_buffer::MultiBuffer; use multi_buffer::{ Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, ToOffset, ToPoint, }; +use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; @@ -3787,6 +3789,48 @@ pub fn diagnostic_style( } } +pub fn settings_builder( + buffer: WeakModelHandle, + settings: watch::Receiver, +) -> impl Fn(&AppContext) -> EditorSettings { + move |cx| { + let settings = settings.borrow(); + let font_cache = cx.font_cache(); + let font_family_id = settings.buffer_font_family; + let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); + let font_properties = Default::default(); + let font_id = font_cache + .select_font(font_family_id, &font_properties) + .unwrap(); + let font_size = settings.buffer_font_size; + + let mut theme = settings.theme.editor.clone(); + theme.text = TextStyle { + color: theme.text.color, + font_family_name, + font_family_id, + font_id, + font_size, + font_properties, + underline: None, + }; + let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language(cx)); + let soft_wrap = match settings.soft_wrap(language) { + workspace::settings::SoftWrap::None => SoftWrap::None, + workspace::settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + workspace::settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length(language).saturating_sub(1)) + } + }; + + EditorSettings { + tab_size: settings.tab_size, + soft_wrap, + style: theme, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6989cc0fcc..0a4ddc8af9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,10 +1,9 @@ -use crate::{Editor, EditorSettings, Event}; +use crate::{Editor, Event}; use crate::{MultiBuffer, ToPoint as _}; use anyhow::Result; use gpui::{ - elements::*, fonts::TextStyle, AppContext, Entity, ModelContext, ModelHandle, - MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, - WeakModelHandle, + elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, + Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, }; use language::{Diagnostic, File as _}; use postage::watch; @@ -13,8 +12,7 @@ use std::fmt::Write; use std::path::Path; use text::{Point, Selection}; use workspace::{ - settings, EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, - WeakItemHandle, + EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle, }; pub struct BufferOpener; @@ -53,42 +51,7 @@ impl ItemHandle for BufferItemHandle { Box::new(cx.add_view(window_id, |cx| { Editor::for_buffer( self.0.clone(), - move |cx| { - let settings = settings.borrow(); - let font_cache = cx.font_cache(); - let font_family_id = settings.buffer_font_family; - let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); - let font_properties = Default::default(); - let font_id = font_cache - .select_font(font_family_id, &font_properties) - .unwrap(); - let font_size = settings.buffer_font_size; - - let mut theme = settings.theme.editor.clone(); - theme.text = TextStyle { - color: theme.text.color, - font_family_name, - font_family_id, - font_id, - font_size, - font_properties, - underline: None, - }; - let language = buffer.upgrade(cx).and_then(|buf| buf.read(cx).language(cx)); - let soft_wrap = match settings.soft_wrap(language) { - settings::SoftWrap::None => crate::SoftWrap::None, - settings::SoftWrap::EditorWidth => crate::SoftWrap::EditorWidth, - settings::SoftWrap::PreferredLineLength => crate::SoftWrap::Column( - settings.preferred_line_length(language).saturating_sub(1), - ), - }; - - EditorSettings { - tab_size: settings.tab_size, - soft_wrap, - style: theme, - } - }, + crate::settings_builder(buffer, settings), cx, ) })) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 120f1b710f..4b518467aa 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -87,6 +87,8 @@ pub struct BufferSnapshot { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Diagnostic { + pub source: Option, + pub code: Option, pub severity: DiagnosticSeverity, pub message: String, pub group_id: usize, @@ -720,7 +722,7 @@ impl Buffer { pub fn update_diagnostics( &mut self, version: Option, - mut diagnostics: Vec, + mut diagnostics: Vec>, cx: &mut ModelContext, ) -> Result { diagnostics.sort_unstable_by_key(|d| (d.range.start, d.range.end)); @@ -736,7 +738,6 @@ impl Buffer { } else { self.deref() }; - let abs_path = self.file.as_ref().and_then(|f| f.abs_path()); let empty_set = HashSet::new(); let disk_based_sources = self @@ -750,26 +751,11 @@ impl Buffer { .peekable(); let mut last_edit_old_end = PointUtf16::zero(); let mut last_edit_new_end = PointUtf16::zero(); - let mut group_ids_by_diagnostic_range = HashMap::new(); - let mut diagnostics_by_group_id = HashMap::new(); - let mut next_group_id = 0; - 'outer: for diagnostic in &diagnostics { - let mut start = diagnostic.range.start.to_point_utf16(); - let mut end = diagnostic.range.end.to_point_utf16(); - let source = diagnostic.source.as_ref(); - let code = diagnostic.code.as_ref(); - let group_id = diagnostic_ranges(&diagnostic, abs_path.as_deref()) - .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range))) - .copied() - .unwrap_or_else(|| { - let group_id = post_inc(&mut next_group_id); - for range in diagnostic_ranges(&diagnostic, abs_path.as_deref()) { - group_ids_by_diagnostic_range.insert((source, code, range), group_id); - } - group_id - }); - - if diagnostic + 'outer: for entry in &mut diagnostics { + let mut start = entry.range.start; + let mut end = entry.range.end; + if entry + .diagnostic .source .as_ref() .map_or(false, |source| disk_based_sources.contains(source)) @@ -790,46 +776,20 @@ impl Buffer { end = last_edit_new_end + (end - last_edit_old_end); } - let mut range = content.clip_point_utf16(start, Bias::Left) + entry.range = content.clip_point_utf16(start, Bias::Left) ..content.clip_point_utf16(end, Bias::Right); - if range.start == range.end { - range.end.column += 1; - range.end = content.clip_point_utf16(range.end, Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = content.clip_point_utf16(range.start, Bias::Left); + if entry.range.start == entry.range.end { + entry.range.end.column += 1; + entry.range.end = content.clip_point_utf16(entry.range.end, Bias::Right); + if entry.range.start == entry.range.end && entry.range.end.column > 0 { + entry.range.start.column -= 1; + entry.range.start = content.clip_point_utf16(entry.range.start, Bias::Left); } } - - diagnostics_by_group_id - .entry(group_id) - .or_insert(Vec::new()) - .push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), - message: diagnostic.message.clone(), - group_id, - is_primary: false, - }, - }); } drop(edits_since_save); - let new_diagnostics = DiagnosticSet::new( - diagnostics_by_group_id - .into_values() - .flat_map(|mut diagnostics| { - let primary = diagnostics - .iter_mut() - .min_by_key(|entry| entry.diagnostic.severity) - .unwrap(); - primary.diagnostic.is_primary = true; - diagnostics - }), - content, - ); - self.diagnostics = new_diagnostics; + self.diagnostics = DiagnosticSet::new(diagnostics, content); if let Some(version) = version { let language_server = self.language_server.as_mut().unwrap(); @@ -1971,16 +1931,6 @@ impl ToTreeSitterPoint for Point { } } -trait ToPointUtf16 { - fn to_point_utf16(self) -> PointUtf16; -} - -impl ToPointUtf16 for lsp::Position { - fn to_point_utf16(self) -> PointUtf16 { - PointUtf16::new(self.line, self.character) - } -} - impl operation_queue::Operation for Operation { fn lamport_timestamp(&self) -> clock::Lamport { match self { @@ -2000,32 +1950,17 @@ impl operation_queue::Operation for Operation { } } -fn diagnostic_ranges<'a>( - diagnostic: &'a lsp::Diagnostic, - abs_path: Option<&'a Path>, -) -> impl 'a + Iterator> { - diagnostic - .related_information - .iter() - .flatten() - .filter_map(move |info| { - if info.location.uri.to_file_path().ok()? == abs_path? { - let info_start = PointUtf16::new( - info.location.range.start.line, - info.location.range.start.character, - ); - let info_end = PointUtf16::new( - info.location.range.end.line, - info.location.range.end.character, - ); - Some(info_start..info_end) - } else { - None - } - }) - .chain(Some( - diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(), - )) +impl Default for Diagnostic { + fn default() -> Self { + Self { + source: Default::default(), + code: Default::default(), + severity: DiagnosticSeverity::ERROR, + message: Default::default(), + group_id: Default::default(), + is_primary: Default::default(), + } + } } pub fn contiguous_ranges( diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 25e2dfe223..304a296088 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -117,6 +117,8 @@ pub fn serialize_diagnostics<'a>( } as i32, group_id: entry.diagnostic.group_id as u64, is_primary: entry.diagnostic.is_primary, + code: entry.diagnostic.code.clone(), + source: entry.diagnostic.source.clone(), }) .collect() } @@ -269,6 +271,8 @@ pub fn deserialize_diagnostics( message: diagnostic.message, group_id: diagnostic.group_id as usize, is_primary: diagnostic.is_primary, + code: diagnostic.code, + source: diagnostic.source, }, }) }) diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 6e2bc43dcd..07f21dabf8 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -516,23 +516,29 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .update_diagnostics( Some(open_notification.text_document.version), vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + ..Default::default() + }, }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'BB'".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + ..Default::default() + }, }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'CCC'".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(2, 9)..PointUtf16::new(2, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'CCC'".to_string(), + ..Default::default() + }, }, ], cx, @@ -553,6 +559,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { message: "undefined variable 'BB'".to_string(), group_id: 1, is_primary: true, + ..Default::default() }, }, DiagnosticEntry { @@ -562,6 +569,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { message: "undefined variable 'CCC'".to_string(), group_id: 2, is_primary: true, + ..Default::default() } } ] @@ -592,17 +600,21 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .update_diagnostics( Some(open_notification.text_document.version), vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + ..Default::default() + }, }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)), - severity: Some(lsp::DiagnosticSeverity::WARNING), - message: "unreachable statement".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(0, 9)..PointUtf16::new(0, 12), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "unreachable statement".to_string(), + ..Default::default() + }, }, ], cx, @@ -621,6 +633,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { message: "unreachable statement".to_string(), group_id: 1, is_primary: true, + ..Default::default() } }, DiagnosticEntry { @@ -630,6 +643,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { message: "undefined variable 'A'".to_string(), group_id: 0, is_primary: true, + ..Default::default() }, } ] @@ -670,19 +684,23 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .update_diagnostics( Some(change_notification_2.text_document.version), vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'BB'".to_string(), - source: Some("disk".to_string()), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(1, 9)..PointUtf16::new(1, 11), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'BB'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - source: Some("disk".to_string()), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(0, 9)..PointUtf16::new(0, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + source: Some("disk".to_string()), + ..Default::default() + }, }, ], cx, @@ -701,6 +719,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { message: "undefined variable 'A'".to_string(), group_id: 0, is_primary: true, + ..Default::default() } }, DiagnosticEntry { @@ -710,6 +729,7 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { message: "undefined variable 'BB'".to_string(), group_id: 1, is_primary: true, + ..Default::default() }, } ] @@ -732,23 +752,21 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { .update_diagnostics( None, vec![ - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 10), - lsp::Position::new(0, 10), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "syntax error 1".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 1".to_string(), + ..Default::default() + }, }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(1, 10), - lsp::Position::new(1, 10), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "syntax error 2".to_string(), - ..Default::default() + DiagnosticEntry { + range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "syntax error 2".to_string(), + ..Default::default() + }, }, ], cx, @@ -766,9 +784,9 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { .collect::>(), &[ ("let one = ", None), - (";", Some(lsp::DiagnosticSeverity::ERROR)), + (";", Some(DiagnosticSeverity::ERROR)), ("\nlet two =", None), - (" ", Some(lsp::DiagnosticSeverity::ERROR)), + (" ", Some(DiagnosticSeverity::ERROR)), ("\nlet three = 3;\n", None) ] ); @@ -776,224 +794,6 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { }); } -#[gpui::test] -async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { - cx.add_model(|cx| { - let text = " - fn foo(mut v: Vec) { - for x in &v { - v.push(1); - } - } - " - .unindent(); - - let file = FakeFile::new("/example.rs"); - let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx); - buffer.set_language(Some(Arc::new(rust_lang())), None, cx); - let diagnostics = vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), - severity: Some(DiagnosticSeverity::WARNING), - message: "error 1".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), - range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), - }, - message: "error 1 hint 1".to_string(), - }]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), - severity: Some(DiagnosticSeverity::HINT), - message: "error 1 hint 1".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), - range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), - }, - message: "original diagnostic".to_string(), - }]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), - severity: Some(DiagnosticSeverity::ERROR), - message: "error 2".to_string(), - related_information: Some(vec![ - lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), - range: lsp::Range::new( - lsp::Position::new(1, 13), - lsp::Position::new(1, 15), - ), - }, - message: "error 2 hint 1".to_string(), - }, - lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), - range: lsp::Range::new( - lsp::Position::new(1, 13), - lsp::Position::new(1, 15), - ), - }, - message: "error 2 hint 2".to_string(), - }, - ]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), - severity: Some(DiagnosticSeverity::HINT), - message: "error 2 hint 1".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), - range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), - }, - message: "original diagnostic".to_string(), - }]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), - severity: Some(DiagnosticSeverity::HINT), - message: "error 2 hint 2".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), - range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), - }, - message: "original diagnostic".to_string(), - }]), - ..Default::default() - }, - ]; - buffer.update_diagnostics(None, diagnostics, cx).unwrap(); - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(0..buffer.len()) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "error 1".to_string(), - group_id: 0, - is_primary: true, - } - }, - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 1 hint 1".to_string(), - group_id: 0, - is_primary: false, - } - }, - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 1".to_string(), - group_id: 1, - is_primary: false, - } - }, - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 2".to_string(), - group_id: 1, - is_primary: false, - } - }, - DiagnosticEntry { - range: Point::new(2, 8)..Point::new(2, 17), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "error 2".to_string(), - group_id: 1, - is_primary: true, - } - } - ] - ); - - assert_eq!( - buffer - .snapshot() - .diagnostic_group::(0) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "error 1".to_string(), - group_id: 0, - is_primary: true, - } - }, - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 1 hint 1".to_string(), - group_id: 0, - is_primary: false, - } - }, - ] - ); - assert_eq!( - buffer - .snapshot() - .diagnostic_group::(1) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 1".to_string(), - group_id: 1, - is_primary: false, - } - }, - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 2".to_string(), - group_id: 1, - is_primary: false, - } - }, - DiagnosticEntry { - range: Point::new(2, 8)..Point::new(2, 17), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "error 2".to_string(), - group_id: 1, - is_primary: true, - } - } - ] - ); - - buffer - }); -} - fn chunks_with_diagnostics( buffer: &Buffer, range: Range, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b200db63dd..10e92ae56a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4,10 +4,11 @@ mod worktree; use anyhow::Result; use client::{Client, UserStore}; +use clock::ReplicaId; use futures::Future; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; -use language::LanguageRegistry; +use language::{DiagnosticEntry, LanguageRegistry, PointUtf16}; use std::{ path::Path, sync::{atomic::AtomicBool, Arc}, @@ -62,6 +63,11 @@ impl Project { } } + pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { + // TODO + self.worktrees.first().unwrap().read(cx).replica_id() + } + pub fn worktrees(&self) -> &[ModelHandle] { &self.worktrees } @@ -159,6 +165,13 @@ impl Project { } } + pub fn diagnostics<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl Iterator])> { + std::iter::empty() + } + pub fn active_entry(&self) -> Option { self.active_entry } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index fe6cb2e394..166de727a7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -12,7 +12,10 @@ use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; -use language::{Buffer, Language, LanguageRegistry, Operation, Rope}; +use language::{ + Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, Operation, + PointUtf16, Rope, +}; use lazy_static::lazy_static; use lsp::LanguageServer; use parking_lot::Mutex; @@ -30,7 +33,7 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - ops::Deref, + ops::{Deref, Range}, path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -40,7 +43,7 @@ use std::{ }; use sum_tree::Bias; use sum_tree::{Edit, SeekTarget, SumTree}; -use util::{ResultExt, TryFutureExt}; +use util::{post_inc, ResultExt, TryFutureExt}; lazy_static! { static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); @@ -747,20 +750,67 @@ impl Worktree { cx: &mut ModelContext, ) -> Result<()> { let this = self.as_local_mut().ok_or_else(|| anyhow!("not local"))?; - let file_path = params + let abs_path = params .uri .to_file_path() - .map_err(|_| anyhow!("URI is not a file"))? + .map_err(|_| anyhow!("URI is not a file"))?; + let worktree_path = abs_path .strip_prefix(&this.abs_path) .context("path is not within worktree")? .to_owned(); + let mut group_ids_by_diagnostic_range = HashMap::new(); + let mut diagnostics_by_group_id = HashMap::new(); + let mut next_group_id = 0; + for diagnostic in ¶ms.diagnostics { + let source = diagnostic.source.as_ref(); + let code = diagnostic.code.as_ref(); + let group_id = diagnostic_ranges(&diagnostic, &abs_path) + .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range))) + .copied() + .unwrap_or_else(|| { + let group_id = post_inc(&mut next_group_id); + for range in diagnostic_ranges(&diagnostic, &abs_path) { + group_ids_by_diagnostic_range.insert((source, code, range), group_id); + } + group_id + }); + + diagnostics_by_group_id + .entry(group_id) + .or_insert(Vec::new()) + .push(DiagnosticEntry { + range: diagnostic.range.start.to_point_utf16() + ..diagnostic.range.end.to_point_utf16(), + diagnostic: Diagnostic { + source: diagnostic.source.clone(), + code: diagnostic.code.clone(), + severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), + message: diagnostic.message.clone(), + group_id, + is_primary: false, + }, + }); + } + + let diagnostics = diagnostics_by_group_id + .into_values() + .flat_map(|mut diagnostics| { + let primary = diagnostics + .iter_mut() + .min_by_key(|entry| entry.diagnostic.severity) + .unwrap(); + primary.diagnostic.is_primary = true; + diagnostics + }) + .collect::>(); + for buffer in this.open_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { if buffer .read(cx) .file() - .map_or(false, |file| file.path().as_ref() == file_path) + .map_or(false, |file| file.path().as_ref() == worktree_path) { let (remote_id, operation) = buffer.update(cx, |buffer, cx| { ( @@ -774,7 +824,7 @@ impl Worktree { } } - this.diagnostics.insert(file_path, params.diagnostics); + this.diagnostics.insert(worktree_path, diagnostics); Ok(()) } @@ -838,7 +888,7 @@ pub struct LocalWorktree { share: Option, open_buffers: HashMap>, shared_buffers: HashMap>>, - diagnostics: HashMap>, + diagnostics: HashMap>>, collaborators: HashMap, queued_operations: Vec<(u64, Operation)>, languages: Arc, @@ -2998,6 +3048,44 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } +trait ToPointUtf16 { + fn to_point_utf16(self) -> PointUtf16; +} + +impl ToPointUtf16 for lsp::Position { + fn to_point_utf16(self) -> PointUtf16 { + PointUtf16::new(self.line, self.character) + } +} + +fn diagnostic_ranges<'a>( + diagnostic: &'a lsp::Diagnostic, + abs_path: &'a Path, +) -> impl 'a + Iterator> { + diagnostic + .related_information + .iter() + .flatten() + .filter_map(move |info| { + if info.location.uri.to_file_path().ok()? == abs_path { + let info_start = PointUtf16::new( + info.location.range.start.line, + info.location.range.start.character, + ); + let info_end = PointUtf16::new( + info.location.range.end.line, + info.location.range.end.character, + ); + Some(info_start..info_end) + } else { + None + } + }) + .chain(Some( + diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(), + )) +} + #[cfg(test)] mod tests { use super::*; @@ -3740,6 +3828,224 @@ mod tests { }); } + #[gpui::test] + async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { + cx.add_model(|cx| { + let text = " + fn foo(mut v: Vec) { + for x in &v { + v.push(1); + } + } + " + .unindent(); + + let file = FakeFile::new("/example.rs"); + let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx); + buffer.set_language(Some(Arc::new(rust_lang())), None, cx); + let diagnostics = vec![ + DiagnosticEntry { + range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9), + diagnostic: Diagnostic { + severity: Some(DiagnosticSeverity::WARNING), + message: "error 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9), + }, + message: "error 1 hint 1".to_string(), + }]), + ..Default::default() + }, + }, + DiagnosticEntry { + range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9), + diagnostic: Diagnostic {}, + severity: Some(DiagnosticSeverity::HINT), + message: "error 1 hint 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + DiagnosticEntry { + range: PointUtf16::new(2, 8)..PointUtf16::new(2, 17), + diagnostic: Diagnostic {}, + severity: Some(DiagnosticSeverity::ERROR), + message: "error 2".to_string(), + related_information: Some(vec![ + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15), + }, + message: "error 2 hint 1".to_string(), + }, + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15), + }, + message: "error 2 hint 2".to_string(), + }, + ]), + ..Default::default() + }, + DiagnosticEntry { + range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15), + diagnostic: Diagnostic {}, + severity: Some(DiagnosticSeverity::HINT), + message: "error 2 hint 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: PointUtf16::new(2, 8)..PointUtf16::new(2, 17), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + DiagnosticEntry { + range: PointUtf16::new(1, 13)..PointUtf16::new(1, 15), + diagnostic: Diagnostic {}, + severity: Some(DiagnosticSeverity::HINT), + message: "error 2 hint 2".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: PointUtf16::new(2, 8)..PointUtf16::new(2, 17), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + ]; + buffer.update_diagnostics(None, diagnostics, cx).unwrap(); + assert_eq!( + buffer + .snapshot() + .diagnostics_in_range::<_, Point>(0..buffer.len()) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 0, + is_primary: true, + } + }, + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 0, + is_primary: false, + } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 1, + is_primary: false, + } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 1, + is_primary: false, + } + }, + DiagnosticEntry { + range: Point::new(2, 8)..Point::new(2, 17), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 1, + is_primary: true, + } + } + ] + ); + + assert_eq!( + buffer + .snapshot() + .diagnostic_group::(0) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 0, + is_primary: true, + } + }, + DiagnosticEntry { + range: Point::new(1, 8)..Point::new(1, 9), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 0, + is_primary: false, + } + }, + ] + ); + assert_eq!( + buffer + .snapshot() + .diagnostic_group::(1) + .collect::>(), + &[ + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 1, + is_primary: false, + } + }, + DiagnosticEntry { + range: Point::new(1, 13)..Point::new(1, 15), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 1, + is_primary: false, + } + }, + DiagnosticEntry { + range: Point::new(2, 8)..Point::new(2, 17), + diagnostic: Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 1, + is_primary: true, + } + } + ] + ); + + buffer + }); + } + #[gpui::test(iterations = 100)] fn test_random(mut rng: StdRng) { let operations = env::var("OPERATIONS") diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5ea70b7d8a..fa4efe695b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -269,6 +269,8 @@ message Diagnostic { string message = 4; uint64 group_id = 5; bool is_primary = 6; + optional string code = 7; + optional string source = 8; enum Severity { None = 0; Error = 1;