From 97ff79138a996b81e51b3955c48a815d92eb0192 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 10 Jul 2025 14:51:34 -0500 Subject: [PATCH] try sending schema to json language server Co-Authored-By: Cole --- crates/languages/src/json.rs | 2 +- crates/lsp/src/lsp.rs | 12 +++ .../src/lsp_store/json_language_server_ext.rs | 89 +++++++++++++++++-- crates/settings_ui/src/keybindings.rs | 76 ++++++++++++++-- 4 files changed, 164 insertions(+), 15 deletions(-) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 7a3300eb010d9da30111023e660ef56a2070ea9e..da3c7cdbd0aaf19c0d9d77d2e2005ff569f7bb43 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -216,7 +216,7 @@ impl JsonLspAdapter { paths::local_debug_file_relative_path() ], "schema": debug_schema, - }, + } ]); #[cfg(debug_assertions)] diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 53dc24a21a93fecee9a320a44a9b9c46655f31be..e487441234ec92bb3b9e8a1e460e624133918cc5 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1242,6 +1242,18 @@ impl LanguageServer { params, }) .unwrap(); + eprintln!("{}", { + let value = serde_json::from_str::(&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(()) } diff --git a/crates/project/src/lsp_store/json_language_server_ext.rs b/crates/project/src/lsp_store/json_language_server_ext.rs index 4ee0d79a08ccee295a05acd74a8d2f50358bc5a9..9e535c3129e0f5a46acef8e9df0847a5ba9c1c6c 100644 --- a/crates/project/src/lsp_store/json_language_server_ext.rs +++ b/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, 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::>() + { + if dbg!(!adapter.adapter.is_primary_zed_json_schema_adapter()) { continue; } + server .notify::(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::( + &DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier { uri: uri.clone() }, + }, + ) + .log_err() + else { + continue; + }; + + let initial_text = buffer.text(); + + server + .notify::(&DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + adapter.language_id(&language.name()), + version, + initial_text, + ), + }) + .log_err(); } }) }) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 63618291678b4b2635a8fd94d390cd2725383ad4..b237a1b08ae8900028d2931b7d205ffd4ad7027a 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/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, fs: Arc, cx: &mut AsyncApp, ) -> anyhow::Result<(Entity, 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, ) -> 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, focus_handle: FocusHandle,