Detailed changes
@@ -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",
@@ -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"] }
@@ -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<DiagnosticEntry<Point>>,
+ buffer_id: BufferId,
+ diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+ cx: &mut App,
+ ) -> Vec<DiagnosticBlock> {
+ 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<DiagnosticEntry<Point>>,
+ buffer_id: BufferId,
+ snapshot: EditorSnapshot,
+ editor: WeakEntity<Editor>,
+ cx: &mut App,
+ ) -> Vec<BlockProperties<Anchor>> {
+ 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<Point>,
+ pub(crate) severity: DiagnosticSeverity,
+ pub(crate) buffer_id: BufferId,
+ pub(crate) markdown: Entity<Markdown>,
+ pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+}
+
+impl DiagnosticBlock {
+ pub fn render_block(&self, editor: WeakEntity<Editor>, 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<Editor>,
+ diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
+ 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<T: ToOffset>(
+ editor: &mut Editor,
+ range: Range<T>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ 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));
+ }
+}
@@ -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<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
editor: Entity<Editor>,
+ diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,
+ blocks: HashMap<BufferId, Vec<CustomBlockId>>,
summary: DiagnosticSummary,
- excerpts: Entity<MultiBuffer>,
- path_states: Vec<PathState>,
- paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,
+ multibuffer: Entity<MultiBuffer>,
+ paths_to_update: BTreeSet<ProjectPath>,
include_warnings: bool,
- context: u32,
update_excerpts_task: Option<Task<Result<()>>>,
_subscription: Subscription,
}
-struct PathState {
- path: ProjectPath,
- diagnostic_groups: Vec<DiagnosticGroupState>,
-}
-
-struct DiagnosticGroupState {
- language_server_id: LanguageServerId,
- primary_diagnostic: DiagnosticEntry<language::Anchor>,
- primary_excerpt_ix: usize,
- excerpts: Vec<ExcerptId>,
- blocks: HashSet<CustomBlockId>,
- block_count: usize,
-}
-
impl EventEmitter<EditorEvent> 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<Self>) -> 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<Project>,
workspace: WeakEntity<Workspace>,
@@ -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<Project>,
- include_warnings: bool,
- workspace: WeakEntity<Workspace>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> 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<Self>) {
- 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::<BTreeSet<_>>();
- 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<DiagnosticEntry<text::Anchor>>,
+ new: &Vec<DiagnosticEntry<text::Anchor>>,
+ 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<LanguageServerId>,
buffer: Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
- 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(),
- }
- }
- }
- 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<Point>, Range<Point>, 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::<Point>(&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;
- }
- }
+ 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::<Vec<_>>();
+ 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(());
+ }
- 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,
- });
- }
+ let mut grouped: HashMap<usize, Vec<_>> = 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<DiagnosticBlock> = Vec::new();
- 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();
- }
+ 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,
+ )
+ })?;
- 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,
- });
- }
+ 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);
- 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<ExcerptRange<Point>> = 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)
+ });
+ })
}
+ 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.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 =
+ 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.refresh()
- });
- selections = editor.selections.all::<usize>(cx);
+ s.select_anchor_ranges([range_to_select]);
+ })
+ })
}
+ }
- // 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;
- }
+ 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,
}
- }
- }
- editor.change_selections(None, window, cx, |s| {
- s.select(selections);
- });
- Some(())
+ });
+ let block_ids = this.editor.update(cx, |editor, cx| {
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map.insert_blocks(editor_blocks, cx)
+ })
});
- })?;
- 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);
+ #[cfg(test)]
+ {
+ 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,
+ )
+ },
+ );
}
- } else if this.focus_handle.is_focused(window) {
- let focus_handle = this.editor.focus_handle(cx);
- window.focus(&focus_handle);
}
- #[cfg(test)]
- this.check_invariants(cx);
-
- cx.notify();
+ this.blocks.insert(buffer_id, block_ids);
+ cx.notify()
})
})
}
-
- #[cfg(test)]
- fn check_invariants(&self, cx: &mut Context<Self>) {
- 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<language::Anchor>,
- new: &DiagnosticEntry<language::Anchor>,
- 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<Point>,
context: u32,
snapshot: BufferSnapshot,
- cx: AsyncApp,
-) -> Task<Range<Point>> {
- 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<Point> {
+ 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
@@ -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<char>`, 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<char>`, 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<char>`, 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<char>`, 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<char>`, which does not implement
+ § the `Copy` trait (back)
+ let y = vec![];
+ § move occurs because `y` has type `Vec<char>`, 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<char>`, 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<char>`, 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<char>`, which does not implement
+ § the `Copy` trait (back)
+ let y = vec![];
+ § move occurs because `y` has type `Vec<char>`, 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<char>`, 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<char>`, 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<char>`, which does not implement
+ § the `Copy` trait (back)
+ let y = vec![];
+ § move occurs because `y` has type `Vec<char>`, 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<char>`, 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<char>`, 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();"
+ }
);
}
@@ -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<CustomBlockId> for ElementId {
fn from(val: CustomBlockId) -> Self {
@@ -89,7 +89,7 @@ pub enum BlockPlacement<T> {
}
impl<T> BlockPlacement<T> {
- 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<Anchor> {
}
pub struct CustomBlock {
- id: CustomBlockId,
- placement: BlockPlacement<Anchor>,
- height: Option<u32>,
+ pub id: CustomBlockId,
+ pub placement: BlockPlacement<Anchor>,
+ pub height: Option<u32>,
style: BlockStyle,
render: Arc<Mutex<RenderBlock>>,
priority: usize,
}
+#[derive(Clone)]
pub struct BlockProperties<P> {
pub placement: BlockPlacement<P>,
// 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) => {
@@ -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<DiagnosticEntry<Point>>,
+ buffer_id: BufferId,
+ snapshot: EditorSnapshot,
+ editor: WeakEntity<Editor>,
+ cx: &mut App,
+ ) -> Vec<BlockProperties<Anchor>>;
+}
+
+pub(crate) struct GlobalDiagnosticRenderer(pub Arc<dyn DiagnosticRenderer>);
+
+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<SnippetState>,
select_syntax_node_history: SelectSyntaxNodeHistory,
ime_transaction: Option<TransactionId>,
- active_diagnostics: Option<ActiveDiagnosticGroup>,
+ 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<Anchor>,
- primary_message: String,
- group_id: usize,
- blocks: HashMap<CustomBlockId, Diagnostic>,
- is_valid: bool,
+pub struct ActiveDiagnosticGroup {
+ pub active_range: Range<Anchor>,
+ pub active_message: String,
+ pub group_id: usize,
+ pub blocks: HashSet<CustomBlockId>,
+}
+
+#[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::<usize>(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<Item = DiagnosticEntry<usize>>,
+ ) -> impl Iterator<Item = DiagnosticEntry<usize>> {
+ 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::<usize>(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::<Vec<_>>();
- 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::<usize>(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::<Vec<_>>();
- 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<DiagnosticEntry<usize>> = None;
+ if direction == Direction::Prev {
+ 'outer: for prev_diagnostics in [before.collect::<Vec<_>>(), after.collect::<Vec<_>>()]
+ {
+ 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(),
+ }
+ }
+ } 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;
};
- // 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(),
+ let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else {
+ return;
};
-
- 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);
- }
- }
+ 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<Self>) {
@@ -14502,110 +14465,91 @@ impl Editor {
}
fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
- 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::<usize>(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>) {
+ self.dismiss_diagnostics(cx);
+ self.active_diagnostics = ActiveDiagnostic::All;
+ }
+
fn activate_diagnostics(
&mut self,
buffer_id: BufferId,
- group_id: usize,
+ diagnostic: DiagnosticEntry<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
+ 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::<GlobalDiagnosticRenderer>()
+ .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::<Vec<_>>();
- let primary_range = primary_range?;
- let primary_message = primary_message?;
-
- 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 diagnostic_group = buffer
+ .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
+ .collect::<Vec<_>>();
- 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 = diagnostic_renderer.render_group(
+ diagnostic_group,
+ buffer_id,
+ snapshot,
+ cx.weak_entity(),
+ cx,
+ );
+
+ 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<Self>) {
- 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<Crease<T>>,
auto_scroll: bool,
- window: &mut Window,
+ _window: &mut Window,
cx: &mut Context<Self>,
) {
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<u8>,
- 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<Anchor>, 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<u8>,
-) -> (SharedString, Vec<Range<usize>>) {
- 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,
@@ -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, |_| {});
@@ -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<ExcerptId>,
window: &mut Window,
cx: &mut App,
- ) -> (AnyElement, Size<Pixels>, DisplayRow, Pixels) {
+ ) -> Option<(AnyElement, Size<Pixels>, 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(),
+ });
+ }
}
}
}
@@ -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::<usize>(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::<usize>(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<InfoPopover>,
+ pub(crate) info_popovers: Vec<InfoPopover>,
pub diagnostic_popover: Option<DiagnosticPopover>,
pub triggered_from: Option<Anchor>,
pub info_task: Option<Task<Option<()>>>,
@@ -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<dyn Fn(&mut VisualTestContext) -> String>>,
+);
+
+impl gpui::Global for TestBlockContent {}
+
+pub fn set_block_content_for_tests(
+ editor: &Entity<Editor>,
+ id: CustomBlockId,
+ cx: &mut App,
+ f: impl Fn(&mut VisualTestContext) -> String + 'static,
+) {
+ cx.update_default_global::<TestBlockContent, _>(|bc, _| {
+ bc.0.insert((editor.entity_id(), id), Rc::new(f))
+ });
+}
+
+pub fn block_content_for_tests(
+ editor: &Entity<Editor>,
+ id: CustomBlockId,
+ cx: &mut VisualTestContext,
+) -> Option<String> {
+ let f = cx.update(|_, cx| {
+ cx.default_global::<TestBlockContent>()
+ .0
+ .get(&(editor.entity_id(), id))
+ .cloned()
+ })?;
+ Some(f(cx))
+}
+
+pub fn editor_content_with_blocks(editor: &Entity<Editor>, 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::<Vec<String>>();
+ let blocks = snapshot
+ .blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
+ .map(|(row, block)| (row, block.clone()))
+ .collect::<Vec<_>>();
+ (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::<Vec<_>>();
+ 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")
+}
@@ -556,6 +556,25 @@ impl TextLayout {
.collect::<Vec<_>>()
.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.
@@ -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 {
@@ -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<Vec<CodeActionKind>> {
- Some(vec![
- CodeActionKind::EMPTY,
- CodeActionKind::QUICKFIX,
- CodeActionKind::REFACTOR,
- CodeActionKind::REFACTOR_EXTRACT,
- CodeActionKind::SOURCE,
- ])
+ None
}
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
@@ -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<HeadingLevelStyles>,
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<str> {
+ 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<Self>) {
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<Markdown>,
+ 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::<Vec<_>>()
+ .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,
@@ -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>,
+ buffer_snapshot: &BufferSnapshot,
+ excerpt_ranges: Vec<ExcerptRange<Point>>,
+ cx: &mut Context<Self>,
+ ) -> (Vec<Range<Anchor>>, 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<Buffer>,
ranges: Vec<ExcerptRange<Point>>,
- buffer_snapshot: BufferSnapshot,
+ buffer_snapshot: &BufferSnapshot,
new: Vec<ExcerptRange<Point>>,
counts: Vec<usize>,
cx: &mut Context<Self>,
@@ -1665,6 +1685,7 @@ impl MultiBuffer {
let mut counts: Vec<usize> = 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<Item = DiagnosticEntry<Point>> + '_ {
- 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)),
)
})