Cargo.lock 🔗
@@ -1421,6 +1421,7 @@ dependencies = [
"language",
"postage",
"project",
+ "unindent",
"workspace",
]
Max Brunsfeld created
Cargo.lock | 1
crates/diagnostics/Cargo.toml | 7
crates/diagnostics/src/diagnostics.rs | 285 +++++++++++++++++++---------
crates/editor/src/editor.rs | 10
crates/language/src/diagnostic_set.rs | 36 +--
5 files changed, 225 insertions(+), 114 deletions(-)
@@ -1421,6 +1421,7 @@ dependencies = [
"language",
"postage",
"project",
+ "unindent",
"workspace",
]
@@ -15,3 +15,10 @@ gpui = { path = "../gpui" }
project = { path = "../project" }
workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] }
+
+[dev-dependencies]
+unindent = "0.1"
+editor = { path = "../editor", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
@@ -1,16 +1,16 @@
use editor::{
- diagnostic_block_renderer, diagnostic_header_renderer,
+ context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
display_map::{BlockDisposition, BlockProperties},
- Editor, ExcerptProperties, MultiBuffer,
+ BuildSettings, Editor, ExcerptProperties, MultiBuffer,
};
use gpui::{
action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, View, ViewContext, ViewHandle,
};
-use language::Point;
+use language::{Bias, Buffer, Point};
use postage::watch;
use project::Project;
-use std::cmp;
+use std::ops::Range;
use workspace::Workspace;
action!(Toggle);
@@ -27,6 +27,7 @@ struct ProjectDiagnostics {
struct ProjectDiagnosticsEditor {
editor: ViewHandle<Editor>,
excerpts: ModelHandle<MultiBuffer>,
+ build_settings: BuildSettings,
}
impl ProjectDiagnostics {
@@ -58,10 +59,114 @@ impl View for ProjectDiagnosticsEditor {
}
impl ProjectDiagnosticsEditor {
+ fn new(
+ replica_id: u16,
+ settings: watch::Receiver<workspace::Settings>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let excerpts = cx.add_model(|_| MultiBuffer::new(replica_id));
+ let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
+ let editor =
+ cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
+ Self {
+ excerpts,
+ editor,
+ build_settings,
+ }
+ }
+
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
workspace.add_item(diagnostics, cx);
}
+
+ fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
+ let mut blocks = Vec::new();
+ let snapshot = buffer.read(cx).snapshot();
+
+ let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
+ for group in snapshot.diagnostic_groups::<Point>() {
+ let mut pending_range: Option<(Range<Point>, usize)> = None;
+ let mut is_first_excerpt = true;
+ for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
+ if let Some((range, start_ix)) = &mut pending_range {
+ if let Some(entry) = entry {
+ if entry.range.start.row <= range.end.row + 1 {
+ range.end = range.end.max(entry.range.end);
+ continue;
+ }
+ }
+
+ let excerpt_start = Point::new(range.start.row.saturating_sub(1), 0);
+ let excerpt_end = snapshot
+ .clip_point(Point::new(range.end.row + 1, u32::MAX), Bias::Left);
+
+ let mut excerpt = ExcerptProperties {
+ buffer: &buffer,
+ range: excerpt_start..excerpt_end,
+ header_height: 0,
+ render_header: None,
+ };
+
+ if is_first_excerpt {
+ let primary = &group.entries[group.primary_ix].diagnostic;
+ excerpt.header_height = primary.message.matches('\n').count() as u8 + 1;
+ excerpt.render_header = Some(diagnostic_header_renderer(
+ primary.clone(),
+ self.build_settings.clone(),
+ ));
+ } else {
+ excerpt.header_height = 1;
+ excerpt.render_header =
+ Some(context_header_renderer(self.build_settings.clone()));
+ }
+
+ is_first_excerpt = false;
+ let excerpt_id = excerpts.push_excerpt(excerpt, excerpts_cx);
+ for entry in &group.entries[*start_ix..ix] {
+ if !entry.diagnostic.is_primary {
+ let buffer_anchor = snapshot.anchor_before(entry.range.start);
+ blocks.push(BlockProperties {
+ position: (excerpt_id.clone(), buffer_anchor),
+ height: entry.diagnostic.message.matches('\n').count() as u8
+ + 1,
+ render: diagnostic_block_renderer(
+ entry.diagnostic.clone(),
+ true,
+ self.build_settings.clone(),
+ ),
+ disposition: BlockDisposition::Below,
+ });
+ }
+ }
+
+ pending_range.take();
+ }
+
+ if let Some(entry) = entry {
+ pending_range = Some((entry.range.clone(), ix));
+ }
+ }
+ }
+
+ excerpts.snapshot(excerpts_cx)
+ });
+
+ self.editor.update(cx, |editor, cx| {
+ editor.insert_blocks(
+ blocks.into_iter().map(|block| {
+ let (excerpt_id, text_anchor) = block.position;
+ BlockProperties {
+ position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
+ height: block.height,
+ render: block.render,
+ disposition: block.disposition,
+ }
+ }),
+ cx,
+ );
+ });
+ }
}
impl workspace::Item for ProjectDiagnostics {
@@ -73,113 +178,27 @@ impl workspace::Item for ProjectDiagnostics {
cx: &mut ViewContext<Self::View>,
) -> Self::View {
let project = handle.read(cx).project.clone();
- let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id(cx)));
- let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
- let editor =
- cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
-
let project_paths = project
.read(cx)
.diagnostic_summaries(cx)
.map(|e| e.0)
.collect::<Vec<_>>();
- cx.spawn(|this, mut cx| {
+ cx.spawn(|view, mut cx| {
let project = project.clone();
async move {
for project_path in project_paths {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
.await?;
- let snapshot = buffer.read_with(&cx, |b, _| b.snapshot());
-
- this.update(&mut cx, |this, cx| {
- let mut blocks = Vec::new();
- let excerpts_snapshot =
- this.excerpts.update(cx, |excerpts, excerpts_cx| {
- for group in snapshot.diagnostic_groups::<Point>() {
- let excerpt_start = cmp::min(
- group.primary.range.start.row,
- group
- .supporting
- .first()
- .map_or(u32::MAX, |entry| entry.range.start.row),
- );
- let excerpt_end = cmp::max(
- group.primary.range.end.row,
- group
- .supporting
- .last()
- .map_or(0, |entry| entry.range.end.row),
- );
-
- let primary_diagnostic = group.primary.diagnostic;
- let excerpt_id = excerpts.push_excerpt(
- ExcerptProperties {
- buffer: &buffer,
- range: Point::new(excerpt_start, 0)
- ..Point::new(
- excerpt_end,
- snapshot.line_len(excerpt_end),
- ),
- header_height: primary_diagnostic
- .message
- .matches('\n')
- .count()
- as u8
- + 1,
- render_header: Some(diagnostic_header_renderer(
- primary_diagnostic,
- build_settings.clone(),
- )),
- },
- excerpts_cx,
- );
-
- for entry in group.supporting {
- let buffer_anchor =
- snapshot.anchor_before(entry.range.start);
- blocks.push(BlockProperties {
- position: (excerpt_id.clone(), buffer_anchor),
- height: entry.diagnostic.message.matches('\n').count()
- as u8
- + 1,
- render: diagnostic_block_renderer(
- entry.diagnostic,
- true,
- build_settings.clone(),
- ),
- disposition: BlockDisposition::Below,
- });
- }
- }
-
- excerpts.snapshot(excerpts_cx)
- });
-
- this.editor.update(cx, |editor, cx| {
- editor.insert_blocks(
- blocks.into_iter().map(|block| {
- let (excerpt_id, text_anchor) = block.position;
- BlockProperties {
- position: excerpts_snapshot
- .anchor_in_excerpt(excerpt_id, text_anchor),
- height: block.height,
- render: block.render,
- disposition: block.disposition,
- }
- }),
- cx,
- );
- });
- })
+ view.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
}
Result::Ok::<_, anyhow::Error>(())
}
})
.detach();
- ProjectDiagnosticsEditor { editor, excerpts }
+ ProjectDiagnosticsEditor::new(project.read(cx).replica_id(cx), settings, cx)
}
fn project_path(&self) -> Option<project::ProjectPath> {
@@ -212,3 +231,83 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
todo!()
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
+ use unindent::Unindent as _;
+ use workspace::WorkspaceParams;
+
+ #[gpui::test]
+ fn test_diagnostics(cx: &mut MutableAppContext) {
+ let settings = WorkspaceParams::test(cx).settings;
+ let view = cx.add_view(Default::default(), |cx| {
+ ProjectDiagnosticsEditor::new(0, settings, cx)
+ });
+
+ let text = "
+ fn main() {
+ let x = vec![];
+ let y = vec![];
+ a(x);
+ b(y);
+ c();
+ d(y);
+ e(x);
+ }
+ "
+ .unindent();
+
+ let buffer = cx.add_model(|cx| {
+ let mut buffer = Buffer::new(0, text, cx);
+ buffer
+ .update_diagnostics(
+ None,
+ vec![
+ DiagnosticEntry {
+ range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
+ diagnostic: Diagnostic {
+ message:
+ "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
+ .to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ group_id: 0,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
+ diagnostic: Diagnostic {
+ message:
+ "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
+ .to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ group_id: 1,
+ ..Default::default()
+ },
+ },
+ DiagnosticEntry {
+ range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
+ diagnostic: Diagnostic {
+ message: "value moved here".to_string(),
+ severity: DiagnosticSeverity::INFORMATION,
+ is_primary: false,
+ group_id: 0,
+ ..Default::default()
+ },
+ },
+ ],
+ cx,
+ )
+ .unwrap();
+ buffer
+ });
+
+ view.update(cx, |view, cx| {
+ view.populate_excerpts(buffer, cx);
+ });
+ }
+}
@@ -357,7 +357,7 @@ pub enum SoftWrap {
Column(u32),
}
-type BuildSettings = Arc<dyn 'static + Send + Sync + Fn(&AppContext) -> EditorSettings>;
+pub type BuildSettings = Arc<dyn 'static + Send + Sync + Fn(&AppContext) -> EditorSettings>;
pub struct Editor {
handle: WeakViewHandle<Self>,
@@ -3794,6 +3794,14 @@ pub fn diagnostic_header_renderer(
})
}
+pub fn context_header_renderer(build_settings: BuildSettings) -> RenderHeaderFn {
+ Arc::new(move |cx| {
+ let settings = build_settings(cx);
+ let text_style = settings.style.text.clone();
+ Text::new("...".to_string(), text_style).boxed()
+ })
+}
+
pub fn diagnostic_style(
severity: DiagnosticSeverity,
valid: bool,
@@ -20,8 +20,8 @@ pub struct DiagnosticEntry<T> {
}
pub struct DiagnosticGroup<T> {
- pub primary: DiagnosticEntry<T>,
- pub supporting: Vec<DiagnosticEntry<T>>,
+ pub entries: Vec<DiagnosticEntry<T>>,
+ pub primary_ix: usize,
}
#[derive(Clone, Debug)]
@@ -108,33 +108,29 @@ impl DiagnosticSet {
where
O: FromAnchor + Ord + Copy,
{
- let mut groups =
- HashMap::<usize, (Option<DiagnosticEntry<O>>, Vec<DiagnosticEntry<O>>)>::default();
-
+ let mut groups = HashMap::default();
for entry in self.diagnostics.iter() {
let entry = entry.resolve(buffer);
- let (ref mut primary, ref mut supporting) = groups
+ groups
.entry(entry.diagnostic.group_id)
- .or_insert((None, Vec::new()));
- if entry.diagnostic.is_primary {
- *primary = Some(entry);
- } else {
- supporting.push(entry);
- }
+ .or_insert(Vec::new())
+ .push(entry);
}
let mut groups = groups
.into_values()
- .map(|(primary, mut supporting)| {
- supporting.sort_unstable_by_key(|entry| entry.range.start);
- DiagnosticGroup {
- primary: primary.unwrap(),
- supporting,
- }
+ .filter_map(|mut entries| {
+ entries.sort_unstable_by_key(|entry| entry.range.start);
+ entries
+ .iter()
+ .position(|entry| entry.diagnostic.is_primary)
+ .map(|primary_ix| DiagnosticGroup {
+ entries,
+ primary_ix,
+ })
})
.collect::<Vec<_>>();
- groups.sort_unstable_by_key(|group| group.primary.range.start);
-
+ groups.sort_unstable_by_key(|group| group.entries[group.primary_ix].range.start);
groups
}