diff --git a/crates/buffer/src/anchor.rs b/crates/buffer/src/anchor.rs index 9cafd673a9d5fd45bfa7c70873a93f5487e08ba6..e1e6eaa2191eddf883620852ac2497722489666c 100644 --- a/crates/buffer/src/anchor.rs +++ b/crates/buffer/src/anchor.rs @@ -175,10 +175,10 @@ impl Default for AnchorRangeMultimap { } impl AnchorRangeMultimap { - fn intersecting_point_ranges<'a, O>( + pub fn intersecting_point_ranges<'a, O>( &'a self, range: Range, - content: &'a Content<'a>, + content: Content<'a>, inclusive: bool, ) -> impl Iterator, &T)> + 'a where @@ -187,10 +187,11 @@ impl AnchorRangeMultimap { use super::ToPoint as _; let end_bias = if inclusive { Bias::Right } else { Bias::Left }; - let range = range.start.to_full_offset(content, Bias::Left) - ..range.end.to_full_offset(content, end_bias); + let range = range.start.to_full_offset(&content, Bias::Left) + ..range.end.to_full_offset(&content, end_bias); let mut cursor = self.entries.filter::<_, usize>( { + let content = content.clone(); let mut endpoint = Anchor { full_offset: 0, bias: Bias::Right, @@ -199,12 +200,12 @@ impl AnchorRangeMultimap { move |summary: &AnchorRangeMultimapSummary| { endpoint.full_offset = summary.max_end; 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); endpoint.full_offset = summary.min_start; 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); if inclusive { @@ -228,10 +229,10 @@ impl AnchorRangeMultimap { let ix = *cursor.start(); endpoint.full_offset = item.range.start; endpoint.bias = self.start_bias; - let start = endpoint.to_point(content); + let start = endpoint.to_point(&content); endpoint.full_offset = item.range.end; endpoint.bias = self.end_bias; - let end = endpoint.to_point(content); + let end = endpoint.to_point(&content); let value = &item.value; cursor.next(&()); Some((ix, start..end, value)) diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 4fac433781afd0e262114583a1dbcf81ef2a55b1..37b66f5f62d96e161c4350cf3369b1944d16e036 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -1592,6 +1592,7 @@ impl Snapshot { } } +#[derive(Clone)] pub struct Content<'a> { visible_text: &'a Rope, deleted_text: &'a Rope, diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 4aba95f97a8a58ddb26b470ec6bb4a8c90207fd7..b0a384e16a7667323bbbac702956b0b5992b7011 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2018" [features] -test-support = ["rand", "buffer/test-support"] +test-support = ["rand", "buffer/test-support", "lsp/test-support"] [dependencies] buffer = { path = "../buffer" } @@ -29,6 +29,7 @@ tree-sitter = "0.19.5" [dev-dependencies] buffer = { path = "../buffer", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } rand = "0.8.3" tree-sitter-rust = "0.19.0" unindent = "0.1.7" diff --git a/crates/language/src/lib.rs b/crates/language/src/lib.rs index cee5db9441f62a6df1b94e39f1214b76c5380eed..86714def0f427ff9064893d391e77596401aa839 100644 --- a/crates/language/src/lib.rs +++ b/crates/language/src/lib.rs @@ -13,7 +13,7 @@ use clock::ReplicaId; use futures::FutureExt as _; use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; -use lsp::LanguageServer; +use lsp::{DiagnosticSeverity, LanguageServer}; use parking_lot::Mutex; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto; @@ -59,7 +59,7 @@ pub struct Buffer { syntax_tree: Mutex>, parsing_in_background: bool, parse_count: usize, - diagnostics: AnchorRangeMultimap<()>, + diagnostics: AnchorRangeMultimap<(DiagnosticSeverity, String)>, language_server: Option, #[cfg(test)] operations: Vec, @@ -73,6 +73,13 @@ pub struct Snapshot { query_cursor: QueryCursorHandle, } +#[derive(Debug, PartialEq, Eq)] +pub struct Diagnostic { + pub range: Range, + pub severity: DiagnosticSeverity, + pub message: String, +} + struct LanguageServerState { server: Arc, latest_snapshot: watch::Sender>, @@ -613,43 +620,67 @@ impl Buffer { pub fn update_diagnostics( &mut self, - params: lsp::PublishDiagnosticsParams, + version: Option, + diagnostics: Vec, cx: &mut ModelContext, ) -> Result<()> { - dbg!(¶ms); - let language_server = self.language_server.as_mut().unwrap(); - let version = params.version.ok_or_else(|| anyhow!("missing version"))? as usize; - let snapshot = language_server - .pending_snapshots - .get(&version) - .ok_or_else(|| anyhow!("missing snapshot"))?; - self.diagnostics = snapshot.buffer_snapshot.content().anchor_range_multimap( + let version = version.map(|version| version as usize); + let content = if let Some(version) = version { + let language_server = self.language_server.as_mut().unwrap(); + let snapshot = language_server + .pending_snapshots + .get(&version) + .ok_or_else(|| anyhow!("missing snapshot"))?; + snapshot.buffer_snapshot.content() + } else { + self.content() + }; + self.diagnostics = content.anchor_range_multimap( Bias::Left, Bias::Right, - params.diagnostics.into_iter().map(|diagnostic| { + diagnostics.into_iter().map(|diagnostic| { // TODO: Use UTF-16 positions. let start = Point::new( diagnostic.range.start.line, diagnostic.range.start.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 - .pending_snapshots - .range(..version) - .map(|(v, _)| *v) - .collect::>(); - for version in versions_to_delete { - language_server.pending_snapshots.remove(&version); + if let Some(version) = version { + let language_server = self.language_server.as_mut().unwrap(); + let versions_to_delete = language_server + .pending_snapshots + .range(..version) + .map(|(v, _)| *v) + .collect::>(); + for version in versions_to_delete { + language_server.pending_snapshots.remove(&version); + } } cx.notify(); Ok(()) } + pub fn diagnostics_in_range<'a, T: ToOffset>( + &'a self, + range: Range, + ) -> impl Iterator + '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) { if let Some(indent_columns) = self.compute_autoindents() { let indent_columns = cx.background().spawn(indent_columns); @@ -987,17 +1018,16 @@ impl Buffer { } else { return; }; - let file = if let Some(file) = self.file.as_ref() { - file - } else { - return; - }; + let abs_path = self + .file + .as_ref() + .map_or(PathBuf::new(), |file| file.abs_path(cx).unwrap()); let version = post_inc(&mut language_server.next_version); let snapshot = LanguageServerSnapshot { buffer_snapshot: self.text.snapshot(), version, - path: Arc::from(file.abs_path(cx).unwrap()), + path: Arc::from(abs_path), }; language_server .pending_snapshots diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 5d8f2ebc3e8b70c70334f66c352cd736c5873da2..988b1d0c058e8994ff2179197dcde369f7817658 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -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::() + .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::>(), + &[ + 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] fn test_contiguous_ranges() { assert_eq!(