diff --git a/Cargo.lock b/Cargo.lock index 9ee0f5c3ea..e2f767145f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4315,19 +4315,24 @@ dependencies = [ "anyhow", "client", "collections", + "component", "ctor", "editor", "env_logger 0.11.8", "gpui", + "indoc", "language", + "linkme", "log", "lsp", + "markdown", "pretty_assertions", "project", "rand 0.8.5", "serde", "serde_json", "settings", + "text", "theme", "ui", "unindent", diff --git a/crates/auto_update_helper/app-icon.ico b/crates/auto_update_helper/app-icon.ico index 321e90fcfa..e69de29bb2 100644 Binary files a/crates/auto_update_helper/app-icon.ico and b/crates/auto_update_helper/app-icon.ico differ diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 97790cd259..eeba3d6c1e 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -15,17 +15,22 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true +component.workspace = true ctor.workspace = true editor.workspace = true env_logger.workspace = true gpui.workspace = true +indoc.workspace = true language.workspace = true +linkme.workspace = true log.workspace = true lsp.workspace = true +markdown.workspace = true project.workspace = true rand.workspace = true serde.workspace = true settings.workspace = true +text.workspace = true theme.workspace = true ui.workspace = true util.workspace = true @@ -37,6 +42,7 @@ client = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } +markdown = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } serde_json.workspace = true theme = { workspace = true, features = ["test-support"] } diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs new file mode 100644 index 0000000000..5a0c97866b --- /dev/null +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -0,0 +1,302 @@ +use std::{ops::Range, sync::Arc}; + +use editor::{ + Anchor, Editor, EditorSnapshot, ToOffset, + display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle}, + hover_markdown_style, + scroll::Autoscroll, +}; +use gpui::{AppContext, Entity, Focusable, WeakEntity}; +use language::{BufferId, DiagnosticEntry}; +use lsp::DiagnosticSeverity; +use markdown::{Markdown, MarkdownElement}; +use settings::Settings; +use text::{AnchorRangeExt, Point}; +use theme::ThemeSettings; +use ui::{ + ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, + Window, div, px, +}; +use util::maybe; + +use crate::ProjectDiagnosticsEditor; + +pub struct DiagnosticRenderer; + +impl DiagnosticRenderer { + pub fn diagnostic_blocks_for_group( + diagnostic_group: Vec>, + buffer_id: BufferId, + diagnostics_editor: Option>, + cx: &mut App, + ) -> Vec { + let Some(primary_ix) = diagnostic_group + .iter() + .position(|d| d.diagnostic.is_primary) + else { + return Vec::new(); + }; + let primary = diagnostic_group[primary_ix].clone(); + let mut same_row = Vec::new(); + let mut close = Vec::new(); + let mut distant = Vec::new(); + let group_id = primary.diagnostic.group_id; + for (ix, entry) in diagnostic_group.into_iter().enumerate() { + if entry.diagnostic.is_primary { + continue; + } + if entry.range.start.row == primary.range.start.row { + same_row.push(entry) + } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 { + close.push(entry) + } else { + distant.push((ix, entry)) + } + } + + let mut markdown = + Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() { + format!("{}: {}", source, primary.diagnostic.message) + } else { + primary.diagnostic.message + }) + .to_string(); + for entry in same_row { + markdown.push_str("\n- hint: "); + markdown.push_str(&Markdown::escape(&entry.diagnostic.message)) + } + + for (ix, entry) in &distant { + markdown.push_str("\n- hint: ["); + markdown.push_str(&Markdown::escape(&entry.diagnostic.message)); + markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",)) + } + + let mut results = vec![DiagnosticBlock { + initial_range: primary.range, + severity: primary.diagnostic.severity, + buffer_id, + diagnostics_editor: diagnostics_editor.clone(), + markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), + }]; + + for entry in close { + let markdown = if let Some(source) = entry.diagnostic.source.as_ref() { + format!("{}: {}", source, entry.diagnostic.message) + } else { + entry.diagnostic.message + }; + let markdown = Markdown::escape(&markdown).to_string(); + + results.push(DiagnosticBlock { + initial_range: entry.range, + severity: entry.diagnostic.severity, + buffer_id, + diagnostics_editor: diagnostics_editor.clone(), + markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), + }); + } + + for (_, entry) in distant { + let markdown = if let Some(source) = entry.diagnostic.source.as_ref() { + format!("{}: {}", source, entry.diagnostic.message) + } else { + entry.diagnostic.message + }; + let mut markdown = Markdown::escape(&markdown).to_string(); + markdown.push_str(&format!( + " ([back](file://#diagnostic-{group_id}-{primary_ix}))" + )); + // problem: group-id changes... + // - only an issue in diagnostics because caching + + results.push(DiagnosticBlock { + initial_range: entry.range, + severity: entry.diagnostic.severity, + buffer_id, + diagnostics_editor: diagnostics_editor.clone(), + markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)), + }); + } + + results + } +} + +impl editor::DiagnosticRenderer for DiagnosticRenderer { + fn render_group( + &self, + diagnostic_group: Vec>, + buffer_id: BufferId, + snapshot: EditorSnapshot, + editor: WeakEntity, + cx: &mut App, + ) -> Vec> { + let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx); + blocks + .into_iter() + .map(|block| { + let editor = editor.clone(); + BlockProperties { + placement: BlockPlacement::Near( + snapshot + .buffer_snapshot + .anchor_after(block.initial_range.start), + ), + height: Some(1), + style: BlockStyle::Flex, + render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)), + priority: 1, + } + }) + .collect() + } +} + +#[derive(Clone)] +pub(crate) struct DiagnosticBlock { + pub(crate) initial_range: Range, + pub(crate) severity: DiagnosticSeverity, + pub(crate) buffer_id: BufferId, + pub(crate) markdown: Entity, + pub(crate) diagnostics_editor: Option>, +} + +impl DiagnosticBlock { + pub fn render_block(&self, editor: WeakEntity, bcx: &BlockContext) -> AnyElement { + let cx = &bcx.app; + let status_colors = bcx.app.theme().status(); + let max_width = px(600.); + + let (background_color, border_color) = match self.severity { + DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error), + DiagnosticSeverity::WARNING => { + (status_colors.warning_background, status_colors.warning) + } + DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info), + DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info), + _ => (status_colors.ignored_background, status_colors.ignored), + }; + let settings = ThemeSettings::get_global(cx); + let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round(); + let line_height = editor_line_height; + let buffer_id = self.buffer_id; + let diagnostics_editor = self.diagnostics_editor.clone(); + + div() + .border_l_2() + .px_2() + .line_height(line_height) + .bg(background_color) + .border_color(border_color) + .max_w(max_width) + .child( + MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx)) + .on_url_click({ + move |link, window, cx| { + Self::open_link( + editor.clone(), + &diagnostics_editor, + link, + window, + buffer_id, + cx, + ) + } + }), + ) + .into_any_element() + } + + pub fn open_link( + editor: WeakEntity, + diagnostics_editor: &Option>, + link: SharedString, + window: &mut Window, + buffer_id: BufferId, + cx: &mut App, + ) { + editor + .update(cx, |editor, cx| { + let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else { + editor::hover_popover::open_markdown_url(link, window, cx); + return; + }; + let Some((group_id, ix)) = maybe!({ + let (group_id, ix) = diagnostic_link.split_once('-')?; + let group_id: usize = group_id.parse().ok()?; + let ix: usize = ix.parse().ok()?; + Some((group_id, ix)) + }) else { + return; + }; + + if let Some(diagnostics_editor) = diagnostics_editor { + if let Some(diagnostic) = diagnostics_editor + .update(cx, |diagnostics, _| { + diagnostics + .diagnostics + .get(&buffer_id) + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|d| d.diagnostic.group_id == group_id) + .nth(ix) + }) + .ok() + .flatten() + { + let multibuffer = editor.buffer().read(cx); + let Some(snapshot) = multibuffer + .buffer(buffer_id) + .map(|entity| entity.read(cx).snapshot()) + else { + return; + }; + + for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) { + if range.context.overlaps(&diagnostic.range, &snapshot) { + Self::jump_to( + editor, + Anchor::range_in_buffer( + excerpt_id, + buffer_id, + diagnostic.range, + ), + window, + cx, + ); + return; + } + } + } + } else { + if let Some(diagnostic) = editor + .snapshot(window, cx) + .buffer_snapshot + .diagnostic_group(buffer_id, group_id) + .nth(ix) + { + Self::jump_to(editor, diagnostic.range, window, cx) + } + }; + }) + .ok(); + } + + fn jump_to( + editor: &mut Editor, + range: Range, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = &editor.buffer().read(cx).snapshot(cx); + let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + + editor.unfold_ranges(&[range.start..range.end], true, false, cx); + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([range.start..range.start]); + }); + window.focus(&editor.focus_handle(cx)); + } +} diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index fc336c7836..4f43db0a5c 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,38 +1,39 @@ pub mod items; mod toolbar_controls; +mod diagnostic_renderer; + #[cfg(test)] mod diagnostics_tests; use anyhow::Result; -use collections::{BTreeSet, HashSet}; +use collections::{BTreeSet, HashMap}; +use diagnostic_renderer::DiagnosticBlock; use editor::{ - Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, diagnostic_block_renderer, - display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock}, - highlight_diagnostic_message, + DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, + display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, scroll::Autoscroll, }; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, - Global, HighlightStyle, InteractiveElement, IntoElement, ParentElement, Render, SharedString, - Styled, StyledText, Subscription, Task, WeakEntity, Window, actions, div, svg, + Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Task, WeakEntity, Window, actions, div, }; use language::{ - Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity, - Point, Selection, SelectionGoal, ToTreeSitterPoint, + Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint, }; -use lsp::LanguageServerId; +use lsp::DiagnosticSeverity; use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings}; use settings::Settings; use std::{ any::{Any, TypeId}, cmp, cmp::Ordering, - mem, ops::{Range, RangeInclusive}, sync::Arc, time::Duration, }; +use text::{BufferId, OffsetRangeExt}; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; use ui::{Icon, IconName, Label, h_flex, prelude::*}; @@ -49,41 +50,28 @@ struct IncludeWarnings(bool); impl Global for IncludeWarnings {} pub fn init(cx: &mut App) { + editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx); cx.observe_new(ProjectDiagnosticsEditor::register).detach(); } -struct ProjectDiagnosticsEditor { +pub(crate) struct ProjectDiagnosticsEditor { project: Entity, workspace: WeakEntity, focus_handle: FocusHandle, editor: Entity, + diagnostics: HashMap>>, + blocks: HashMap>, summary: DiagnosticSummary, - excerpts: Entity, - path_states: Vec, - paths_to_update: BTreeSet<(ProjectPath, Option)>, + multibuffer: Entity, + paths_to_update: BTreeSet, include_warnings: bool, - context: u32, update_excerpts_task: Option>>, _subscription: Subscription, } -struct PathState { - path: ProjectPath, - diagnostic_groups: Vec, -} - -struct DiagnosticGroupState { - language_server_id: LanguageServerId, - primary_diagnostic: DiagnosticEntry, - primary_excerpt_ix: usize, - excerpts: Vec, - blocks: HashSet, - block_count: usize, -} - impl EventEmitter for ProjectDiagnosticsEditor {} -const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); +const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); impl Render for ProjectDiagnosticsEditor { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { @@ -149,8 +137,7 @@ impl ProjectDiagnosticsEditor { workspace.register_action(Self::deploy); } - fn new_with_context( - context: u32, + fn new( include_warnings: bool, project_handle: Entity, workspace: WeakEntity, @@ -170,8 +157,7 @@ impl ProjectDiagnosticsEditor { language_server_id, path, } => { - this.paths_to_update - .insert((path.clone(), Some(*language_server_id))); + this.paths_to_update.insert(path.clone()); this.summary = project.read(cx).diagnostic_summary(false, cx); cx.emit(EditorEvent::TitleChanged); @@ -201,6 +187,7 @@ impl ProjectDiagnosticsEditor { Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx); editor.set_vertical_scroll_margin(5, cx); editor.disable_inline_diagnostics(); + editor.set_all_diagnostics_active(cx); editor }); cx.subscribe_in( @@ -210,7 +197,7 @@ impl ProjectDiagnosticsEditor { cx.emit(event.clone()); match event { EditorEvent::Focused => { - if this.path_states.is_empty() { + if this.multibuffer.read(cx).is_empty() { window.focus(&this.focus_handle); } } @@ -229,14 +216,14 @@ impl ProjectDiagnosticsEditor { let project = project_handle.read(cx); let mut this = Self { project: project_handle.clone(), - context, summary: project.diagnostic_summary(false, cx), + diagnostics: Default::default(), + blocks: Default::default(), include_warnings, workspace, - excerpts, + multibuffer: excerpts, focus_handle, editor, - path_states: Default::default(), paths_to_update: Default::default(), update_excerpts_task: None, _subscription: project_event_subscription, @@ -252,15 +239,15 @@ impl ProjectDiagnosticsEditor { let project_handle = self.project.clone(); self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| { cx.background_executor() - .timer(DIAGNOSTICS_UPDATE_DEBOUNCE) + .timer(DIAGNOSTICS_UPDATE_DELAY) .await; loop { - let Some((path, language_server_id)) = this.update(cx, |this, _| { - let Some((path, language_server_id)) = this.paths_to_update.pop_first() else { + let Some(path) = this.update(cx, |this, _| { + let Some(path) = this.paths_to_update.pop_first() else { this.update_excerpts_task.take(); return None; }; - Some((path, language_server_id)) + Some(path) })? else { break; @@ -272,7 +259,7 @@ impl ProjectDiagnosticsEditor { .log_err() { this.update_in(cx, |this, window, cx| { - this.update_excerpts(path, language_server_id, buffer, window, cx) + this.update_excerpts(buffer, window, cx) })? .await?; } @@ -281,23 +268,6 @@ impl ProjectDiagnosticsEditor { })); } - fn new( - project_handle: Entity, - include_warnings: bool, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::new_with_context( - editor::DEFAULT_MULTIBUFFER_CONTEXT, - include_warnings, - project_handle, - workspace, - window, - cx, - ) - } - fn deploy( workspace: &mut Workspace, _: &Deploy, @@ -319,8 +289,8 @@ impl ProjectDiagnosticsEditor { let diagnostics = cx.new(|cx| { ProjectDiagnosticsEditor::new( - workspace.project().clone(), include_warnings, + workspace.project().clone(), workspace_handle, window, cx, @@ -338,7 +308,7 @@ impl ProjectDiagnosticsEditor { } fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - if self.focus_handle.is_focused(window) && !self.path_states.is_empty() { + if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() { self.editor.focus_handle(cx).focus(window) } } @@ -356,396 +326,212 @@ impl ProjectDiagnosticsEditor { self.project.update(cx, |project, cx| { let mut paths = project .diagnostic_summaries(false, cx) - .map(|(path, _, _)| (path, None)) + .map(|(path, _, _)| path) .collect::>(); - paths.extend( - self.path_states - .iter() - .map(|state| (state.path.clone(), None)), - ); - let paths_to_update = std::mem::take(&mut self.paths_to_update); - paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None))); + self.multibuffer.update(cx, |multibuffer, cx| { + for buffer in multibuffer.all_buffers() { + if let Some(file) = buffer.read(cx).file() { + paths.insert(ProjectPath { + path: file.path().clone(), + worktree_id: file.worktree_id(cx), + }); + } + } + }); self.paths_to_update = paths; }); self.update_stale_excerpts(window, cx); } + fn diagnostics_are_unchanged( + &self, + existing: &Vec>, + new: &Vec>, + snapshot: &BufferSnapshot, + ) -> bool { + if existing.len() != new.len() { + return false; + } + existing.iter().zip(new.iter()).all(|(existing, new)| { + existing.diagnostic.message == new.diagnostic.message + && existing.diagnostic.severity == new.diagnostic.severity + && existing.diagnostic.is_primary == new.diagnostic.is_primary + && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot) + }) + } + fn update_excerpts( &mut self, - path_to_update: ProjectPath, - server_to_update: Option, buffer: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { - let was_empty = self.path_states.is_empty(); - let snapshot = buffer.read(cx).snapshot(); - let path_ix = match self - .path_states - .binary_search_by_key(&&path_to_update, |e| &e.path) - { - Ok(ix) => ix, - Err(ix) => { - self.path_states.insert( - ix, - PathState { - path: path_to_update.clone(), - diagnostic_groups: Default::default(), - }, - ); - ix - } - }; - let mut prev_excerpt_id = if path_ix > 0 { - let prev_path_last_group = &self.path_states[path_ix - 1] - .diagnostic_groups - .last() - .unwrap(); - *prev_path_last_group.excerpts.last().unwrap() - } else { - ExcerptId::min() - }; - - let mut new_group_ixs = Vec::new(); - let mut blocks_to_add = Vec::new(); - let mut blocks_to_remove = HashSet::default(); - let mut first_excerpt_id = None; + let was_empty = self.multibuffer.read(cx).is_empty(); + let buffer_snapshot = buffer.read(cx).snapshot(); + let buffer_id = buffer_snapshot.remote_id(); let max_severity = if self.include_warnings { DiagnosticSeverity::WARNING } else { DiagnosticSeverity::ERROR }; - let excerpts = self.excerpts.clone().downgrade(); - let context = self.context; - let editor = self.editor.clone().downgrade(); - cx.spawn_in(window, async move |this, cx| { - let mut old_groups = this - .update(cx, |this, _| { - mem::take(&mut this.path_states[path_ix].diagnostic_groups) - })? - .into_iter() - .enumerate() - .peekable(); - let mut new_groups = snapshot - .diagnostic_groups(server_to_update) - .into_iter() - .filter(|(_, group)| { - group.entries[group.primary_ix].diagnostic.severity <= max_severity - }) - .peekable(); - loop { - let mut to_insert = None; - let mut to_remove = None; - let mut to_keep = None; - match (old_groups.peek(), new_groups.peek()) { - (None, None) => break, - (None, Some(_)) => to_insert = new_groups.next(), - (Some((_, old_group)), None) => { - if server_to_update.map_or(true, |id| id == old_group.language_server_id) { - to_remove = old_groups.next(); - } else { - to_keep = old_groups.next(); - } - } - (Some((_, old_group)), Some((new_language_server_id, new_group))) => { - let old_primary = &old_group.primary_diagnostic; - let new_primary = &new_group.entries[new_group.primary_ix]; - match compare_diagnostics(old_primary, new_primary, &snapshot) - .then_with(|| old_group.language_server_id.cmp(new_language_server_id)) - { - Ordering::Less => { - if server_to_update - .map_or(true, |id| id == old_group.language_server_id) - { - to_remove = old_groups.next(); - } else { - to_keep = old_groups.next(); - } - } - Ordering::Equal => { - to_keep = old_groups.next(); - new_groups.next(); - } - Ordering::Greater => to_insert = new_groups.next(), - } - } + + cx.spawn_in(window, async move |this, mut cx| { + let diagnostics = buffer_snapshot + .diagnostics_in_range::<_, text::Anchor>( + Point::zero()..buffer_snapshot.max_point(), + false, + ) + .filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary)) + .collect::>(); + let unchanged = this.update(cx, |this, _| { + if this.diagnostics.get(&buffer_id).is_some_and(|existing| { + this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot) + }) { + return true; } + this.diagnostics.insert(buffer_id, diagnostics.clone()); + return false; + })?; + if unchanged { + return Ok(()); + } - if let Some((language_server_id, group)) = to_insert { - let mut group_state = DiagnosticGroupState { - language_server_id, - primary_diagnostic: group.entries[group.primary_ix].clone(), - primary_excerpt_ix: 0, - excerpts: Default::default(), - blocks: Default::default(), - block_count: 0, - }; - let mut pending_range: Option<(Range, Range, usize)> = None; - let mut is_first_excerpt_for_group = true; - for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() { - let resolved_entry = entry.map(|e| e.resolve::(&snapshot)); - let expanded_range = if let Some(entry) = &resolved_entry { - Some( - context_range_for_entry( - entry.range.clone(), - context, - snapshot.clone(), - (**cx).clone(), - ) - .await, - ) - } else { - None - }; - if let Some((range, context_range, start_ix)) = &mut pending_range { - if let Some(expanded_range) = expanded_range.clone() { - // If the entries are overlapping or next to each-other, merge them into one excerpt. - if context_range.end.row + 1 >= expanded_range.start.row { - context_range.end = context_range.end.max(expanded_range.end); - continue; - } + let mut grouped: HashMap> = HashMap::default(); + for entry in diagnostics { + grouped + .entry(entry.diagnostic.group_id) + .or_default() + .push(DiagnosticEntry { + range: entry.range.to_point(&buffer_snapshot), + diagnostic: entry.diagnostic, + }) + } + let mut blocks: Vec = Vec::new(); + + for (_, group) in grouped { + let group_severity = group.iter().map(|d| d.diagnostic.severity).min(); + if group_severity.is_none_or(|s| s > max_severity) { + continue; + } + let more = cx.update(|_, cx| { + crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group( + group, + buffer_snapshot.remote_id(), + Some(this.clone()), + cx, + ) + })?; + + for item in more { + let insert_pos = blocks + .binary_search_by(|existing| { + match existing.initial_range.start.cmp(&item.initial_range.start) { + Ordering::Equal => item + .initial_range + .end + .cmp(&existing.initial_range.end) + .reverse(), + other => other, } + }) + .unwrap_or_else(|pos| pos); - let excerpt_id = excerpts.update(cx, |excerpts, cx| { - excerpts - .insert_excerpts_after( - prev_excerpt_id, - buffer.clone(), - [ExcerptRange { - context: context_range.clone(), - primary: range.clone(), - }], - cx, - ) - .pop() - .unwrap() - })?; - - prev_excerpt_id = excerpt_id; - first_excerpt_id.get_or_insert(prev_excerpt_id); - group_state.excerpts.push(excerpt_id); - let header_position = (excerpt_id, language::Anchor::MIN); - - if is_first_excerpt_for_group { - is_first_excerpt_for_group = false; - let mut primary = - group.entries[group.primary_ix].diagnostic.clone(); - primary.message = - primary.message.split('\n').next().unwrap().to_string(); - group_state.block_count += 1; - blocks_to_add.push(BlockProperties { - placement: BlockPlacement::Above(header_position), - height: Some(2), - style: BlockStyle::Sticky, - render: diagnostic_header_renderer(primary), - priority: 0, - }); - } - - for entry in &group.entries[*start_ix..ix] { - let mut diagnostic = entry.diagnostic.clone(); - if diagnostic.is_primary { - group_state.primary_excerpt_ix = group_state.excerpts.len() - 1; - diagnostic.message = - entry.diagnostic.message.split('\n').skip(1).collect(); - } - - if !diagnostic.message.is_empty() { - group_state.block_count += 1; - blocks_to_add.push(BlockProperties { - placement: BlockPlacement::Below(( - excerpt_id, - entry.range.start, - )), - height: Some( - diagnostic.message.matches('\n').count() as u32 + 1, - ), - style: BlockStyle::Fixed, - render: diagnostic_block_renderer(diagnostic, None, true), - priority: 0, - }); - } - } - - pending_range.take(); - } - - if let Some(entry) = resolved_entry.as_ref() { - let range = entry.range.clone(); - pending_range = Some((range, expanded_range.unwrap(), ix)); - } - } - - this.update(cx, |this, _| { - new_group_ixs.push(this.path_states[path_ix].diagnostic_groups.len()); - this.path_states[path_ix] - .diagnostic_groups - .push(group_state); - })?; - } else if let Some((_, group_state)) = to_remove { - excerpts.update(cx, |excerpts, cx| { - excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx) - })?; - blocks_to_remove.extend(group_state.blocks.iter().copied()); - } else if let Some((_, group_state)) = to_keep { - prev_excerpt_id = *group_state.excerpts.last().unwrap(); - first_excerpt_id.get_or_insert(prev_excerpt_id); - - this.update(cx, |this, _| { - this.path_states[path_ix] - .diagnostic_groups - .push(group_state) - })?; + blocks.insert(insert_pos, item); } } - let excerpts_snapshot = excerpts.update(cx, |excerpts, cx| excerpts.snapshot(cx))?; - editor.update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - let block_ids = editor.insert_blocks( - blocks_to_add.into_iter().flat_map(|block| { - let placement = match block.placement { - BlockPlacement::Above((excerpt_id, text_anchor)) => { - BlockPlacement::Above( - excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?, - ) - } - BlockPlacement::Below((excerpt_id, text_anchor)) => { - BlockPlacement::Below( - excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?, - ) - } - BlockPlacement::Replace(_) | BlockPlacement::Near(_) => { - unreachable!( - "no Near/Replace block should have been pushed to blocks_to_add" - ) - } - }; - Some(BlockProperties { - placement, - height: block.height, - style: block.style, - render: block.render, - priority: 0, - }) - }), - Some(Autoscroll::fit()), - cx, - ); - - let mut block_ids = block_ids.into_iter(); - this.update(cx, |this, _| { - for ix in new_group_ixs { - let group_state = &mut this.path_states[path_ix].diagnostic_groups[ix]; - group_state.blocks = - block_ids.by_ref().take(group_state.block_count).collect(); - } - })?; - Result::<(), anyhow::Error>::Ok(()) - })??; + let mut excerpt_ranges: Vec> = Vec::new(); + for b in blocks.iter() { + let excerpt_range = context_range_for_entry( + b.initial_range.clone(), + DEFAULT_MULTIBUFFER_CONTEXT, + buffer_snapshot.clone(), + &mut cx, + ) + .await; + excerpt_ranges.push(ExcerptRange { + context: excerpt_range, + primary: b.initial_range.clone(), + }) + } this.update_in(cx, |this, window, cx| { - if this.path_states[path_ix].diagnostic_groups.is_empty() { - this.path_states.remove(path_ix); + if let Some(block_ids) = this.blocks.remove(&buffer_id) { + this.editor.update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(block_ids.into_iter().collect(), cx) + }); + }) } - - this.editor.update(cx, |editor, cx| { - let groups; - let mut selections; - let new_excerpt_ids_by_selection_id; - if was_empty { - groups = this.path_states.first()?.diagnostic_groups.as_slice(); - new_excerpt_ids_by_selection_id = - [(0, ExcerptId::min())].into_iter().collect(); - selections = vec![Selection { - id: 0, - start: 0, - end: 0, - reversed: false, - goal: SelectionGoal::None, - }]; - } else { - groups = this.path_states.get(path_ix)?.diagnostic_groups.as_slice(); - new_excerpt_ids_by_selection_id = - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.refresh() - }); - selections = editor.selections.all::(cx); - } - - // If any selection has lost its position, move it to start of the next primary diagnostic. - let snapshot = editor.snapshot(window, cx); - for selection in &mut selections { - if let Some(new_excerpt_id) = - new_excerpt_ids_by_selection_id.get(&selection.id) - { - let group_ix = match groups.binary_search_by(|probe| { - probe - .excerpts - .last() - .unwrap() - .cmp(new_excerpt_id, &snapshot.buffer_snapshot) - }) { - Ok(ix) | Err(ix) => ix, - }; - if let Some(group) = groups.get(group_ix) { - if let Some(offset) = excerpts_snapshot - .anchor_in_excerpt( - group.excerpts[group.primary_excerpt_ix], - group.primary_diagnostic.range.start, - ) - .map(|anchor| anchor.to_offset(&excerpts_snapshot)) - { - selection.start = offset; - selection.end = offset; - } - } - } - } - editor.change_selections(None, window, cx, |s| { - s.select(selections); - }); - Some(()) + let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| { + multi_buffer.set_excerpt_ranges_for_path( + PathKey::for_buffer(&buffer, cx), + buffer.clone(), + &buffer_snapshot, + excerpt_ranges, + cx, + ) }); - })?; + #[cfg(test)] + let cloned_blocks = blocks.clone(); - this.update_in(cx, |this, window, cx| { - if this.path_states.is_empty() { - if this.editor.focus_handle(cx).is_focused(window) { - window.focus(&this.focus_handle); + if was_empty { + if let Some(anchor_range) = anchor_ranges.first() { + let range_to_select = anchor_range.start..anchor_range.start; + this.editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchor_ranges([range_to_select]); + }) + }) } - } else if this.focus_handle.is_focused(window) { - let focus_handle = this.editor.focus_handle(cx); - window.focus(&focus_handle); } + let editor_blocks = + anchor_ranges + .into_iter() + .zip(blocks.into_iter()) + .map(|(anchor, block)| { + let editor = this.editor.downgrade(); + BlockProperties { + placement: BlockPlacement::Near(anchor.start), + height: Some(1), + style: BlockStyle::Flex, + render: Arc::new(move |bcx| { + block.render_block(editor.clone(), bcx) + }), + priority: 1, + } + }); + let block_ids = this.editor.update(cx, |editor, cx| { + editor.display_map.update(cx, |display_map, cx| { + display_map.insert_blocks(editor_blocks, cx) + }) + }); + #[cfg(test)] - this.check_invariants(cx); + { + for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) { + let markdown = block.markdown.clone(); + editor::test::set_block_content_for_tests( + &this.editor, + *block_id, + cx, + move |cx| { + markdown::MarkdownElement::rendered_text( + markdown.clone(), + cx, + editor::hover_markdown_style, + ) + }, + ); + } + } - cx.notify(); + this.blocks.insert(buffer_id, block_ids); + cx.notify() }) }) } - - #[cfg(test)] - fn check_invariants(&self, cx: &mut Context) { - let mut excerpts = Vec::new(); - for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() { - if let Some(file) = buffer.file() { - excerpts.push((id, file.path().clone())); - } - } - - let mut prev_path = None; - for (_, path) in &excerpts { - if let Some(prev_path) = prev_path { - if path < prev_path { - panic!("excerpts are not sorted by path {:?}", excerpts); - } - } - prev_path = Some(path); - } - } } impl Focusable for ProjectDiagnosticsEditor { @@ -857,8 +643,8 @@ impl Item for ProjectDiagnosticsEditor { { Some(cx.new(|cx| { ProjectDiagnosticsEditor::new( - self.project.clone(), self.include_warnings, + self.project.clone(), self.workspace.clone(), window, cx, @@ -867,15 +653,15 @@ impl Item for ProjectDiagnosticsEditor { } fn is_dirty(&self, cx: &App) -> bool { - self.excerpts.read(cx).is_dirty(cx) + self.multibuffer.read(cx).is_dirty(cx) } fn has_deleted_file(&self, cx: &App) -> bool { - self.excerpts.read(cx).has_deleted_file(cx) + self.multibuffer.read(cx).has_deleted_file(cx) } fn has_conflict(&self, cx: &App) -> bool { - self.excerpts.read(cx).has_conflict(cx) + self.multibuffer.read(cx).has_conflict(cx) } fn can_save(&self, _: &App) -> bool { @@ -950,128 +736,31 @@ impl Item for ProjectDiagnosticsEditor { } } -const DIAGNOSTIC_HEADER: &str = "diagnostic header"; - -fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { - let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None); - let message: SharedString = message; - Arc::new(move |cx| { - let color = cx.theme().colors(); - let highlight_style: HighlightStyle = color.text_accent.into(); - - h_flex() - .id(DIAGNOSTIC_HEADER) - .block_mouse_down() - .h(2. * cx.window.line_height()) - .w_full() - .px_9() - .justify_between() - .gap_2() - .child( - h_flex() - .gap_2() - .px_1() - .rounded_sm() - .bg(color.surface_background.opacity(0.5)) - .map(|stack| { - stack.child( - svg() - .size(cx.window.text_style().font_size) - .flex_none() - .map(|icon| { - if diagnostic.severity == DiagnosticSeverity::ERROR { - icon.path(IconName::XCircle.path()) - .text_color(Color::Error.color(cx)) - } else { - icon.path(IconName::Warning.path()) - .text_color(Color::Warning.color(cx)) - } - }), - ) - }) - .child( - h_flex() - .gap_1() - .child( - StyledText::new(message.clone()).with_default_highlights( - &cx.window.text_style(), - code_ranges - .iter() - .map(|range| (range.clone(), highlight_style)), - ), - ) - .when_some(diagnostic.code.as_ref(), |stack, code| { - stack.child( - div() - .child(SharedString::from(format!("({code:?})"))) - .text_color(color.text_muted), - ) - }), - ), - ) - .when_some(diagnostic.source.as_ref(), |stack, source| { - stack.child( - div() - .child(SharedString::from(source.clone())) - .text_color(color.text_muted), - ) - }) - .into_any_element() - }) -} - -fn compare_diagnostics( - old: &DiagnosticEntry, - new: &DiagnosticEntry, - snapshot: &language::BufferSnapshot, -) -> Ordering { - use language::ToOffset; - - // The diagnostics may point to a previously open Buffer for this file. - if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) { - return Ordering::Greater; - } - - old.range - .start - .to_offset(snapshot) - .cmp(&new.range.start.to_offset(snapshot)) - .then_with(|| { - old.range - .end - .to_offset(snapshot) - .cmp(&new.range.end.to_offset(snapshot)) - }) - .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message)) -} - const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32; -fn context_range_for_entry( +async fn context_range_for_entry( range: Range, context: u32, snapshot: BufferSnapshot, - cx: AsyncApp, -) -> Task> { - cx.spawn(async move |cx| { - if let Some(rows) = heuristic_syntactic_expand( - range.clone(), - DIAGNOSTIC_EXPANSION_ROW_LIMIT, - snapshot.clone(), - cx, - ) - .await - { - return Range { - start: Point::new(*rows.start(), 0), - end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left), - }; - } - Range { - start: Point::new(range.start.row.saturating_sub(context), 0), - end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left), - } - }) + cx: &mut AsyncApp, +) -> Range { + if let Some(rows) = heuristic_syntactic_expand( + range.clone(), + DIAGNOSTIC_EXPANSION_ROW_LIMIT, + snapshot.clone(), + cx, + ) + .await + { + return Range { + start: Point::new(*rows.start(), 0), + end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left), + }; + } + Range { + start: Point::new(range.start.row.saturating_sub(context), 0), + end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left), + } } /// Expands the input range using syntax information from TreeSitter. This expansion will be limited diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index d5428d581c..09d18166d9 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1,13 +1,15 @@ use super::*; -use collections::HashMap; +use collections::{HashMap, HashSet}; use editor::{ - DisplayPoint, GutterDimensions, - display_map::{Block, BlockContext, DisplayRow}, -}; -use gpui::{AvailableSpace, Stateful, TestAppContext, VisualTestContext, px}; -use language::{ - Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, PointUtf16, Rope, Unclipped, + DisplayPoint, + actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning}, + display_map::DisplayRow, + test::{editor_content_with_blocks, editor_test_context::EditorTestContext}, }; +use gpui::{TestAppContext, VisualTestContext}; +use indoc::indoc; +use language::Rope; +use lsp::LanguageServerId; use pretty_assertions::assert_eq; use project::FakeFs; use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _}; @@ -64,163 +66,91 @@ async fn test_diagnostics(cx: &mut TestAppContext) { let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let cx = &mut VisualTestContext::from_window(*window, cx); let workspace = window.root(cx).unwrap(); + let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(); // Create some diagnostics lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .update_diagnostic_entries( - language_server_id, - PathBuf::from(path!("/test/main.rs")), - None, - vec![ - DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)), - diagnostic: Diagnostic { - message: - "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait" - .to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)), - diagnostic: Diagnostic { - message: - "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait" - .to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)), - diagnostic: Diagnostic { - message: "value moved here".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)), - diagnostic: Diagnostic { - message: "value moved here".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)), - diagnostic: Diagnostic { - message: "use of moved value\nvalue used here after move".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)), - diagnostic: Diagnostic { - message: "use of moved value\nvalue used here after move".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - ], - cx, - ) - .unwrap(); + lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics: vec![lsp::Diagnostic{ + range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)), + severity:Some(lsp::DiagnosticSeverity::ERROR), + message: "use of moved value\nvalue used here after move".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))), + message: "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait".to_string() + }, + lsp::DiagnosticRelatedInformation { + location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))), + message: "value moved here".to_string() + }, + ]), + ..Default::default() + }, + lsp::Diagnostic{ + range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)), + severity:Some(lsp::DiagnosticSeverity::ERROR), + message: "use of moved value\nvalue used here after move".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))), + message: "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait".to_string() + }, + lsp::DiagnosticRelatedInformation { + location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))), + message: "value moved here".to_string() + }, + ]), + ..Default::default() + } + ], + version: None + }, &[], cx).unwrap(); }); // Open the project diagnostics view while there are already diagnostics. let diagnostics = window.build_entity(cx, |window, cx| { - ProjectDiagnosticsEditor::new_with_context( - 1, - true, - project.clone(), - workspace.downgrade(), - window, - cx, - ) + ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) }); let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone()); diagnostics - .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx) .await; - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - (DisplayRow(15), EXCERPT_HEADER.into()), - (DisplayRow(16), DIAGNOSTIC_HEADER.into()), - (DisplayRow(25), EXCERPT_HEADER.into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}", - ) + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.rs + § ----- + fn main() { + let x = vec![]; + § move occurs because `x` has type `Vec`, which does not implement + § the `Copy` trait (back) + let y = vec![]; + § move occurs because `y` has type `Vec`, which does not implement + § the `Copy` trait (back) + a(x); § value moved here (back) + b(y); § value moved here + // comment 1 + // comment 2 + c(y); + § use of moved value value used here after move + § hint: move occurs because `y` has type `Vec`, which does not + § implement the `Copy` trait + d(x); + § use of moved value value used here after move + § hint: move occurs because `x` has type `Vec`, which does not + § implement the `Copy` trait + § hint: value moved here + }" + } ); // Cursor is at the first diagnostic editor.update(cx, |editor, cx| { assert_eq!( editor.selections.display_ranges(cx), - [DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)] + [DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)] ); }); @@ -228,21 +158,22 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp_store.update(cx, |lsp_store, cx| { lsp_store.disk_based_diagnostics_started(language_server_id, cx); lsp_store - .update_diagnostic_entries( + .update_diagnostics( language_server_id, - PathBuf::from(path!("/test/consts.rs")), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), - diagnostic: Diagnostic { + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 15), + lsp::Position::new(0, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), message: "mismatched types\nexpected `usize`, found `char`".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, ..Default::default() - }, - }], + }], + version: None, + }, + &[], cx, ) .unwrap(); @@ -250,78 +181,48 @@ async fn test_diagnostics(cx: &mut TestAppContext) { }); diagnostics - .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx) .await; - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - (DisplayRow(7), FILE_HEADER.into()), - (DisplayRow(9), DIAGNOSTIC_HEADER.into()), - (DisplayRow(22), EXCERPT_HEADER.into()), - (DisplayRow(23), DIAGNOSTIC_HEADER.into()), - (DisplayRow(32), EXCERPT_HEADER.into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // consts.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "\n", // supporting diagnostic - "const b: i32 = c;\n", - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // filename - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}", - ) + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ consts.rs + § ----- + const a: i32 = 'a'; § mismatched types expected `usize`, found `char` + const b: i32 = c; + + § main.rs + § ----- + fn main() { + let x = vec![]; + § move occurs because `x` has type `Vec`, which does not implement + § the `Copy` trait (back) + let y = vec![]; + § move occurs because `y` has type `Vec`, which does not implement + § the `Copy` trait (back) + a(x); § value moved here (back) + b(y); § value moved here + // comment 1 + // comment 2 + c(y); + § use of moved value value used here after move + § hint: move occurs because `y` has type `Vec`, which does not + § implement the `Copy` trait + d(x); + § use of moved value value used here after move + § hint: move occurs because `x` has type `Vec`, which does not + § implement the `Copy` trait + § hint: value moved here + }" + } ); // Cursor keeps its position. editor.update(cx, |editor, cx| { assert_eq!( editor.selections.display_ranges(cx), - [DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)] + [DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)] ); }); @@ -329,34 +230,33 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp_store.update(cx, |lsp_store, cx| { lsp_store.disk_based_diagnostics_started(language_server_id, cx); lsp_store - .update_diagnostic_entries( + .update_diagnostics( language_server_id, - PathBuf::from(path!("/test/consts.rs")), - None, - vec![ - DiagnosticEntry { - range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), - diagnostic: Diagnostic { + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(), + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 15), + lsp::Position::new(0, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), message: "mismatched types\nexpected `usize`, found `char`".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, ..Default::default() }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 15))..Unclipped(PointUtf16::new(1, 15)), - diagnostic: Diagnostic { + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(1, 15), + lsp::Position::new(1, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), message: "unresolved name `c`".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 1, ..Default::default() }, - }, - ], + ], + version: None, + }, + &[], cx, ) .unwrap(); @@ -364,80 +264,148 @@ async fn test_diagnostics(cx: &mut TestAppContext) { }); diagnostics - .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx) .await; - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - (DisplayRow(7), EXCERPT_HEADER.into()), - (DisplayRow(8), DIAGNOSTIC_HEADER.into()), - (DisplayRow(13), FILE_HEADER.into()), - (DisplayRow(15), DIAGNOSTIC_HEADER.into()), - (DisplayRow(28), EXCERPT_HEADER.into()), - (DisplayRow(29), DIAGNOSTIC_HEADER.into()), - (DisplayRow(38), EXCERPT_HEADER.into()), - ] + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ consts.rs + § ----- + const a: i32 = 'a'; § mismatched types expected `usize`, found `char` + const b: i32 = c; § unresolved name `c` + + § main.rs + § ----- + fn main() { + let x = vec![]; + § move occurs because `x` has type `Vec`, which does not implement + § the `Copy` trait (back) + let y = vec![]; + § move occurs because `y` has type `Vec`, which does not implement + § the `Copy` trait (back) + a(x); § value moved here (back) + b(y); § value moved here + // comment 1 + // comment 2 + c(y); + § use of moved value value used here after move + § hint: move occurs because `y` has type `Vec`, which does not + § implement the `Copy` trait + d(x); + § use of moved value value used here after move + § hint: move occurs because `x` has type `Vec`, which does not + § implement the `Copy` trait + § hint: value moved here + }" + } + ); +} + +#[gpui::test] +async fn test_diagnostics_with_folds(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/test"), + json!({ + "main.js": " + function test() { + return 1 + }; + + tset(); + ".unindent() + }), + ) + .await; + + let server_id_1 = LanguageServerId(100); + let server_id_2 = LanguageServerId(101); + let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; + let lsp_store = project.read_with(cx, |project, _| project.lsp_store()); + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + + let diagnostics = window.build_entity(cx, |window, cx| { + ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) + }); + let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone()); + + // Two language servers start updating diagnostics + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.disk_based_diagnostics_started(server_id_1, cx); + lsp_store.disk_based_diagnostics_started(server_id_2, cx); + lsp_store + .update_diagnostics( + server_id_1, + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)), + severity: Some(lsp::DiagnosticSeverity::WARNING), + message: "no method `tset`".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location::new( + lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + lsp::Range::new( + lsp::Position::new(0, 9), + lsp::Position::new(0, 13), + ), + ), + message: "method `test` defined here".to_string(), + }]), + ..Default::default() + }], + version: None, + }, + &[], + cx, + ) + .unwrap(); + }); + + // The first language server finishes + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.disk_based_diagnostics_finished(server_id_1, cx); + }); + + // Only the first language server's diagnostics are shown. + cx.executor() + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); + cx.executor().run_until_parked(); + editor.update_in(cx, |editor, window, cx| { + editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx); + }); + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + ⋯ + + tset(); § no method `tset`" + } ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // consts.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "\n", // supporting diagnostic - "const b: i32 = c;\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "const b: i32 = c;\n", - "\n", // supporting diagnostic - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // filename - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}", - ) + editor.update(cx, |editor, cx| { + editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx); + }); + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + function test() { § method `test` defined here + return 1 + }; + + tset(); § no method `tset`" + } ); } @@ -469,14 +437,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { let workspace = window.root(cx).unwrap(); let diagnostics = window.build_entity(cx, |window, cx| { - ProjectDiagnosticsEditor::new_with_context( - 1, - true, - project.clone(), - workspace.downgrade(), - window, - cx, - ) + ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) }); let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone()); @@ -485,21 +446,19 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { lsp_store.disk_based_diagnostics_started(server_id_1, cx); lsp_store.disk_based_diagnostics_started(server_id_2, cx); lsp_store - .update_diagnostic_entries( + .update_diagnostics( server_id_1, - PathBuf::from(path!("/test/main.js")), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)), - diagnostic: Diagnostic { + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)), + severity: Some(lsp::DiagnosticSeverity::WARNING), message: "error 1".to_string(), - severity: DiagnosticSeverity::WARNING, - is_primary: true, - is_disk_based: true, - group_id: 1, ..Default::default() - }, - }], + }], + version: None, + }, + &[], cx, ) .unwrap(); @@ -512,46 +471,36 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { // Only the first language server's diagnostics are shown. cx.executor() - .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // - "b();", - ) + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + a(); § error 1 + b(); + c();" + } ); // The second language server finishes lsp_store.update(cx, |lsp_store, cx| { lsp_store - .update_diagnostic_entries( + .update_diagnostics( server_id_2, - PathBuf::from(path!("/test/main.js")), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)), - diagnostic: Diagnostic { + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)), + severity: Some(lsp::DiagnosticSeverity::ERROR), message: "warning 1".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 2, ..Default::default() - }, - }], + }], + version: None, + }, + &[], cx, ) .unwrap(); @@ -560,35 +509,19 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { // Both language server's diagnostics are shown. cx.executor() - .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - (DisplayRow(6), EXCERPT_HEADER.into()), - (DisplayRow(7), DIAGNOSTIC_HEADER.into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // location - "b();\n", // - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "a();\n", // context - "b();\n", // - "c();", // context - ) + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + a(); § error 1 + b(); § warning 1 + c(); + d();" + } ); // Both language servers start updating diagnostics, and the first server finishes. @@ -596,30 +529,31 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { lsp_store.disk_based_diagnostics_started(server_id_1, cx); lsp_store.disk_based_diagnostics_started(server_id_2, cx); lsp_store - .update_diagnostic_entries( + .update_diagnostics( server_id_1, - PathBuf::from(path!("/test/main.js")), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)), - diagnostic: Diagnostic { + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)), + severity: Some(lsp::DiagnosticSeverity::WARNING), message: "warning 2".to_string(), - severity: DiagnosticSeverity::WARNING, - is_primary: true, - is_disk_based: true, - group_id: 1, ..Default::default() - }, - }], + }], + version: None, + }, + &[], cx, ) .unwrap(); lsp_store - .update_diagnostic_entries( + .update_diagnostics( server_id_2, - PathBuf::from(path!("/test/main.rs")), - None, - vec![], + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(), + diagnostics: vec![], + version: None, + }, + &[], cx, ) .unwrap(); @@ -628,56 +562,38 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { // Only the first language server's diagnostics are updated. cx.executor() - .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - (DisplayRow(7), EXCERPT_HEADER.into()), - (DisplayRow(8), DIAGNOSTIC_HEADER.into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // location - "b();\n", // - "c();\n", // context - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "b();\n", // context - "c();\n", // - "d();", // context - ) + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + a(); + b(); § warning 1 + c(); § warning 2 + d(); + e();" + } ); // The second language server finishes. lsp_store.update(cx, |lsp_store, cx| { lsp_store - .update_diagnostic_entries( + .update_diagnostics( server_id_2, - PathBuf::from(path!("/test/main.js")), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)), - diagnostic: Diagnostic { + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)), + severity: Some(lsp::DiagnosticSeverity::WARNING), message: "warning 2".to_string(), - severity: DiagnosticSeverity::WARNING, - is_primary: true, - is_disk_based: true, - group_id: 1, ..Default::default() - }, - }], + }], + version: None, + }, + &[], cx, ) .unwrap(); @@ -686,36 +602,20 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { // Both language servers' diagnostics are updated. cx.executor() - .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (DisplayRow(0), FILE_HEADER.into()), - (DisplayRow(2), DIAGNOSTIC_HEADER.into()), - (DisplayRow(7), EXCERPT_HEADER.into()), - (DisplayRow(8), DIAGNOSTIC_HEADER.into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "b();\n", // location - "c();\n", // - "d();\n", // context - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "c();\n", // context - "d();\n", // - "e();", // context - ) + + pretty_assertions::assert_eq!( + editor_content_with_blocks(&editor, cx), + indoc::indoc! { + "§ main.js + § ----- + a(); + b(); + c(); § warning 2 + d(); § warning 2 + e();" + } ); } @@ -737,14 +637,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { let workspace = window.root(cx).unwrap(); let mutated_diagnostics = window.build_entity(cx, |window, cx| { - ProjectDiagnosticsEditor::new_with_context( - 1, - true, - project.clone(), - workspace.downgrade(), - window, - cx, - ) + ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) }); workspace.update_in(cx, |workspace, window, cx| { @@ -754,14 +647,12 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { assert!(diagnostics.focus_handle.is_focused(window)); }); - let mut next_group_id = 0; + let mut next_id = 0; let mut next_filename = 0; let mut language_server_ids = vec![LanguageServerId(0)]; let mut updated_language_servers = HashSet::default(); - let mut current_diagnostics: HashMap< - (PathBuf, LanguageServerId), - Vec>>, - > = Default::default(); + let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec> = + Default::default(); for _ in 0..operations { match rng.gen_range(0..100) { @@ -821,15 +712,26 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { &fs, &path, diagnostics, - &mut next_group_id, + &mut next_id, &mut rng, ); lsp_store - .update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx) + .update_diagnostics( + server_id, + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| { + lsp::Url::parse("file:///test/fallback.rs").unwrap() + }), + diagnostics: diagnostics.clone(), + version: None, + }, + &[], + cx, + ) .unwrap() }); cx.executor() - .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); cx.run_until_parked(); } @@ -840,39 +742,298 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { mutated_diagnostics.update_in(cx, |diagnostics, window, cx| { diagnostics.update_stale_excerpts(window, cx) }); - cx.run_until_parked(); log::info!("constructing reference diagnostics view"); let reference_diagnostics = window.build_entity(cx, |window, cx| { - ProjectDiagnosticsEditor::new_with_context( - 1, - true, - project.clone(), - workspace.downgrade(), - window, - cx, - ) + ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx) }); cx.executor() - .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); + .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10)); cx.run_until_parked(); - let mutated_excerpts = get_diagnostics_excerpts(&mutated_diagnostics, cx); - let reference_excerpts = get_diagnostics_excerpts(&reference_diagnostics, cx); + let mutated_excerpts = + editor_content_with_blocks(&mutated_diagnostics.update(cx, |d, _| d.editor.clone()), cx); + let reference_excerpts = editor_content_with_blocks( + &reference_diagnostics.update(cx, |d, _| d.editor.clone()), + cx, + ); - for ((path, language_server_id), diagnostics) in current_diagnostics { - for diagnostic in diagnostics { - let found_excerpt = reference_excerpts.iter().any(|info| { - let row_range = info.range.context.start.row..info.range.context.end.row; - info.path == path.strip_prefix(path!("/test")).unwrap() - && info.language_server == language_server_id - && row_range.contains(&diagnostic.range.start.0.row) - }); - assert!(found_excerpt, "diagnostic not found in reference view"); + // The mutated view may contain more than the reference view as + // we don't currently shrink excerpts when diagnostics were removed. + let mut ref_iter = reference_excerpts.lines(); + let mut next_ref_line = ref_iter.next(); + let mut skipped_block = false; + + for mut_line in mutated_excerpts.lines() { + if let Some(ref_line) = next_ref_line { + if mut_line == ref_line { + next_ref_line = ref_iter.next(); + } else if mut_line.contains('§') { + skipped_block = true; + } } } - assert_eq!(mutated_excerpts, reference_excerpts); + if next_ref_line.is_some() || skipped_block { + pretty_assertions::assert_eq!(mutated_excerpts, reference_excerpts); + } +} + +#[gpui::test] +async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) { + init_test(cx); + + let mut cx = EditorTestContext::new(cx).await; + let lsp_store = + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + + cx.set_state(indoc! {" + ˇfn func(abc def: i32) -> u32 { + } + "}); + + let message = "Something's wrong!"; + cx.update(|_, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 11), + lsp::Position::new(0, 12), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: message.to_string(), + ..Default::default() + }], + }, + &[], + cx, + ) + .unwrap() + }); + }); + cx.run_until_parked(); + + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + assert_eq!( + editor + .active_diagnostic_group() + .map(|diagnostics_group| diagnostics_group.active_message.as_str()), + Some(message), + "Should have a diagnostics group activated" + ); + }); + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); + + cx.update(|_, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + version: None, + diagnostics: Vec::new(), + }, + &[], + cx, + ) + .unwrap() + }); + }); + cx.run_until_parked(); + cx.update_editor(|editor, _, _| { + assert_eq!(editor.active_diagnostic_group(), None); + }); + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); + + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + assert_eq!(editor.active_diagnostic_group(), None); + }); + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); +} + +#[gpui::test] +async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { + init_test(cx); + + let mut cx = EditorTestContext::new(cx).await; + let lsp_store = + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + + cx.set_state(indoc! {" + ˇfn func(abc def: i32) -> u32 { + } + "}); + + cx.update(|_, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + version: None, + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 11), + lsp::Position::new(0, 12), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 12), + lsp::Position::new(0, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 12), + lsp::Position::new(0, 15), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 25), + lsp::Position::new(0, 28), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + ], + }, + &[], + cx, + ) + .unwrap() + }); + }); + cx.run_until_parked(); + + //// Backward + + // Fourth diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); + + // Third diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); + + // Second diagnostic, same place + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); + + // First diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); + + // Wrapped over, fourth diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); + + cx.update_editor(|editor, window, cx| { + editor.move_to_beginning(&MoveToBeginning, window, cx); + }); + cx.assert_editor_state(indoc! {" + ˇfn func(abc def: i32) -> u32 { + } + "}); + + //// Forward + + // First diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); + + // Second diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); + + // Third diagnostic, same place + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc ˇdef: i32) -> u32 { + } + "}); + + // Fourth diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abc def: i32) -> ˇu32 { + } + "}); + + // Wrapped around, first diagnostic + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + }); + cx.assert_editor_state(indoc! {" + fn func(abcˇ def: i32) -> u32 { + } + "}); } fn init_test(cx: &mut TestAppContext) { @@ -889,199 +1050,114 @@ fn init_test(cx: &mut TestAppContext) { }); } -#[derive(Debug, PartialEq, Eq)] -struct ExcerptInfo { - path: PathBuf, - range: ExcerptRange, - group_id: usize, - primary: bool, - language_server: LanguageServerId, -} - -fn get_diagnostics_excerpts( - diagnostics: &Entity, - cx: &mut VisualTestContext, -) -> Vec { - diagnostics.update(cx, |diagnostics, cx| { - let mut result = vec![]; - let mut excerpt_indices_by_id = HashMap::default(); - diagnostics.excerpts.update(cx, |multibuffer, cx| { - let snapshot = multibuffer.snapshot(cx); - for (id, buffer, range) in snapshot.excerpts() { - excerpt_indices_by_id.insert(id, result.len()); - result.push(ExcerptInfo { - path: buffer.file().unwrap().path().to_path_buf(), - range: ExcerptRange { - context: range.context.to_point(buffer), - primary: range.primary.to_point(buffer), - }, - group_id: usize::MAX, - primary: false, - language_server: LanguageServerId(0), - }); - } - }); - - for state in &diagnostics.path_states { - for group in &state.diagnostic_groups { - for (ix, excerpt_id) in group.excerpts.iter().enumerate() { - let excerpt_ix = excerpt_indices_by_id[excerpt_id]; - let excerpt = &mut result[excerpt_ix]; - excerpt.group_id = group.primary_diagnostic.diagnostic.group_id; - excerpt.language_server = group.language_server_id; - excerpt.primary = ix == group.primary_excerpt_ix; - } - } - } - - result - }) -} - fn randomly_update_diagnostics_for_path( fs: &FakeFs, path: &Path, - diagnostics: &mut Vec>>, - next_group_id: &mut usize, + diagnostics: &mut Vec, + next_id: &mut usize, rng: &mut impl Rng, ) { - let file_content = fs.read_file_sync(path).unwrap(); - let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref()); - - let mut group_ids = diagnostics - .iter() - .map(|d| d.diagnostic.group_id) - .collect::>(); - let mutation_count = rng.gen_range(1..=3); for _ in 0..mutation_count { - if rng.gen_bool(0.5) && !group_ids.is_empty() { - let group_id = *group_ids.iter().choose(rng).unwrap(); - log::info!(" removing diagnostic group {group_id}"); - diagnostics.retain(|d| d.diagnostic.group_id != group_id); - group_ids.remove(&group_id); + if rng.gen_bool(0.3) && !diagnostics.is_empty() { + let idx = rng.gen_range(0..diagnostics.len()); + log::info!(" removing diagnostic at index {idx}"); + diagnostics.remove(idx); } else { - let group_id = *next_group_id; - *next_group_id += 1; + let unique_id = *next_id; + *next_id += 1; - let mut new_diagnostics = vec![random_diagnostic(rng, &file_text, group_id, true)]; - for _ in 0..rng.gen_range(0..=1) { - new_diagnostics.push(random_diagnostic(rng, &file_text, group_id, false)); - } + let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id); let ix = rng.gen_range(0..=diagnostics.len()); log::info!( - " inserting diagnostic group {group_id} at index {ix}. ranges: {:?}", - new_diagnostics - .iter() - .map(|d| (d.range.start.0, d.range.end.0)) - .collect::>() + " inserting {} at index {ix}. {},{}..{},{}", + new_diagnostic.message, + new_diagnostic.range.start.line, + new_diagnostic.range.start.character, + new_diagnostic.range.end.line, + new_diagnostic.range.end.character, ); - diagnostics.splice(ix..ix, new_diagnostics); + for related in new_diagnostic.related_information.iter().flatten() { + log::info!( + " {}. {},{}..{},{}", + related.message, + related.location.range.start.line, + related.location.range.start.character, + related.location.range.end.line, + related.location.range.end.character, + ); + } + diagnostics.insert(ix, new_diagnostic); } } } -fn random_diagnostic( +fn random_lsp_diagnostic( rng: &mut impl Rng, - file_text: &Rope, - group_id: usize, - is_primary: bool, -) -> DiagnosticEntry> { + fs: &FakeFs, + path: &Path, + unique_id: usize, +) -> lsp::Diagnostic { // Intentionally allow erroneous ranges some of the time (that run off the end of the file), // because language servers can potentially give us those, and we should handle them gracefully. const ERROR_MARGIN: usize = 10; + let file_content = fs.read_file_sync(path).unwrap(); + let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref()); + let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN)); let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN)); - let range = Range { - start: Unclipped(file_text.offset_to_point_utf16(start)), - end: Unclipped(file_text.offset_to_point_utf16(end)), - }; - let severity = if rng.gen_bool(0.5) { - DiagnosticSeverity::WARNING - } else { - DiagnosticSeverity::ERROR - }; - let message = format!("diagnostic group {group_id}"); - DiagnosticEntry { + let start_point = file_text.offset_to_point_utf16(start); + let end_point = file_text.offset_to_point_utf16(end); + + let range = lsp::Range::new( + lsp::Position::new(start_point.row, start_point.column), + lsp::Position::new(end_point.row, end_point.column), + ); + + let severity = if rng.gen_bool(0.5) { + Some(lsp::DiagnosticSeverity::ERROR) + } else { + Some(lsp::DiagnosticSeverity::WARNING) + }; + + let message = format!("diagnostic {unique_id}"); + + let related_information = if rng.gen_bool(0.3) { + let info_count = rng.gen_range(1..=3); + let mut related_info = Vec::with_capacity(info_count); + + for i in 0..info_count { + let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN)); + let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN)); + + let info_start_point = file_text.offset_to_point_utf16(info_start); + let info_end_point = file_text.offset_to_point_utf16(info_end); + + let info_range = lsp::Range::new( + lsp::Position::new(info_start_point.row, info_start_point.column), + lsp::Position::new(info_end_point.row, info_end_point.column), + ); + + related_info.push(lsp::DiagnosticRelatedInformation { + location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range), + message: format!("related info {i} for diagnostic {unique_id}"), + }); + } + + Some(related_info) + } else { + None + }; + + lsp::Diagnostic { range, - diagnostic: Diagnostic { - source: None, // (optional) service that created the diagnostic - code: None, // (optional) machine-readable code that identifies the diagnostic - severity, - message, - group_id, - is_primary, - is_disk_based: false, - is_unnecessary: false, - data: None, - }, + severity, + message, + related_information, + data: None, + ..Default::default() } } - -const FILE_HEADER: &str = "file header"; -const EXCERPT_HEADER: &str = "excerpt header"; - -fn editor_blocks( - editor: &Entity, - cx: &mut VisualTestContext, -) -> Vec<(DisplayRow, SharedString)> { - let mut blocks = Vec::new(); - cx.draw( - gpui::Point::default(), - AvailableSpace::min_size(), - |window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - blocks.extend( - snapshot - .blocks_in_range(DisplayRow(0)..snapshot.max_point().row()) - .filter_map(|(row, block)| { - let block_id = block.id(); - let name: SharedString = match block { - Block::Custom(block) => { - let mut element = block.render(&mut BlockContext { - app: cx, - window, - anchor_x: px(0.), - gutter_dimensions: &GutterDimensions::default(), - line_height: px(0.), - em_width: px(0.), - max_width: px(0.), - block_id, - selected: false, - editor_style: &editor::EditorStyle::default(), - }); - let element = element.downcast_mut::>().unwrap(); - element - .interactivity() - .element_id - .clone()? - .try_into() - .ok()? - } - - Block::FoldedBuffer { .. } => FILE_HEADER.into(), - Block::ExcerptBoundary { - starts_new_buffer, .. - } => { - if *starts_new_buffer { - FILE_HEADER.into() - } else { - EXCERPT_HEADER.into() - } - } - }; - - Some((row, name)) - }), - ) - }); - - div().into_any() - }, - ); - blocks -} diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index f57ae8b96b..39bc41b9dc 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -61,7 +61,7 @@ pub struct BlockSnapshot { } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct CustomBlockId(usize); +pub struct CustomBlockId(pub usize); impl From for ElementId { fn from(val: CustomBlockId) -> Self { @@ -89,7 +89,7 @@ pub enum BlockPlacement { } impl BlockPlacement { - fn start(&self) -> &T { + pub fn start(&self) -> &T { match self { BlockPlacement::Above(position) => position, BlockPlacement::Below(position) => position, @@ -187,14 +187,15 @@ impl BlockPlacement { } pub struct CustomBlock { - id: CustomBlockId, - placement: BlockPlacement, - height: Option, + pub id: CustomBlockId, + pub placement: BlockPlacement, + pub height: Option, style: BlockStyle, render: Arc>, priority: usize, } +#[derive(Clone)] pub struct BlockProperties

{ pub placement: BlockPlacement

, // None if the block takes up no space @@ -686,6 +687,9 @@ impl BlockMap { rows_before_block = position.0 - new_transforms.summary().input_rows; } BlockPlacement::Near(position) | BlockPlacement::Below(position) => { + if position.0 + 1 < new_transforms.summary().input_rows { + continue; + } rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows; } BlockPlacement::Replace(range) => { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6e779ebed4..18ca699603 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23,7 +23,7 @@ mod element; mod git; mod highlight_matching_bracket; mod hover_links; -mod hover_popover; +pub mod hover_popover; mod indent_guides; mod inlay_hint_cache; pub mod items; @@ -88,10 +88,9 @@ use gpui::{ ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, - SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle, - TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, - WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative, - size, + SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, + UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; @@ -105,7 +104,7 @@ pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, Diagnostic, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, + CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ @@ -143,12 +142,12 @@ use language::BufferSnapshot; pub use lsp_ext::lsp_tasks; use movement::TextLayoutDetails; pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo, - ToOffset, ToPoint, + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, + RowInfo, ToOffset, ToPoint, }; use multi_buffer::{ ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - MultiOrSingleBufferOffsetRange, PathKey, ToOffsetUtf16, + MultiOrSingleBufferOffsetRange, ToOffsetUtf16, }; use parking_lot::Mutex; use project::{ @@ -356,6 +355,24 @@ pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) cx.set_global(GlobalBlameRenderer(Arc::new(renderer))); } +pub trait DiagnosticRenderer { + fn render_group( + &self, + diagnostic_group: Vec>, + buffer_id: BufferId, + snapshot: EditorSnapshot, + editor: WeakEntity, + cx: &mut App, + ) -> Vec>; +} + +pub(crate) struct GlobalDiagnosticRenderer(pub Arc); + +impl gpui::Global for GlobalDiagnosticRenderer {} +pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { + cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); +} + pub struct SearchWithinRange; trait InvalidationRegion { @@ -701,7 +718,7 @@ pub struct Editor { snippet_stack: InvalidationStack, select_syntax_node_history: SelectSyntaxNodeHistory, ime_transaction: Option, - active_diagnostics: Option, + active_diagnostics: ActiveDiagnostic, show_inline_diagnostics: bool, inline_diagnostics_update: Task<()>, inline_diagnostics_enabled: bool, @@ -1074,12 +1091,19 @@ struct RegisteredInlineCompletionProvider { } #[derive(Debug, PartialEq, Eq)] -struct ActiveDiagnosticGroup { - primary_range: Range, - primary_message: String, - group_id: usize, - blocks: HashMap, - is_valid: bool, +pub struct ActiveDiagnosticGroup { + pub active_range: Range, + pub active_message: String, + pub group_id: usize, + pub blocks: HashSet, +} + +#[derive(Debug, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum ActiveDiagnostic { + None, + All, + Group(ActiveDiagnosticGroup), } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -1475,7 +1499,7 @@ impl Editor { snippet_stack: Default::default(), select_syntax_node_history: SelectSyntaxNodeHistory::default(), ime_transaction: Default::default(), - active_diagnostics: None, + active_diagnostics: ActiveDiagnostic::None, show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled, inline_diagnostics_update: Task::ready(()), inline_diagnostics: Vec::new(), @@ -3076,7 +3100,7 @@ impl Editor { return true; } - if self.mode.is_full() && self.active_diagnostics.is_some() { + if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) { self.dismiss_diagnostics(cx); return true; } @@ -13052,7 +13076,7 @@ impl Editor { }); } - fn go_to_diagnostic( + pub fn go_to_diagnostic( &mut self, _: &GoToDiagnostic, window: &mut Window, @@ -13062,7 +13086,7 @@ impl Editor { self.go_to_diagnostic_impl(Direction::Next, window, cx) } - fn go_to_prev_diagnostic( + pub fn go_to_prev_diagnostic( &mut self, _: &GoToPreviousDiagnostic, window: &mut Window, @@ -13080,137 +13104,76 @@ impl Editor { ) { let buffer = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest::(cx); - // If there is an active Diagnostic Popover jump to its diagnostic instead. - if direction == Direction::Next { - if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { - let Some(buffer_id) = popover.local_diagnostic.range.start.buffer_id else { - return; - }; - self.activate_diagnostics( - buffer_id, - popover.local_diagnostic.diagnostic.group_id, - window, - cx, - ); - if let Some(active_diagnostics) = self.active_diagnostics.as_ref() { - let primary_range_start = active_diagnostics.primary_range.start; - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut new_selection = s.newest_anchor().clone(); - new_selection.collapse_to(primary_range_start, SelectionGoal::None); - s.select_anchors(vec![new_selection.clone()]); - }); - self.refresh_inline_completion(false, true, window, cx); - } - return; + + let mut active_group_id = None; + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { + if active_group.active_range.start.to_offset(&buffer) == selection.start { + active_group_id = Some(active_group.group_id); } } - let active_group_id = self - .active_diagnostics - .as_ref() - .map(|active_group| active_group.group_id); - let active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { - active_diagnostics - .primary_range - .to_offset(&buffer) - .to_inclusive() - }); - let search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { - if active_primary_range.contains(&selection.head()) { - *active_primary_range.start() - } else { - selection.head() - } - } else { - selection.head() - }; + fn filtered( + snapshot: EditorSnapshot, + diagnostics: impl Iterator>, + ) -> impl Iterator> { + diagnostics + .filter(|entry| entry.range.start != entry.range.end) + .filter(|entry| !entry.diagnostic.is_unnecessary) + .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) + } let snapshot = self.snapshot(window, cx); - let primary_diagnostics_before = buffer - .diagnostics_in_range::(0..search_start) - .filter(|entry| entry.diagnostic.is_primary) - .filter(|entry| entry.range.start != entry.range.end) - .filter(|entry| entry.diagnostic.severity <= DiagnosticSeverity::WARNING) - .filter(|entry| !snapshot.intersects_fold(entry.range.start)) - .collect::>(); - let last_same_group_diagnostic_before = active_group_id.and_then(|active_group_id| { - primary_diagnostics_before - .iter() - .position(|entry| entry.diagnostic.group_id == active_group_id) - }); + let before = filtered( + snapshot.clone(), + buffer + .diagnostics_in_range(0..selection.start) + .filter(|entry| entry.range.start <= selection.start), + ); + let after = filtered( + snapshot, + buffer + .diagnostics_in_range(selection.start..buffer.len()) + .filter(|entry| entry.range.start >= selection.start), + ); - let primary_diagnostics_after = buffer - .diagnostics_in_range::(search_start..buffer.len()) - .filter(|entry| entry.diagnostic.is_primary) - .filter(|entry| entry.range.start != entry.range.end) - .filter(|entry| entry.diagnostic.severity <= DiagnosticSeverity::WARNING) - .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start)) - .collect::>(); - let last_same_group_diagnostic_after = active_group_id.and_then(|active_group_id| { - primary_diagnostics_after - .iter() - .enumerate() - .rev() - .find_map(|(i, entry)| { - if entry.diagnostic.group_id == active_group_id { - Some(i) - } else { - None + let mut found: Option> = None; + if direction == Direction::Prev { + 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] + { + for diagnostic in prev_diagnostics.into_iter().rev() { + if diagnostic.range.start != selection.start + || active_group_id + .is_some_and(|active| diagnostic.diagnostic.group_id < active) + { + found = Some(diagnostic); + break 'outer; } - }) - }); - - let next_primary_diagnostic = match direction { - Direction::Prev => primary_diagnostics_before - .iter() - .take(last_same_group_diagnostic_before.unwrap_or(usize::MAX)) - .rev() - .next(), - Direction::Next => primary_diagnostics_after - .iter() - .skip( - last_same_group_diagnostic_after - .map(|index| index + 1) - .unwrap_or(0), - ) - .next(), - }; - - // Cycle around to the start of the buffer, potentially moving back to the start of - // the currently active diagnostic. - let cycle_around = || match direction { - Direction::Prev => primary_diagnostics_after - .iter() - .rev() - .chain(primary_diagnostics_before.iter().rev()) - .next(), - Direction::Next => primary_diagnostics_before - .iter() - .chain(primary_diagnostics_after.iter()) - .next(), - }; - - if let Some((primary_range, group_id)) = next_primary_diagnostic - .or_else(cycle_around) - .map(|entry| (&entry.range, entry.diagnostic.group_id)) - { - let Some(buffer_id) = buffer.anchor_after(primary_range.start).buffer_id else { - return; - }; - self.activate_diagnostics(buffer_id, group_id, window, cx); - if self.active_diagnostics.is_some() { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(vec![Selection { - id: selection.id, - start: primary_range.start, - end: primary_range.start, - reversed: false, - goal: SelectionGoal::None, - }]); - }); - self.refresh_inline_completion(false, true, window, cx); + } + } + } else { + for diagnostic in after.chain(before) { + if diagnostic.range.start != selection.start + || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) + { + found = Some(diagnostic); + break; + } } } + let Some(next_diagnostic) = found else { + return; + }; + + let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + return; + }; + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![ + next_diagnostic.range.start..next_diagnostic.range.start, + ]) + }); + self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); + self.refresh_inline_completion(false, true, window, cx); } fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { @@ -14502,110 +14465,91 @@ impl Editor { } fn refresh_active_diagnostics(&mut self, cx: &mut Context) { - if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { + if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { let buffer = self.buffer.read(cx).snapshot(cx); - let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); - let primary_range_end = active_diagnostics.primary_range.end.to_offset(&buffer); + let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); + let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); let is_valid = buffer .diagnostics_in_range::(primary_range_start..primary_range_end) .any(|entry| { entry.diagnostic.is_primary && !entry.range.is_empty() && entry.range.start == primary_range_start - && entry.diagnostic.message == active_diagnostics.primary_message + && entry.diagnostic.message == active_diagnostics.active_message }); - if is_valid != active_diagnostics.is_valid { - active_diagnostics.is_valid = is_valid; - if is_valid { - let mut new_styles = HashMap::default(); - for (block_id, diagnostic) in &active_diagnostics.blocks { - new_styles.insert( - *block_id, - diagnostic_block_renderer(diagnostic.clone(), None, true), - ); - } - self.display_map.update(cx, |display_map, _cx| { - display_map.replace_blocks(new_styles); - }); - } else { - self.dismiss_diagnostics(cx); - } + if !is_valid { + self.dismiss_diagnostics(cx); } } } + pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> { + match &self.active_diagnostics { + ActiveDiagnostic::Group(group) => Some(group), + _ => None, + } + } + + pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { + self.dismiss_diagnostics(cx); + self.active_diagnostics = ActiveDiagnostic::All; + } + fn activate_diagnostics( &mut self, buffer_id: BufferId, - group_id: usize, + diagnostic: DiagnosticEntry, window: &mut Window, cx: &mut Context, ) { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + } self.dismiss_diagnostics(cx); let snapshot = self.snapshot(window, cx); - self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { - let buffer = self.buffer.read(cx).snapshot(cx); + let Some(diagnostic_renderer) = cx + .try_global::() + .map(|g| g.0.clone()) + else { + return; + }; + let buffer = self.buffer.read(cx).snapshot(cx); - let mut primary_range = None; - let mut primary_message = None; - let diagnostic_group = buffer - .diagnostic_group(buffer_id, group_id) - .filter_map(|entry| { - let start = entry.range.start; - let end = entry.range.end; - if snapshot.is_line_folded(MultiBufferRow(start.row)) - && (start.row == end.row - || snapshot.is_line_folded(MultiBufferRow(end.row))) - { - return None; - } - if entry.diagnostic.is_primary { - primary_range = Some(entry.range.clone()); - primary_message = Some(entry.diagnostic.message.clone()); - } - Some(entry) - }) - .collect::>(); - let primary_range = primary_range?; - let primary_message = primary_message?; + let diagnostic_group = buffer + .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) + .collect::>(); - let blocks = display_map - .insert_blocks( - diagnostic_group.iter().map(|entry| { - let diagnostic = entry.diagnostic.clone(); - let message_height = diagnostic.message.matches('\n').count() as u32 + 1; - BlockProperties { - style: BlockStyle::Fixed, - placement: BlockPlacement::Below( - buffer.anchor_after(entry.range.start), - ), - height: Some(message_height), - render: diagnostic_block_renderer(diagnostic, None, true), - priority: 0, - } - }), - cx, - ) - .into_iter() - .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic)) - .collect(); + let blocks = diagnostic_renderer.render_group( + diagnostic_group, + buffer_id, + snapshot, + cx.weak_entity(), + cx, + ); - Some(ActiveDiagnosticGroup { - primary_range: buffer.anchor_before(primary_range.start) - ..buffer.anchor_after(primary_range.end), - primary_message, - group_id, - blocks, - is_valid: true, - }) + let blocks = self.display_map.update(cx, |display_map, cx| { + display_map.insert_blocks(blocks, cx).into_iter().collect() }); + self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { + active_range: buffer.anchor_before(diagnostic.range.start) + ..buffer.anchor_after(diagnostic.range.end), + active_message: diagnostic.diagnostic.message.clone(), + group_id: diagnostic.diagnostic.group_id, + blocks, + }); + cx.notify(); } fn dismiss_diagnostics(&mut self, cx: &mut Context) { - if let Some(active_diagnostic_group) = self.active_diagnostics.take() { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + }; + + let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); + if let ActiveDiagnostic::Group(group) = prev { self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); + display_map.remove_blocks(group.blocks, cx); }); cx.notify(); } @@ -14658,6 +14602,8 @@ impl Editor { None }; self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { + let editor = editor.upgrade().unwrap(); + if let Some(debounce) = debounce { cx.background_executor().timer(debounce).await; } @@ -15230,7 +15176,7 @@ impl Editor { &mut self, creases: Vec>, auto_scroll: bool, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { if creases.is_empty() { @@ -15255,18 +15201,6 @@ impl Editor { cx.notify(); - if let Some(active_diagnostics) = self.active_diagnostics.take() { - // Clear diagnostics block when folding a range that contains it. - let snapshot = self.snapshot(window, cx); - if snapshot.intersects_fold(active_diagnostics.primary_range.start) { - drop(snapshot); - self.active_diagnostics = Some(active_diagnostics); - self.dismiss_diagnostics(cx); - } else { - self.active_diagnostics = Some(active_diagnostics); - } - } - self.scrollbar_marker_state.dirty = true; self.folds_did_change(cx); } @@ -20120,103 +20054,6 @@ impl InvalidationRegion for SnippetState { } } -pub fn diagnostic_block_renderer( - diagnostic: Diagnostic, - max_message_rows: Option, - allow_closing: bool, -) -> RenderBlock { - let (text_without_backticks, code_ranges) = - highlight_diagnostic_message(&diagnostic, max_message_rows); - - Arc::new(move |cx: &mut BlockContext| { - let group_id: SharedString = cx.block_id.to_string().into(); - - let mut text_style = cx.window.text_style().clone(); - text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status()); - let theme_settings = ThemeSettings::get_global(cx); - text_style.font_family = theme_settings.buffer_font.family.clone(); - text_style.font_style = theme_settings.buffer_font.style; - text_style.font_features = theme_settings.buffer_font.features.clone(); - text_style.font_weight = theme_settings.buffer_font.weight; - - let multi_line_diagnostic = diagnostic.message.contains('\n'); - - let buttons = |diagnostic: &Diagnostic| { - if multi_line_diagnostic { - v_flex() - } else { - h_flex() - } - .when(allow_closing, |div| { - div.children(diagnostic.is_primary.then(|| { - IconButton::new("close-block", IconName::XCircle) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .style(ButtonStyle::Transparent) - .visible_on_hover(group_id.clone()) - .on_click(move |_click, window, cx| { - window.dispatch_action(Box::new(Cancel), cx) - }) - .tooltip(|window, cx| { - Tooltip::for_action("Close Diagnostics", &Cancel, window, cx) - }) - })) - }) - .child( - IconButton::new("copy-block", IconName::Copy) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .style(ButtonStyle::Transparent) - .visible_on_hover(group_id.clone()) - .on_click({ - let message = diagnostic.message.clone(); - move |_click, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) - } - }) - .tooltip(Tooltip::text("Copy diagnostic message")), - ) - }; - - let icon_size = buttons(&diagnostic).into_any_element().layout_as_root( - AvailableSpace::min_size(), - cx.window, - cx.app, - ); - - h_flex() - .id(cx.block_id) - .group(group_id.clone()) - .relative() - .size_full() - .block_mouse_down() - .pl(cx.gutter_dimensions.width) - .w(cx.max_width - cx.gutter_dimensions.full_width()) - .child( - div() - .flex() - .w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width) - .flex_shrink(), - ) - .child(buttons(&diagnostic)) - .child(div().flex().flex_shrink_0().child( - StyledText::new(text_without_backticks.clone()).with_default_highlights( - &text_style, - code_ranges.iter().map(|range| { - ( - range.clone(), - HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - }, - ) - }), - ), - )) - .into_any_element() - }) -} - fn inline_completion_edit_text( current_snapshot: &BufferSnapshot, edits: &[(Range, String)], @@ -20237,74 +20074,7 @@ fn inline_completion_edit_text( edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) } -pub fn highlight_diagnostic_message( - diagnostic: &Diagnostic, - mut max_message_rows: Option, -) -> (SharedString, Vec>) { - let mut text_without_backticks = String::new(); - let mut code_ranges = Vec::new(); - - if let Some(source) = &diagnostic.source { - text_without_backticks.push_str(source); - code_ranges.push(0..source.len()); - text_without_backticks.push_str(": "); - } - - let mut prev_offset = 0; - let mut in_code_block = false; - let has_row_limit = max_message_rows.is_some(); - let mut newline_indices = diagnostic - .message - .match_indices('\n') - .filter(|_| has_row_limit) - .map(|(ix, _)| ix) - .fuse() - .peekable(); - - for (quote_ix, _) in diagnostic - .message - .match_indices('`') - .chain([(diagnostic.message.len(), "")]) - { - let mut first_newline_ix = None; - let mut last_newline_ix = None; - while let Some(newline_ix) = newline_indices.peek() { - if *newline_ix < quote_ix { - if first_newline_ix.is_none() { - first_newline_ix = Some(*newline_ix); - } - last_newline_ix = Some(*newline_ix); - - if let Some(rows_left) = &mut max_message_rows { - if *rows_left == 0 { - break; - } else { - *rows_left -= 1; - } - } - let _ = newline_indices.next(); - } else { - break; - } - } - let prev_len = text_without_backticks.len(); - let new_text = &diagnostic.message[prev_offset..first_newline_ix.unwrap_or(quote_ix)]; - text_without_backticks.push_str(new_text); - if in_code_block { - code_ranges.push(prev_len..text_without_backticks.len()); - } - prev_offset = last_newline_ix.unwrap_or(quote_ix) + 1; - in_code_block = !in_code_block; - if first_newline_ix.map_or(false, |newline_ix| newline_ix < quote_ix) { - text_without_backticks.push_str("..."); - break; - } - } - - (text_without_backticks.into(), code_ranges) -} - -fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla { +pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla { match severity { DiagnosticSeverity::ERROR => colors.error, DiagnosticSeverity::WARNING => colors.warning, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 51647b0226..3d55e20a8a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12585,276 +12585,6 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); } -#[gpui::test] -async fn cycle_through_same_place_diagnostics( - executor: BackgroundExecutor, - cx: &mut TestAppContext, -) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); - - cx.set_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - cx.update(|_, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), - version: None, - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 11), - lsp::Position::new(0, 12), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 12), - lsp::Position::new(0, 15), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 12), - lsp::Position::new(0, 15), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 25), - lsp::Position::new(0, 28), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - ], - }, - &[], - cx, - ) - .unwrap() - }); - }); - executor.run_until_parked(); - - //// Backward - - // Fourth diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - // Third diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // Second diagnostic, same place - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // First diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - // Wrapped over, fourth diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - cx.update_editor(|editor, window, cx| { - editor.move_to_beginning(&MoveToBeginning, window, cx); - }); - cx.assert_editor_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - //// Forward - - // First diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - // Second diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // Third diagnostic, same place - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - // Fourth diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - // Wrapped around, first diagnostic - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); -} - -#[gpui::test] -async fn active_diagnostics_dismiss_after_invalidation( - executor: BackgroundExecutor, - cx: &mut TestAppContext, -) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); - - cx.set_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - let message = "Something's wrong!"; - cx.update(|_, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 11), - lsp::Position::new(0, 12), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: message.to_string(), - ..Default::default() - }], - }, - &[], - cx, - ) - .unwrap() - }); - }); - executor.run_until_parked(); - - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - assert_eq!( - editor - .active_diagnostics - .as_ref() - .map(|diagnostics_group| diagnostics_group.primary_message.as_str()), - Some(message), - "Should have a diagnostics group activated" - ); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - cx.update(|_, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), - version: None, - diagnostics: Vec::new(), - }, - &[], - cx, - ) - .unwrap() - }); - }); - executor.run_until_parked(); - cx.update_editor(|editor, _, _| { - assert_eq!( - editor.active_diagnostics, None, - "After no diagnostics set to the editor, no diagnostics should be active" - ); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); - assert_eq!( - editor.active_diagnostics, None, - "Should be no diagnostics to go to and activate" - ); - }); - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); -} - #[gpui::test] async fn test_diagnostics_with_links(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a05e39e2f0..15e5b4d379 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,11 +1,11 @@ use crate::{ - BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, ChunkRendererContext, - ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, - DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, - Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, - FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, - InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, - MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, + ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, + ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, + DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, + EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, + FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, + HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, + LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, @@ -1614,12 +1614,12 @@ impl EditorElement { project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT, }); - let active_diagnostics_group = self - .editor - .read(cx) - .active_diagnostics - .as_ref() - .map(|active_diagnostics| active_diagnostics.group_id); + let active_diagnostics_group = + if let ActiveDiagnostic::Group(group) = &self.editor.read(cx).active_diagnostics { + Some(group.group_id) + } else { + None + }; let diagnostics_by_rows = self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx); @@ -2643,12 +2643,15 @@ impl EditorElement { sticky_header_excerpt_id: Option, window: &mut Window, cx: &mut App, - ) -> (AnyElement, Size, DisplayRow, Pixels) { + ) -> Option<(AnyElement, Size, DisplayRow, Pixels)> { let mut x_position = None; let mut element = match block { - Block::Custom(block) => { - let block_start = block.start().to_point(&snapshot.buffer_snapshot); - let block_end = block.end().to_point(&snapshot.buffer_snapshot); + Block::Custom(custom) => { + let block_start = custom.start().to_point(&snapshot.buffer_snapshot); + let block_end = custom.end().to_point(&snapshot.buffer_snapshot); + if block.place_near() && snapshot.is_line_folded(MultiBufferRow(block_start.row)) { + return None; + } let align_to = block_start.to_display_point(snapshot); let x_and_width = |layout: &LineWithInvisibles| { Some(( @@ -2686,7 +2689,7 @@ impl EditorElement { div() .size_full() - .child(block.render(&mut BlockContext { + .child(custom.render(&mut BlockContext { window, app: cx, anchor_x, @@ -2774,6 +2777,7 @@ impl EditorElement { } else { element.layout_as_root(size(available_width, quantized_height.into()), window, cx) }; + let mut element_height_in_lines = ((final_size.height / line_height).ceil() as u32).max(1); let mut row = block_row_start; let mut x_offset = px(0.); @@ -2781,20 +2785,19 @@ impl EditorElement { if let BlockId::Custom(custom_block_id) = block_id { if block.has_height() { - let mut element_height_in_lines = - ((final_size.height / line_height).ceil() as u32).max(1); - - if block.place_near() && element_height_in_lines == 1 { + if block.place_near() { if let Some((x_target, line_width)) = x_position { let margin = em_width * 2; if line_width + final_size.width + margin < editor_width + gutter_dimensions.full_width() && !row_block_types.contains_key(&(row - 1)) + && element_height_in_lines == 1 { x_offset = line_width + margin; row = row - 1; is_block = false; element_height_in_lines = 0; + row_block_types.insert(row, is_block); } else { let max_offset = editor_width + gutter_dimensions.full_width() - final_size.width; @@ -2809,9 +2812,11 @@ impl EditorElement { } } } - row_block_types.insert(row, is_block); + for i in 0..element_height_in_lines { + row_block_types.insert(row + i, is_block); + } - (element, final_size, row, x_offset) + Some((element, final_size, row, x_offset)) } fn render_buffer_header( @@ -3044,7 +3049,7 @@ impl EditorElement { focused_block = None; } - let (element, element_size, row, x_offset) = self.render_block( + if let Some((element, element_size, row, x_offset)) = self.render_block( block, AvailableSpace::MinContent, block_id, @@ -3067,19 +3072,19 @@ impl EditorElement { sticky_header_excerpt_id, window, cx, - ); - - fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); - blocks.push(BlockLayout { - id: block_id, - x_offset, - row: Some(row), - element, - available_space: size(AvailableSpace::MinContent, element_size.height.into()), - style: BlockStyle::Fixed, - overlaps_gutter: true, - is_buffer_header: block.is_buffer_header(), - }); + ) { + fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); + blocks.push(BlockLayout { + id: block_id, + x_offset, + row: Some(row), + element, + available_space: size(AvailableSpace::MinContent, element_size.height.into()), + style: BlockStyle::Fixed, + overlaps_gutter: true, + is_buffer_header: block.is_buffer_header(), + }); + } } for (row, block) in non_fixed_blocks { @@ -3101,7 +3106,7 @@ impl EditorElement { focused_block = None; } - let (element, element_size, row, x_offset) = self.render_block( + if let Some((element, element_size, row, x_offset)) = self.render_block( block, width, block_id, @@ -3124,18 +3129,18 @@ impl EditorElement { sticky_header_excerpt_id, window, cx, - ); - - blocks.push(BlockLayout { - id: block_id, - x_offset, - row: Some(row), - element, - available_space: size(width, element_size.height.into()), - style, - overlaps_gutter: !block.place_near(), - is_buffer_header: block.is_buffer_header(), - }); + ) { + blocks.push(BlockLayout { + id: block_id, + x_offset, + row: Some(row), + element, + available_space: size(width, element_size.height.into()), + style, + overlaps_gutter: !block.place_near(), + is_buffer_header: block.is_buffer_header(), + }); + } } if let Some(focused_block) = focused_block { @@ -3155,7 +3160,7 @@ impl EditorElement { BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), }; - let (element, element_size, _, x_offset) = self.render_block( + if let Some((element, element_size, _, x_offset)) = self.render_block( &block, width, focused_block.id, @@ -3178,18 +3183,18 @@ impl EditorElement { sticky_header_excerpt_id, window, cx, - ); - - blocks.push(BlockLayout { - id: block.id(), - x_offset, - row: None, - element, - available_space: size(width, element_size.height.into()), - style, - overlaps_gutter: true, - is_buffer_header: block.is_buffer_header(), - }); + ) { + blocks.push(BlockLayout { + id: block.id(), + x_offset, + row: None, + element, + available_space: size(width, element_size.height.into()), + style, + overlaps_gutter: true, + is_buffer_header: block.is_buffer_header(), + }); + } } } } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fd53c4b0ad..55a35fb11f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ - Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, - Hover, + ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, + EditorSnapshot, Hover, display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible}, hover_links::{InlayHighlight, RangeInEditor}, scroll::{Autoscroll, ScrollAmount}, @@ -95,7 +95,7 @@ pub fn show_keyboard_hover( } pub struct InlayHover { - pub range: InlayHighlight, + pub(crate) range: InlayHighlight, pub tooltip: HoverBlock, } @@ -276,6 +276,12 @@ fn show_hover( } let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; + let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All; + let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics { + Some(group.group_id) + } else { + None + }; let task = cx.spawn_in(window, async move |this, cx| { async move { @@ -302,11 +308,16 @@ fn show_hover( } let offset = anchor.to_offset(&snapshot.buffer_snapshot); - let local_diagnostic = snapshot - .buffer_snapshot - .diagnostics_in_range::(offset..offset) - // Find the entry with the most specific range - .min_by_key(|entry| entry.range.len()); + let local_diagnostic = if all_diagnostics_active { + None + } else { + snapshot + .buffer_snapshot + .diagnostics_in_range::(offset..offset) + .filter(|diagnostic| Some(diagnostic.diagnostic.group_id) != active_group_id) + // Find the entry with the most specific range + .min_by_key(|entry| entry.range.len()) + }; let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic { let text = match local_diagnostic.diagnostic.source { @@ -638,6 +649,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { }, syntax: cx.theme().syntax().clone(), selection_background_color: { cx.theme().players().local().selection }, + height_is_multiple_of_line_height: true, heading: StyleRefinement::default() .font_weight(FontWeight::BOLD) .text_base() @@ -707,7 +719,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) #[derive(Default)] pub struct HoverState { - pub info_popovers: Vec, + pub(crate) info_popovers: Vec, pub diagnostic_popover: Option, pub triggered_from: Option, pub info_task: Option>>, diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index b197c56bbc..5ab6866495 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,18 +1,25 @@ pub mod editor_lsp_test_context; pub mod editor_test_context; +use std::{rc::Rc, sync::LazyLock}; + pub use crate::rust_analyzer_ext::expand_macro_recursively; use crate::{ DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, - display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + display_map::{ + Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot, + ToDisplayPoint, + }, }; +use collections::HashMap; use gpui::{ - AppContext as _, Context, Entity, Font, FontFeatures, FontStyle, FontWeight, Pixels, Window, - font, + AppContext as _, Context, Entity, EntityId, Font, FontFeatures, FontStyle, FontWeight, Pixels, + VisualTestContext, Window, font, size, }; +use multi_buffer::ToPoint; use pretty_assertions::assert_eq; use project::Project; -use std::sync::LazyLock; +use ui::{App, BorrowAppContext, px}; use util::test::{marked_text_offsets, marked_text_ranges}; #[cfg(test)] @@ -122,3 +129,126 @@ pub(crate) fn build_editor_with_project( ) -> Editor { Editor::new(EditorMode::full(), buffer, Some(project), window, cx) } + +#[derive(Default)] +struct TestBlockContent( + HashMap<(EntityId, CustomBlockId), Rc String>>, +); + +impl gpui::Global for TestBlockContent {} + +pub fn set_block_content_for_tests( + editor: &Entity, + id: CustomBlockId, + cx: &mut App, + f: impl Fn(&mut VisualTestContext) -> String + 'static, +) { + cx.update_default_global::(|bc, _| { + bc.0.insert((editor.entity_id(), id), Rc::new(f)) + }); +} + +pub fn block_content_for_tests( + editor: &Entity, + id: CustomBlockId, + cx: &mut VisualTestContext, +) -> Option { + let f = cx.update(|_, cx| { + cx.default_global::() + .0 + .get(&(editor.entity_id(), id)) + .cloned() + })?; + Some(f(cx)) +} + +pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestContext) -> String { + cx.draw( + gpui::Point::default(), + size(px(3000.0), px(3000.0)), + |_, _| editor.clone(), + ); + let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let text = editor.display_text(cx); + let lines = text.lines().map(|s| s.to_string()).collect::>(); + let blocks = snapshot + .blocks_in_range(DisplayRow(0)..snapshot.max_point().row()) + .map(|(row, block)| (row, block.clone())) + .collect::>(); + (snapshot, lines, blocks) + }); + for (row, block) in blocks { + match block { + Block::Custom(custom_block) => { + if let BlockPlacement::Near(x) = &custom_block.placement { + if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) { + continue; + } + }; + let content = block_content_for_tests(&editor, custom_block.id, cx) + .expect("block content not found"); + // 2: "related info 1 for diagnostic 0" + if let Some(height) = custom_block.height { + if height == 0 { + lines[row.0 as usize - 1].push_str(" § "); + lines[row.0 as usize - 1].push_str(&content); + } else { + let block_lines = content.lines().collect::>(); + assert_eq!(block_lines.len(), height as usize); + lines[row.0 as usize].push_str("§ "); + lines[row.0 as usize].push_str(block_lines[0].trim_end()); + for i in 1..height as usize { + lines[row.0 as usize + i].push_str("§ "); + lines[row.0 as usize + i].push_str(block_lines[i].trim_end()); + } + } + } + } + Block::FoldedBuffer { + first_excerpt, + height, + } => { + lines[row.0 as usize].push_str(&cx.update(|_, cx| { + format!( + "§ {}", + first_excerpt + .buffer + .file() + .unwrap() + .file_name(cx) + .to_string_lossy() + ) + })); + for row in row.0 + 1..row.0 + height { + lines[row as usize].push_str("§ -----"); + } + } + Block::ExcerptBoundary { + excerpt, + height, + starts_new_buffer, + } => { + if starts_new_buffer { + lines[row.0 as usize].push_str(&cx.update(|_, cx| { + format!( + "§ {}", + excerpt + .buffer + .file() + .unwrap() + .file_name(cx) + .to_string_lossy() + ) + })); + } else { + lines[row.0 as usize].push_str("§ -----") + } + for row in row.0 + 1..row.0 + height { + lines[row as usize].push_str("§ -----"); + } + } + } + } + lines.join("\n") +} diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 2407606a66..04e43f5b09 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -556,6 +556,25 @@ impl TextLayout { .collect::>() .join("\n") } + + /// The text for this layout (with soft-wraps as newlines) + pub fn wrapped_text(&self) -> String { + let mut lines = Vec::new(); + for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() { + let mut seen = 0; + for boundary in wrapped.layout.wrap_boundaries.iter() { + let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs + [boundary.glyph_ix] + .index; + + lines.push(wrapped.text[seen..index].to_string()); + seen = index; + } + lines.push(wrapped.text[seen..].to_string()); + } + + lines.join("\n") + } } /// A text element that can be interacted with. diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0d6f797bc3..a95799845f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1265,6 +1265,7 @@ impl Buffer { self.reload_task = Some(cx.spawn(async move |this, cx| { let Some((new_mtime, new_text)) = this.update(cx, |this, cx| { let file = this.file.as_ref()?.as_local()?; + Some((file.disk_state().mtime(), file.load(cx))) })? else { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ffae4b18d7..f341bf09c9 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -550,13 +550,7 @@ pub trait LspAdapter: 'static + Send + Sync { /// Returns a list of code actions supported by a given LspAdapter fn code_action_kinds(&self) -> Option> { - Some(vec![ - CodeActionKind::EMPTY, - CodeActionKind::QUICKFIX, - CodeActionKind::REFACTOR, - CodeActionKind::REFACTOR_EXTRACT, - CodeActionKind::SOURCE, - ]) + None } fn disk_based_diagnostic_sources(&self) -> Vec { diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 16f4f621e0..03039f41fe 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1,6 +1,7 @@ pub mod parser; mod path_range; +use std::borrow::Cow; use std::collections::HashSet; use std::iter; use std::mem; @@ -59,6 +60,7 @@ pub struct MarkdownStyle { pub heading: StyleRefinement, pub heading_level_styles: Option, pub table_overflow_x_scroll: bool, + pub height_is_multiple_of_line_height: bool, } impl Default for MarkdownStyle { @@ -78,6 +80,7 @@ impl Default for MarkdownStyle { heading: Default::default(), heading_level_styles: None, table_overflow_x_scroll: false, + height_is_multiple_of_line_height: false, } } } @@ -205,6 +208,22 @@ impl Markdown { &self.parsed_markdown } + pub fn escape(s: &str) -> Cow { + let count = s.bytes().filter(|c| c.is_ascii_punctuation()).count(); + if count > 0 { + let mut output = String::with_capacity(s.len() + count); + for c in s.chars() { + if c.is_ascii_punctuation() { + output.push('\\') + } + output.push(c) + } + output.into() + } else { + s.into() + } + } + fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context) { if self.selection.end <= self.selection.start { return; @@ -367,6 +386,27 @@ impl MarkdownElement { } } + #[cfg(any(test, feature = "test-support"))] + pub fn rendered_text( + markdown: Entity, + cx: &mut gpui::VisualTestContext, + style: impl FnOnce(&Window, &App) -> MarkdownStyle, + ) -> String { + use gpui::size; + + let (text, _) = cx.draw( + Default::default(), + size(px(600.0), px(600.0)), + |window, cx| Self::new(markdown, style(window, cx)), + ); + text.text + .lines + .iter() + .map(|line| line.layout.wrapped_text()) + .collect::>() + .join("\n") + } + pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self { self.code_block_renderer = variant; self @@ -496,9 +536,9 @@ impl MarkdownElement { pending: true, }; window.focus(&markdown.focus_handle); - window.prevent_default(); } + window.prevent_default(); cx.notify(); } } else if phase.capture() { @@ -634,7 +674,9 @@ impl Element for MarkdownElement { match tag { MarkdownTag::Paragraph => { builder.push_div( - div().mb_2().line_height(rems(1.3)), + div().when(!self.style.height_is_multiple_of_line_height, |el| { + el.mb_2().line_height(rems(1.3)) + }), range, markdown_end, ); @@ -767,11 +809,11 @@ impl Element for MarkdownElement { }; builder.push_div( div() - .mb_1() + .when(!self.style.height_is_multiple_of_line_height, |el| { + el.mb_1().gap_1().line_height(rems(1.3)) + }) .h_flex() .items_start() - .gap_1() - .line_height(rems(1.3)) .child(bullet), range, markdown_end, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 994815910c..2a4c558b5a 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1578,7 +1578,27 @@ impl MultiBuffer { let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot); let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_excerpt_ranges_for_path( + self.set_merged_excerpt_ranges_for_path( + path, + buffer, + excerpt_ranges, + &buffer_snapshot, + new, + counts, + cx, + ) + } + + pub fn set_excerpt_ranges_for_path( + &mut self, + path: PathKey, + buffer: Entity, + buffer_snapshot: &BufferSnapshot, + excerpt_ranges: Vec>, + cx: &mut Context, + ) -> (Vec>, bool) { + let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); + self.set_merged_excerpt_ranges_for_path( path, buffer, excerpt_ranges, @@ -1612,11 +1632,11 @@ impl MultiBuffer { multi_buffer .update(cx, move |multi_buffer, cx| { - let (ranges, _) = multi_buffer.set_excerpt_ranges_for_path( + let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( path_key, buffer, excerpt_ranges, - buffer_snapshot, + &buffer_snapshot, new, counts, cx, @@ -1629,12 +1649,12 @@ impl MultiBuffer { } /// Sets excerpts, returns `true` if at least one new excerpt was added. - fn set_excerpt_ranges_for_path( + fn set_merged_excerpt_ranges_for_path( &mut self, path: PathKey, buffer: Entity, ranges: Vec>, - buffer_snapshot: BufferSnapshot, + buffer_snapshot: &BufferSnapshot, new: Vec>, counts: Vec, cx: &mut Context, @@ -1665,6 +1685,7 @@ impl MultiBuffer { let mut counts: Vec = Vec::new(); for range in expanded_ranges { if let Some(last_range) = merged_ranges.last_mut() { + debug_assert!(last_range.context.start <= range.context.start); if last_range.context.end >= range.context.start { last_range.context.end = range.context.end; *counts.last_mut().unwrap() += 1; @@ -5878,13 +5899,14 @@ impl MultiBufferSnapshot { buffer_id: BufferId, group_id: usize, ) -> impl Iterator> + '_ { - self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, _| { + self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, range| { if buffer.remote_id() != buffer_id { return None; }; Some( buffer - .diagnostic_group(group_id) + .diagnostics_in_range(range, false) + .filter(move |diagnostic| diagnostic.diagnostic.group_id == group_id) .map(move |DiagnosticEntry { diagnostic, range }| (range, diagnostic)), ) })