Allow retrieving a buffer's diagnostics

This commit is contained in:
Max Brunsfeld 2021-10-26 15:46:08 -07:00
parent 5bfbeb55c0
commit ef4fc42d93
5 changed files with 140 additions and 35 deletions

View File

@ -175,10 +175,10 @@ impl<T: Clone> Default for AnchorRangeMultimap<T> {
} }
impl<T: Clone> AnchorRangeMultimap<T> { impl<T: Clone> AnchorRangeMultimap<T> {
fn intersecting_point_ranges<'a, O>( pub fn intersecting_point_ranges<'a, O>(
&'a self, &'a self,
range: Range<O>, range: Range<O>,
content: &'a Content<'a>, content: Content<'a>,
inclusive: bool, inclusive: bool,
) -> impl Iterator<Item = (usize, Range<Point>, &T)> + 'a ) -> impl Iterator<Item = (usize, Range<Point>, &T)> + 'a
where where
@ -187,10 +187,11 @@ impl<T: Clone> AnchorRangeMultimap<T> {
use super::ToPoint as _; use super::ToPoint as _;
let end_bias = if inclusive { Bias::Right } else { Bias::Left }; let end_bias = if inclusive { Bias::Right } else { Bias::Left };
let range = range.start.to_full_offset(content, Bias::Left) let range = range.start.to_full_offset(&content, Bias::Left)
..range.end.to_full_offset(content, end_bias); ..range.end.to_full_offset(&content, end_bias);
let mut cursor = self.entries.filter::<_, usize>( let mut cursor = self.entries.filter::<_, usize>(
{ {
let content = content.clone();
let mut endpoint = Anchor { let mut endpoint = Anchor {
full_offset: 0, full_offset: 0,
bias: Bias::Right, bias: Bias::Right,
@ -199,12 +200,12 @@ impl<T: Clone> AnchorRangeMultimap<T> {
move |summary: &AnchorRangeMultimapSummary| { move |summary: &AnchorRangeMultimapSummary| {
endpoint.full_offset = summary.max_end; endpoint.full_offset = summary.max_end;
endpoint.bias = self.end_bias; endpoint.bias = self.end_bias;
let max_end = endpoint.to_full_offset(content, self.end_bias); let max_end = endpoint.to_full_offset(&content, self.end_bias);
let start_cmp = range.start.cmp(&max_end); let start_cmp = range.start.cmp(&max_end);
endpoint.full_offset = summary.min_start; endpoint.full_offset = summary.min_start;
endpoint.bias = self.start_bias; endpoint.bias = self.start_bias;
let min_start = endpoint.to_full_offset(content, self.start_bias); let min_start = endpoint.to_full_offset(&content, self.start_bias);
let end_cmp = range.end.cmp(&min_start); let end_cmp = range.end.cmp(&min_start);
if inclusive { if inclusive {
@ -228,10 +229,10 @@ impl<T: Clone> AnchorRangeMultimap<T> {
let ix = *cursor.start(); let ix = *cursor.start();
endpoint.full_offset = item.range.start; endpoint.full_offset = item.range.start;
endpoint.bias = self.start_bias; endpoint.bias = self.start_bias;
let start = endpoint.to_point(content); let start = endpoint.to_point(&content);
endpoint.full_offset = item.range.end; endpoint.full_offset = item.range.end;
endpoint.bias = self.end_bias; endpoint.bias = self.end_bias;
let end = endpoint.to_point(content); let end = endpoint.to_point(&content);
let value = &item.value; let value = &item.value;
cursor.next(&()); cursor.next(&());
Some((ix, start..end, value)) Some((ix, start..end, value))

View File

@ -1592,6 +1592,7 @@ impl Snapshot {
} }
} }
#[derive(Clone)]
pub struct Content<'a> { pub struct Content<'a> {
visible_text: &'a Rope, visible_text: &'a Rope,
deleted_text: &'a Rope, deleted_text: &'a Rope,

View File

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2018" edition = "2018"
[features] [features]
test-support = ["rand", "buffer/test-support"] test-support = ["rand", "buffer/test-support", "lsp/test-support"]
[dependencies] [dependencies]
buffer = { path = "../buffer" } buffer = { path = "../buffer" }
@ -29,6 +29,7 @@ tree-sitter = "0.19.5"
[dev-dependencies] [dev-dependencies]
buffer = { path = "../buffer", features = ["test-support"] } buffer = { path = "../buffer", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
rand = "0.8.3" rand = "0.8.3"
tree-sitter-rust = "0.19.0" tree-sitter-rust = "0.19.0"
unindent = "0.1.7" unindent = "0.1.7"

View File

@ -13,7 +13,7 @@ use clock::ReplicaId;
use futures::FutureExt as _; use futures::FutureExt as _;
use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use lsp::LanguageServer; use lsp::{DiagnosticSeverity, LanguageServer};
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::{prelude::Stream, sink::Sink, watch}; use postage::{prelude::Stream, sink::Sink, watch};
use rpc::proto; use rpc::proto;
@ -59,7 +59,7 @@ pub struct Buffer {
syntax_tree: Mutex<Option<SyntaxTree>>, syntax_tree: Mutex<Option<SyntaxTree>>,
parsing_in_background: bool, parsing_in_background: bool,
parse_count: usize, parse_count: usize,
diagnostics: AnchorRangeMultimap<()>, diagnostics: AnchorRangeMultimap<(DiagnosticSeverity, String)>,
language_server: Option<LanguageServerState>, language_server: Option<LanguageServerState>,
#[cfg(test)] #[cfg(test)]
operations: Vec<Operation>, operations: Vec<Operation>,
@ -73,6 +73,13 @@ pub struct Snapshot {
query_cursor: QueryCursorHandle, query_cursor: QueryCursorHandle,
} }
#[derive(Debug, PartialEq, Eq)]
pub struct Diagnostic {
pub range: Range<Point>,
pub severity: DiagnosticSeverity,
pub message: String,
}
struct LanguageServerState { struct LanguageServerState {
server: Arc<LanguageServer>, server: Arc<LanguageServer>,
latest_snapshot: watch::Sender<Option<LanguageServerSnapshot>>, latest_snapshot: watch::Sender<Option<LanguageServerSnapshot>>,
@ -613,43 +620,67 @@ impl Buffer {
pub fn update_diagnostics( pub fn update_diagnostics(
&mut self, &mut self,
params: lsp::PublishDiagnosticsParams, version: Option<i32>,
diagnostics: Vec<lsp::Diagnostic>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<()> { ) -> Result<()> {
dbg!(&params); let version = version.map(|version| version as usize);
let language_server = self.language_server.as_mut().unwrap(); let content = if let Some(version) = version {
let version = params.version.ok_or_else(|| anyhow!("missing version"))? as usize; let language_server = self.language_server.as_mut().unwrap();
let snapshot = language_server let snapshot = language_server
.pending_snapshots .pending_snapshots
.get(&version) .get(&version)
.ok_or_else(|| anyhow!("missing snapshot"))?; .ok_or_else(|| anyhow!("missing snapshot"))?;
self.diagnostics = snapshot.buffer_snapshot.content().anchor_range_multimap( snapshot.buffer_snapshot.content()
} else {
self.content()
};
self.diagnostics = content.anchor_range_multimap(
Bias::Left, Bias::Left,
Bias::Right, Bias::Right,
params.diagnostics.into_iter().map(|diagnostic| { diagnostics.into_iter().map(|diagnostic| {
// TODO: Use UTF-16 positions. // TODO: Use UTF-16 positions.
let start = Point::new( let start = Point::new(
diagnostic.range.start.line, diagnostic.range.start.line,
diagnostic.range.start.character, diagnostic.range.start.character,
); );
let end = Point::new(diagnostic.range.end.line, diagnostic.range.end.character); let end = Point::new(diagnostic.range.end.line, diagnostic.range.end.character);
(start..end, ()) let severity = diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR);
(start..end, (severity, diagnostic.message))
}), }),
); );
let versions_to_delete = language_server if let Some(version) = version {
.pending_snapshots let language_server = self.language_server.as_mut().unwrap();
.range(..version) let versions_to_delete = language_server
.map(|(v, _)| *v) .pending_snapshots
.collect::<Vec<_>>(); .range(..version)
for version in versions_to_delete { .map(|(v, _)| *v)
language_server.pending_snapshots.remove(&version); .collect::<Vec<_>>();
for version in versions_to_delete {
language_server.pending_snapshots.remove(&version);
}
} }
cx.notify(); cx.notify();
Ok(()) Ok(())
} }
pub fn diagnostics_in_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = Diagnostic> + 'a {
let content = self.content();
let range = range.start.to_offset(&content)..range.end.to_offset(&content);
self.diagnostics
.intersecting_point_ranges(range, content, true)
.map(move |(_, range, (severity, message))| Diagnostic {
range,
severity: *severity,
message: message.clone(),
})
}
fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) { fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
if let Some(indent_columns) = self.compute_autoindents() { if let Some(indent_columns) = self.compute_autoindents() {
let indent_columns = cx.background().spawn(indent_columns); let indent_columns = cx.background().spawn(indent_columns);
@ -987,17 +1018,16 @@ impl Buffer {
} else { } else {
return; return;
}; };
let file = if let Some(file) = self.file.as_ref() { let abs_path = self
file .file
} else { .as_ref()
return; .map_or(PathBuf::new(), |file| file.abs_path(cx).unwrap());
};
let version = post_inc(&mut language_server.next_version); let version = post_inc(&mut language_server.next_version);
let snapshot = LanguageServerSnapshot { let snapshot = LanguageServerSnapshot {
buffer_snapshot: self.text.snapshot(), buffer_snapshot: self.text.snapshot(),
version, version,
path: Arc::from(file.abs_path(cx).unwrap()), path: Arc::from(abs_path),
}; };
language_server language_server
.pending_snapshots .pending_snapshots

View File

@ -407,6 +407,78 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
}); });
} }
#[gpui::test]
async fn test_diagnostics(mut cx: gpui::TestAppContext) {
let (language_server, mut fake) = lsp::LanguageServer::fake(&cx.background()).await;
let text = "
fn a() { A }
fn b() { BB }
fn c() { CCC }
"
.unindent();
let buffer = cx.add_model(|cx| {
Buffer::new(0, text, cx).with_language(rust_lang(), Some(language_server), cx)
});
let open_notification = fake
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await;
buffer.update(&mut cx, |buffer, cx| {
// Edit the buffer, moving the content down
buffer.edit([0..0], "\n\n", cx);
// Receive diagnostics for an earlier version of the buffer.
buffer
.update_diagnostics(
Some(open_notification.text_document.version),
vec![
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'A'".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'BB'".to_string(),
..Default::default()
},
lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "undefined variable 'CCC'".to_string(),
..Default::default()
},
],
cx,
)
.unwrap();
// The diagnostics have moved down since they were created.
assert_eq!(
buffer
.diagnostics_in_range(Point::new(3, 0)..Point::new(5, 0))
.collect::<Vec<_>>(),
&[
Diagnostic {
range: Point::new(3, 9)..Point::new(3, 11),
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string()
},
Diagnostic {
range: Point::new(4, 9)..Point::new(4, 12),
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'CCC'".to_string()
}
]
)
});
}
#[test] #[test]
fn test_contiguous_ranges() { fn test_contiguous_ranges() {
assert_eq!( assert_eq!(