Replicate diagnostics to remote buffers

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/buffer/src/anchor.rs  |  44 ++++++++++++
crates/language/src/lib.rs   |  49 +++++++++++---
crates/language/src/proto.rs | 126 +++++++++++++++++++++++++++----------
crates/rpc/proto/zed.proto   |  23 ++++++
crates/rpc/src/peer.rs       |   4 +
script/server                |   2 
6 files changed, 197 insertions(+), 51 deletions(-)

Detailed changes

crates/buffer/src/anchor.rs 🔗

@@ -176,12 +176,17 @@ impl<T> AnchorRangeMap<T> {
         self.entries.len()
     }
 
-    pub fn from_raw(version: clock::Global, entries: Vec<(Range<(FullOffset, Bias)>, T)>) -> Self {
+    pub fn from_full_offset_ranges(
+        version: clock::Global,
+        entries: Vec<(Range<(FullOffset, Bias)>, T)>,
+    ) -> Self {
         Self { version, entries }
     }
 
-    pub fn raw_entries(&self) -> &[(Range<(FullOffset, Bias)>, T)] {
-        &self.entries
+    pub fn full_offset_ranges(&self) -> impl Iterator<Item = (Range<FullOffset>, &T)> {
+        self.entries
+            .iter()
+            .map(|(range, value)| (range.start.0..range.end.0, value))
     }
 
     pub fn point_ranges<'a>(
@@ -270,6 +275,10 @@ impl<T: Clone> Default for AnchorRangeMultimap<T> {
 }
 
 impl<T: Clone> AnchorRangeMultimap<T> {
+    pub fn version(&self) -> &clock::Global {
+        &self.version
+    }
+
     pub fn intersecting_ranges<'a, I, O>(
         &'a self,
         range: Range<I>,
@@ -336,6 +345,35 @@ impl<T: Clone> AnchorRangeMultimap<T> {
             }
         })
     }
