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:
authored and GitHub committed 2026-03-07 11:05:10 +01:00
1 parent 6db3dc07f1
commit 59c32db61f
8 files changed
+252 -30

No files matched your search

+12
View File
@@ -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,
+42
View File
@@ -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>;
+42
View File
@@ -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(),
);
}
}
}
}