diagnostics.rs

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