lsp: Pass back diagnostic .data when querying code actions for it (#14962)

Piotr Osiewicz created

Per the LSP spec, we should pass .data field of diagnostics into code
action request:
```
	/**
	 * A data entry field that is preserved between a
	 * `textDocument/publishDiagnostics` notification and
	 * `textDocument/codeAction` request. *
	 * @since 3.16.0 */ data?: LSPAny;
```


Release Notes:

- Fixed rare cases where a code action triggered by diagnostic may not
be available for use.

Change summary

crates/diagnostics/src/diagnostics_tests.rs |  1 +
crates/language/src/buffer.rs               |  4 ++++
crates/language/src/diagnostic_set.rs       |  1 +
crates/language/src/proto.rs                | 10 +++++++++-
crates/project/src/project.rs               |  2 ++
crates/proto/proto/zed.proto                |  1 +
6 files changed, 18 insertions(+), 1 deletion(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -27,6 +27,7 @@ use gpui::{
 use lazy_static::lazy_static;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
+use serde_json::Value;
 use similar::{ChangeTag, TextDiff};
 use smallvec::SmallVec;
 use smol::future::yield_now;
@@ -213,6 +214,8 @@ pub struct Diagnostic {
     pub is_disk_based: bool,
     /// Whether this diagnostic marks unnecessary code.
     pub is_unnecessary: bool,
+    /// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
+    pub data: Option<Value>,
 }
 
 /// TODO - move this into the `project` crate and make it private.
@@ -3844,6 +3847,7 @@ impl Default for Diagnostic {
             is_primary: false,
             is_disk_based: false,
             is_unnecessary: false,
+            data: None,
         }
     }
 }

crates/language/src/diagnostic_set.rs 🔗

@@ -69,6 +69,7 @@ impl DiagnosticEntry<PointUtf16> {
             severity: Some(self.diagnostic.severity),
             source: self.diagnostic.source.clone(),
             message: self.diagnostic.message.clone(),
+            data: self.diagnostic.data.clone(),
             ..Default::default()
         }
     }

crates/language/src/proto.rs 🔗

@@ -5,7 +5,8 @@ use anyhow::{anyhow, Context as _, Result};
 use clock::ReplicaId;
 use lsp::{DiagnosticSeverity, LanguageServerId};
 use rpc::proto;
-use std::{ops::Range, sync::Arc};
+use serde_json::Value;
+use std::{ops::Range, str::FromStr, sync::Arc};
 use text::*;
 
 pub use proto::{BufferState, Operation};
@@ -213,6 +214,7 @@ pub fn serialize_diagnostics<'a>(
             code: entry.diagnostic.code.clone(),
             is_disk_based: entry.diagnostic.is_disk_based,
             is_unnecessary: entry.diagnostic.is_unnecessary,
+            data: entry.diagnostic.data.as_ref().map(|data| data.to_string()),
         })
         .collect()
 }
@@ -396,6 +398,11 @@ pub fn deserialize_diagnostics(
     diagnostics
         .into_iter()
         .filter_map(|diagnostic| {
+            let data = if let Some(data) = diagnostic.data {
+                Some(Value::from_str(&data).ok()?)
+            } else {
+                None
+            };
             Some(DiagnosticEntry {
                 range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
                 diagnostic: Diagnostic {
@@ -413,6 +420,7 @@ pub fn deserialize_diagnostics(
                     is_primary: diagnostic.is_primary,
                     is_disk_based: diagnostic.is_disk_based,
                     is_unnecessary: diagnostic.is_unnecessary,
+                    data,
                 },
             })
         })

crates/project/src/project.rs 🔗

@@ -4595,6 +4595,7 @@ impl Project {
                         is_primary: true,
                         is_disk_based,
                         is_unnecessary,
+                        data: diagnostic.data.clone(),
                     },
                 });
                 if let Some(infos) = &diagnostic.related_information {
@@ -4612,6 +4613,7 @@ impl Project {
                                     is_primary: false,
                                     is_disk_based,
                                     is_unnecessary: false,
+                                    data: diagnostic.data.clone(),
                                 },
                             });
                         }