diff --git a/crates/dap/src/registry.rs b/crates/dap/src/registry.rs index 30b6a94874..2786de227e 100644 --- a/crates/dap/src/registry.rs +++ b/crates/dap/src/registry.rs @@ -50,20 +50,23 @@ impl DapRegistry { let name = adapter.name(); let _previous_value = self.0.write().adapters.insert(name, adapter); } + pub fn add_locator(&self, locator: Arc) { + self.0.write().locators.insert(locator.name(), locator); + } + + pub fn remove_adapter(&self, name: &str) { + self.0.write().adapters.remove(name); + } + + pub fn remove_locator(&self, locator: &str) { + self.0.write().locators.remove(locator); + } pub fn adapter_language(&self, adapter_name: &str) -> Option { self.adapter(adapter_name) .and_then(|adapter| adapter.adapter_language_name()) } - pub fn add_locator(&self, locator: Arc) { - let _previous_value = self.0.write().locators.insert(locator.name(), locator); - debug_assert!( - _previous_value.is_none(), - "Attempted to insert a new debug locator when one is already registered" - ); - } - pub async fn adapters_schema(&self) -> task::AdapterSchemas { let mut schemas = AdapterSchemas(vec![]); diff --git a/crates/debug_adapter_extension/src/debug_adapter_extension.rs b/crates/debug_adapter_extension/src/debug_adapter_extension.rs index 22ba9175b2..2f8e3dc50e 100644 --- a/crates/debug_adapter_extension/src/debug_adapter_extension.rs +++ b/crates/debug_adapter_extension/src/debug_adapter_extension.rs @@ -1,7 +1,7 @@ mod extension_dap_adapter; mod extension_locator_adapter; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use dap::DapRegistry; use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy}; @@ -34,8 +34,11 @@ impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy { &self, extension: Arc, debug_adapter_name: Arc, + schema_path: &Path, ) { - if let Some(adapter) = ExtensionDapAdapter::new(extension, debug_adapter_name).log_err() { + if let Some(adapter) = + ExtensionDapAdapter::new(extension, debug_adapter_name, schema_path).log_err() + { self.debug_adapter_registry.add_adapter(Arc::new(adapter)); } } @@ -51,4 +54,13 @@ impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy { locator_name, ))); } + + fn unregister_debug_adapter(&self, debug_adapter_name: Arc) { + self.debug_adapter_registry + .remove_adapter(&debug_adapter_name); + } + + fn unregister_debug_locator(&self, locator_name: Arc) { + self.debug_adapter_registry.remove_locator(&locator_name); + } } diff --git a/crates/debug_adapter_extension/src/extension_dap_adapter.rs b/crates/debug_adapter_extension/src/extension_dap_adapter.rs index e02ce3ecdf..26b9f2e8ad 100644 --- a/crates/debug_adapter_extension/src/extension_dap_adapter.rs +++ b/crates/debug_adapter_extension/src/extension_dap_adapter.rs @@ -26,11 +26,13 @@ impl ExtensionDapAdapter { pub(crate) fn new( extension: Arc, debug_adapter_name: Arc, + schema_path: &Path, ) -> Result { - let schema = std::fs::read_to_string(extension.path_from_extension( - &Path::new("debug_adapter_schemas").join(debug_adapter_name.as_ref()), - )) - .with_context(|| format!("Failed to read debug adapter schema for {debug_adapter_name}"))?; + let schema = std::fs::read_to_string(&schema_path).with_context(|| { + format!( + "Failed to read debug adapter schema for {debug_adapter_name} (from path: `{schema_path:?}`)" + ) + })?; let schema = serde_json::Value::from_str(&schema).with_context(|| { format!("Debug adapter schema for {debug_adapter_name} is not a valid JSON") })?; diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 76ab5492a7..7a3897eea7 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -1,7 +1,7 @@ use crate::{ ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version, }; -use anyhow::{Context as _, Result, bail, ensure}; +use anyhow::{Context as _, Result, bail}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use futures::io::BufReader; @@ -98,21 +98,20 @@ impl ExtensionBuilder { log::info!("compiled Rust extension {}", extension_dir.display()); } - let debug_adapters_dir = extension_dir.join("debug_adapter_schemas"); - if !extension_manifest.debug_adapters.is_empty() { - ensure!( - debug_adapters_dir.exists(), - "Expected debug adapter schemas directory to exist" - ); - } - for debug_adapter_name in &extension_manifest.debug_adapters { - let debug_adapter_schema_path = debug_adapters_dir.join(debug_adapter_name.as_ref()); + for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters { + let debug_adapter_relative_schema_path = + meta.schema_path.clone().unwrap_or_else(|| { + Path::new("debug_adapter_schemas") + .join(Path::new(debug_adapter_name.as_ref()).with_extension("json")) + }); + let debug_adapter_schema_path = extension_dir.join(debug_adapter_relative_schema_path); + let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path) .with_context(|| { - format!("failed to read debug adapter schema for `{debug_adapter_name}`") + format!("failed to read debug adapter schema for `{debug_adapter_name}` from `{debug_adapter_schema_path:?}`") })?; _ = serde_json::Value::from_str(&debug_adapter_schema).with_context(|| { - format!("Debug adapter schema for `{debug_adapter_name}` is not a valid JSON") + format!("Debug adapter schema for `{debug_adapter_name}` (path: `{debug_adapter_schema_path:?}`) is not a valid JSON") })?; } for (grammar_name, grammar_metadata) in &extension_manifest.grammars { diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index a288da3263..f6ae9090fc 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Result; @@ -411,17 +411,29 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { } pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static { - fn register_debug_adapter(&self, extension: Arc, debug_adapter_name: Arc); + fn register_debug_adapter( + &self, + extension: Arc, + debug_adapter_name: Arc, + schema_path: &Path, + ); fn register_debug_locator(&self, extension: Arc, locator_name: Arc); + fn unregister_debug_adapter(&self, debug_adapter_name: Arc); + fn unregister_debug_locator(&self, locator_name: Arc); } impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy { - fn register_debug_adapter(&self, extension: Arc, debug_adapter_name: Arc) { + fn register_debug_adapter( + &self, + extension: Arc, + debug_adapter_name: Arc, + schema_path: &Path, + ) { let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { return; }; - proxy.register_debug_adapter(extension, debug_adapter_name) + proxy.register_debug_adapter(extension, debug_adapter_name, schema_path) } fn register_debug_locator(&self, extension: Arc, locator_name: Arc) { @@ -431,4 +443,18 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy { proxy.register_debug_locator(extension, locator_name) } + fn unregister_debug_adapter(&self, debug_adapter_name: Arc) { + let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { + return; + }; + + proxy.unregister_debug_adapter(debug_adapter_name) + } + fn unregister_debug_locator(&self, locator_name: Arc) { + let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { + return; + }; + + proxy.unregister_debug_locator(locator_name) + } } diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index d7e8c149c3..3c20fb4e56 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -88,9 +88,9 @@ pub struct ExtensionManifest { #[serde(default)] pub capabilities: Vec, #[serde(default)] - pub debug_adapters: Vec>, + pub debug_adapters: BTreeMap, DebugAdapterManifestEntry>, #[serde(default)] - pub debug_locators: Vec>, + pub debug_locators: BTreeMap, DebugLocatorManifestEntry>, } impl ExtensionManifest { @@ -210,6 +210,14 @@ pub struct SlashCommandManifestEntry { #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct IndexedDocsProviderEntry {} +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct DebugAdapterManifestEntry { + pub schema_path: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +pub struct DebugLocatorManifestEntry {} + impl ExtensionManifest { pub async fn load(fs: Arc, extension_dir: &Path) -> Result { let extension_name = extension_dir @@ -278,8 +286,8 @@ fn manifest_from_old_manifest( indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: Vec::new(), - debug_adapters: vec![], - debug_locators: vec![], + debug_adapters: Default::default(), + debug_locators: Default::default(), } } diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index df5d734c2a..aacc5d8795 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -20,9 +20,10 @@ pub use wit::{ make_file_executable, zed::extension::context_server::ContextServerConfiguration, zed::extension::dap::{ + AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, BuildTaskTemplate, DebugAdapterBinary, DebugConfig, DebugRequest, DebugScenario, DebugTaskDefinition, - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TaskTemplate, - TcpArguments, TcpArgumentsTemplate, resolve_tcp_template, + LaunchRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, + TaskTemplate, TcpArguments, TcpArgumentsTemplate, resolve_tcp_template, }, zed::extension::github::{ GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name, @@ -198,12 +199,15 @@ pub trait Extension: Send + Sync { &mut self, _adapter_name: String, _config: DebugTaskDefinition, - _user_provided_path: Option, + _user_provided_debug_adapter_path: Option, _worktree: &Worktree, ) -> Result { Err("`get_dap_binary` not implemented".to_string()) } + /// Determines whether the specified adapter configuration should *launch* a new debuggee process + /// or *attach* to an existing one. This function should not perform any further validation (outside of determining the kind of a request). + /// This function should return an error when the kind cannot be determined (rather than fall back to a known default). fn dap_request_kind( &mut self, _adapter_name: String, @@ -211,12 +215,31 @@ pub trait Extension: Send + Sync { ) -> Result { Err("`dap_request_kind` not implemented".to_string()) } - fn dap_config_to_scenario( - &mut self, - _adapter_name: DebugConfig, - ) -> Result { + /// Converts a high-level definition of a debug scenario (originating in a new session UI) to a "low-level" configuration suitable for a particular adapter. + /// + /// In layman's terms: given a program, list of arguments, current working directory and environment variables, + /// create a configuration that can be used to start a debug session. + fn dap_config_to_scenario(&mut self, _config: DebugConfig) -> Result { Err("`dap_config_to_scenario` not implemented".to_string()) } + + /// Locators are entities that convert a Zed task into a debug scenario. + /// + /// They can be provided even by extensions that don't provide a debug adapter. + /// For all tasks applicable to a given buffer, Zed will query all locators to find one that can turn the task into a debug scenario. + /// A converted debug scenario can include a build task (it shouldn't contain any configuration in such case); a build task result will later + /// be resolved with [`Extension::run_dap_locator`]. + /// + /// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator: + /// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with + /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope. + /// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function + /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user + /// found the artifact path by themselves. + /// + /// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of + /// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case. + /// This might be of particular relevance to interpreted languages. fn dap_locator_create_scenario( &mut self, _locator_name: String, @@ -226,6 +249,9 @@ pub trait Extension: Send + Sync { ) -> Option { None } + + /// Runs the second phase of locator resolution. + /// See [`Extension::dap_locator_create_scenario`] for a hefty comment on locators. fn run_dap_locator( &mut self, _locator_name: String, diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index f6ea9e6e7f..3256fff280 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1149,6 +1149,12 @@ impl ExtensionStore { for (server_id, _) in extension.manifest.context_servers.iter() { self.proxy.unregister_context_server(server_id.clone(), cx); } + for (adapter, _) in extension.manifest.debug_adapters.iter() { + self.proxy.unregister_debug_adapter(adapter.clone()); + } + for (locator, _) in extension.manifest.debug_locators.iter() { + self.proxy.unregister_debug_locator(locator.clone()); + } } self.wasm_extensions @@ -1344,12 +1350,24 @@ impl ExtensionStore { .register_indexed_docs_provider(extension.clone(), provider_id.clone()); } - for debug_adapter in &manifest.debug_adapters { - this.proxy - .register_debug_adapter(extension.clone(), debug_adapter.clone()); + for (debug_adapter, meta) in &manifest.debug_adapters { + let mut path = root_dir.clone(); + path.push(Path::new(manifest.id.as_ref())); + if let Some(schema_path) = &meta.schema_path { + path.push(schema_path); + } else { + path.push("debug_adapter_schemas"); + path.push(Path::new(debug_adapter.as_ref()).with_extension("json")); + } + + this.proxy.register_debug_adapter( + extension.clone(), + debug_adapter.clone(), + &path, + ); } - for debug_adapter in &manifest.debug_adapters { + for debug_adapter in manifest.debug_locators.keys() { this.proxy .register_debug_locator(extension.clone(), debug_adapter.clone()); } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 72210a498e..b13bc5558a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -63,6 +63,7 @@ - [Installing Extensions](./extensions/installing-extensions.md) - [Developing Extensions](./extensions/developing-extensions.md) - [Language Extensions](./extensions/languages.md) +- [Debugger Extensions](./extensions/debugger-extensions.md) - [Theme Extensions](./extensions/themes.md) - [Icon Theme Extensions](./extensions/icon-themes.md) - [Slash Command Extensions](./extensions/slash-commands.md) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index 9ce87ae445..776b903c8a 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -29,6 +29,8 @@ Zed supports a variety of debug adapters for different programming languages out These adapters enable Zed to provide a consistent debugging experience across multiple languages while leveraging the specific features and capabilities of each debugger. +> Is your desired debugger not listed? You can contribute by adding support for your favorite language or debugger. Check out our [debugger extensions](extensions/debugger-extensions.md) documentation for more information. + ## Getting Started For basic debugging, you can set up a new configuration by opening the `New Session Modal` either via the `debugger: start` (default: f4) or by clicking the plus icon at the top right of the debug panel. diff --git a/docs/src/extensions.md b/docs/src/extensions.md index fa40eedbee..5378222e56 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -5,6 +5,7 @@ Zed lets you add new functionality using user-defined extensions. - [Installing Extensions](./extensions/installing-extensions.md) - [Developing Extensions](./extensions/developing-extensions.md) - [Developing Language Extensions](./extensions/languages.md) + - [Developing Debugger Extensions](./extensions/debugger-extensions.md) - [Developing Themes](./extensions/themes.md) - [Developing Icon Themes](./extensions/icon-themes.md) - [Developing Slash Commands](./extensions/slash-commands.md) diff --git a/docs/src/extensions/debugger-extensions.md b/docs/src/extensions/debugger-extensions.md new file mode 100644 index 0000000000..3c9e291058 --- /dev/null +++ b/docs/src/extensions/debugger-extensions.md @@ -0,0 +1,117 @@ +# Debugger Extensions + +[Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol) Servers can be exposed as extensions for use in the [debugger](../debugger.md). + +## Defining Debugger Extensions + +A given extension may provide one or more DAP servers. +Each DAP server must be registered in the `extension.toml`: + +```toml +[debug-adapters.my-debug-adapter] +# Optional relative path to the JSON schema for the debug adapter configuration schema. Defaults to `debug_adapter_schemas/$DEBUG_ADAPTER_NAME_ID.json`. +# Note that while this field is optional, a schema is mandatory. +schema_path = "relative/path/to/schema.json" +``` + +Then, in the Rust code for your extension, implement the `get_dap_binary` method on your extension: + +```rust +impl zed::Extension for MyExtension { + fn get_dap_binary( + &mut self, + adapter_name: String, + config: DebugTaskDefinition, + user_provided_debug_adapter_path: Option, + worktree: &Worktree, + ) -> Result; +} +``` + +This method should return the command to start up a debug adapter protocol server, along with any arguments or environment variables necessary for it to function. + +If you need to download the DAP server from an external source—like GitHub Releases or npm—you can also do that in this function. Make sure to check for updates only periodically, as this function is called whenever a user spawns a new debug session with your debug adapter. + +You must also implement `dap_request_kind`. This function is used to determine whether a given debug scenario will _launch_ a new debuggee or _attach_ to an existing one. +We also use it to determine that a given debug scenario requires running a _locator_. + +```rust +impl zed::Extension for MyExtension { + fn dap_request_kind( + &mut self, + _adapter_name: String, + _config: Value, + ) -> Result; +} +``` + +These two functions are sufficient to expose your debug adapter in `debug.json`-based user workflows, but you should strongly consider implementing `dap_config_to_scenario` as well. + +```rust +impl zed::Extension for MyExtension { + fn dap_config_to_scenario( + &mut self, + _adapter_name: DebugConfig, + ) -> Result; +} +``` + +`dap_config_to_scenario` is used when the user spawns a session via new session modal UI. At a high level, it takes a generic debug configuration (that isn't specific to any +debug adapter) and tries to turn it into a concrete debug scenario for your adapter. +Put another way, it is supposed to answer the question: "Given a program, a list of arguments, current working directory and environment variables, what would the configuration for spawning this debug adapter look like?". + +## Defining Debug Locators + +Zed offers an automatic way to create debug scenarios with _debug locators_. +A locator locates the debug target and figures out how to spawn a debug session for it. Thanks to locators, we can automatically convert existing user tasks (e.g. `cargo run`) and convert them into debug scenarios (e.g. `cargo build` followed by spawning a debugger with `target/debug/my_program` as the program to debug). + +> Your extension can define it's own debug locators even if it does not expose a debug adapter. We strongly recommend doing so when your extension already exposes language tasks, as it allows users to spawn a debug session without having to manually configure the debug adapter. + +Locators can (but don't have to) be agnostic to the debug adapter they are used with. They are simply responsible for locating the debug target and figuring out how to spawn a debug session for it. This allows for a more flexible and extensible debugging experience. + +Your extension can define one or more debug locators. Each debug locator must be registered in the `extension.toml`: + +```toml +[debug-locators.my-debug-locator] +``` + +Locators have two components. +First, each locator is ran on each available task to figure out if any of the available locators can provide a debug scenario for a given task. This is done by calling `dap_locator_create_scenario`. + +```rust +impl zed::Extension for MyExtension { + fn dap_locator_create_scenario( + &mut self, + _locator_name: String, + _build_task: TaskTemplate, + _resolved_label: String, + _debug_adapter_name: String, + ) -> Option; +} +``` + +This function should return `Some` debug scenario when that scenario defines a debugging counterpart to a given user task. +Note that a `DebugScenario` can include a [build task](../debugger.md#build-tasks). If there is one, we will execute `run_dap_locator` after a build task is finished successfully. + +```rust +impl zed::Extension for MyExtension { + fn run_dap_locator( + &mut self, + _locator_name: String, + _build_task: TaskTemplate, + ) -> Result; +} +``` + +`run_dap_locator` is useful in case you cannot determine a build target deterministically. Some build systems may produce artifacts whose names are not known up-front. +Note however that you do _not_ need to go through a 2-phase resolution; if you can determine the full debug configuration with just `dap_locator_create_scenario`, you can omit `build` property on a returned `DebugScenario`. Please also note that your locator **will be** called with tasks it's unlikely to accept; thus you should take some effort to return `None` early before performing any expensive operations. + +## Available Extensions + +Check out all the DAP servers that have already been exposed as extensions [on Zed's site](https://zed.dev/extensions?filter=debug-adapters). + +We recommend taking a look at their repositories as a way to understand how they are generally created and structured. + +## Testing + +To test your new Debug Adapter Protocol server extension, you can [install it as a dev extension](./developing-extensions.md#developing-an-extension-locally). diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index e701956bc7..e18174e95d 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -5,6 +5,7 @@ Extensions can add the following capabilities to Zed: - [Languages](./languages.md) +- [Debuggers](./debugger-extensions.md) - [Themes](./themes.md) - [Icon Themes](./icon-themes.md) - [Slash Commands](./slash-commands.md) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index d090e4a1c7..2e37c5ad2c 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -27,6 +27,7 @@ line_comments = ["# "] - `tab_size` defines the indentation/tab size used for this language (default is `4`). - `hard_tabs` whether to indent with tabs (`true`) or spaces (`false`, the default). - `first_line_pattern` is a regular expression, that in addition to `path_suffixes` (above) or `file_types` in settings can be used to match files which should use this language. For example Zed uses this to identify Shell Scripts by matching the [shebangs lines](https://github.com/zed-industries/zed/blob/main/crates/languages/src/bash/config.toml) in the first line of a script. +- `debuggers` is an array of strings that are used to identify debuggers in the language. When launching a debugger's `New Process Modal`, Zed will order available debuggers by the order of entries in this array.