diagnostics.rs

  1use editor::{
  2    context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
  3    display_map::{BlockDisposition, BlockProperties},
  4    BuildSettings, Editor, ExcerptProperties, MultiBuffer,
  5};
  6use gpui::{
  7    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
  8    RenderContext, View, ViewContext, ViewHandle,
  9};
 10use language::{Bias, Buffer, Point};
 11use postage::watch;
 12use project::Project;
 13use std::ops::Range;
 14use workspace::Workspace;
 15
 16action!(Toggle);
 17
 18pub fn init(cx: &mut MutableAppContext) {
 19    cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
 20    cx.add_action(ProjectDiagnosticsEditor::toggle);
 21}
 22
 23struct ProjectDiagnostics {
 24    project: ModelHandle<Project>,
 25}
 26
 27struct ProjectDiagnosticsEditor {
 28    editor: ViewHandle<Editor>,
 29    excerpts: ModelHandle<MultiBuffer>,
 30    build_settings: BuildSettings,
 31}
 32
 33impl ProjectDiagnostics {
 34    fn new(project: ModelHandle<Project>) -> Self {
 35        Self { project }
 36    }
 37}
 38
 39impl Entity for ProjectDiagnostics {
 40    type Event = ();
 41}
 42
 43impl Entity for ProjectDiagnosticsEditor {
 44    type Event = ();
 45}
 46
 47impl View for ProjectDiagnosticsEditor {
 48    fn ui_name() -> &'static str {
 49        "ProjectDiagnosticsEditor"
 50    }
 51
 52    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 53        ChildView::new(self.editor.id()).boxed()
 54    }
 55
 56    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 57        cx.focus(&self.editor);
 58    }
 59}
 60
 61impl ProjectDiagnosticsEditor {
 62    fn new(
 63        replica_id: u16,
 64        settings: watch::Receiver<workspace::Settings>,
 65        cx: &mut ViewContext<Self>,
 66    ) -> Self {
 67        let excerpts = cx.add_model(|_| MultiBuffer::new(replica_id));
 68        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
 69        let editor =
 70            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
 71        Self {
 72            excerpts,
 73            editor,
 74            build_settings,
 75        }
 76    }
 77
 78    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 79        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
 80        workspace.add_item(diagnostics, cx);
 81    }
 82
 83    fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
 84        let mut blocks = Vec::new();
 85        let snapshot = buffer.read(cx).snapshot();
 86
 87        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 88            for group in snapshot.diagnostic_groups::<Point>() {
 89                let mut pending_range: Option<(Range<Point>, usize)> = None;
 90                let mut is_first_excerpt = true;
 91                for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 92                    if let Some((range, start_ix)) = &mut pending_range {
 93                        if let Some(entry) = entry {
 94                            if entry.range.start.row <= range.end.row + 1 {
 95                                range.end = range.end.max(entry.range.end);
 96                                continue;
 97                            }
 98                        }
 99
100                        let excerpt_start = Point::new(range.start.row.saturating_sub(1), 0);
101                        let excerpt_end = snapshot
102                            .clip_point(Point::new(range.end.row + 1, u32::MAX), Bias::Left);
103
104                        let mut excerpt = ExcerptProperties {
105                            buffer: &buffer,
106                            range: excerpt_start..excerpt_end,
107                            header_height: 0,
108                            render_header: None,
109                        };
110
111                        if is_first_excerpt {
112                            let primary = &group.entries[group.primary_ix].diagnostic;
113                            excerpt.header_height = primary.message.matches('\n').count() as u8 + 1;
114                            excerpt.render_header = Some(diagnostic_header_renderer(
115                                primary.clone(),
116                                self.build_settings.clone(),
117                            ));
118                        } else {
119                            excerpt.header_height = 1;
120                            excerpt.render_header =
121                                Some(context_header_renderer(self.build_settings.clone()));
122                        }
123
124                        is_first_excerpt = false;
125                        let excerpt_id = excerpts.push_excerpt(excerpt, excerpts_cx);
126                        for entry in &group.entries[*start_ix..ix] {
127                            if !entry.diagnostic.is_primary {
128                                let buffer_anchor = snapshot.anchor_before(entry.range.start);
129                                blocks.push(BlockProperties {
130                                    position: (excerpt_id.clone(), buffer_anchor),
131                                    height: entry.diagnostic.message.matches('\n').count() as u8
132                                        + 1,
133                                    render: diagnostic_block_renderer(
134                                        entry.diagnostic.clone(),
135                                        true,
136                                        self.build_settings.clone(),
137                                    ),
138                                    disposition: BlockDisposition::Below,
139                                });
140                            }
141                        }
142
143                        pending_range.take();
144                    }
145
146                    if let Some(entry) = entry {
147                        pending_range = Some((entry.range.clone(), ix));
148                    }
149                }
150            }
151
152            excerpts.snapshot(excerpts_cx)
153        });
154
155        self.editor.update(cx, |editor, cx| {
156            editor.insert_blocks(
157                blocks.into_iter().map(|block| {
158                    let (excerpt_id, text_anchor) = block.position;
159                    BlockProperties {
160                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
161                        height: block.height,
162                        render: block.render,
163                        disposition: block.disposition,
164                    }
165                }),
166                cx,
167            );
168        });
169    }
170}
171
172impl workspace::Item for ProjectDiagnostics {
173    type View = ProjectDiagnosticsEditor;
174
175    fn build_view(
176        handle: ModelHandle<Self>,
177        settings: watch::Receiver<workspace::Settings>,
178        cx: &mut ViewContext<Self::View>,
179    ) -> Self::View {
180        let project = handle.read(cx).project.clone();
181        let project_paths = project
182            .read(cx)
183            .diagnostic_summaries(cx)
184            .map(|e| e.0)
185            .collect::<Vec<_>>();
186
187        cx.spawn(|view, mut cx| {
188            let project = project.clone();
189            async move {
190                for project_path in project_paths {
191                    let buffer = project
192                        .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
193                        .await?;
194                    view.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
195                }
196                Result::Ok::<_, anyhow::Error>(())
197            }
198        })
199        .detach();
200
201        ProjectDiagnosticsEditor::new(project.read(cx).replica_id(cx), settings, cx)
202    }
203
204    fn project_path(&self) -> Option<project::ProjectPath> {
205        None
206    }
207}
208
209impl workspace::ItemView for ProjectDiagnosticsEditor {
210    fn title(&self, _: &AppContext) -> String {
211        "Project Diagnostics".to_string()
212    }
213
214    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
215        None
216    }
217
218    fn save(
219        &mut self,
220        _: &mut ViewContext<Self>,
221    ) -> anyhow::Result<gpui::Task<anyhow::Result<()>>> {
222        todo!()
223    }
224
225    fn save_as(
226        &mut self,
227        _: ModelHandle<project::Worktree>,
228        _: &std::path::Path,
229        _: &mut ViewContext<Self>,
230    ) -> gpui::Task<anyhow::Result<()>> {
231        todo!()
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
239    use unindent::Unindent as _;
240    use workspace::WorkspaceParams;
241
242    #[gpui::test]
243    fn test_diagnostics(cx: &mut MutableAppContext) {
244        let settings = WorkspaceParams::test(cx).settings;
245        let view = cx.add_view(Default::default(), |cx| {
246            ProjectDiagnosticsEditor::new(0, settings, cx)
247        });
248
249        let text = "
250        fn main() {
251            let x = vec![];
252            let y = vec![];
253            a(x);
254            b(y);
255            c();
256            d(y);
257            e(x);
258        }
259        "
260        .unindent();
261
262        let buffer = cx.add_model(|cx| {
263            let mut buffer = Buffer::new(0, text, cx);
264            buffer
265                .update_diagnostics(
266                    None,
267                    vec![
268                        DiagnosticEntry {
269                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
270                            diagnostic: Diagnostic {
271                                message:
272                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
273                                        .to_string(),
274                                severity: DiagnosticSeverity::INFORMATION,
275                                is_primary: false,
276                                group_id: 0,
277                                ..Default::default()
278                            },
279                        },
280                        DiagnosticEntry {
281                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
282                            diagnostic: Diagnostic {
283                                message:
284                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
285                                        .to_string(),
286                                severity: DiagnosticSeverity::INFORMATION,
287                                is_primary: false,
288                                group_id: 1,
289                                ..Default::default()
290                            },
291                        },
292                        DiagnosticEntry {
293                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
294                            diagnostic: Diagnostic {
295                                message: "value moved here".to_string(),
296                                severity: DiagnosticSeverity::INFORMATION,
297                                is_primary: false,
298                                group_id: 0,
299                                ..Default::default()
300                            },
301                        },
302                    ],
303                    cx,
304                )
305                .unwrap();
306            buffer
307        });
308
309        view.update(cx, |view, cx| {
310            view.populate_excerpts(buffer, cx);
311        });
312    }
313}