Use a different invalidation strategy for project-wide diagnostics
This commit is contained in:
parent
da460edb8b
commit
dcf26acaac
5 changed files with 336 additions and 360 deletions
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
|
context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
|
||||||
display_map::{BlockDisposition, BlockId, BlockProperties},
|
display_map::{BlockDisposition, BlockId, BlockProperties},
|
||||||
|
@ -9,20 +9,29 @@ use gpui::{
|
||||||
action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
|
action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use language::{Bias, Buffer, Point};
|
use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point};
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::{ops::Range, path::Path, sync::Arc};
|
use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
action!(Toggle);
|
action!(Toggle);
|
||||||
|
action!(ClearInvalid);
|
||||||
|
|
||||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
|
cx.add_bindings([
|
||||||
|
Binding::new("alt-shift-D", Toggle, None),
|
||||||
|
Binding::new(
|
||||||
|
"alt-shift-C",
|
||||||
|
ClearInvalid,
|
||||||
|
Some("ProjectDiagnosticsEditor"),
|
||||||
|
),
|
||||||
|
]);
|
||||||
cx.add_action(ProjectDiagnosticsEditor::toggle);
|
cx.add_action(ProjectDiagnosticsEditor::toggle);
|
||||||
|
cx.add_action(ProjectDiagnosticsEditor::clear_invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event = editor::Event;
|
type Event = editor::Event;
|
||||||
|
@ -34,20 +43,22 @@ struct ProjectDiagnostics {
|
||||||
struct ProjectDiagnosticsEditor {
|
struct ProjectDiagnosticsEditor {
|
||||||
editor: ViewHandle<Editor>,
|
editor: ViewHandle<Editor>,
|
||||||
excerpts: ModelHandle<MultiBuffer>,
|
excerpts: ModelHandle<MultiBuffer>,
|
||||||
path_states: Vec<(Arc<Path>, PathState)>,
|
path_states: Vec<(Arc<Path>, Vec<DiagnosticGroupState>)>,
|
||||||
build_settings: BuildSettings,
|
build_settings: BuildSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
struct DiagnosticGroupState {
|
||||||
struct PathState {
|
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
||||||
last_excerpt: ExcerptId,
|
excerpts: Vec<ExcerptId>,
|
||||||
diagnostic_group_states: HashMap<usize, DiagnosticGroupState>,
|
blocks: HashMap<BlockId, DiagnosticBlock>,
|
||||||
|
block_count: usize,
|
||||||
|
is_valid: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
enum DiagnosticBlock {
|
||||||
struct DiagnosticGroupState {
|
Header(Diagnostic),
|
||||||
excerpts: Vec<ExcerptId>,
|
Inline(Diagnostic),
|
||||||
blocks: Vec<BlockId>,
|
Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectDiagnostics {
|
impl ProjectDiagnostics {
|
||||||
|
@ -146,6 +157,38 @@ impl ProjectDiagnosticsEditor {
|
||||||
workspace.add_item(diagnostics, cx);
|
workspace.add_item(diagnostics, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_invalid(&mut self, _: &ClearInvalid, cx: &mut ViewContext<Self>) {
|
||||||
|
let mut blocks_to_delete = HashSet::default();
|
||||||
|
let mut excerpts_to_delete = Vec::new();
|
||||||
|
let mut path_ixs_to_delete = Vec::new();
|
||||||
|
for (ix, (_, groups)) in self.path_states.iter_mut().enumerate() {
|
||||||
|
groups.retain(|group| {
|
||||||
|
if group.is_valid {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
blocks_to_delete.extend(group.blocks.keys().copied());
|
||||||
|
excerpts_to_delete.extend(group.excerpts.iter().cloned());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if groups.is_empty() {
|
||||||
|
path_ixs_to_delete.push(ix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ix in path_ixs_to_delete.into_iter().rev() {
|
||||||
|
self.path_states.remove(ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.excerpts.update(cx, |excerpts, cx| {
|
||||||
|
excerpts_to_delete.sort_unstable();
|
||||||
|
excerpts.remove_excerpts(&excerpts_to_delete, cx)
|
||||||
|
});
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.remove_blocks(blocks_to_delete, cx));
|
||||||
|
}
|
||||||
|
|
||||||
fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
|
fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
|
||||||
let snapshot;
|
let snapshot;
|
||||||
let path;
|
let path;
|
||||||
|
@ -165,189 +208,260 @@ impl ProjectDiagnosticsEditor {
|
||||||
{
|
{
|
||||||
Ok(ix) => ix,
|
Ok(ix) => ix,
|
||||||
Err(ix) => {
|
Err(ix) => {
|
||||||
self.path_states.insert(
|
self.path_states
|
||||||
ix,
|
.insert(ix, (path.clone(), Default::default()));
|
||||||
(
|
|
||||||
path.clone(),
|
|
||||||
PathState {
|
|
||||||
last_excerpt: ExcerptId::max(),
|
|
||||||
diagnostic_group_states: Default::default(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
ix
|
ix
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prev_excerpt_id = if path_ix > 0 {
|
let mut prev_excerpt_id = if path_ix > 0 {
|
||||||
self.path_states[path_ix - 1].1.last_excerpt.clone()
|
let prev_path_last_group = &self.path_states[path_ix - 1].1.last().unwrap();
|
||||||
|
prev_path_last_group.excerpts.last().unwrap().clone()
|
||||||
} else {
|
} else {
|
||||||
ExcerptId::min()
|
ExcerptId::min()
|
||||||
};
|
};
|
||||||
let path_state = &mut self.path_states[path_ix].1;
|
|
||||||
|
|
||||||
|
let groups = &mut self.path_states[path_ix].1;
|
||||||
|
let mut groups_to_add = Vec::new();
|
||||||
let mut blocks_to_add = Vec::new();
|
let mut blocks_to_add = Vec::new();
|
||||||
let mut blocks_to_remove = HashSet::default();
|
let mut blocks_to_restyle = HashMap::default();
|
||||||
let mut excerpts_to_remove = Vec::new();
|
let mut diagnostic_blocks = Vec::new();
|
||||||
let mut block_counts_by_group = Vec::new();
|
|
||||||
|
|
||||||
let diagnostic_groups = snapshot.diagnostic_groups::<Point>();
|
|
||||||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
|
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
|
||||||
for group in &diagnostic_groups {
|
let mut old_groups = groups.iter_mut().peekable();
|
||||||
let group_id = group.entries[0].diagnostic.group_id;
|
let mut new_groups = snapshot.diagnostic_groups().into_iter().peekable();
|
||||||
|
|
||||||
let group_state = match path_state.diagnostic_group_states.entry(group_id) {
|
loop {
|
||||||
hash_map::Entry::Occupied(e) => {
|
let mut to_insert = None;
|
||||||
prev_excerpt_id = e.get().excerpts.last().unwrap().clone();
|
let mut to_invalidate = None;
|
||||||
block_counts_by_group.push(0);
|
let mut to_validate = None;
|
||||||
continue;
|
match (old_groups.peek(), new_groups.peek()) {
|
||||||
}
|
(None, None) => break,
|
||||||
hash_map::Entry::Vacant(e) => e.insert(DiagnosticGroupState::default()),
|
(None, Some(_)) => to_insert = new_groups.next(),
|
||||||
};
|
(Some(_), None) => to_invalidate = old_groups.next(),
|
||||||
|
(Some(old_group), Some(new_group)) => {
|
||||||
let mut block_count = 0;
|
let old_primary = &old_group.primary_diagnostic;
|
||||||
let mut pending_range: Option<(Range<Point>, usize)> = None;
|
let new_primary = &new_group.entries[new_group.primary_ix];
|
||||||
let mut is_first_excerpt_for_group = true;
|
match compare_diagnostics(old_primary, new_primary, &snapshot) {
|
||||||
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
|
Ordering::Less => to_invalidate = old_groups.next(),
|
||||||
if let Some((range, start_ix)) = &mut pending_range {
|
Ordering::Equal => {
|
||||||
if let Some(entry) = entry {
|
to_validate = old_groups.next();
|
||||||
if entry.range.start.row <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2 {
|
new_groups.next();
|
||||||
range.end = range.end.max(entry.range.end);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
Ordering::Greater => to_insert = new_groups.next(),
|
||||||
}
|
}
|
||||||
|
|
||||||
let excerpt_start =
|
|
||||||
Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
|
|
||||||
let excerpt_end = snapshot.clip_point(
|
|
||||||
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
|
|
||||||
Bias::Left,
|
|
||||||
);
|
|
||||||
let excerpt_id = excerpts.insert_excerpt_after(
|
|
||||||
&prev_excerpt_id,
|
|
||||||
ExcerptProperties {
|
|
||||||
buffer: &buffer,
|
|
||||||
range: excerpt_start..excerpt_end,
|
|
||||||
},
|
|
||||||
excerpts_cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
prev_excerpt_id = excerpt_id.clone();
|
|
||||||
group_state.excerpts.push(excerpt_id.clone());
|
|
||||||
let header_position = (excerpt_id.clone(), language::Anchor::min());
|
|
||||||
|
|
||||||
if is_first_excerpt_for_group {
|
|
||||||
is_first_excerpt_for_group = false;
|
|
||||||
let primary = &group.entries[group.primary_ix].diagnostic;
|
|
||||||
let mut header = primary.clone();
|
|
||||||
header.message =
|
|
||||||
primary.message.split('\n').next().unwrap().to_string();
|
|
||||||
block_count += 1;
|
|
||||||
blocks_to_add.push(BlockProperties {
|
|
||||||
position: header_position,
|
|
||||||
height: 2,
|
|
||||||
render: diagnostic_header_renderer(
|
|
||||||
buffer.clone(),
|
|
||||||
header,
|
|
||||||
self.build_settings.clone(),
|
|
||||||
),
|
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
block_count += 1;
|
|
||||||
blocks_to_add.push(BlockProperties {
|
|
||||||
position: header_position,
|
|
||||||
height: 1,
|
|
||||||
render: context_header_renderer(self.build_settings.clone()),
|
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in &group.entries[*start_ix..ix] {
|
|
||||||
let mut diagnostic = entry.diagnostic.clone();
|
|
||||||
if diagnostic.is_primary {
|
|
||||||
let mut lines = entry.diagnostic.message.split('\n');
|
|
||||||
lines.next();
|
|
||||||
diagnostic.message = lines.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !diagnostic.message.is_empty() {
|
|
||||||
let buffer_anchor = snapshot.anchor_before(entry.range.start);
|
|
||||||
block_count += 1;
|
|
||||||
blocks_to_add.push(BlockProperties {
|
|
||||||
position: (excerpt_id.clone(), buffer_anchor),
|
|
||||||
height: diagnostic.message.matches('\n').count() as u8 + 1,
|
|
||||||
render: diagnostic_block_renderer(
|
|
||||||
diagnostic,
|
|
||||||
true,
|
|
||||||
self.build_settings.clone(),
|
|
||||||
),
|
|
||||||
disposition: BlockDisposition::Below,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pending_range.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
pending_range = Some((entry.range.clone(), ix));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
block_counts_by_group.push(block_count);
|
if let Some(group) = to_insert {
|
||||||
|
let mut group_state = DiagnosticGroupState {
|
||||||
|
primary_diagnostic: group.entries[group.primary_ix].clone(),
|
||||||
|
excerpts: Default::default(),
|
||||||
|
blocks: Default::default(),
|
||||||
|
block_count: 0,
|
||||||
|
is_valid: true,
|
||||||
|
};
|
||||||
|
let mut pending_range: Option<(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));
|
||||||
|
if let Some((range, start_ix)) = &mut pending_range {
|
||||||
|
if let Some(entry) = resolved_entry.as_ref() {
|
||||||
|
if entry.range.start.row
|
||||||
|
<= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
|
||||||
|
{
|
||||||
|
range.end = range.end.max(entry.range.end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let excerpt_start =
|
||||||
|
Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
|
||||||
|
let excerpt_end = snapshot.clip_point(
|
||||||
|
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
|
||||||
|
Bias::Left,
|
||||||
|
);
|
||||||
|
let excerpt_id = excerpts.insert_excerpt_after(
|
||||||
|
&prev_excerpt_id,
|
||||||
|
ExcerptProperties {
|
||||||
|
buffer: &buffer,
|
||||||
|
range: excerpt_start..excerpt_end,
|
||||||
|
},
|
||||||
|
excerpts_cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
prev_excerpt_id = excerpt_id.clone();
|
||||||
|
group_state.excerpts.push(excerpt_id.clone());
|
||||||
|
let header_position = (excerpt_id.clone(), language::Anchor::min());
|
||||||
|
|
||||||
|
if is_first_excerpt_for_group {
|
||||||
|
is_first_excerpt_for_group = false;
|
||||||
|
let primary = &group.entries[group.primary_ix].diagnostic;
|
||||||
|
let mut header = primary.clone();
|
||||||
|
header.message =
|
||||||
|
primary.message.split('\n').next().unwrap().to_string();
|
||||||
|
group_state.block_count += 1;
|
||||||
|
diagnostic_blocks.push(DiagnosticBlock::Header(header.clone()));
|
||||||
|
blocks_to_add.push(BlockProperties {
|
||||||
|
position: header_position,
|
||||||
|
height: 2,
|
||||||
|
render: diagnostic_header_renderer(
|
||||||
|
buffer.clone(),
|
||||||
|
header,
|
||||||
|
true,
|
||||||
|
self.build_settings.clone(),
|
||||||
|
),
|
||||||
|
disposition: BlockDisposition::Above,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
group_state.block_count += 1;
|
||||||
|
diagnostic_blocks.push(DiagnosticBlock::Context);
|
||||||
|
blocks_to_add.push(BlockProperties {
|
||||||
|
position: header_position,
|
||||||
|
height: 1,
|
||||||
|
render: context_header_renderer(self.build_settings.clone()),
|
||||||
|
disposition: BlockDisposition::Above,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in &group.entries[*start_ix..ix] {
|
||||||
|
let mut diagnostic = entry.diagnostic.clone();
|
||||||
|
if diagnostic.is_primary {
|
||||||
|
let mut lines = entry.diagnostic.message.split('\n');
|
||||||
|
lines.next();
|
||||||
|
diagnostic.message = lines.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !diagnostic.message.is_empty() {
|
||||||
|
group_state.block_count += 1;
|
||||||
|
diagnostic_blocks
|
||||||
|
.push(DiagnosticBlock::Inline(diagnostic.clone()));
|
||||||
|
blocks_to_add.push(BlockProperties {
|
||||||
|
position: (excerpt_id.clone(), entry.range.start.clone()),
|
||||||
|
height: diagnostic.message.matches('\n').count() as u8 + 1,
|
||||||
|
render: diagnostic_block_renderer(
|
||||||
|
diagnostic,
|
||||||
|
true,
|
||||||
|
self.build_settings.clone(),
|
||||||
|
),
|
||||||
|
disposition: BlockDisposition::Below,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_range.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(entry) = resolved_entry {
|
||||||
|
pending_range = Some((entry.range.clone(), ix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups_to_add.push(group_state);
|
||||||
|
} else if let Some(to_invalidate) = to_invalidate {
|
||||||
|
for (block_id, block) in &to_invalidate.blocks {
|
||||||
|
match block {
|
||||||
|
DiagnosticBlock::Header(diagnostic) => {
|
||||||
|
blocks_to_restyle.insert(
|
||||||
|
*block_id,
|
||||||
|
diagnostic_header_renderer(
|
||||||
|
buffer.clone(),
|
||||||
|
diagnostic.clone(),
|
||||||
|
false,
|
||||||
|
self.build_settings.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DiagnosticBlock::Inline(diagnostic) => {
|
||||||
|
blocks_to_restyle.insert(
|
||||||
|
*block_id,
|
||||||
|
diagnostic_block_renderer(
|
||||||
|
diagnostic.clone(),
|
||||||
|
false,
|
||||||
|
self.build_settings.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DiagnosticBlock::Context => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
to_invalidate.is_valid = false;
|
||||||
|
prev_excerpt_id = to_invalidate.excerpts.last().unwrap().clone();
|
||||||
|
} else if let Some(to_validate) = to_validate {
|
||||||
|
for (block_id, block) in &to_validate.blocks {
|
||||||
|
match block {
|
||||||
|
DiagnosticBlock::Header(diagnostic) => {
|
||||||
|
blocks_to_restyle.insert(
|
||||||
|
*block_id,
|
||||||
|
diagnostic_header_renderer(
|
||||||
|
buffer.clone(),
|
||||||
|
diagnostic.clone(),
|
||||||
|
true,
|
||||||
|
self.build_settings.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DiagnosticBlock::Inline(diagnostic) => {
|
||||||
|
blocks_to_restyle.insert(
|
||||||
|
*block_id,
|
||||||
|
diagnostic_block_renderer(
|
||||||
|
diagnostic.clone(),
|
||||||
|
true,
|
||||||
|
self.build_settings.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DiagnosticBlock::Context => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
to_validate.is_valid = true;
|
||||||
|
prev_excerpt_id = to_validate.excerpts.last().unwrap().clone();
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path_state
|
|
||||||
.diagnostic_group_states
|
|
||||||
.retain(|group_id, group_state| {
|
|
||||||
if diagnostic_groups
|
|
||||||
.iter()
|
|
||||||
.any(|group| group.entries[0].diagnostic.group_id == *group_id)
|
|
||||||
{
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
excerpts_to_remove.extend(group_state.excerpts.drain(..));
|
|
||||||
blocks_to_remove.extend(group_state.blocks.drain(..));
|
|
||||||
false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
excerpts_to_remove.sort();
|
|
||||||
excerpts.remove_excerpts(excerpts_to_remove.iter(), excerpts_cx);
|
|
||||||
excerpts.snapshot(excerpts_cx)
|
excerpts.snapshot(excerpts_cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
path_state.last_excerpt = prev_excerpt_id;
|
|
||||||
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.remove_blocks(blocks_to_remove, cx);
|
editor.replace_blocks(blocks_to_restyle, cx);
|
||||||
let block_ids = editor.insert_blocks(
|
let mut block_ids = editor
|
||||||
blocks_to_add.into_iter().map(|block| {
|
.insert_blocks(
|
||||||
let (excerpt_id, text_anchor) = block.position;
|
blocks_to_add.into_iter().map(|block| {
|
||||||
BlockProperties {
|
let (excerpt_id, text_anchor) = block.position;
|
||||||
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
|
BlockProperties {
|
||||||
height: block.height,
|
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
|
||||||
render: block.render,
|
height: block.height,
|
||||||
disposition: block.disposition,
|
render: block.render,
|
||||||
}
|
disposition: block.disposition,
|
||||||
}),
|
}
|
||||||
cx,
|
}),
|
||||||
);
|
cx,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.zip(diagnostic_blocks);
|
||||||
|
|
||||||
let mut block_ids = block_ids.into_iter();
|
for group_state in &mut groups_to_add {
|
||||||
let mut block_counts_by_group = block_counts_by_group.into_iter();
|
group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
|
||||||
for group in &diagnostic_groups {
|
|
||||||
let group_id = group.entries[0].diagnostic.group_id;
|
|
||||||
let block_count = block_counts_by_group.next().unwrap();
|
|
||||||
let group_state = path_state
|
|
||||||
.diagnostic_group_states
|
|
||||||
.get_mut(&group_id)
|
|
||||||
.unwrap();
|
|
||||||
group_state
|
|
||||||
.blocks
|
|
||||||
.extend(block_ids.by_ref().take(block_count));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
groups.extend(groups_to_add);
|
||||||
|
groups.sort_unstable_by(|a, b| {
|
||||||
|
let range_a = &a.primary_diagnostic.range;
|
||||||
|
let range_b = &b.primary_diagnostic.range;
|
||||||
|
range_a
|
||||||
|
.start
|
||||||
|
.cmp(&range_b.start, &snapshot)
|
||||||
|
.unwrap()
|
||||||
|
.then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
|
||||||
|
});
|
||||||
|
|
||||||
|
if groups.is_empty() {
|
||||||
|
self.path_states.remove(path_ix);
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,6 +529,24 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
|
||||||
|
lhs: &DiagnosticEntry<L>,
|
||||||
|
rhs: &DiagnosticEntry<R>,
|
||||||
|
snapshot: &language::BufferSnapshot,
|
||||||
|
) -> Ordering {
|
||||||
|
lhs.range
|
||||||
|
.start
|
||||||
|
.to_offset(&snapshot)
|
||||||
|
.cmp(&rhs.range.start.to_offset(snapshot))
|
||||||
|
.then_with(|| {
|
||||||
|
lhs.range
|
||||||
|
.end
|
||||||
|
.to_offset(&snapshot)
|
||||||
|
.cmp(&rhs.range.end.to_offset(snapshot))
|
||||||
|
})
|
||||||
|
.then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -3423,12 +3423,6 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
|
|
||||||
self.display_map.update(cx, |display_map, cx| {
|
|
||||||
display_map.remove_blocks(block_ids, cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_blocks<P>(
|
pub fn insert_blocks<P>(
|
||||||
&mut self,
|
&mut self,
|
||||||
blocks: impl IntoIterator<Item = BlockProperties<P>>,
|
blocks: impl IntoIterator<Item = BlockProperties<P>>,
|
||||||
|
@ -3444,6 +3438,22 @@ impl Editor {
|
||||||
blocks
|
blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn replace_blocks(
|
||||||
|
&mut self,
|
||||||
|
blocks: HashMap<BlockId, RenderBlock>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.display_map
|
||||||
|
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||||
|
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.remove_blocks(block_ids, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn longest_row(&self, cx: &mut MutableAppContext) -> u32 {
|
pub fn longest_row(&self, cx: &mut MutableAppContext) -> u32 {
|
||||||
self.display_map
|
self.display_map
|
||||||
.update(cx, |map, cx| map.snapshot(cx))
|
.update(cx, |map, cx| map.snapshot(cx))
|
||||||
|
@ -3790,12 +3800,13 @@ pub fn diagnostic_block_renderer(
|
||||||
pub fn diagnostic_header_renderer(
|
pub fn diagnostic_header_renderer(
|
||||||
buffer: ModelHandle<Buffer>,
|
buffer: ModelHandle<Buffer>,
|
||||||
diagnostic: Diagnostic,
|
diagnostic: Diagnostic,
|
||||||
|
is_valid: bool,
|
||||||
build_settings: BuildSettings,
|
build_settings: BuildSettings,
|
||||||
) -> RenderBlock {
|
) -> RenderBlock {
|
||||||
Arc::new(move |cx| {
|
Arc::new(move |cx| {
|
||||||
let settings = build_settings(cx);
|
let settings = build_settings(cx);
|
||||||
let mut text_style = settings.style.text.clone();
|
let mut text_style = settings.style.text.clone();
|
||||||
text_style.color = diagnostic_style(diagnostic.severity, true, &settings.style).text;
|
text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text;
|
||||||
let file_path = if let Some(file) = buffer.read(&**cx).file() {
|
let file_path = if let Some(file) = buffer.read(&**cx).file() {
|
||||||
file.path().to_string_lossy().to_string()
|
file.path().to_string_lossy().to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,7 +7,6 @@ pub use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::hash_map;
|
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task};
|
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -69,8 +68,6 @@ pub struct Buffer {
|
||||||
remote_selections: TreeMap<ReplicaId, Arc<[Selection<Anchor>]>>,
|
remote_selections: TreeMap<ReplicaId, Arc<[Selection<Anchor>]>>,
|
||||||
diagnostics: DiagnosticSet,
|
diagnostics: DiagnosticSet,
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
clear_invalid_diagnostics_task: Option<Task<()>>,
|
|
||||||
next_diagnostic_group_id: usize,
|
|
||||||
language_server: Option<LanguageServerState>,
|
language_server: Option<LanguageServerState>,
|
||||||
deferred_ops: OperationQueue<Operation>,
|
deferred_ops: OperationQueue<Operation>,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -363,8 +360,6 @@ impl Buffer {
|
||||||
remote_selections: Default::default(),
|
remote_selections: Default::default(),
|
||||||
diagnostics: Default::default(),
|
diagnostics: Default::default(),
|
||||||
diagnostics_update_count: 0,
|
diagnostics_update_count: 0,
|
||||||
next_diagnostic_group_id: 0,
|
|
||||||
clear_invalid_diagnostics_task: None,
|
|
||||||
language_server: None,
|
language_server: None,
|
||||||
deferred_ops: OperationQueue::new(),
|
deferred_ops: OperationQueue::new(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -777,7 +772,6 @@ impl Buffer {
|
||||||
.peekable();
|
.peekable();
|
||||||
let mut last_edit_old_end = PointUtf16::zero();
|
let mut last_edit_old_end = PointUtf16::zero();
|
||||||
let mut last_edit_new_end = PointUtf16::zero();
|
let mut last_edit_new_end = PointUtf16::zero();
|
||||||
let mut has_disk_based_diagnostics = false;
|
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
'outer: while ix < diagnostics.len() {
|
'outer: while ix < diagnostics.len() {
|
||||||
let entry = &mut diagnostics[ix];
|
let entry = &mut diagnostics[ix];
|
||||||
|
@ -793,7 +787,6 @@ impl Buffer {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |source| disk_based_sources.contains(source))
|
.map_or(false, |source| disk_based_sources.contains(source))
|
||||||
{
|
{
|
||||||
has_disk_based_diagnostics = true;
|
|
||||||
while let Some(edit) = edits_since_save.peek() {
|
while let Some(edit) = edits_since_save.peek() {
|
||||||
if edit.old.end <= start {
|
if edit.old.end <= start {
|
||||||
last_edit_old_end = edit.old.end;
|
last_edit_old_end = edit.old.end;
|
||||||
|
@ -826,145 +819,14 @@ impl Buffer {
|
||||||
ix += 1;
|
ix += 1;
|
||||||
}
|
}
|
||||||
drop(edits_since_save);
|
drop(edits_since_save);
|
||||||
|
self.diagnostics = DiagnosticSet::new(diagnostics, content);
|
||||||
let mut merged_diagnostics = Vec::with_capacity(diagnostics.len());
|
|
||||||
let mut old_diagnostics = self
|
|
||||||
.diagnostics
|
|
||||||
.iter()
|
|
||||||
.map(|entry| {
|
|
||||||
(
|
|
||||||
entry,
|
|
||||||
entry
|
|
||||||
.diagnostic
|
|
||||||
.source
|
|
||||||
.as_ref()
|
|
||||||
.map_or(false, |source| disk_based_sources.contains(source)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.peekable();
|
|
||||||
let mut new_diagnostics = diagnostics
|
|
||||||
.into_iter()
|
|
||||||
.map(|entry| DiagnosticEntry {
|
|
||||||
range: content.anchor_before(entry.range.start)
|
|
||||||
..content.anchor_after(entry.range.end),
|
|
||||||
diagnostic: entry.diagnostic,
|
|
||||||
})
|
|
||||||
.peekable();
|
|
||||||
|
|
||||||
// Incorporate the *old* diagnostics into the new diagnostics set, in two ways:
|
|
||||||
// 1. Recycle group ids - diagnostic groups whose primary diagnostic has not
|
|
||||||
// changed should use the same group id as before, so that downstream code
|
|
||||||
// can determine which diagnostics are new.
|
|
||||||
// 2. Preserve disk-based diagnostics - Some diagnostic sources are reported
|
|
||||||
// on a less frequent basis than others. If these sources are absent from this
|
|
||||||
// message, then preserve the previous diagnostics for those sources, but mark
|
|
||||||
// them as invalid, and set a timer to clear them out.
|
|
||||||
let mut group_id_replacements = HashMap::new();
|
|
||||||
let mut merged_old_disk_based_diagnostics = false;
|
|
||||||
loop {
|
|
||||||
match (old_diagnostics.peek(), new_diagnostics.peek()) {
|
|
||||||
(None, None) => break,
|
|
||||||
(None, Some(_)) => {
|
|
||||||
merged_diagnostics.push(new_diagnostics.next().unwrap());
|
|
||||||
}
|
|
||||||
(Some(_), None) => {
|
|
||||||
let (old_entry, is_disk_based) = old_diagnostics.next().unwrap();
|
|
||||||
if is_disk_based && !has_disk_based_diagnostics {
|
|
||||||
let mut old_entry = old_entry.clone();
|
|
||||||
old_entry.diagnostic.is_valid = false;
|
|
||||||
merged_old_disk_based_diagnostics = true;
|
|
||||||
merged_diagnostics.push(old_entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some((old, _)), Some(new)) => {
|
|
||||||
let ordering = Ordering::Equal
|
|
||||||
.then_with(|| old.range.start.cmp(&new.range.start, content).unwrap())
|
|
||||||
.then_with(|| new.range.end.cmp(&old.range.end, content).unwrap())
|
|
||||||
.then_with(|| compare_diagnostics(&old.diagnostic, &new.diagnostic));
|
|
||||||
match ordering {
|
|
||||||
Ordering::Less => {
|
|
||||||
let (old_entry, is_disk_based) = old_diagnostics.next().unwrap();
|
|
||||||
if is_disk_based && !has_disk_based_diagnostics {
|
|
||||||
let mut old_entry = old_entry.clone();
|
|
||||||
old_entry.diagnostic.is_valid = false;
|
|
||||||
merged_old_disk_based_diagnostics = true;
|
|
||||||
merged_diagnostics.push(old_entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ordering::Equal => {
|
|
||||||
let (old_entry, _) = old_diagnostics.next().unwrap();
|
|
||||||
let new_entry = new_diagnostics.next().unwrap();
|
|
||||||
if new_entry.diagnostic.is_primary {
|
|
||||||
group_id_replacements.insert(
|
|
||||||
new_entry.diagnostic.group_id,
|
|
||||||
old_entry.diagnostic.group_id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
merged_diagnostics.push(new_entry);
|
|
||||||
}
|
|
||||||
Ordering::Greater => {
|
|
||||||
let new_entry = new_diagnostics.next().unwrap();
|
|
||||||
merged_diagnostics.push(new_entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(old_diagnostics);
|
|
||||||
|
|
||||||
// Having determined which group ids should be recycled, renumber all of
|
|
||||||
// groups. Any new group that does not correspond to an old group receives
|
|
||||||
// a brand new group id.
|
|
||||||
let mut next_diagnostic_group_id = self.next_diagnostic_group_id;
|
|
||||||
for entry in &mut merged_diagnostics {
|
|
||||||
if entry.diagnostic.is_valid {
|
|
||||||
match group_id_replacements.entry(entry.diagnostic.group_id) {
|
|
||||||
hash_map::Entry::Occupied(e) => entry.diagnostic.group_id = *e.get(),
|
|
||||||
hash_map::Entry::Vacant(e) => {
|
|
||||||
entry.diagnostic.group_id = post_inc(&mut next_diagnostic_group_id);
|
|
||||||
e.insert(entry.diagnostic.group_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.diagnostics = DiagnosticSet::from_sorted_entries(merged_diagnostics, content);
|
|
||||||
self.next_diagnostic_group_id = next_diagnostic_group_id;
|
|
||||||
|
|
||||||
// If old disk-based diagnostics were included in this new set, then
|
|
||||||
// set a timer to remove them if enough time passes before the next
|
|
||||||
// diagnostics update.
|
|
||||||
if merged_old_disk_based_diagnostics {
|
|
||||||
self.clear_invalid_diagnostics_task = Some(cx.spawn(|this, mut cx| async move {
|
|
||||||
smol::Timer::after(Duration::from_secs(2)).await;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
let content = this.snapshot();
|
|
||||||
this.diagnostics = DiagnosticSet::from_sorted_entries(
|
|
||||||
this.diagnostics
|
|
||||||
.iter()
|
|
||||||
.filter(|d| d.diagnostic.is_valid)
|
|
||||||
.cloned(),
|
|
||||||
&content,
|
|
||||||
);
|
|
||||||
let operation = this.did_update_diagnostics(cx);
|
|
||||||
this.send_operation(operation, cx);
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
} else if has_disk_based_diagnostics {
|
|
||||||
self.clear_invalid_diagnostics_task.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.did_update_diagnostics(cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn did_update_diagnostics(&mut self, cx: &mut ModelContext<Self>) -> Operation {
|
|
||||||
self.diagnostics_update_count += 1;
|
self.diagnostics_update_count += 1;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
cx.emit(Event::DiagnosticsUpdated);
|
cx.emit(Event::DiagnosticsUpdated);
|
||||||
Operation::UpdateDiagnostics {
|
Ok(Operation::UpdateDiagnostics {
|
||||||
diagnostics: Arc::from(self.diagnostics.iter().cloned().collect::<Vec<_>>()),
|
diagnostics: Arc::from(self.diagnostics.iter().cloned().collect::<Vec<_>>()),
|
||||||
lamport_timestamp: self.text.lamport_clock.tick(),
|
lamport_timestamp: self.text.lamport_clock.tick(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
|
fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
@ -1874,10 +1736,7 @@ impl BufferSnapshot {
|
||||||
self.diagnostics.range(search_range, self, true)
|
self.diagnostics.range(search_range, self, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diagnostic_groups<O>(&self) -> Vec<DiagnosticGroup<O>>
|
pub fn diagnostic_groups(&self) -> Vec<DiagnosticGroup<Anchor>> {
|
||||||
where
|
|
||||||
O: FromAnchor + Ord + Copy,
|
|
||||||
{
|
|
||||||
self.diagnostics.groups(self)
|
self.diagnostics.groups(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,23 +104,19 @@ impl DiagnosticSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn groups<O>(&self, buffer: &text::BufferSnapshot) -> Vec<DiagnosticGroup<O>>
|
pub fn groups(&self, buffer: &text::BufferSnapshot) -> Vec<DiagnosticGroup<Anchor>> {
|
||||||
where
|
|
||||||
O: FromAnchor + Ord + Copy,
|
|
||||||
{
|
|
||||||
let mut groups = HashMap::default();
|
let mut groups = HashMap::default();
|
||||||
for entry in self.diagnostics.iter() {
|
for entry in self.diagnostics.iter() {
|
||||||
let entry = entry.resolve(buffer);
|
|
||||||
groups
|
groups
|
||||||
.entry(entry.diagnostic.group_id)
|
.entry(entry.diagnostic.group_id)
|
||||||
.or_insert(Vec::new())
|
.or_insert(Vec::new())
|
||||||
.push(entry);
|
.push(entry.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut groups = groups
|
let mut groups = groups
|
||||||
.into_values()
|
.into_values()
|
||||||
.filter_map(|mut entries| {
|
.filter_map(|mut entries| {
|
||||||
entries.sort_unstable_by_key(|entry| entry.range.start);
|
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap());
|
||||||
entries
|
entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|entry| entry.diagnostic.is_primary)
|
.position(|entry| entry.diagnostic.is_primary)
|
||||||
|
@ -130,7 +126,13 @@ impl DiagnosticSet {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
groups.sort_unstable_by_key(|group| group.entries[group.primary_ix].range.start);
|
groups.sort_unstable_by(|a, b| {
|
||||||
|
a.entries[a.primary_ix]
|
||||||
|
.range
|
||||||
|
.start
|
||||||
|
.cmp(&b.entries[b.primary_ix].range.start, buffer)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1097,43 +1097,15 @@ impl LocalWorktree {
|
||||||
buffer
|
buffer
|
||||||
});
|
});
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, _| {
|
||||||
let this = this.as_local_mut().unwrap();
|
let this = this.as_local_mut().unwrap();
|
||||||
this.open_buffers.insert(buffer.id(), buffer.downgrade());
|
this.open_buffers.insert(buffer.id(), buffer.downgrade());
|
||||||
cx.subscribe(&buffer, |worktree, buffer, event, cx| {
|
|
||||||
worktree
|
|
||||||
.as_local_mut()
|
|
||||||
.unwrap()
|
|
||||||
.on_buffer_event(buffer, event, cx);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_buffer_event(
|
|
||||||
&mut self,
|
|
||||||
buffer: ModelHandle<Buffer>,
|
|
||||||
event: &language::Event,
|
|
||||||
cx: &mut ModelContext<Worktree>,
|
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
language::Event::DiagnosticsUpdated => {
|
|
||||||
let buffer = buffer.read(cx);
|
|
||||||
if let Some(path) = buffer.file().map(|file| file.path().clone()) {
|
|
||||||
let diagnostics = buffer.all_diagnostics();
|
|
||||||
self.diagnostic_summaries
|
|
||||||
.insert(path.clone(), DiagnosticSummary::new(diagnostics));
|
|
||||||
cx.emit(Event::DiagnosticsUpdated(path));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open_remote_buffer(
|
pub fn open_remote_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
envelope: TypedEnvelope<proto::OpenBuffer>,
|
envelope: TypedEnvelope<proto::OpenBuffer>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue