try sending schema to json language server

Ben Kunkle and Cole created

Co-Authored-By: Cole <cole@zed.dev>

Change summary

crates/languages/src/json.rs                             |  2 
crates/lsp/src/lsp.rs                                    | 12 +
crates/project/src/lsp_store/json_language_server_ext.rs | 89 +++++++++
crates/settings_ui/src/keybindings.rs                    | 76 +++++++
4 files changed, 164 insertions(+), 15 deletions(-)

Detailed changes

crates/languages/src/json.rs 🔗

@@ -216,7 +216,7 @@ impl JsonLspAdapter {
                     paths::local_debug_file_relative_path()
                 ],
                 "schema": debug_schema,
-            },
+            }
         ]);
 
         #[cfg(debug_assertions)]

crates/lsp/src/lsp.rs 🔗

@@ -1242,6 +1242,18 @@ impl LanguageServer {
             params,
         })
         .unwrap();
+        eprintln!("{}", {
+            let value = serde_json::from_str::<serde_json::Value>(&message).unwrap();
+            if !value
+                .get("method")
+                .and_then(|method| method.as_str())
+                .map_or(false, |method| method.starts_with("json"))
+            {
+                "other".to_string()
+            } else {
+                serde_json::to_string_pretty(&value).unwrap()
+            }
+        });
         outbound_tx.try_send(message)?;
         Ok(())
     }

crates/project/src/lsp_store/json_language_server_ext.rs 🔗

@@ -1,15 +1,12 @@
 use ::serde::{Deserialize, Serialize};
 use gpui::{App, Entity, WeakEntity};
 use language::Buffer;
-use lsp::LanguageServer;
+use language::{File as _, LocalFile as _};
+use lsp::{DidCloseTextDocumentParams, DidOpenTextDocumentParams, LanguageServer};
 use util::ResultExt as _;
 
 use crate::{LspStore, Project};
 
-pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
-    let name = language_server.name();
-}
-
 // https://github.com/microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#schema-associations-notification
 struct SchemaAssociationsNotification {}
 
@@ -57,17 +54,93 @@ pub fn send_schema_associations_notification(
 ) {
     let lsp_store = project.read(cx).lsp_store();
     lsp_store.update(cx, |lsp_store, cx| {
-        let Some(local) = lsp_store.as_local() else {
+        let Some(local) = lsp_store.as_local_mut() else {
             return;
         };
         buffer.update(cx, |buffer, cx| {
-            for (adapter, server) in local.language_servers_for_buffer(buffer, cx) {
-                if !adapter.adapter.is_primary_zed_json_schema_adapter() {
+            for (adapter, server) in local
+                .language_servers_for_buffer(buffer, cx)
+                .map(|(a, b)| (a.clone(), b.clone()))
+                .collect::<Vec<_>>()
+            {
+                if dbg!(!adapter.adapter.is_primary_zed_json_schema_adapter()) {
                     continue;
                 }
+
                 server
                     .notify::<SchemaAssociationsNotification>(schema_associations)
                     .log_err(); // todo! don't ignore error
+
+                let file = match worktree::File::from_dyn(buffer.file()) {
+                    Some(file) => file,
+                    None => continue,
+                };
+                let language = match buffer.language() {
+                    Some(language) => language,
+                    None => continue,
+                };
+                let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+
+                let versions = local
+                    .buffer_snapshots
+                    .entry(buffer.remote_id())
+                    .or_default()
+                    .entry(server.server_id())
+                    // .and_modify(|_| {
+                    //     assert!(
+                    //         false,
+                    //         "There should not be an existing snapshot for a newly inserted buffer"
+                    //     )
+                    // })
+                    .or_insert_with(|| {
+                        vec![crate::lsp_store::LspBufferSnapshot {
+                            version: 0,
+                            snapshot: buffer.text_snapshot(),
+                        }]
+                    });
+
+                let snapshot = versions.last().unwrap();
+                let version = snapshot.version;
+                let initial_snapshot = &snapshot.snapshot;
+
+                // if file.worktree.read(cx).id() != key.0
+                //     || !self
+                //         .languages
+                //         .lsp_adapters(&language.name())
+                //         .iter()
+                //         .any(|a| a.name == key.1)
+                // {
+                //     continue;
+                // }
+
+                // didOpen
+                let file = match file.as_local() {
+                    Some(file) => file,
+                    None => continue,
+                };
+                let Some(_) = server
+                    .notify::<lsp::notification::DidCloseTextDocument>(
+                        &DidCloseTextDocumentParams {
+                            text_document: lsp::TextDocumentIdentifier { uri: uri.clone() },
+                        },
+                    )
+                    .log_err()
+                else {
+                    continue;
+                };
+
+                let initial_text = buffer.text();
+
+                server
+                    .notify::<lsp::notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
+                        text_document: lsp::TextDocumentItem::new(
+                            uri,
+                            adapter.language_id(&language.name()),
+                            version,
+                            initial_text,
+                        ),
+                    })
+                    .log_err();
             }
         })
     })

crates/settings_ui/src/keybindings.rs 🔗

@@ -17,6 +17,7 @@ use gpui::{
 };
 use language::{Language, LanguageConfig, ToOffset as _};
 use project::Project;
+use schemars::JsonSchema as _;
 use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
 
 use util::ResultExt;
@@ -397,7 +398,9 @@ impl KeymapEditor {
                 action_name: action_name.into(),
                 action_input,
                 action_docs,
-                action_schema: action_schema.get(action_name).cloned(),
+                action_schema: action_schema.get(action_name).map(|action_schema| {
+                    root_schema_from_action_schema(action_schema, &mut generator)
+                }),
                 context: Some(context),
                 source,
             });
@@ -414,7 +417,9 @@ impl KeymapEditor {
                 action_name: action_name.into(),
                 action_input: None,
                 action_docs: action_documentation.get(action_name).copied(),
-                action_schema: action_schema.get(action_name).cloned(),
+                action_schema: action_schema.get(action_name).map(|action_schema| {
+                    root_schema_from_action_schema(action_schema, &mut generator)
+                }),
                 context: None,
                 source: None,
             });
