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