debugger: Rework language association with the debuggers (#29945)

- Languages now define their preferred debuggers in `config.toml`.
- `LanguageRegistry` now exposes language config even for languages that
are not yet loaded. This necessitated extension registry changes (we now
deserialize config.toml of all language entries when loading new
extension index), but it should be backwards compatible with the old
format. /cc @maxdeviant

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-05-06 20:16:41 +02:00 committed by GitHub
parent 544e8fc46c
commit 09d3ff9dbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 386 additions and 216 deletions

View file

@ -908,6 +908,8 @@
"hard_tabs": false, "hard_tabs": false,
// How many columns a tab should occupy. // How many columns a tab should occupy.
"tab_size": 4, "tab_size": 4,
// What debuggers are preferred by default for all languages.
"debuggers": [],
// Control what info is collected by Zed. // Control what info is collected by Zed.
"telemetry": { "telemetry": {
// Send debug info like crash reports. // Send debug info like crash reports.

View file

@ -621,20 +621,32 @@ impl DebugPanel {
move |_, window, cx| { move |_, window, cx| {
let weak_panel = weak_panel.clone(); let weak_panel = weak_panel.clone();
let past_debug_definition = past_debug_definition.clone(); let past_debug_definition = past_debug_definition.clone();
let workspace = workspace.clone();
window
.spawn(cx, async move |cx| {
let task_contexts = workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
let _ = workspace.update(cx, |this, cx| { workspace.update_in(cx, |this, window, cx| {
let workspace = cx.weak_entity(); this.toggle_modal(window, cx, |window, cx| {
this.toggle_modal(window, cx, |window, cx| { NewSessionModal::new(
NewSessionModal::new( past_debug_definition,
past_debug_definition, weak_panel,
weak_panel, workspace.clone(),
workspace, None,
None, task_contexts,
window, window,
cx, cx,
) )
}); });
}); })?;
Result::<_, anyhow::Error>::Ok(())
})
.detach();
} }
}) })
.tooltip({ .tooltip({

View file

@ -153,16 +153,29 @@ pub fn init(cx: &mut App) {
let weak_panel = debug_panel.downgrade(); let weak_panel = debug_panel.downgrade();
let weak_workspace = cx.weak_entity(); let weak_workspace = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| { cx.spawn_in(window, async move |this, cx| {
NewSessionModal::new( let task_contexts = this
debug_panel.read(cx).past_debug_definition.clone(), .update_in(cx, |workspace, window, cx| {
weak_panel, tasks_ui::task_contexts(workspace, window, cx)
weak_workspace, })?
None, .await;
window, this.update_in(cx, |workspace, window, cx| {
cx, workspace.toggle_modal(window, cx, |window, cx| {
) NewSessionModal::new(
}); debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
None,
task_contexts,
window,
cx,
)
});
})?;
Result::<_, anyhow::Error>::Ok(())
})
.detach();
} }
}, },
) )
@ -172,16 +185,30 @@ pub fn init(cx: &mut App) {
let weak_workspace = cx.weak_entity(); let weak_workspace = cx.weak_entity();
let task_store = workspace.project().read(cx).task_store().clone(); let task_store = workspace.project().read(cx).task_store().clone();
workspace.toggle_modal(window, cx, |window, cx| { cx.spawn_in(window, async move |this, cx| {
NewSessionModal::new( let task_contexts = this
debug_panel.read(cx).past_debug_definition.clone(), .update_in(cx, |workspace, window, cx| {
weak_panel, tasks_ui::task_contexts(workspace, window, cx)
weak_workspace, })?
Some(task_store), .await;
window,
cx, this.update_in(cx, |workspace, window, cx| {
) workspace.toggle_modal(window, cx, |window, cx| {
}); NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
Some(task_store),
task_contexts,
window,
cx,
)
});
})?;
anyhow::Ok(())
})
.detach()
} }
}); });
}) })

View file

@ -1,9 +1,12 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::Reverse,
ops::Not, ops::Not,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use collections::{HashMap, HashSet};
use dap::{ use dap::{
DapRegistry, DebugRequest, DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition}, adapters::{DebugAdapterName, DebugTaskDefinition},
@ -15,10 +18,9 @@ use gpui::{
Subscription, TextStyle, WeakEntity, Subscription, TextStyle, WeakEntity,
}; };
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskSourceKind, task_store::TaskStore}; use project::{TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings; use settings::Settings;
use task::{DebugScenario, LaunchRequest}; use task::{DebugScenario, LaunchRequest};
use tasks_ui::task_contexts;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@ -32,7 +34,6 @@ use workspace::{ModalView, Workspace};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
#[derive(Clone)]
pub(super) struct NewSessionModal { pub(super) struct NewSessionModal {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>, debug_panel: WeakEntity<DebugPanel>,
@ -41,6 +42,7 @@ pub(super) struct NewSessionModal {
initialize_args: Option<serde_json::Value>, initialize_args: Option<serde_json::Value>,
debugger: Option<DebugAdapterName>, debugger: Option<DebugAdapterName>,
last_selected_profile_name: Option<SharedString>, last_selected_profile_name: Option<SharedString>,
task_contexts: Arc<TaskContexts>,
} }
fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString { fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
@ -67,6 +69,7 @@ impl NewSessionModal {
debug_panel: WeakEntity<DebugPanel>, debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
task_store: Option<Entity<TaskStore>>, task_store: Option<Entity<TaskStore>>,
task_contexts: TaskContexts,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
@ -105,6 +108,7 @@ impl NewSessionModal {
.unwrap_or(ToggleState::Unselected), .unwrap_or(ToggleState::Unselected),
last_selected_profile_name: None, last_selected_profile_name: None,
initialize_args: None, initialize_args: None,
task_contexts: Arc::new(task_contexts),
} }
} }
@ -145,22 +149,10 @@ impl NewSessionModal {
}; };
let debug_panel = self.debug_panel.clone(); let debug_panel = self.debug_panel.clone();
let workspace = self.workspace.clone(); let task_contexts = self.task_contexts.clone();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let task_contexts = workspace let task_context = task_contexts.active_context().cloned().unwrap_or_default();
.update_in(cx, |this, window, cx| task_contexts(this, window, cx))?
.await;
let worktree_id = task_contexts.worktree(); let worktree_id = task_contexts.worktree();
let task_context = task_contexts
.active_item_context
.map(|(_, _, context)| context)
.or_else(|| {
task_contexts
.active_worktree_context
.map(|(_, context)| context)
})
.unwrap_or_default();
debug_panel.update_in(cx, |debug_panel, window, cx| { debug_panel.update_in(cx, |debug_panel, window, cx| {
debug_panel.start_session(config, task_context, None, worktree_id, window, cx) debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
})?; })?;
@ -198,14 +190,27 @@ impl NewSessionModal {
&self, &self,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> ui::DropdownMenu { ) -> Option<ui::DropdownMenu> {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let language_registry = self
.workspace
.update(cx, |this, _| this.app_state().languages.clone())
.ok()?;
let weak = cx.weak_entity(); let weak = cx.weak_entity();
let label = self let label = self
.debugger .debugger
.as_ref() .as_ref()
.map(|d| d.0.clone()) .map(|d| d.0.clone())
.unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone()); .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
let active_buffer_language_name =
self.task_contexts
.active_item_context
.as_ref()
.and_then(|item| {
item.1
.as_ref()
.and_then(|location| location.buffer.read(cx).language()?.name().into())
});
DropdownMenu::new( DropdownMenu::new(
"dap-adapter-picker", "dap-adapter-picker",
label, label,
@ -224,17 +229,50 @@ impl NewSessionModal {
} }
}; };
let available_adapters = workspace let available_languages = language_registry.language_names();
let mut debugger_to_languages = HashMap::default();
for language in available_languages {
let Some(language) =
language_registry.available_language_for_name(language.as_str())
else {
continue;
};
language.config().debuggers.iter().for_each(|adapter| {
debugger_to_languages
.entry(adapter.clone())
.or_insert_with(HashSet::default)
.insert(language.name());
});
}
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters()) .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.ok() .ok()
.unwrap_or_default(); .unwrap_or_default();
for adapter in available_adapters { available_adapters.sort_by_key(|name| {
let languages_for_debugger = debugger_to_languages.get(name.as_ref());
let languages_count =
languages_for_debugger.map_or(0, |languages| languages.len());
let contains_language_of_active_buffer = languages_for_debugger
.zip(active_buffer_language_name.as_ref())
.map_or(false, |(languages, active_buffer_language)| {
languages.contains(active_buffer_language)
});
(
Reverse(contains_language_of_active_buffer),
Reverse(languages_count),
)
});
for adapter in available_adapters.into_iter() {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone())); menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
} }
menu menu
}), }),
) )
.into()
} }
fn debug_config_drop_down_menu( fn debug_config_drop_down_menu(
@ -591,7 +629,9 @@ impl Render for NewSessionModal {
), ),
) )
.justify_between() .justify_between()
.child(self.adapter_drop_down_menu(window, cx)) .when(!matches!(self.mode, NewSessionMode::Scenario(_)), |this| {
this.children(self.adapter_drop_down_menu(window, cx))
})
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.border_b_1(), .border_b_1(),
) )

View file