+
+    pub fn from_full_offset_ranges(
+        version: clock::Global,
+        start_bias: Bias,
+        end_bias: Bias,
+        entries: impl Iterator<Item = (Range<FullOffset>, T)>,
+    ) -> Self {
+        Self {
+            version,
+            start_bias,
+            end_bias,
+            entries: SumTree::from_iter(
+                entries.map(|(range, value)| AnchorRangeMultimapEntry {
+                    range: FullOffsetRange {
+                        start: range.start,
+                        end: range.end,
+                    },
+                    value,
+                }),
+                &(),
+            ),
+        }
+    }
+
+    pub fn full_offset_ranges(&self) -> impl Iterator<Item = (Range<FullOffset>, &T)> {
+        self.entries
+            .cursor::<()>()
+            .map(|entry| (entry.range.start..entry.range.end, &entry.value))
+    }
 }
 
 impl<T: Clone> sum_tree::Item for AnchorRangeMultimapEntry<T> {

crates/language/src/lib.rs 🔗

@@ -9,7 +9,7 @@ pub use self::{
     language::{BracketPair, Language, LanguageConfig, LanguageRegistry},
 };
 use anyhow::{anyhow, Result};
-pub use buffer::{Buffer as TextBuffer, *};
+pub use buffer::{Buffer as TextBuffer, Operation as _, *};
 use clock::ReplicaId;
 use futures::FutureExt as _;
 use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
@@ -99,6 +99,12 @@ struct LanguageServerSnapshot {
     path: Arc<Path>,
 }
 
+#[derive(Clone)]
+pub enum Operation {
+    Buffer(buffer::Operation),
+    UpdateDiagnostics(AnchorRangeMultimap<Diagnostic>),
+}
+
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum Event {
     Edited,
@@ -256,7 +262,7 @@ impl Buffer {
         let ops = message
             .history
             .into_iter()
-            .map(|op| Operation::Edit(proto::deserialize_edit_operation(op)));
+            .map(|op| buffer::Operation::Edit(proto::deserialize_edit_operation(op)));
         buffer.apply_ops(ops)?;
         for set in message.selections {
             let set = proto::deserialize_selection_set(set);
@@ -278,6 +284,7 @@ impl Buffer {
                 .selection_sets()
                 .map(|(_, set)| proto::serialize_selection_set(set))
                 .collect(),
+            diagnostics: Some(proto::serialize_diagnostics(&self.diagnostics)),
         }
     }
 
@@ -761,6 +768,7 @@ impl Buffer {
         }
 
         self.diagnostics_update_count += 1;
+        self.send_operation(Operation::UpdateDiagnostics(self.diagnostics.clone()), cx);
         cx.notify();
         Ok(())
     }
@@ -1240,7 +1248,7 @@ impl Buffer {
         }
 
         self.end_transaction(None, cx).unwrap();
-        self.send_operation(Operation::Edit(edit), cx);
+        self.send_operation(Operation::Buffer(buffer::Operation::Edit(edit)), cx);
     }
 
     fn did_edit(
@@ -1269,10 +1277,10 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) -> SelectionSetId {
         let operation = self.text.add_selection_set(selections);
-        if let Operation::UpdateSelections { set_id, .. } = &operation {
+        if let buffer::Operation::UpdateSelections { set_id, .. } = &operation {
             let set_id = *set_id;
             cx.notify();
-            self.send_operation(operation, cx);
+            self.send_operation(Operation::Buffer(operation), cx);
             set_id
         } else {
             unreachable!()
@@ -1287,7 +1295,7 @@ impl Buffer {
     ) -> Result<()> {
         let operation = self.text.update_selection_set(set_id, selections)?;
         cx.notify();
-        self.send_operation(operation, cx);
+        self.send_operation(Operation::Buffer(operation), cx);
         Ok(())
     }
 
@@ -1297,7 +1305,7 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         let operation = self.text.set_active_selection_set(set_id)?;
-        self.send_operation(operation, cx);
+        self.send_operation(Operation::Buffer(operation), cx);
         Ok(())
     }
 
@@ -1308,7 +1316,7 @@ impl Buffer {
     ) -> Result<()> {
         let operation = self.text.remove_selection_set(set_id)?;
         cx.notify();
-        self.send_operation(operation, cx);
+        self.send_operation(Operation::Buffer(operation), cx);
         Ok(())
     }
 
@@ -1320,7 +1328,17 @@ impl Buffer {
         self.pending_autoindent.take();
         let was_dirty = self.is_dirty();
         let old_version = self.version.clone();
-        self.text.apply_ops(ops)?;
+        let buffer_ops = ops
+            .into_iter()
+            .filter_map(|op| match op {
+                Operation::Buffer(op) => Some(op),
+                Operation::UpdateDiagnostics(diagnostics) => {
+                    self.apply_diagnostic_update(diagnostics, cx);
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+        self.text.apply_ops(buffer_ops)?;
         self.did_edit(&old_version, was_dirty, cx);
         // Notify independently of whether the buffer was edited as the operations could include a
         // selection update.
@@ -1328,6 +1346,15 @@ impl Buffer {
         Ok(())
     }
 
+    fn apply_diagnostic_update(
+        &mut self,
+        diagnostics: AnchorRangeMultimap<Diagnostic>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.diagnostics = diagnostics;
+        cx.notify();
+    }
+
     #[cfg(not(test))]
     pub fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext<Self>) {
         if let Some(file) = &self.file {
@@ -1350,7 +1377,7 @@ impl Buffer {
         let old_version = self.version.clone();
 
         for operation in self.text.undo() {
-            self.send_operation(operation, cx);
+            self.send_operation(Operation::Buffer(operation), cx);
         }
 
         self.did_edit(&old_version, was_dirty, cx);
@@ -1361,7 +1388,7 @@ impl Buffer {
         let old_version = self.version.clone();
 
         for operation in self.text.redo() {
-            self.send_operation(operation, cx);
+            self.send_operation(Operation::Buffer(operation), cx);
         }
 
         self.did_edit(&old_version, was_dirty, cx);

crates/language/src/proto.rs 🔗

@@ -1,8 +1,12 @@
 use std::sync::Arc;
 
+use crate::Diagnostic;
+
+use super::Operation;
 use anyhow::{anyhow, Result};
 use buffer::*;
 use clock::ReplicaId;
+use lsp::DiagnosticSeverity;
 use rpc::proto;
 
 pub use proto::Buffer;
@@ -10,13 +14,13 @@ pub use proto::Buffer;
 pub fn serialize_operation(operation: &Operation) -> proto::Operation {
     proto::Operation {
         variant: Some(match operation {
-            Operation::Edit(edit) => {
+            Operation::Buffer(buffer::Operation::Edit(edit)) => {
                 proto::operation::Variant::Edit(serialize_edit_operation(edit))
             }
-            Operation::Undo {
+            Operation::Buffer(buffer::Operation::Undo {
                 undo,
                 lamport_timestamp,
-            } => proto::operation::Variant::Undo(proto::operation::Undo {
+            }) => proto::operation::Variant::Undo(proto::operation::Undo {
                 replica_id: undo.id.replica_id as u32,
                 local_timestamp: undo.id.value,
                 lamport_timestamp: lamport_timestamp.value,
@@ -39,44 +43,46 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
                     .collect(),
                 version: From::from(&undo.version),
             }),
-            Operation::UpdateSelections {
+            Operation::Buffer(buffer::Operation::UpdateSelections {
                 set_id,
                 selections,
                 lamport_timestamp,
-            } => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
+            }) => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections {
                 replica_id: set_id.replica_id as u32,
                 local_timestamp: set_id.value,
                 lamport_timestamp: lamport_timestamp.value,
                 version: selections.version().into(),
                 selections: selections
-                    .raw_entries()
-                    .iter()
+                    .full_offset_ranges()
                     .map(|(range, state)| proto::Selection {
                         id: state.id as u64,
-                        start: range.start.0 .0 as u64,
-                        end: range.end.0 .0 as u64,
+                        start: range.start.0 as u64,
+                        end: range.end.0 as u64,
                         reversed: state.reversed,
                     })
                     .collect(),
             }),
-            Operation::RemoveSelections {
+            Operation::Buffer(buffer::Operation::RemoveSelections {
                 set_id,
                 lamport_timestamp,
-            } => proto::operation::Variant::RemoveSelections(proto::operation::RemoveSelections {
+            }) => proto::operation::Variant::RemoveSelections(proto::operation::RemoveSelections {
                 replica_id: set_id.replica_id as u32,
                 local_timestamp: set_id.value,
                 lamport_timestamp: lamport_timestamp.value,
             }),
-            Operation::SetActiveSelections {
+            Operation::Buffer(buffer::Operation::SetActiveSelections {
                 set_id,
                 lamport_timestamp,
-            } => proto::operation::Variant::SetActiveSelections(
+            }) => proto::operation::Variant::SetActiveSelections(
                 proto::operation::SetActiveSelections {
                     replica_id: lamport_timestamp.replica_id as u32,
                     local_timestamp: set_id.map(|set_id| set_id.value),
                     lamport_timestamp: lamport_timestamp.value,
                 },
             ),
+            Operation::UpdateDiagnostics(diagnostic_set) => {
+                proto::operation::Variant::UpdateDiagnostics(serialize_diagnostics(diagnostic_set))
+            }
         }),
     }
 }
@@ -102,24 +108,44 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::
 
 pub fn serialize_selection_set(set: &SelectionSet) -> proto::SelectionSet {
     let version = set.selections.version();
-    let entries = set.selections.raw_entries();
+    let entries = set.selections.full_offset_ranges();
     proto::SelectionSet {
         replica_id: set.id.replica_id as u32,
         lamport_timestamp: set.id.value as u32,
         is_active: set.active,
         version: version.into(),
         selections: entries
-            .iter()
             .map(|(range, state)| proto::Selection {
                 id: state.id as u64,
-                start: range.start.0 .0 as u64,
-                end: range.end.0 .0 as u64,
+                start: range.start.0 as u64,
+                end: range.end.0 as u64,
                 reversed: state.reversed,
             })
             .collect(),
     }
 }
 
+pub fn serialize_diagnostics(map: &AnchorRangeMultimap<Diagnostic>) -> proto::DiagnosticSet {
+    proto::DiagnosticSet {
+        version: map.version().into(),
+        diagnostics: map
+            .full_offset_ranges()
+            .map(|(range, diagnostic)| proto::Diagnostic {
+                start: range.start.0 as u64,
+                end: range.end.0 as u64,
+                message: diagnostic.message.clone(),
+                severity: match diagnostic.severity {
+                    DiagnosticSeverity::ERROR => proto::diagnostic::Severity::Error,
+                    DiagnosticSeverity::WARNING => proto::diagnostic::Severity::Warning,
+                    DiagnosticSeverity::INFORMATION => proto::diagnostic::Severity::Information,
+                    DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint,
+                    _ => proto::diagnostic::Severity::None,
+                } as i32,
+            })
+            .collect(),
+    }
+}
+
 pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
     Ok(
         match message
@@ -127,9 +153,9 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
             .ok_or_else(|| anyhow!("missing operation variant"))?
         {
             proto::operation::Variant::Edit(edit) => {
-                Operation::Edit(deserialize_edit_operation(edit))
+                Operation::Buffer(buffer::Operation::Edit(deserialize_edit_operation(edit)))
             }
-            proto::operation::Variant::Undo(undo) => Operation::Undo {
+            proto::operation::Variant::Undo(undo) => Operation::Buffer(buffer::Operation::Undo {
                 lamport_timestamp: clock::Lamport {
                     replica_id: undo.replica_id as ReplicaId,
                     value: undo.lamport_timestamp,
@@ -159,7 +185,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                         .collect(),
                     version: undo.version.into(),
                 },
-            },
+            }),
             proto::operation::Variant::UpdateSelections(message) => {
                 let version = message.version.into();
                 let entries = message
@@ -176,9 +202,9 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                         (range, state)
                     })
                     .collect();
-                let selections = AnchorRangeMap::from_raw(version, entries);
+                let selections = AnchorRangeMap::from_full_offset_ranges(version, entries);
 
-                Operation::UpdateSelections {
+                Operation::Buffer(buffer::Operation::UpdateSelections {
                     set_id: clock::Lamport {
                         replica_id: message.replica_id as ReplicaId,
                         value: message.local_timestamp,
@@ -188,20 +214,22 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                         value: message.lamport_timestamp,
                     },
                     selections: Arc::from(selections),
-                }
+                })
+            }
+            proto::operation::Variant::RemoveSelections(message) => {
+                Operation::Buffer(buffer::Operation::RemoveSelections {
+                    set_id: clock::Lamport {
+                        replica_id: message.replica_id as ReplicaId,
+                        value: message.local_timestamp,
+                    },
+                    lamport_timestamp: clock::Lamport {
+                        replica_id: message.replica_id as ReplicaId,
+                        value: message.lamport_timestamp,
+                    },
+                })
             }
-            proto::operation::Variant::RemoveSelections(message) => Operation::RemoveSelections {
-                set_id: clock::Lamport {
-                    replica_id: message.replica_id as ReplicaId,
-                    value: message.local_timestamp,
-                },
-                lamport_timestamp: clock::Lamport {
-                    replica_id: message.replica_id as ReplicaId,
-                    value: message.lamport_timestamp,
-                },
-            },
             proto::operation::Variant::SetActiveSelections(message) => {
-                Operation::SetActiveSelections {
+                Operation::Buffer(buffer::Operation::SetActiveSelections {
                     set_id: message.local_timestamp.map(|value| clock::Lamport {
                         replica_id: message.replica_id as ReplicaId,
                         value,
@@ -210,7 +238,10 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                         replica_id: message.replica_id as ReplicaId,
                         value: message.lamport_timestamp,
                     },
-                }
+                })
+            }
+            proto::operation::Variant::UpdateDiagnostics(message) => {
+                Operation::UpdateDiagnostics(deserialize_diagnostics(message))
             }
         },
     )
@@ -241,7 +272,7 @@ pub fn deserialize_selection_set(set: proto::SelectionSet) -> SelectionSet {
             value: set.lamport_timestamp,
         },
         active: set.is_active,
-        selections: Arc::new(AnchorRangeMap::from_raw(
+        selections: Arc::new(AnchorRangeMap::from_full_offset_ranges(
             set.version.into(),
             set.selections
                 .into_iter()
@@ -259,3 +290,26 @@ pub fn deserialize_selection_set(set: proto::SelectionSet) -> SelectionSet {
         )),
     }
 }
+
+pub fn deserialize_diagnostics(message: proto::DiagnosticSet) -> AnchorRangeMultimap<Diagnostic> {
+    AnchorRangeMultimap::from_full_offset_ranges(
+        message.version.into(),
+        Bias::Left,
+        Bias::Right,
+        message.diagnostics.into_iter().filter_map(|diagnostic| {
+            Some((
+                FullOffset(diagnostic.start as usize)..FullOffset(diagnostic.end as usize),
+                Diagnostic {
+                    severity: match proto::diagnostic::Severity::from_i32(diagnostic.severity)? {
+                        proto::diagnostic::Severity::Error => DiagnosticSeverity::ERROR,
+                        proto::diagnostic::Severity::Warning => DiagnosticSeverity::WARNING,
+                        proto::diagnostic::Severity::Information => DiagnosticSeverity::INFORMATION,
+                        proto::diagnostic::Severity::Hint => DiagnosticSeverity::HINT,
+                        proto::diagnostic::Severity::None => return None,
+                    },
+                    message: diagnostic.message,
+                },
+            ))
+        }),
+    )
+}

crates/rpc/proto/zed.proto 🔗

@@ -228,6 +228,7 @@ message Buffer {
     string content = 2;
     repeated Operation.Edit history = 3;
     repeated SelectionSet selections = 4;
+    DiagnosticSet diagnostics = 5;
 }
 
 message SelectionSet {
@@ -245,6 +246,27 @@ message Selection {
     bool reversed = 4;
 }
 
+message DiagnosticSet {
+    repeated VectorClockEntry version = 1;
+    repeated Diagnostic diagnostics = 2;
+}
+
+message Diagnostic {
+    uint64 start = 1;
+    uint64 end = 2;
+    Severity severity = 3;
+    string message = 4;
+    enum Severity {
+        None = 0;
+        Error = 1;
+        Warning = 2;
+        Information = 3;
+        Hint = 4;
+    }
+}
+
+
+
 message Operation {
     oneof variant {
         Edit edit = 1;
@@ -252,6 +274,7 @@ message Operation {
         UpdateSelections update_selections = 3;
         RemoveSelections remove_selections = 4;
         SetActiveSelections set_active_selections = 5;
+        DiagnosticSet update_diagnostics = 6;
     }
 
     message Edit {

crates/rpc/src/peer.rs 🔗

@@ -398,6 +398,7 @@ mod tests {
                         content: "path/one content".to_string(),
                         history: vec![],
                         selections: vec![],
+                        diagnostics: None,
                     }),
                 }
             );
@@ -419,6 +420,7 @@ mod tests {
                         content: "path/two content".to_string(),
                         history: vec![],
                         selections: vec![],
+                        diagnostics: None,
                     }),
                 }
             );
@@ -449,6 +451,7 @@ mod tests {
                                         content: "path/one content".to_string(),
                                         history: vec![],
                                         selections: vec![],
+                                        diagnostics: None,
                                     }),
                                 }
                             }
@@ -460,6 +463,7 @@ mod tests {
                                         content: "path/two content".to_string(),
                                         history: vec![],
                                         selections: vec![],
+                                        diagnostics: None,
                                     }),
                                 }
                             }

script/server 🔗

@@ -2,5 +2,5 @@
 
 set -e
 
-cd server
+cd crates/server
 cargo run $@