Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2021-12-13 16:46:35 +01:00
parent 6645e2820c
commit 85674ba506
11 changed files with 554 additions and 417 deletions

11
Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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<Editor>,
project: ModelHandle<Project>,
}
impl ProjectDiagnostics {
fn new(
project: ModelHandle<Project>,
settings: watch::Receiver<workspace::Settings>,
cx: &mut ViewContext<Self>,
) -> 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<Self>) -> ElementBox {
ChildView::new(self.editor.id()).boxed()
}
}

View File

@ -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<MultiBuffer>,
settings: watch::Receiver<workspace::Settings>,
) -> 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::*;

View File

@ -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,
)
}))

View File

@ -87,6 +87,8 @@ pub struct BufferSnapshot {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub source: Option<String>,
pub code: Option<String>,
pub severity: DiagnosticSeverity,
pub message: String,
pub group_id: usize,
@ -720,7 +722,7 @@ impl Buffer {
pub fn update_diagnostics(
&mut self,
version: Option<i32>,
mut diagnostics: Vec<lsp::Diagnostic>,
mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
cx: &mut ModelContext<Self>,
) -> Result<Operation> {
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<Item = Range<PointUtf16>> {
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(

View File

@ -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,
},
})
})

View File

@ -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::<Vec<_>>(),
&[
("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<usize>) {
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::<Vec<_>>(),
&[
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::<Point>(0)
.collect::<Vec<_>>(),
&[
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::<Point>(1)
.collect::<Vec<_>>(),
&[
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<T: ToOffset + ToPoint>(
buffer: &Buffer,
range: Range<T>,

View File

@ -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<Worktree>] {
&self.worktrees
}
@ -159,6 +165,13 @@ impl Project {
}
}
pub fn diagnostics<'a>(
&'a self,
cx: &'a AppContext,
) -> impl Iterator<Item = (&'a Path, &'a [DiagnosticEntry<PointUtf16>])> {
std::iter::empty()
}
pub fn active_entry(&self) -> Option<ProjectEntry> {
self.active_entry
}

View File

@ -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<Worktree>,
) -> 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 &params.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::<Vec<_>>();
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<ShareState>,
open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
diagnostics: HashMap<PathBuf, Vec<lsp::Diagnostic>>,
diagnostics: HashMap<PathBuf, Vec<DiagnosticEntry<PointUtf16>>>,
collaborators: HashMap<PeerId, Collaborator>,
queued_operations: Vec<(u64, Operation)>,
languages: Arc<LanguageRegistry>,
@ -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<Item = Range<PointUtf16>> {
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<usize>) {
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::<Vec<_>>(),
&[
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::<Point>(0)
.collect::<Vec<_>>(),
&[
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::<Point>(1)
.collect::<Vec<_>>(),
&[
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")

View File

@ -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;