@ -5206,12 +5206,22 @@ impl Editor {
let dap_store = project.read(cx).dap_store(); let dap_store = project.read(cx).dap_store();
let mut scenarios = vec![]; let mut scenarios = vec![];
let resolved_tasks = resolved_tasks.as_ref()?; let resolved_tasks = resolved_tasks.as_ref()?;
let debug_adapter: SharedString = buffer let buffer = buffer.read(cx);
.read(cx) let language = buffer.language()?;
.language()? let file = buffer.file();
.context_provider()? let debug_adapter =
.debug_adapter()? language_settings(language.name().into(), file, cx)
.into(); .debuggers
.first()
.map(SharedString::from)
.or_else(|| {
language
.config()
.debuggers
.first()
.map(SharedString::from)
})?;
dap_store.update(cx, |this, cx| { dap_store.update(cx, |this, cx| {
for (_, task) in &resolved_tasks.templates { for (_, task) in &resolved_tasks.templates {
if let Some(scenario) = this if let Some(scenario) = this

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use fs::Fs; use fs::Fs;
use gpui::{App, Global, ReadGlobal, SharedString, Task}; use gpui::{App, Global, ReadGlobal, SharedString, Task};
use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage}; use language::{BinaryStatus, LanguageConfig, LanguageName, LoadedLanguage};
use lsp::LanguageServerName; use lsp::LanguageServerName;
use parking_lot::RwLock; use parking_lot::RwLock;
@ -224,10 +224,7 @@ impl ExtensionGrammarProxy for ExtensionHostProxy {
pub trait ExtensionLanguageProxy: Send + Sync + 'static { pub trait ExtensionLanguageProxy: Send + Sync + 'static {
fn register_language( fn register_language(
&self, &self,
language: LanguageName, config: LanguageConfig,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
); );
@ -241,17 +238,14 @@ pub trait ExtensionLanguageProxy: Send + Sync + 'static {
impl ExtensionLanguageProxy for ExtensionHostProxy { impl ExtensionLanguageProxy for ExtensionHostProxy {
fn register_language( fn register_language(
&self, &self,
language: LanguageName, language: LanguageConfig,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) { ) {
let Some(proxy) = self.language_proxy.read().clone() else { let Some(proxy) = self.language_proxy.read().clone() else {
return; return;
}; };
proxy.register_language(language, grammar, matcher, hidden, load) proxy.register_language(language, load)
} }
fn remove_languages( fn remove_languages(

View file

@ -34,8 +34,7 @@ use gpui::{
}; };
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{ use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage, QUERY_FILENAME_PREFIXES, Rope,
QUERY_FILENAME_PREFIXES, Rope,
}; };
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks; use project::ContextProviderWithTasks;
@ -140,7 +139,7 @@ struct GlobalExtensionStore(Entity<ExtensionStore>);
impl Global for GlobalExtensionStore {} impl Global for GlobalExtensionStore {}
#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] #[derive(Deserialize, Serialize, Default)]
pub struct ExtensionIndex { pub struct ExtensionIndex {
pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>, pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>,
pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>, pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>,
@ -167,13 +166,12 @@ pub struct ExtensionIndexIconThemeEntry {
pub path: PathBuf, pub path: PathBuf,
} }
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexLanguageEntry { pub struct ExtensionIndexLanguageEntry {
pub extension: Arc<str>, pub extension: Arc<str>,
pub path: PathBuf, pub path: PathBuf,
pub matcher: LanguageMatcher, #[serde(skip)]
pub hidden: bool, pub config: LanguageConfig,
pub grammar: Option<Arc<str>>,
} }
actions!(zed, [ReloadExtensions]); actions!(zed, [ReloadExtensions]);
@ -1015,7 +1013,7 @@ impl ExtensionStore {
/// added to the manifest, or whose files have changed on disk. /// added to the manifest, or whose files have changed on disk.
fn extensions_updated( fn extensions_updated(
&mut self, &mut self,
new_index: ExtensionIndex, mut new_index: ExtensionIndex,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<()> { ) -> Task<()> {
let old_index = &self.extension_index; let old_index = &self.extension_index;
@ -1143,11 +1141,6 @@ impl ExtensionStore {
self.proxy self.proxy
.remove_languages(&languages_to_remove, &grammars_to_remove); .remove_languages(&languages_to_remove, &grammars_to_remove);
let languages_to_add = new_index
.languages
.iter()
.filter(|(_, entry)| extensions_to_load.contains(&entry.extension))
.collect::<Vec<_>>();
let mut grammars_to_add = Vec::new(); let mut grammars_to_add = Vec::new();
let mut themes_to_add = Vec::new(); let mut themes_to_add = Vec::new();
let mut icon_themes_to_add = Vec::new(); let mut icon_themes_to_add = Vec::new();
@ -1189,39 +1182,7 @@ impl ExtensionStore {
self.proxy.register_grammars(grammars_to_add); self.proxy.register_grammars(grammars_to_add);
for (language_name, language) in languages_to_add { let installed_dir = self.installed_dir.clone();
let mut language_path = self.installed_dir.clone();
language_path.extend([
Path::new(language.extension.as_ref()),
language.path.as_path(),
]);
self.proxy.register_language(
language_name.clone(),
language.grammar.clone(),
language.matcher.clone(),
language.hidden,
Arc::new(move || {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
let queries = load_plugin_queries(&language_path);
let context_provider =
std::fs::read_to_string(language_path.join("tasks.json"))
.ok()
.and_then(|contents| {
let definitions =
serde_json_lenient::from_str(&contents).log_err()?;
Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
});
Ok(LoadedLanguage {
config,
queries,
context_provider,
toolchain_provider: None,
})
}),
);
}
let fs = self.fs.clone(); let fs = self.fs.clone();
let wasm_host = self.wasm_host.clone(); let wasm_host = self.wasm_host.clone();
@ -1232,11 +1193,59 @@ impl ExtensionStore {
.filter_map(|name| new_index.extensions.get(name).cloned()) .filter_map(|name| new_index.extensions.get(name).cloned())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.extension_index = new_index;
cx.notify();
cx.emit(Event::ExtensionsUpdated);
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let languages_to_add = new_index
.languages
.iter_mut()
.filter(|(_, entry)| extensions_to_load.contains(&entry.extension))
.collect::<Vec<_>>();
for (_, language) in languages_to_add {
let mut language_path = installed_dir.clone();
language_path.extend([
Path::new(language.extension.as_ref()),
language.path.as_path(),
]);
let Some(config) = fs.load(&language_path.join("config.toml")).await.ok() else {
log::error!("Could not load config.toml in {:?}", language_path);
continue;
};
let Some(config) = ::toml::from_str::<LanguageConfig>(&config).ok() else {
log::error!(
"Could not parse language config.toml in {:?}",
language_path
);
continue;
};
language.config = config.clone();
proxy.register_language(
language.config.clone(),
Arc::new(move || {
let queries = load_plugin_queries(&language_path);
let context_provider =
std::fs::read_to_string(language_path.join("tasks.json"))
.ok()
.and_then(|contents| {
let definitions =
serde_json_lenient::from_str(&contents).log_err()?;
Some(Arc::new(ContextProviderWithTasks::new(definitions))
as Arc<_>)
});
Ok(LoadedLanguage {
config: config.clone(),
queries,
context_provider,
toolchain_provider: None,
})
}),
);
}
this.update(cx, |this, cx| {
this.extension_index = new_index;
cx.notify();
cx.emit(Event::ExtensionsUpdated);
})
.ok();
cx.background_spawn({ cx.background_spawn({
let fs = fs.clone(); let fs = fs.clone();
async move { async move {
@ -1439,9 +1448,7 @@ impl ExtensionStore {
ExtensionIndexLanguageEntry { ExtensionIndexLanguageEntry {
extension: extension_id.clone(), extension: extension_id.clone(),
path: relative_path, path: relative_path,
matcher: config.matcher, config,
hidden: config.hidden,
grammar: config.grammar,
}, },
); );
} }

View file

@ -10,7 +10,7 @@ use fs::{FakeFs, Fs, RealFs};
use futures::{AsyncReadExt, StreamExt, io::BufReader}; use futures::{AsyncReadExt, StreamExt, io::BufReader};
use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext}; use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext};
use http_client::{FakeHttpClient, Response}; use http_client::{FakeHttpClient, Response};
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; use language::{BinaryStatus, LanguageConfig, LanguageMatcher, LanguageRegistry};
use lsp::LanguageServerName; use lsp::LanguageServerName;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -206,11 +206,14 @@ async fn test_extension_store(cx: &mut TestAppContext) {
ExtensionIndexLanguageEntry { ExtensionIndexLanguageEntry {
extension: "zed-ruby".into(), extension: "zed-ruby".into(),
path: "languages/erb".into(), path: "languages/erb".into(),
grammar: Some("embedded_template".into()), config: LanguageConfig {
hidden: false, grammar: Some("embedded_template".into()),
matcher: LanguageMatcher { hidden: false,
path_suffixes: vec!["erb".into()], matcher: LanguageMatcher {
first_line_pattern: None, path_suffixes: vec!["erb".into()],
first_line_pattern: None,
},
..Default::default()
}, },
}, },
), ),
@ -219,11 +222,14 @@ async fn test_extension_store(cx: &mut TestAppContext) {
ExtensionIndexLanguageEntry { ExtensionIndexLanguageEntry {
extension: "zed-ruby".into(), extension: "zed-ruby".into(),
path: "languages/ruby".into(), path: "languages/ruby".into(),
grammar: Some("ruby".into()), config: LanguageConfig {
hidden: false, grammar: Some("ruby".into()),
matcher: LanguageMatcher { hidden: false,
path_suffixes: vec!["rb".into()], matcher: LanguageMatcher {
first_line_pattern: None, path_suffixes: vec!["rb".into()],
first_line_pattern: None,
},
..Default::default()
}, },
}, },
), ),
@ -290,7 +296,24 @@ async fn test_extension_store(cx: &mut TestAppContext) {
store.read_with(cx, |store, _| { store.read_with(cx, |store, _| {
let index = &store.extension_index; let index = &store.extension_index;
assert_eq!(index.extensions, expected_index.extensions); assert_eq!(index.extensions, expected_index.extensions);
assert_eq!(index.languages, expected_index.languages);
for ((actual_key, actual_language), (expected_key, expected_language)) in
index.languages.iter().zip(expected_index.languages.iter())
{
assert_eq!(actual_key, expected_key);
assert_eq!(
actual_language.config.grammar,
expected_language.config.grammar
);
assert_eq!(
actual_language.config.matcher,
expected_language.config.matcher
);
assert_eq!(
actual_language.config.hidden,
expected_language.config.hidden
);
}
assert_eq!(index.themes, expected_index.themes); assert_eq!(index.themes, expected_index.themes);
assert_eq!( assert_eq!(
@ -377,8 +400,26 @@ async fn test_extension_store(cx: &mut TestAppContext) {
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
store.read_with(cx, |store, _| { store.read_with(cx, |store, _| {
let index = &store.extension_index; let index = &store.extension_index;
for ((actual_key, actual_language), (expected_key, expected_language)) in
index.languages.iter().zip(expected_index.languages.iter())
{
assert_eq!(actual_key, expected_key);
assert_eq!(
actual_language.config.grammar,
expected_language.config.grammar
);
assert_eq!(
actual_language.config.matcher,
expected_language.config.matcher
);
assert_eq!(
actual_language.config.hidden,
expected_language.config.hidden
);
}
assert_eq!(index.extensions, expected_index.extensions); assert_eq!(index.extensions, expected_index.extensions);
assert_eq!(index.languages, expected_index.languages);
assert_eq!(index.themes, expected_index.themes); assert_eq!(index.themes, expected_index.themes);
assert_eq!( assert_eq!(
@ -415,7 +456,34 @@ async fn test_extension_store(cx: &mut TestAppContext) {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
store.read_with(cx, |store, _| { store.read_with(cx, |store, _| {
assert_eq!(store.extension_index, expected_index); assert_eq!(store.extension_index.extensions, expected_index.extensions);
assert_eq!(store.extension_index.themes, expected_index.themes);
assert_eq!(
store.extension_index.icon_themes,
expected_index.icon_themes
);
for ((actual_key, actual_language), (expected_key, expected_language)) in store
.extension_index
.languages
.iter()
.zip(expected_index.languages.iter())
{
assert_eq!(actual_key, expected_key);
assert_eq!(
actual_language.config.grammar,
expected_language.config.grammar
);
assert_eq!(
actual_language.config.matcher,
expected_language.config.matcher
);
assert_eq!(
actual_language.config.hidden,
expected_language.config.hidden
);
}
assert_eq!( assert_eq!(
language_registry.language_names(), language_registry.language_names(),
["ERB", "Plain Text", "Ruby"] ["ERB", "Plain Text", "Ruby"]
@ -452,7 +520,34 @@ async fn test_extension_store(cx: &mut TestAppContext) {
expected_index.languages.remove("ERB"); expected_index.languages.remove("ERB");
store.read_with(cx, |store, _| { store.read_with(cx, |store, _| {
assert_eq!(store.extension_index, expected_index); assert_eq!(store.extension_index.extensions, expected_index.extensions);
assert_eq!(store.extension_index.themes, expected_index.themes);
assert_eq!(
store.extension_index.icon_themes,
expected_index.icon_themes
);
for ((actual_key, actual_language), (expected_key, expected_language)) in store
.extension_index
.languages
.iter()
.zip(expected_index.languages.iter())
{
assert_eq!(actual_key, expected_key);
assert_eq!(
actual_language.config.grammar,
expected_language.config.grammar
);
assert_eq!(
actual_language.config.matcher,
expected_language.config.matcher
);
assert_eq!(
actual_language.config.hidden,
expected_language.config.hidden
);
}
assert_eq!(language_registry.language_names(), ["Plain Text"]); assert_eq!(language_registry.language_names(), ["Plain Text"]);
assert_eq!(language_registry.grammar_names(), []); assert_eq!(language_registry.grammar_names(), []);
}); });

View file

@ -149,10 +149,7 @@ impl HeadlessExtensionStore {
config.grammar = None; config.grammar = None;
this.proxy.register_language( this.proxy.register_language(
config.name.clone(), config.clone(),
None,
config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),

View file

@ -26,7 +26,7 @@ pub use crate::language_settings::EditPredictionsMode;
use crate::language_settings::SoftWrap; use crate::language_settings::SoftWrap;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait; use async_trait::async_trait;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet, IndexSet};
use fs::Fs; use fs::Fs;
use futures::Future; use futures::Future;
use gpui::{App, AsyncApp, Entity, SharedString, Task}; use gpui::{App, AsyncApp, Entity, SharedString, Task};
@ -666,7 +666,7 @@ pub struct CodeLabel {
pub filter_range: Range<usize>, pub filter_range: Range<usize>,
} }
#[derive(Clone, Deserialize, JsonSchema)] #[derive(Clone, Deserialize, JsonSchema, Serialize, Debug)]
pub struct LanguageConfig { pub struct LanguageConfig {
/// Human-readable name of the language. /// Human-readable name of the language.
pub name: LanguageName, pub name: LanguageName,
@ -690,12 +690,20 @@ pub struct LanguageConfig {
pub auto_indent_on_paste: Option<bool>, pub auto_indent_on_paste: Option<bool>,
/// A regex that is used to determine whether the indentation level should be /// A regex that is used to determine whether the indentation level should be
/// increased in the following line. /// increased in the following line.
#[serde(default, deserialize_with = "deserialize_regex")] #[serde(
default,
deserialize_with = "deserialize_regex",
serialize_with = "serialize_regex"
)]
#[schemars(schema_with = "regex_json_schema")] #[schemars(schema_with = "regex_json_schema")]
pub increase_indent_pattern: Option<Regex>, pub increase_indent_pattern: Option<Regex>,
/// A regex that is used to determine whether the indentation level should be /// A regex that is used to determine whether the indentation level should be
/// decreased in the following line. /// decreased in the following line.
#[serde(default, deserialize_with = "deserialize_regex")] #[serde(
default,
deserialize_with = "deserialize_regex",
serialize_with = "serialize_regex"
)]
#[schemars(schema_with = "regex_json_schema")] #[schemars(schema_with = "regex_json_schema")]
pub decrease_indent_pattern: Option<Regex>, pub decrease_indent_pattern: Option<Regex>,
/// A list of characters that trigger the automatic insertion of a closing /// A list of characters that trigger the automatic insertion of a closing
@ -748,6 +756,9 @@ pub struct LanguageConfig {
/// A list of characters that Zed should treat as word characters for completion queries. /// A list of characters that Zed should treat as word characters for completion queries.
#[serde(default)] #[serde(default)]
pub completion_query_characters: HashSet<char>, pub completion_query_characters: HashSet<char>,
/// A list of preferred debuggers for this language.
#[serde(default)]
pub debuggers: IndexSet<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@ -766,7 +777,7 @@ pub struct LanguageMatcher {
} }
/// The configuration for JSX tag auto-closing. /// The configuration for JSX tag auto-closing.
#[derive(Clone, Deserialize, JsonSchema)] #[derive(Clone, Deserialize, JsonSchema, Serialize, Debug)]
pub struct JsxTagAutoCloseConfig { pub struct JsxTagAutoCloseConfig {
/// The name of the node for a opening tag /// The name of the node for a opening tag
pub open_tag_node_name: String, pub open_tag_node_name: String,
@ -807,7 +818,7 @@ pub struct LanguageScope {
override_id: Option<u32>, override_id: Option<u32>,
} }
#[derive(Clone, Deserialize, Default, Debug, JsonSchema)] #[derive(Clone, Deserialize, Default, Debug, JsonSchema, Serialize)]
pub struct LanguageConfigOverride { pub struct LanguageConfigOverride {
#[serde(default)] #[serde(default)]
pub line_comments: Override<Vec<Arc<str>>>, pub line_comments: Override<Vec<Arc<str>>>,
@ -872,6 +883,7 @@ impl Default for LanguageConfig {
hidden: false, hidden: false,
jsx_tag_auto_close: None, jsx_tag_auto_close: None,
completion_query_characters: Default::default(), completion_query_characters: Default::default(),
debuggers: Default::default(),
} }
} }
} }
@ -932,7 +944,7 @@ pub struct FakeLspAdapter {
/// ///
/// This struct includes settings for defining which pairs of characters are considered brackets and /// This struct includes settings for defining which pairs of characters are considered brackets and
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes. /// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
#[derive(Clone, Debug, Default, JsonSchema)] #[derive(Clone, Debug, Default, JsonSchema, Serialize)]
pub struct BracketPairConfig { pub struct BracketPairConfig {
/// A list of character pairs that should be treated as brackets in the context of a given language. /// A list of character pairs that should be treated as brackets in the context of a given language.
pub pairs: Vec<BracketPair>, pub pairs: Vec<BracketPair>,
@ -982,7 +994,7 @@ impl<'de> Deserialize<'de> for BracketPairConfig {
/// Describes a single bracket pair and how an editor should react to e.g. inserting /// Describes a single bracket pair and how an editor should react to e.g. inserting
/// an opening bracket or to a newline character insertion in between `start` and `end` characters. /// an opening bracket or to a newline character insertion in between `start` and `end` characters.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, JsonSchema)] #[derive(Clone, Debug, Default, Deserialize, PartialEq, JsonSchema, Serialize)]
pub struct BracketPair { pub struct BracketPair {
/// Starting substring for a bracket. /// Starting substring for a bracket.
pub start: String, pub start: String,

View file

@ -145,24 +145,24 @@ pub enum BinaryStatus {
#[derive(Clone)] #[derive(Clone)]
pub struct AvailableLanguage { pub struct AvailableLanguage {
id: LanguageId, id: LanguageId,
name: LanguageName, config: LanguageConfig,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
loaded: bool, loaded: bool,
} }
impl AvailableLanguage { impl AvailableLanguage {
pub fn name(&self) -> LanguageName { pub fn name(&self) -> LanguageName {
self.name.clone() self.config.name.clone()
} }
pub fn matcher(&self) -> &LanguageMatcher { pub fn matcher(&self) -> &LanguageMatcher {
&self.matcher &self.config.matcher
} }
pub fn hidden(&self) -> bool { pub fn hidden(&self) -> bool {
self.hidden self.config.hidden
}
pub fn config(&self) -> &LanguageConfig {
&self.config
} }
} }
@ -326,10 +326,7 @@ impl LanguageRegistry {
#[cfg(any(feature = "test-support", test))] #[cfg(any(feature = "test-support", test))]
pub fn register_test_language(&self, config: LanguageConfig) { pub fn register_test_language(&self, config: LanguageConfig) {
self.register_language( self.register_language(
config.name.clone(), config.clone(),
config.grammar.clone(),
config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),
@ -488,18 +485,14 @@ impl LanguageRegistry {
/// Adds a language to the registry, which can be loaded if needed. /// Adds a language to the registry, which can be loaded if needed.
pub fn register_language( pub fn register_language(
&self, &self,
name: LanguageName, config: LanguageConfig,
grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) { ) {
let state = &mut *self.state.write(); let state = &mut *self.state.write();
for existing_language in &mut state.available_languages { for existing_language in &mut state.available_languages {
if existing_language.name == name { if existing_language.config.name == config.name {
existing_language.grammar = grammar_name; existing_language.config = config;
existing_language.matcher = matcher;
existing_language.load = load; existing_language.load = load;
return; return;
} }
@ -507,11 +500,8 @@ impl LanguageRegistry {
state.available_languages.push(AvailableLanguage { state.available_languages.push(AvailableLanguage {
id: LanguageId::new(), id: LanguageId::new(),
name, config,
grammar: grammar_name,
matcher,
load, load,
hidden,
loaded: false, loaded: false,
}); });
state.version += 1; state.version += 1;
@ -557,7 +547,7 @@ impl LanguageRegistry {
let mut result = state let mut result = state
.available_languages .available_languages
.iter() .iter()
.filter_map(|l| l.loaded.not().then_some(l.name.to_string())) .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
.chain(state.languages.iter().map(|l| l.config.name.to_string())) .chain(state.languages.iter().map(|l| l.config.name.to_string()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
@ -576,10 +566,7 @@ impl LanguageRegistry {
let mut state = self.state.write(); let mut state = self.state.write();
state.available_languages.push(AvailableLanguage { state.available_languages.push(AvailableLanguage {
id: language.id, id: language.id,
name: language.name(), config: language.config.clone(),
grammar: language.config.grammar.clone(),
matcher: language.config.matcher.clone(),
hidden: language.config.hidden,
load: Arc::new(|| Err(anyhow!("already loaded"))), load: Arc::new(|| Err(anyhow!("already loaded"))),
loaded: true, loaded: true,
}); });
@ -648,7 +635,7 @@ impl LanguageRegistry {
state state
.available_languages .available_languages
.iter() .iter()
.find(|l| l.name.0.as_ref() == name) .find(|l| l.config.name.0.as_ref() == name)
.cloned() .cloned()
} }
@ -765,8 +752,11 @@ impl LanguageRegistry {
let current_match_type = best_language_match let current_match_type = best_language_match
.as_ref() .as_ref()
.map_or(LanguageMatchPrecedence::default(), |(_, score)| *score); .map_or(LanguageMatchPrecedence::default(), |(_, score)| *score);
let language_score = let language_score = callback(
callback(&language.name, &language.matcher, current_match_type); &language.config.name,
&language.config.matcher,
current_match_type,
);
debug_assert!( debug_assert!(
language_score.is_none_or(|new_score| new_score > current_match_type), language_score.is_none_or(|new_score| new_score > current_match_type),
"Matching callback should only return a better match than the current one" "Matching callback should only return a better match than the current one"
@ -814,7 +804,7 @@ impl LanguageRegistry {
let this = self.clone(); let this = self.clone();
let id = language.id; let id = language.id;
let name = language.name.clone(); let name = language.config.name.clone();
let language_load = language.load.clone(); let language_load = language.load.clone();
self.executor self.executor
@ -1130,7 +1120,7 @@ impl LanguageRegistryState {
self.languages self.languages
.retain(|language| !languages_to_remove.contains(&language.name())); .retain(|language| !languages_to_remove.contains(&language.name()));
self.available_languages self.available_languages
.retain(|language| !languages_to_remove.contains(&language.name)); .retain(|language| !languages_to_remove.contains(&language.config.name));
self.grammars self.grammars
.retain(|name, _| !grammars_to_remove.contains(name)); .retain(|name, _| !grammars_to_remove.contains(name));
self.version += 1; self.version += 1;

View file

@ -153,6 +153,8 @@ pub struct LanguageSettings {
pub show_completion_documentation: bool, pub show_completion_documentation: bool,
/// Completion settings for this language. /// Completion settings for this language.
pub completions: CompletionSettings, pub completions: CompletionSettings,
/// Preferred debuggers for this language.
pub debuggers: Vec<String>,
} }
impl LanguageSettings { impl LanguageSettings {
@ -551,6 +553,10 @@ pub struct LanguageSettingsContent {
pub show_completion_documentation: Option<bool>, pub show_completion_documentation: Option<bool>,
/// Controls how completions are processed for this language. /// Controls how completions are processed for this language.
pub completions: Option<CompletionSettings>, pub completions: Option<CompletionSettings>,
/// Preferred debuggers for this language.
///
/// Default: []
pub debuggers: Option<Vec<String>>,
} }
/// The behavior of `editor::Rewrap`. /// The behavior of `editor::Rewrap`.

View file

@ -47,7 +47,4 @@ pub trait ContextProvider: Send + Sync {
fn lsp_task_source(&self) -> Option<LanguageServerName> { fn lsp_task_source(&self) -> Option<LanguageServerName> {
None None
} }
/// Default debug adapter for a given language.
fn debug_adapter(&self) -> Option<String>;
} }

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use extension::{ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy}; use extension::{ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy};
use language::{LanguageMatcher, LanguageName, LanguageRegistry, LoadedLanguage}; use language::{LanguageConfig, LanguageName, LanguageRegistry, LoadedLanguage};
pub fn init( pub fn init(
extension_host_proxy: Arc<ExtensionHostProxy>, extension_host_proxy: Arc<ExtensionHostProxy>,
@ -31,14 +31,10 @@ impl ExtensionGrammarProxy for LanguageServerRegistryProxy {
impl ExtensionLanguageProxy for LanguageServerRegistryProxy { impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
fn register_language( fn register_language(
&self, &self,
language: LanguageName, language: LanguageConfig,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) { ) {
self.language_registry self.language_registry.register_language(language, load);
.register_language(language, grammar, matcher, hidden, load);
} }
fn remove_languages( fn remove_languages(

View file

@ -11,3 +11,4 @@ brackets = [
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
] ]
debuggers = ["CodeLLDB", "GDB"]

View file

@ -11,3 +11,4 @@ brackets = [
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
] ]
debuggers = ["CodeLLDB", "GDB"]

View file

@ -630,10 +630,6 @@ impl ContextProvider for GoContextProvider {
}, },
])) ]))
} }
fn debug_adapter(&self) -> Option<String> {
Some("Delve".into())
}
} }
fn extract_subtest_name(input: &str) -> Option<String> { fn extract_subtest_name(input: &str) -> Option<String> {

View file

@ -14,3 +14,4 @@ brackets = [
] ]
tab_size = 4 tab_size = 4
hard_tabs = true hard_tabs = true
debuggers = ["Delve"]

View file

@ -19,6 +19,7 @@ word_characters = ["$", "#"]
tab_size = 2 tab_size = 2
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"] scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
prettier_parser_name = "babel" prettier_parser_name = "babel"
debuggers = ["JavaScript"]
[jsx_tag_auto_close] [jsx_tag_auto_close]
open_tag_node_name = "jsx_opening_element" open_tag_node_name = "jsx_opening_element"

View file

@ -325,10 +325,7 @@ fn register_language(
languages.register_lsp_adapter(config.name.clone(), adapter); languages.register_lsp_adapter(config.name.clone(), adapter);
} }
languages.register_language( languages.register_language(
config.name.clone(), config.clone(),
config.grammar.clone(),
config.matcher.clone(),
config.hidden,
Arc::new(move || { Arc::new(move || {
Ok(LoadedLanguage { Ok(LoadedLanguage {
config: config.clone(), config: config.clone(),

View file

@ -474,10 +474,6 @@ impl ContextProvider for PythonContextProvider {
Some(TaskTemplates(tasks)) Some(TaskTemplates(tasks))
} }
fn debug_adapter(&self) -> Option<String> {
Some("Debugpy".into())
}
} }
fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner { fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {

View file

@ -29,3 +29,4 @@ brackets = [
auto_indent_using_last_non_empty_line = false auto_indent_using_last_non_empty_line = false
increase_indent_pattern = "^[^#].*:\\s*$" increase_indent_pattern = "^[^#].*:\\s*$"
decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:" decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"
debuggers = ["Debugpy"]

View file

@ -803,10 +803,6 @@ impl ContextProvider for RustContextProvider {
fn lsp_task_source(&self) -> Option<LanguageServerName> { fn lsp_task_source(&self) -> Option<LanguageServerName> {
Some(SERVER_NAME) Some(SERVER_NAME)
} }
fn debug_adapter(&self) -> Option<String> {
Some("CodeLLDB".to_owned())
}
} }
/// Part of the data structure of Cargo metadata /// Part of the data structure of Cargo metadata

View file

@ -15,3 +15,4 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
] ]
collapsed_placeholder = " /* ... */ " collapsed_placeholder = " /* ... */ "
debuggers = ["CodeLLDB", "GDB"]

View file

@ -17,6 +17,7 @@ word_characters = ["#", "$"]
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"] scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
prettier_parser_name = "typescript" prettier_parser_name = "typescript"
tab_size = 2 tab_size = 2
debuggers = ["JavaScript"]
[jsx_tag_auto_close] [jsx_tag_auto_close]
open_tag_node_name = "jsx_opening_element" open_tag_node_name = "jsx_opening_element"

View file

@ -17,6 +17,7 @@ brackets = [
word_characters = ["#", "$"] word_characters = ["#", "$"]
prettier_parser_name = "typescript" prettier_parser_name = "typescript"
tab_size = 2 tab_size = 2
debuggers = ["JavaScript"]
[overrides.string] [overrides.string]
completion_query_characters = ["."] completion_query_characters = ["."]

View file

@ -808,10 +808,6 @@ impl ContextProvider for BasicContextProvider {
Task::ready(Ok(task_variables)) Task::ready(Ok(task_variables))
} }
fn debug_adapter(&self) -> Option<String> {
None
}
} }
/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks. /// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
@ -835,10 +831,6 @@ impl ContextProvider for ContextProviderWithTasks {
) -> Option<TaskTemplates> { ) -> Option<TaskTemplates> {
Some(self.templates.clone()) Some(self.templates.clone())
} }
fn debug_adapter(&self) -> Option<String> {
None
}
} }
#[cfg(test)] #[cfg(test)]