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}