@@ -1053,9 +1058,10 @@ impl KeybindingEditorModal {
             editor
         });
 
-        if let Some(_schema) = editing_keybind.action_schema.clone() {
+        if let Some(schema) = editing_keybind.action_schema.clone() {
             let project = project.downgrade();
             let fs = fs.clone();
+            let file_name = file_name_for_action_input(&editing_keybind.action_name);
             let action_input = editing_keybind
                 .action_input
                 .as_ref()
@@ -1063,7 +1069,7 @@ impl KeybindingEditorModal {
             cx.spawn_in(window, async move |this, cx| {
                 // todo! fix when modal is dropped, buffer and temp_dir are dropped before worktree, resulting in worktree scan errors
                 // being printed due to the non existant worktree
-                let (buffer, temp_dir) = create_temp_buffer_for_action_input(project.clone(), fs, cx)
+                let (buffer, temp_dir) = create_temp_buffer_for_action_input(file_name.clone(), project.clone(), fs, cx)
                     .await
                     .context("Failed to create temporary buffer for action input. Auto-complete will not work")
                     .log_err()
@@ -1092,6 +1098,34 @@ impl KeybindingEditorModal {
 
                 }}).detach();
 
+                cx.spawn({
+                    let project = project.clone();
+                    let buffer = buffer.downgrade();
+
+
+                    async move |cx| {
+                        cx.background_executor().timer(std::time::Duration::from_secs(10)).await;
+                        let Some(project) = project.upgrade() else {
+                            return;
+                        };
+                        let Some(buffer) = buffer.upgrade() else {
+                            return;
+                        };
+                        let uri = "lol://some.uri".into();
+                        let schema_associations = vec![
+                            project::lsp_store::json_language_server_ext::SchemaAssociation {
+                                uri,
+                                file_match: vec![file_name],
+                                folder_uri: None,
+                                schema: Some(schema.to_value()),
+                            }
+                        ];
+                        cx.update(|_, cx| {
+                            project::lsp_store::json_language_server_ext::send_schema_associations_notification(project, buffer, &schema_associations, cx);
+                        }).ok();
+                    }
+                }).detach();
+
                 let editor = cx.new_window_entity(|window, cx| {
                     let multi_buffer =
                         cx.new(|cx| editor::MultiBuffer::singleton(buffer.clone(), cx));
@@ -1344,12 +1378,19 @@ impl Render for KeybindingEditorModal {
     }
 }
 
+fn file_name_for_action_input(action_name: &SharedString) -> String {
+    let mut file_name = action_name.as_ref().replace("::", "_");
+    file_name.push_str(".json");
+    file_name
+}
+
 async fn create_temp_buffer_for_action_input(
+    file_name: String,
     project: WeakEntity<Project>,
     fs: Arc<dyn Fs>,
     cx: &mut AsyncApp,
 ) -> anyhow::Result<(Entity<language::Buffer>, tempfile::TempDir)> {
-    let (temp_file_path, temp_dir) = create_temp_file_for_action_input(fs)
+    let (temp_file_path, temp_dir) = create_temp_file_for_action_input(file_name.clone(), fs)
         .await
         .context("Failed to create backing file")?;
 
@@ -1363,6 +1404,7 @@ async fn create_temp_buffer_for_action_input(
 }
 
 async fn create_temp_file_for_action_input(
+    file_name: String,
     fs: Arc<dyn Fs>,
 ) -> anyhow::Result<(PathBuf, tempfile::TempDir)> {
     let temp_dir = paths::temp_dir();
@@ -1370,7 +1412,7 @@ async fn create_temp_file_for_action_input(
         .tempdir_in(temp_dir)
         .context("Failed to create temporary directory")?;
 
-    let path = sub_temp_dir.path().join("todo.json");
+    let path = sub_temp_dir.path().join(file_name);
     fs.create_file(
         &path,
         fs::CreateOptions {
@@ -1566,6 +1608,28 @@ async fn save_keybinding_update(
     Ok(())
 }
 
+fn root_schema_from_action_schema(
+    action_schema: &schemars::Schema,
+    generator: &mut schemars::SchemaGenerator,
+) -> schemars::Schema {
+    let meta_schema = generator
+        .settings()
+        .meta_schema
+        .as_ref()
+        .expect("meta_schema should be present in schemars settings")
+        .to_string();
+    let defs = generator.definitions();
+    let mut schema = schemars::json_schema!({
+        "$schema": meta_schema,
+        "allowTrailingCommas": true,
+        "$defs": defs,
+    });
+    schema
+        .ensure_object()
+        .extend(std::mem::take(action_schema.clone().ensure_object()).into_iter());
+    schema
+}
+
 struct KeystrokeInput {
     keystrokes: Vec<Keystroke>,
     focus_handle: FocusHandle,