extension_api: Add language server schema methods (#48334)
(This should be merged after #48332) This PR exposes the LSP settings schema functionality to extensions, allowing them to provide JSON schema for `initialization_options` and `settings` fields to enable autocomplete in settings files. New extension API methods (v0.8.0+): - `language_server_initialization_options_schema` - `language_server_settings_schema` Both methods return an optional JSON string conforming to JSON schema. Older extension versions gracefully return `None`. Release Notes: - Added support for settings schemas for the next version of the extension API so that settings autocompletion can be provided for language server settings. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
1 parent
6db3dc07f1
commit
59c32db61f
8 files changed
+252
-30
No files matched your search
@@ -80,6 +80,18 @@ pub trait Extension: Send + Sync + 'static {
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn language_server_initialization_options_schema(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn language_server_workspace_configuration_schema(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>>;
|
||||
|
||||
async fn language_server_additional_initialization_options(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
|
||||
@@ -100,6 +100,28 @@ pub trait Extension: Send + Sync {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the JSON schema for the initialization options.
|
||||
///
|
||||
/// The schema must conform to the JSON Schema speification.
|
||||
fn language_server_initialization_options_schema(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
) -> Option<serde_json::Value> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the JSON schema for the workspace configuration.
|
||||
///
|
||||
/// The schema must conform to the JSON Schema specification.
|
||||
fn language_server_workspace_configuration_schema(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
) -> Option<serde_json::Value> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the initialization options to pass to the other language server.
|
||||
fn language_server_additional_initialization_options(
|
||||
&mut self,
|
||||
@@ -370,6 +392,26 @@ impl wit::Guest for Component {
|
||||
.and_then(|value| serde_json::to_string(&value).ok()))
|
||||
}
|
||||
|
||||
fn language_server_initialization_options_schema(
|
||||
language_server_id: String,
|
||||
worktree: &Worktree,
|
||||
) -> Option<String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
extension()
|
||||
.language_server_initialization_options_schema(&language_server_id, worktree)
|
||||
.and_then(|value| serde_json::to_string(&value).ok())
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration_schema(
|
||||
language_server_id: String,
|
||||
worktree: &Worktree,
|
||||
) -> Option<String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
extension()
|
||||
.language_server_workspace_configuration_schema(&language_server_id, worktree)
|
||||
.and_then(|value| serde_json::to_string(&value).ok())
|
||||
}
|
||||
|
||||
fn language_server_additional_initialization_options(
|
||||
language_server_id: String,
|
||||
target_language_server_id: String,
|
||||
|
||||
@@ -101,6 +101,16 @@ world extension {
|
||||
/// Returns the workspace configuration options to pass to the language server.
|
||||
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// Returns the JSON schema for the initialization options.
|
||||
///
|
||||
/// The schema is represented as a JSON string conforming to the JSON Schema specification.
|
||||
export language-server-initialization-options-schema: func(language-server-id: string, worktree: borrow<worktree>) -> option<string>;
|
||||
|
||||
/// Returns the JSON schema for the workspace configuration.
|
||||
///
|
||||
/// The schema is represented as a JSON string conforming to the JSON Schema specification.
|
||||
export language-server-workspace-configuration-schema: func(language-server-id: string, worktree: borrow<worktree>) -> option<string>;
|
||||
|
||||
/// Returns the initialization options to pass to the other language server.
|
||||
export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
|
||||
@@ -159,6 +159,48 @@ impl extension::Extension for WasmExtension {
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_initialization_options_schema(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(worktree)?;
|
||||
extension
|
||||
.call_language_server_initialization_options_schema(
|
||||
store,
|
||||
&language_server_id,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_workspace_configuration_schema(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
worktree: Arc<dyn WorktreeDelegate>,
|
||||
) -> Result<Option<String>> {
|
||||
self.call(|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(worktree)?;
|
||||
extension
|
||||
.call_language_server_workspace_configuration_schema(
|
||||
store,
|
||||
&language_server_id,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_additional_initialization_options(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
|
||||
@@ -465,6 +465,60 @@ impl Extension {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_language_server_initialization_options_schema(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Option<String>> {
|
||||
match self {
|
||||
Extension::V0_8_0(ext) => {
|
||||
ext.call_language_server_initialization_options_schema(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V0_6_0(_)
|
||||
| Extension::V0_5_0(_)
|
||||
| Extension::V0_4_0(_)
|
||||
| Extension::V0_3_0(_)
|
||||
| Extension::V0_2_0(_)
|
||||
| Extension::V0_1_0(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_1(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_language_server_workspace_configuration_schema(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Option<String>> {
|
||||
match self {
|
||||
Extension::V0_8_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration_schema(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V0_6_0(_)
|
||||
| Extension::V0_5_0(_)
|
||||
| Extension::V0_4_0(_)
|
||||
| Extension::V0_3_0(_)
|
||||
| Extension::V0_2_0(_)
|
||||
| Extension::V0_1_0(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_1(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_language_server_additional_initialization_options(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
|
||||
@@ -67,25 +67,22 @@ pub fn init(cx: &mut App) {
|
||||
.detach();
|
||||
|
||||
if let Some(extension_events) = extension::ExtensionEvents::try_global(cx) {
|
||||
cx.subscribe(&extension_events, move |_, evt, cx| {
|
||||
match evt {
|
||||
extension::Event::ExtensionInstalled(_)
|
||||
| extension::Event::ExtensionUninstalled(_)
|
||||
| extension::Event::ConfigureExtensionRequested(_) => return,
|
||||
extension::Event::ExtensionsInstalledChanged => {}
|
||||
cx.subscribe(&extension_events, move |_, evt, cx| match evt {
|
||||
extension::Event::ExtensionsInstalledChanged => {
|
||||
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
|
||||
schema_store.notify_schema_changed(ChangedSchemas::Settings, cx);
|
||||
});
|
||||
}
|
||||
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
|
||||
schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}settings"), cx);
|
||||
schema_store
|
||||
.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}project_settings"), cx);
|
||||
});
|
||||
extension::Event::ExtensionUninstalled(_)
|
||||
| extension::Event::ExtensionInstalled(_)
|
||||
| extension::Event::ConfigureExtensionRequested(_) => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
cx.observe_global::<dap::DapRegistry>(move |cx| {
|
||||
cx.update_global::<SchemaStore, _>(|schema_store, cx| {
|
||||
schema_store.notify_schema_changed(&format!("{SCHEMA_URI_PREFIX}debug_tasks"), cx);
|
||||
schema_store.notify_schema_changed(ChangedSchemas::DebugTasks, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -98,18 +95,42 @@ pub struct SchemaStore {
|
||||
|
||||
impl gpui::Global for SchemaStore {}
|
||||
|
||||
enum ChangedSchemas {
|
||||
Settings,
|
||||
DebugTasks,
|
||||
}
|
||||
|
||||
impl SchemaStore {
|
||||
fn notify_schema_changed(&mut self, uri: &str, cx: &mut App) {
|
||||
DYNAMIC_SCHEMA_CACHE.write().remove(uri);
|
||||
fn notify_schema_changed(&mut self, changed_schemas: ChangedSchemas, cx: &mut App) {
|
||||
let uris_to_invalidate = match changed_schemas {
|
||||
ChangedSchemas::Settings => {
|
||||
let settings_uri_prefix = &format!("{SCHEMA_URI_PREFIX}settings");
|
||||
let project_settings_uri = &format!("{SCHEMA_URI_PREFIX}project_settings");
|
||||
DYNAMIC_SCHEMA_CACHE
|
||||
.write()
|
||||
.extract_if(|uri, _| {
|
||||
uri == project_settings_uri || uri.starts_with(settings_uri_prefix)
|
||||
})
|
||||
.map(|(url, _)| url)
|
||||
.collect()
|
||||
}
|
||||
ChangedSchemas::DebugTasks => DYNAMIC_SCHEMA_CACHE
|
||||
.write()
|
||||
.remove_entry(&format!("{SCHEMA_URI_PREFIX}debug_tasks"))
|
||||
.map_or_else(Vec::new, |(uri, _)| vec![uri]),
|
||||
};
|
||||
|
||||
if uris_to_invalidate.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = uri.to_string();
|
||||
self.lsp_stores.retain(|lsp_store| {
|
||||
let Some(lsp_store) = lsp_store.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
project::lsp_store::json_language_server_ext::notify_schema_changed(
|
||||
project::lsp_store::json_language_server_ext::notify_schemas_changed(
|
||||
lsp_store,
|
||||
uri.clone(),
|
||||
&uris_to_invalidate,
|
||||
cx,
|
||||
);
|
||||
true
|
||||
@@ -238,7 +259,8 @@ async fn resolve_dynamic_schema(
|
||||
(adapter_name, LspSchemaKind::Settings)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Invalid LSP schema path: expected '{{adapter}}/initialization_options' or '{{adapter}}/settings', got '{}'",
|
||||
"Invalid LSP schema path: \
|
||||
Expected '{{adapter}}/initialization_options' or '{{adapter}}/settings', got '{}'",
|
||||
lsp_path
|
||||
);
|
||||
};
|
||||
@@ -484,7 +506,7 @@ pub fn all_schema_file_associations(
|
||||
let file_name = normalized_action_name_to_file_name(normalized_name.clone());
|
||||
serde_json::json!({
|
||||
"fileMatch": [file_name],
|
||||
"url": format!("{}action/{normalized_name}", SCHEMA_URI_PREFIX)
|
||||
"url": format!("{SCHEMA_URI_PREFIX}action/{normalized_name}")
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -350,6 +350,44 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialization_options_schema(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Option<serde_json::Value> {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let json_schema: Option<String> = self
|
||||
.extension
|
||||
.language_server_initialization_options_schema(
|
||||
self.language_server_id.clone(),
|
||||
delegate,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
json_schema.and_then(|s| serde_json::from_str(&s).ok())
|
||||
}
|
||||
|
||||
async fn settings_schema(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Option<serde_json::Value> {
|
||||
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
|
||||
let json_schema: Option<String> = self
|
||||
.extension
|
||||
.language_server_workspace_configuration_schema(
|
||||
self.language_server_id.clone(),
|
||||
delegate,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
json_schema.and_then(|s| serde_json::from_str(&s).ok())
|
||||
}
|
||||
|
||||
async fn additional_initialization_options(
|
||||
self: Arc<Self>,
|
||||
target_language_server_id: LanguageServerName,
|
||||
|
||||
@@ -42,8 +42,8 @@ impl lsp::notification::Notification for SchemaContentsChanged {
|
||||
type Params = String;
|
||||
}
|
||||
|
||||
pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: String, cx: &App) {
|
||||
zlog::trace!(LOGGER => "Notifying schema changed for URI: {:?}", uri);
|
||||
pub fn notify_schemas_changed(lsp_store: Entity<LspStore>, uris: &[String], cx: &App) {
|
||||
zlog::trace!(LOGGER => "Notifying schema changes for URIs: {:?}", uris);
|
||||
let servers = lsp_store.read_with(cx, |lsp_store, _| {
|
||||
let mut servers = Vec::new();
|
||||
let Some(local) = lsp_store.as_local() else {
|
||||
@@ -63,16 +63,18 @@ pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: String, cx: &App)
|
||||
servers
|
||||
});
|
||||
for server in servers {
|
||||
zlog::trace!(LOGGER => "Notifying server {NAME} (id {ID:?}) of schema change for URI: {uri:?}",
|
||||
NAME = server.name(),
|
||||
ID = server.server_id()
|
||||
);
|
||||
if let Err(error) = server.notify::<SchemaContentsChanged>(uri.clone()) {
|
||||
zlog::error!(
|
||||
LOGGER => "Failed to notify server {NAME} (id {ID:?}) of schema change for URI {uri:?}: {error:#}",
|
||||
NAME = server.name(),
|
||||
ID = server.server_id(),
|
||||
for uri in uris {
|
||||
zlog::trace!(LOGGER => "Notifying server {NAME} (id {ID:?}) of schema change for URI: {uri:?}",
|
||||
NAME = server.name(),
|
||||
ID = server.server_id()
|
||||
);
|
||||
if let Err(error) = server.notify::<SchemaContentsChanged>(uri.clone()) {
|
||||
zlog::error!(
|
||||
LOGGER => "Failed to notify server {NAME} (id {ID:?}) of schema change for URI {uri:?}: {error:#}",
|
||||
NAME = server.name(),
|
||||
ID = server.server_id(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user