Compare commits

...

25 Commits

Author SHA1 Message Date
Cole Miller
7c9e1a3669 debugger: Fix environment variables not being substituted in debug tasks (#31198)
Release Notes:

- Debugger Beta: Fixed a bug where environment variables were not
substituted in debug tasks in some cases.

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-22 15:15:20 -04:00
Marshall Bowers
2eebf7d437 language_models: Update default/recommended Anthropic models to Claude Sonnet 4 (#31209)
This PR updates the default/recommended models for the Anthropic and Zed
providers to be Claude Sonnet 4.

Release Notes:

- Updated default/recommended Anthropic models to Claude Sonnet 4.
2025-05-22 15:14:56 -04:00
Marshall Bowers
2b58f16ef2 language_model: Allow Max Mode for Claude 4 models (#31207)
This PR adds the Claude 4 models to the list of models that support Max
Mode.

Release Notes:

- Added Max Mode support for Claude 4 models.
2025-05-22 15:02:54 -04:00
Umesh Yadav
07e412586e mistral: Add DevstralSmallLatest model to Mistral and Ollama (#31099)
Mistral just released a sota coding model:
https://mistral.ai/news/devstral

This PR adds support for it in both ollama and mistral

Release Notes:

- Add DevstralSmallLatest model to Mistral and Ollama
2025-05-22 14:27:01 -04:00
Marshall Bowers
4f539773fb anthropic: Add support for Claude 4 (#31203)
This PR adds support for [Claude
4](https://www.anthropic.com/news/claude-4).

Release Notes:

- Added support for Claude Opus 4 and Claude Sonnet 4.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-05-22 14:11:32 -04:00
Anthony Eid
38f8e5f66b debugger beta: Move path resolution to resolve scenario instead of just in new session modal (#31185)
This move was done so debug configs could use path resolution, and
saving a configuration from the new session modal wouldn't resolve paths
beforehand.

I also added an integration test to make sure path resolution happens
from an arbitrary config. The test was placed under the new session
modal directory because it has to do with starting a session, and that's
what the new session modal typically does, even if it's implicitly used
in the test.

In the future, I plan to add more tests to the new session modal too.

Release Notes:

- debugger beta: Allow configs from debug.json to resolve paths
2025-05-22 14:11:11 -04:00
smit
c5a27c81bc editor: Fix block comment incorrectly continues to next line in some cases (#31204)
Closes #31138

Fix edge case where adding newline if there is text afterwards end
delimiter of multiline comment, would continue the comment prefix. This
is fixed by checking for end delimiter on whole line instead of just
assuming it would always be at end.

- [x] Tests

Release Notes:

- Fixed the issue where in some cases the block comment continues to the
next line even though the comment block is already closed.
2025-05-22 23:32:33 +05:30
Piotr Osiewicz
9a4e44b41f debugger: Always focus the active session whenever it is stopped (#31182)
Closes #ISSUE

Release Notes:

- debugger: Fixed child debug sessions taking precedence over the
parents when spawned.
2025-05-22 11:43:55 -04:00
Piotr Osiewicz
5f82e2c9a3 debugger: Use integrated terminal for Python (#31190)
Closes #ISSUE

Release Notes:

- debugger: Use integrated terminal for Python, allowing one to interact
with standard input/output when debugging Python projects.
2025-05-22 10:38:58 -04:00
Anthony Eid
b126f2e868 debugger beta: Update debugger docs for beta (#31192)
The docs include basic information on starting a session but will need
to be further iterated upon once we get deeper into the beta

Release Notes:

- N/A
2025-05-22 10:38:54 -04:00
smit
866eb9a84c editor: Fix regression causing incorrect delimiter on newline in case of multiple comment prefixes (#31129)
Closes #31115

This fixes regression caused by
https://github.com/zed-industries/zed/pull/30824 while keeping that fix.

- [x] Test

Release Notes:

- Fixed the issue where adding a newline after the `///` comment would
extend it with `//` instead of `///` in Rust and other similar
languages.
2025-05-22 18:23:37 +05:30
Anthony Eid
025993868c debugger beta: Fix panic that could occur when parsing an invalid dap schema (#31175)
Release Notes:

- N/A
2025-05-22 07:27:13 -04:00
Anthony Eid
4837d47c33 debugger beta: Fix dap_schema for DAP extensions (#31173)
We now actually call dap_schema provided by extensions instead of
defaulting to a null `serde_json::Value`. We still need to update the
Json LSP whenever a new dap is installed.

Release Notes:

- N/A
2025-05-22 07:27:02 -04:00
Julia Ryan
10b8bbc478 Handle ~ in debugger launch modal (#31087)
@Anthony-Eid I'm pretty sure this maintains the behavior of #30680, and
I added some tests to be sure.

Release Notes:

- `~` now expands to the home directory in the debugger launch modal.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-22 07:24:02 -04:00
Piotr Osiewicz
b432a144c7 debugger: Add telemetry for new session experience (#31171)
This includes the following data:
- Where we spawned the session from (gutter, scenario list, custom form
filled by the user)
- Which debug adapter was used
- Which dock the debugger is in

Closes #ISSUE

Release Notes:

- debugger: Added telemetry for new session experience that includes
data about:
    - How a session was spawned (gutter, scenario list or custom form)
    - Which debug adapter was used
    - Which dock the debugger is in

---------

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-05-22 07:23:47 -04:00
Remco Smits
6f1fe2d2c0 debugger: Use current worktree directory when spawning an adapter (#31054)
/cc @osiewicz 

I think bringing this back should fix **bloveless** his issue with go
debugger.
This is also nice, so people are not forced to give us a working
directory, because most adapters will use their **cwd** as the project
root directory. For JavaScript, you don't need to specify the **cwd**
anymore because it can already infer it

Release Notes:

- debugger beta: Fixed some adapters fail to determine the right root level of the
debug program.
2025-05-22 07:01:54 -04:00
Anthony Eid
bf72cbecf5 debugger: Use DAP schema to configure daps (#30833)
This PR allows DAPs to define their own schema so users can see
completion items when editing their debug.json files.

Users facing this aren’t the biggest chance, but behind the scenes, this
affected a lot of code because we manually translated common fields from
Zed's config format to be adapter-specific. Now we store the raw JSON
from a user's configuration file and just send that.

I'm ignoring the Protobuf CICD error because the DebugTaskDefinition
message is not yet user facing and we need to deprecate some fields in
it.

Release Notes:

- debugger beta: Show completion items when editing debug.json
- debugger beta: Breaking change, debug.json schema now relays on what
DAP you have selected instead of always having the same based values.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-22 05:51:42 -04:00
Kirill Bulatov
0bd0f8619b Fix unzipping clangd and codelldb on Windows (#31080)
Closes https://github.com/zed-industries/zed/pull/30454

Release Notes:

- N/A
2025-05-22 05:51:37 -04:00
Cole Miller
c5ac8353b3 debugger: Update the default layout (#31057)
- Remove the modules list and loaded sources list from the default
layout
- Move the console to the center pane so it's visible initially

Release Notes:

- Debugger Beta: changed the default layout of the debugger panel,
hiding the modules list and loaded sources list by default and making
the console more prominent.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-22 05:02:16 -04:00
Cole Miller
bd1fe6e600 debugger: Add a couple more keybindings (#31103)
- Add missing handler for `debugger::Continue` so `f5` works
- Add bindings based on VS Code for `debugger::Restart` and
`debug_panel::ToggleFocus`
- Remove breakpoint-related buttons from the debug panel's top strip,
and surface the bindings for `editor::ToggleBreakpoint` in gutter
tooltip instead

Release Notes:

- Debugger Beta: Added keybindings for `debugger::Continue`,
`debugger::Restart`, and `debug_panel::ToggleFocus`.
- Debugger Beta: Removed breakpoint-related buttons from the top of the
debug panel.
- Compatibility note: on Linux, `ctrl-shift-d` is now bound to
`debug_panel::ToggleFocus` by default, instead of
`editor::DuplicateLineDown`.
2025-05-22 05:02:01 -04:00
Cole Miller
a6757d4993 debugger: Add actions and keybindings for opening the thread and session menus (#31135)
Makes it possible to open and navigate these menus from the keyboard.

I also removed the eager previewing behavior for the thread picker,
which was buggy and came with a jarring layout shift.

Release Notes:

- Debugger Beta: Added the `debugger: open thread picker` and `debugger:
open session picker` actions.
2025-05-22 05:01:42 -04:00
Joseph T. Lyons
26930acdad zed 0.188.1 2025-05-21 16:10:47 -04:00
Max Brunsfeld
561710ea0e Meter edit predictions by acceptance in free plan (#30984)
TODO:

- [x] Release  a new version of `zed_llm_client`

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-21 16:08:08 -04:00
Umesh Yadav
a6c0307a06 copilot: Fix rate limit due to Copilot-Vision-Request header (#30989)
Issues: #30994

I've implemented an important optimisation in response to GitHub
Copilot's recent rate limit on concurrent Vision API calls. Previously,
our system was defaulting to vision header: true for all API calls. To
prevent unnecessary calls and adhere to the new limits, I've updated our
logic: the vision header is now only sent if the current message is a
vision message, specifically when the preceding message includes an
image.

Prompt used to reproduce and verify the fix: `Give me a context for my
agent crate about. Browse my repo.`

Release Notes:

- copilot: Set Copilot-Vision-Request header based on message content
2025-05-21 12:52:55 -04:00
Joseph T. Lyons
219c7d5f83 v0.188.x preview 2025-05-21 11:14:17 -04:00
84 changed files with 3432 additions and 1400 deletions

View File

@@ -2,13 +2,13 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"program": "target/debug/zed",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"program": "target/debug/zed",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"initialize_args": {
"stopAtBeginningOfMainSubprogram": true

33
Cargo.lock generated
View File

@@ -3002,6 +3002,7 @@ dependencies = [
"context_server",
"ctor",
"dap",
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
"derive_more",
@@ -4029,6 +4030,7 @@ dependencies = [
"smallvec",
"smol",
"task",
"telemetry",
"util",
"workspace-hack",
]
@@ -4052,10 +4054,12 @@ dependencies = [
"dap",
"futures 0.3.31",
"gpui",
"json_dotpath",
"language",
"paths",
"serde",
"serde_json",
"smol",
"task",
"util",
"workspace-hack",
@@ -4179,6 +4183,8 @@ dependencies = [
"dap",
"extension",
"gpui",
"serde_json",
"task",
"workspace-hack",
]
@@ -4665,6 +4671,7 @@ dependencies = [
"command_palette_hooks",
"convert_case 0.8.0",
"ctor",
"dap",
"db",
"emojis",
"env_logger 0.11.8",
@@ -8535,6 +8542,18 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json_dotpath"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a"
dependencies = [
"serde",
"serde_derive",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "jsonschema"
version = "0.30.0"
@@ -8880,6 +8899,7 @@ dependencies = [
"async-tar",
"async-trait",
"collections",
"dap",
"futures 0.3.31",
"gpui",
"http_client",
@@ -10076,7 +10096,6 @@ dependencies = [
"async-tar",
"async-trait",
"async-watch",
"async_zip",
"futures 0.3.31",
"http_client",
"log",
@@ -10085,9 +10104,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"tempfile",
"util",
"walkdir",
"which 6.0.3",
"workspace-hack",
]
@@ -17029,6 +17046,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-fs",
"async_zip",
"collections",
"dirs 4.0.0",
"dunce",
@@ -17044,12 +17062,14 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
"smol",
"take-until",
"tempfile",
"tendril",
"unicase",
"util_macros",
"walkdir",
"workspace-hack",
]
@@ -19137,6 +19157,7 @@ dependencies = [
"aho-corasick",
"anstream",
"arrayvec",
"async-compression",
"async-std",
"async-tungstenite",
"aws-config",
@@ -19652,7 +19673,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.188.0"
version = "0.188.1"
dependencies = [
"activity_indicator",
"agent",
@@ -19847,9 +19868,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
checksum = "9be71e2f9b271e1eb8eb3e0d986075e770d1a0a299fb036abc3f1fc13a2fa7eb"
dependencies = [
"anyhow",
"serde",

View File

@@ -462,6 +462,7 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -598,7 +599,7 @@ unindent = "0.2.0"
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
walkdir = "2.5"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
@@ -615,7 +616,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.1"
zed_llm_client = "0.8.2"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -33,6 +33,7 @@
"f4": "debugger::Start",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"cmd-f11": "debugger::StepInto",
@@ -558,6 +559,7 @@
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-?": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
@@ -595,7 +597,6 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
@@ -862,6 +863,13 @@
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {

View File

@@ -17,6 +17,7 @@
"f4": "debugger::Start",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"f11": "debugger::StepInto",
@@ -624,6 +625,7 @@
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"ctrl-shift-g": "git_panel::ToggleFocus",
"cmd-shift-d": "debug_panel::ToggleFocus",
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -929,6 +931,13 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,

View File

@@ -34,7 +34,6 @@ pub enum AnthropicModelMode {
pub enum Model {
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[default]
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
@@ -42,6 +41,21 @@ pub enum Model {
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(
rename = "claude-opus-4-thinking",
alias = "claude-opus-4-thinking-latest"
)]
ClaudeOpus4Thinking,
#[default]
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
rename = "claude-sonnet-4-thinking",
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -89,6 +103,14 @@ impl Model {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-sonnet-4-thinking") {
Ok(Self::ClaudeSonnet4Thinking)
} else if id.starts_with("claude-sonnet-4") {
Ok(Self::ClaudeSonnet4)
} else {
anyhow::bail!("invalid model id {id}");
}
@@ -96,6 +118,10 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::ClaudeOpus4 => "claude-opus-4-latest",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
@@ -110,6 +136,8 @@ impl Model {
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
@@ -122,6 +150,10 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Model::ClaudeOpus4 => "Claude 4 Opus",
Model::ClaudeOpus4Thinking => "Claude 4 Opus Thinking",
Model::ClaudeSonnet4 => "Claude 4 Sonnet",
Model::ClaudeSonnet4Thinking => "Claude 4 Sonnet Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -137,7 +169,11 @@ impl Model {
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::Claude3_5Sonnet
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -156,7 +192,11 @@ impl Model {
pub fn max_token_count(&self) -> usize {
match self {
Self::Claude3_5Sonnet
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -173,7 +213,11 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku => 8_192,
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -182,7 +226,11 @@ impl Model {
pub fn default_temperature(&self) -> f32 {
match self {
Self::Claude3_5Sonnet
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku
@@ -201,10 +249,14 @@ impl Model {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::ClaudeOpus4
| Self::ClaudeSonnet4
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
Self::Claude3_7SonnetThinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),

View File

@@ -92,6 +92,7 @@ command_palette_hooks.workspace = true
context_server.workspace = true
ctor.workspace = true
dap = { workspace = true, features = ["test-support"] }
dap_adapters = { workspace = true, features = ["test-support"] }
debugger_ui = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true

View File

@@ -592,9 +592,11 @@ async fn test_remote_server_debugger(
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
dap_adapters::init(cx);
});
server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());

View File

@@ -581,6 +581,15 @@ async fn stream_completion(
api_key: String,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(COPILOT_CHAT_COMPLETION_URL)
@@ -594,7 +603,7 @@ async fn stream_completion(
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", "true");
.header("Copilot-Vision-Request", is_vision_request.to_string());
let is_streaming = request.stream;

View File

@@ -47,6 +47,7 @@ settings.workspace = true
smallvec.workspace = true
smol.workspace = true
task.workspace = true
telemetry.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,5 +1,5 @@
use ::fs::Fs;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
@@ -12,7 +12,7 @@ use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::{self, fs::File};
use smol::fs::File;
use std::{
borrow::Borrow,
ffi::OsStr,
@@ -22,7 +22,8 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
use task::{DebugScenario, TcpArgumentsTemplate, ZedDebugConfig};
use util::archive::extract_zip;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DapStatus {
@@ -130,13 +131,12 @@ impl TcpArguments {
derive(serde::Deserialize, serde::Serialize)
)]
pub struct DebugTaskDefinition {
/// The name of this debug task
pub label: SharedString,
/// The debug adapter to use
pub adapter: DebugAdapterName,
pub request: DebugRequest,
/// Additional initialization arguments to be sent on DAP initialization
pub initialize_args: Option<serde_json::Value>,
/// Whether to tell the debug adapter to stop on entry
pub stop_on_entry: Option<bool>,
/// The configuration to send to the debug adapter
pub config: serde_json::Value,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
@@ -146,86 +146,34 @@ pub struct DebugTaskDefinition {
}
impl DebugTaskDefinition {
pub fn cwd(&self) -> Option<&Path> {
if let DebugRequest::Launch(config) = &self.request {
config.cwd.as_ref().map(Path::new)
} else {
None
}
}
pub fn to_scenario(&self) -> DebugScenario {
DebugScenario {
label: self.label.clone(),
adapter: self.adapter.clone().into(),
build: None,
request: Some(self.request.clone()),
stop_on_entry: self.stop_on_entry,
tcp_connection: self.tcp_connection.clone(),
initialize_args: self.initialize_args.clone(),
config: self.config.clone(),
}
}
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
proto::DebugTaskDefinition {
adapter: self.adapter.to_string(),
request: Some(match &self.request {
DebugRequest::Launch(config) => {
proto::debug_task_definition::Request::DebugLaunchRequest(
proto::DebugLaunchRequest {
program: config.program.clone(),
cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
args: config.args.clone(),
env: config
.env
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
},
)
}
DebugRequest::Attach(attach_request) => {
proto::debug_task_definition::Request::DebugAttachRequest(
proto::DebugAttachRequest {
process_id: attach_request.process_id.unwrap_or_default(),
},
)
}
}),
label: self.label.to_string(),
initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
stop_on_entry: self.stop_on_entry,
label: self.label.clone().into(),
config: self.config.to_string(),
tcp_connection: self.tcp_connection.clone().map(|v| v.to_proto()),
adapter: self.adapter.clone().0.into(),
}
}
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
let request = proto.request.context("request is required")?;
Ok(Self {
label: proto.label.into(),
initialize_args: proto.initialize_args.map(|v| v.into()),
config: serde_json::from_str(&proto.config)?,
tcp_connection: proto
.tcp_connection
.map(TcpArgumentsTemplate::from_proto)
.transpose()?,
stop_on_entry: proto.stop_on_entry,
adapter: DebugAdapterName(proto.adapter.into()),
request: match request {
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
DebugRequest::Attach(AttachRequest {
process_id: Some(config.process_id),
})
}
proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
DebugRequest::Launch(LaunchRequest {
program: config.program,
cwd: config.cwd.map(|cwd| cwd.into()),
args: config.args,
env: Default::default(),
})
}
},
})
}
}
@@ -358,17 +306,13 @@ pub async fn download_adapter_from_github(
}
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
let zip_path = version_path.with_extension("zip");
let mut file = File::create(&zip_path).await?;
futures::io::copy(response.body_mut(), &mut file).await?;
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
util::command::new_smol_command("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_path)
.output()
.await?;
let file = File::open(&zip_path).await?;
extract_zip(&version_path, BufReader::new(file))
.await
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
.ok();
util::fs::remove_matching(&adapter_path, |entry| {
entry
@@ -410,6 +354,8 @@ pub async fn fetch_latest_adapter_version_from_github(
pub trait DebugAdapter: 'static + Send + Sync {
fn name(&self) -> DebugAdapterName;
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario>;
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -422,6 +368,26 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
async fn dap_schema(&self) -> serde_json::Value;
}
#[cfg(any(test, feature = "test-support"))]
@@ -435,29 +401,29 @@ impl FakeAdapter {
Self {}
}
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> StartDebuggingRequestArguments {
use serde_json::json;
use task::DebugRequest;
let obj = task_definition.config.as_object().unwrap();
let request_variant = obj["request"].as_str().unwrap();
let value = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"process_id": if let DebugRequest::Attach(attach_config) = &config.request {
attach_config.process_id
} else {
None
},
"raw_request": serde_json::to_value(config).unwrap()
"request": request_variant,
"process_id": obj.get("process_id"),
"raw_request": serde_json::to_value(task_definition).unwrap()
});
let request = match config.request {
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
};
StartDebuggingRequestArguments {
configuration: value,
request,
request: match request_variant {
"launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
"attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
_ => unreachable!("Wrong fake adapter input for request field"),
},
}
}
}
@@ -469,6 +435,41 @@ impl DebugAdapter for FakeAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
async fn dap_schema(&self) -> serde_json::Value {
serde_json::Value::Null
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let request = config.as_object().unwrap()["request"].as_str().unwrap();
let request = match request {
"launch" => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
"attach" => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
_ => unreachable!("Wrong fake adapter input for request field"),
};
Ok(request)
}
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let config = serde_json::to_value(zed_scenario.request).unwrap();
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config,
tcp_connection: None,
})
}
async fn get_binary(
&self,
_: &Arc<dyn DapDelegate>,
@@ -482,7 +483,7 @@ impl DebugAdapter for FakeAdapter {
connection: None,
envs: HashMap::default(),
cwd: None,
request_args: self.request_args(config),
request_args: self.request_args(&config),
})
}
}

View File

@@ -9,7 +9,11 @@ pub mod transport;
use std::net::Ipv4Addr;
pub use dap_types::*;
use debugger_settings::DebuggerSettings;
use gpui::App;
pub use registry::{DapLocator, DapRegistry};
use serde::Serialize;
use settings::Settings;
pub use task::DebugRequest;
pub type ScopeId = u64;
@@ -18,7 +22,7 @@ pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;
use task::TcpArgumentsTemplate;
use task::{DebugScenario, TcpArgumentsTemplate};
pub async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
@@ -34,3 +38,31 @@ pub async fn configure_tcp_connection(
Ok((host, port, timeout))
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TelemetrySpawnLocation {
Gutter,
ScenarioList,
Custom,
}
pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation, cx: &App) {
let Some(adapter) = cx.global::<DapRegistry>().adapter(&scenario.adapter) else {
return;
};
let kind = adapter
.validate_config(&scenario.config)
.ok()
.map(serde_json::to_value)
.and_then(Result::ok);
let dock = DebuggerSettings::get_global(cx).dock;
telemetry::event!(
"Debugger Session Started",
spawn_location = location,
with_build_task = scenario.build.is_some(),
kind = kind,
adapter = scenario.adapter.as_ref(),
dock_position = dock,
);
}

View File

@@ -4,7 +4,9 @@ use collections::FxHashMap;
use gpui::{App, Global, SharedString};
use language::LanguageName;
use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::{
adapters::{DebugAdapter, DebugAdapterName},
@@ -41,14 +43,7 @@ impl Global for DapRegistry {}
impl DapRegistry {
pub fn global(cx: &mut App) -> &mut Self {
let ret = cx.default_global::<Self>();
#[cfg(any(test, feature = "test-support"))]
if ret.adapter(crate::FakeAdapter::ADAPTER_NAME).is_none() {
ret.add_adapter(Arc::new(crate::FakeAdapter::new()));
}
ret
cx.default_global::<Self>()
}
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
@@ -69,6 +64,22 @@ impl DapRegistry {
);
}
pub async fn adapters_schema(&self) -> task::AdapterSchemas {
let mut schemas = AdapterSchemas(vec![]);
// Clone to avoid holding lock over await points
let adapters = self.0.read().adapters.clone();
for (name, adapter) in adapters.into_iter() {
schemas.0.push(AdapterSchema {
adapter: name.into(),
schema: adapter.dap_schema().await,
});
}
schemas
}
pub fn add_inline_value_provider(
&self,
language: String,

View File

@@ -26,10 +26,12 @@ async-trait.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,11 +1,15 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
use dap::adapters::{DebugTaskDefinition, latest_github_release};
use dap::{
StartDebuggingRequestArgumentsRequest,
adapters::{DebugTaskDefinition, latest_github_release},
};
use futures::StreamExt;
use gpui::AsyncApp;
use task::DebugRequest;
use serde_json::Value;
use task::{DebugRequest, DebugScenario, ZedDebugConfig};
use util::fs::remove_matching;
use crate::*;
@@ -18,45 +22,27 @@ pub(crate) struct CodeLldbDebugAdapter {
impl CodeLldbDebugAdapter {
const ADAPTER_NAME: &'static str = "CodeLLDB";
fn request_args(&self, config: &DebugTaskDefinition) -> dap::StartDebuggingRequestArguments {
let mut configuration = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
// CodeLLDB uses `name` for a terminal label.
map.insert(
"name".into(),
Value::String(String::from(config.label.as_ref())),
);
let request = config.request.to_dap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
let mut configuration = task_definition.config.clone();
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
dap::StartDebuggingRequestArguments {
configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?
.insert(
"name".into(),
Value::String(String::from(task_definition.label.as_ref())),
);
let request = self.validate_config(&configuration)?;
Ok(dap::StartDebuggingRequestArguments {
request,
configuration,
}
})
}
async fn fetch_latest_adapter_version(
@@ -103,6 +89,286 @@ impl DebugAdapter for CodeLldbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config
.as_object()
.ok_or_else(|| anyhow!("Config isn't an object"))?;
let request_variant = map
.get("request")
.and_then(|r| r.as_str())
.ok_or_else(|| anyhow!("request field is required and must be a string"))?;
match request_variant {
"launch" => {
// For launch, verify that one of the required configs exists
if !(map.contains_key("program")
|| map.contains_key("targetCreateCommands")
|| map.contains_key("cargo"))
{
return Err(anyhow!(
"launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
"attach" => {
// For attach, verify that either pid or program exists
if !(map.contains_key("pid") || map.contains_key("program")) {
return Err(anyhow!(
"attach request requires either 'pid' or 'program' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!(
"request must be either 'launch' or 'attach', got '{}'",
request_variant
)),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut configuration = json!({
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = configuration.as_object_mut().unwrap();
// CodeLLDB uses `name` for a terminal label.
map.insert(
"name".into(),
Value::String(String::from(zed_scenario.label.as_ref())),
);
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: configuration,
build: None,
tcp_connection: None,
})
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["attach", "launch"],
"description": "Debug adapter request type"
},
"program": {
"type": "string",
"description": "Path to the program to debug or attach to"
},
"args": {
"type": ["array", "string"],
"description": "Program arguments"
},
"cwd": {
"type": "string",
"description": "Program working directory"
},
"env": {
"type": "object",
"description": "Additional environment variables",
"patternProperties": {
".*": {
"type": "string"
}
}
},
"envFile": {
"type": "string",
"description": "File to read the environment variables from"
},
"stdio": {
"type": ["null", "string", "array", "object"],
"description": "Destination for stdio streams: null = send to debugger console or a terminal, \"<path>\" = attach to a file/tty/fifo"
},
"terminal": {
"type": "string",
"enum": ["integrated", "console"],
"description": "Terminal type to use",
"default": "integrated"
},
"console": {
"type": "string",
"enum": ["integratedTerminal", "internalConsole"],
"description": "Terminal type to use (compatibility alias of 'terminal')"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop debuggee after launch",
"default": false
},
"initCommands": {
"type": "array",
"description": "Initialization commands executed upon debugger startup",
"items": {
"type": "string"
}
},
"targetCreateCommands": {
"type": "array",
"description": "Commands that create the debug target",
"items": {
"type": "string"
}
},
"preRunCommands": {
"type": "array",
"description": "Commands executed just before the program is launched",
"items": {
"type": "string"
}
},
"processCreateCommands": {
"type": "array",
"description": "Commands that create the debuggee process",
"items": {
"type": "string"
}
},
"postRunCommands": {
"type": "array",
"description": "Commands executed just after the program has been launched",
"items": {
"type": "string"
}
},
"preTerminateCommands": {
"type": "array",
"description": "Commands executed just before the debuggee is terminated or disconnected from",
"items": {
"type": "string"
}
},
"exitCommands": {
"type": "array",
"description": "Commands executed at the end of debugging session",
"items": {
"type": "string"
}
},
"expressions": {
"type": "string",
"enum": ["simple", "python", "native"],
"description": "The default evaluator type used for expressions"
},
"sourceMap": {
"type": "object",
"description": "Source path remapping between the build machine and the local machine",
"patternProperties": {
".*": {
"type": ["string", "null"]
}
}
},
"relativePathBase": {
"type": "string",
"description": "Base directory used for resolution of relative source paths. Defaults to the workspace folder"
},
"sourceLanguages": {
"type": "array",
"description": "A list of source languages to enable language-specific features for",
"items": {
"type": "string"
}
},
"reverseDebugging": {
"type": "boolean",
"description": "Enable reverse debugging",
"default": false
},
"breakpointMode": {
"type": "string",
"enum": ["path", "file"],
"description": "Specifies how source breakpoints should be set"
},
"pid": {
"type": ["integer", "string"],
"description": "Process id to attach to"
},
"waitFor": {
"type": "boolean",
"description": "Wait for the process to launch (MacOS only)",
"default": false
}
},
"required": ["request"],
"allOf": [
{
"if": {
"properties": {
"request": {
"enum": ["launch"]
}
}
},
"then": {
"oneOf": [
{
"required": ["program"]
},
{
"required": ["targetCreateCommands"]
},
{
"required": ["cargo"]
}
]
}
},
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"oneOf": [
{
"required": ["pid"]
},
{
"required": ["program"]
}
]
}
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -136,18 +402,46 @@ impl DebugAdapter for CodeLldbDebugAdapter {
};
let adapter_dir = version_path.join("extension").join("adapter");
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
// todo("windows")
#[cfg(not(windows))]
{
use smol::fs;
fs::set_permissions(
&path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
let mut lldb_binaries =
fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
})?;
while let Some(binary) = lldb_binaries.next().await {
let binary_entry = binary?;
let path = binary_entry.path();
fs::set_permissions(
&path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
}
}
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};
Ok(DebugAdapterBinary {
command: command.unwrap(),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: vec![
"--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
],
request_args: self.request_args(config),
request_args: self.request_args(&config)?,
envs: HashMap::default(),
connection: None,
})

View File

@@ -12,7 +12,7 @@ use anyhow::Result;
use async_trait::async_trait;
use codelldb::CodeLldbDebugAdapter;
use dap::{
DapRegistry, DebugRequest,
DapRegistry,
adapters::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
@@ -27,7 +27,8 @@ use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter;
use serde_json::{Value, json};
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
@@ -39,21 +40,13 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GdbDebugAdapter));
#[cfg(any(test, feature = "test-support"))]
{
registry.add_adapter(Arc::from(dap::FakeAdapter {}));
}
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
})
}
trait ToDap {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
}
impl ToDap for DebugRequest {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest {
match self {
Self::Launch(_) => dap::StartDebuggingRequestArgumentsRequest::Launch,
Self::Attach(_) => dap::StartDebuggingRequestArgumentsRequest::Attach,
}
}
}

View File

@@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, bail};
use async_trait::async_trait;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use task::DebugRequest;
use task::{DebugScenario, ZedDebugConfig};
use crate::*;
@@ -13,48 +13,6 @@ pub(crate) struct GdbDebugAdapter;
impl GdbDebugAdapter {
const ADAPTER_NAME: &'static str = "GDB";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("pid".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert(
"stopAtBeginningOfMainSubprogram".into(),
stop_on_entry.into(),
);
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
}
#[async_trait(?Send)]
@@ -63,6 +21,137 @@ impl DebugAdapter for GdbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut obj = serde_json::Map::default();
match &zed_scenario.request {
dap::DebugRequest::Attach(attach) => {
obj.insert("pid".into(), attach.process_id.into());
}
dap::DebugRequest::Launch(launch) => {
obj.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
obj.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
obj.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
obj.insert(
"stopAtBeginningOfMainSubprogram".into(),
stop_on_entry.into(),
);
}
if let Some(cwd) = launch.cwd.as_ref() {
obj.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: serde_json::Value::Object(obj),
tcp_connection: None,
})
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": {
"program": {
"type": "string",
"description": "The program to debug. This corresponds to the GDB 'file' command."
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments passed to the program. These strings are provided as command-line arguments to the inferior.",
"default": []
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program. GDB will change its working directory to this directory."
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program. Each key is the name of an environment variable; each value is the value of that variable."
},
"stopAtBeginningOfMainSubprogram": {
"type": "boolean",
"description": "When true, GDB will set a temporary breakpoint at the program's main procedure, like the 'start' command.",
"default": false
},
"stopOnEntry": {
"type": "boolean",
"description": "When true, GDB will set a temporary breakpoint at the program's first instruction, like the 'starti' command.",
"default": false
}
},
"required": ["program"]
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"pid": {
"type": "number",
"description": "The process ID to which GDB should attach."
},
"program": {
"type": "string",
"description": "The program to debug (optional). This corresponds to the GDB 'file' command. In many cases, GDB can determine which program is running automatically."
},
"target": {
"type": "string",
"description": "The target to which GDB should connect. This is passed to the 'target remote' command."
}
},
"required": ["pid"]
}
]
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -86,13 +175,18 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let request_args = StartDebuggingRequestArguments {
request: self.validate_config(&config.config)?,
configuration: config.config.clone(),
};
Ok(DebugAdapterBinary {
command: gdb_path,
arguments: vec!["-i=dap".into()],
envs: HashMap::default(),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
connection: None,
request_args: self.request_args(config),
request_args,
})
}
}

View File

@@ -1,5 +1,9 @@
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
@@ -11,8 +15,294 @@ pub(crate) struct GoDebugAdapter;
impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "Delve";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = match &config.request {
}
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
}
async fn dap_schema(&self) -> serde_json::Value {
// Create common properties shared between launch and attach
let common_properties = json!({
"debugAdapter": {
"enum": ["legacy", "dlv-dap"],
"description": "Select which debug adapter to use with this configuration.",
"default": "dlv-dap"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch or attach.",
"default": false
},
"showLog": {
"type": "boolean",
"description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
"default": false
},
"cwd": {
"type": "string",
"description": "Workspace relative or absolute path to the working directory of the program being debugged.",
"default": "${ZED_WORKTREE_ROOT}"
},
"dlvFlags": {
"type": "array",
"description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
"items": {
"type": "string"
},
"default": []
},
"port": {
"type": "number",
"description": "Debug server port. For remote configurations, this is where to connect.",
"default": 2345
},
"host": {
"type": "string",
"description": "Debug server host. For remote configurations, this is where to connect.",
"default": "127.0.0.1"
},
"substitutePath": {
"type": "array",
"items": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "The absolute local path to be replaced."
},
"to": {
"type": "string",
"description": "The absolute remote path to replace with."
}
}
},
"description": "Mappings from local to remote paths for debugging.",
"default": []
},
"trace": {
"type": "string",
"enum": ["verbose", "trace", "log", "info", "warn", "error"],
"default": "error",
"description": "Debug logging level."
},
"backend": {
"type": "string",
"enum": ["default", "native", "lldb", "rr"],
"description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
},
"logOutput": {
"type": "string",
"enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
"description": "Components that should produce debug output.",
"default": "debugger"
},
"logDest": {
"type": "string",
"description": "Log destination for delve."
},
"stackTraceDepth": {
"type": "number",
"description": "Maximum depth of stack traces.",
"default": 50
},
"showGlobalVariables": {
"type": "boolean",
"default": false,
"description": "Show global package variables in variables pane."
},
"showRegisters": {
"type": "boolean",
"default": false,
"description": "Show register variables in variables pane."
},
"hideSystemGoroutines": {
"type": "boolean",
"default": false,
"description": "Hide system goroutines from call stack view."
},
"console": {
"default": "internalConsole",
"description": "Where to launch the debugger.",
"enum": ["internalConsole", "integratedTerminal"]
},
"asRoot": {
"default": false,
"description": "Debug with elevated permissions (on Unix).",
"type": "boolean"
}
});
// Create launch-specific properties
let launch_properties = json!({
"program": {
"type": "string",
"description": "Path to the program folder or file to debug.",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments for the program.",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables for the debugged program.",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Path(s) to files with environment variables.",
"default": ""
},
"buildFlags": {
"type": ["string", "array"],
"items": {
"type": "string"
},
"description": "Flags for the Go compiler.",
"default": []
},
"output": {
"type": "string",
"description": "Output path for the binary.",
"default": "debug"
},
"mode": {
"enum": [ "debug", "test", "exec", "replay", "core"],
"description": "Debug mode for launch configuration.",
},
"traceDirPath": {
"type": "string",
"description": "Directory for record trace (for 'replay' mode).",
"default": ""
},
"coreFilePath": {
"type": "string",
"description": "Path to core dump file (for 'core' mode).",
"default": ""
}
});
// Create attach-specific properties
let attach_properties = json!({
"processId": {
"anyOf": [
{
"enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
"description": "Use process picker to select a process."
},
{
"type": "string",
"description": "Process name to attach to."
},
{
"type": "number",
"description": "Process ID to attach to."
}
],
"default": 0
},
"mode": {
"enum": ["local", "remote"],
"description": "Local or remote debugging.",
"default": "local"
},
"remotePath": {
"type": "string",
"description": "Path to source on remote machine.",
"markdownDeprecationMessage": "Use `substitutePath` instead.",
"default": ""
}
});
// Create the final schema
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["program", "mode"],
"properties": launch_properties
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": common_properties
},
{
"type": "object",
"required": ["processId", "mode"],
"properties": attach_properties
}
]
}
]
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => {
json!({
"processId": attach_config.process_id,
@@ -28,31 +318,23 @@ impl GoDebugAdapter {
let map = args.as_object_mut().unwrap();
if let Some(stop_on_entry) = config.stop_on_entry {
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for GoDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: args,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -62,20 +344,23 @@ impl DebugAdapter for GoDebugAdapter {
.and_then(|p| p.to_str().map(|p| p.to_string()))
.context("Dlv not found in path")?;
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
command: delve_path,
arguments: vec!["dap".into(), "--listen".into(), format!("{host}:{port}")],
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
connection: Some(adapters::TcpArguments {
host,
port,
timeout,
}),
request_args: self.request_args(config),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
})
}
}

View File

@@ -1,6 +1,9 @@
use adapters::latest_github_release;
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
@@ -18,43 +21,6 @@ impl JsDebugAdapter {
const ADAPTER_NPM_NAME: &'static str = "vscode-js-debug";
const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"type": "pwa-node",
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
}
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -84,7 +50,7 @@ impl JsDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -102,7 +68,7 @@ impl JsDebugAdapter {
.context("Couldn't find JavaScript dap directory")?
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
@@ -120,14 +86,17 @@ impl JsDebugAdapter {
port.to_string(),
host.to_string(),
],
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
connection: Some(adapters::TcpArguments {
host,
port,
timeout,
}),
request_args: self.request_args(config),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
})
}
}
@@ -138,6 +107,322 @@ impl DebugAdapter for JsDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<dap::StartDebuggingRequestArgumentsRequest> {
match config.get("request") {
Some(val) if val == "launch" => {
if config.get("program").is_none() {
return Err(anyhow!("program is required"));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
Some(val) if val == "attach" => {
if !config.get("processId").is_some_and(|val| val.is_u64()) {
return Err(anyhow!("processId must be a number"));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!("missing or invalid request field in config")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"type": "pwa-node",
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
});
let map = args.as_object_mut().unwrap();
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: args,
tcp_connection: None,
})
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
"program": {
"type": "string",
"description": "Path to the program or file to debug"
},
"cwd": {
"type": "string",
"description": "Absolute path to the working directory of the program being debugged"
},
"args": {
"type": ["array", "string"],
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Environment variables passed to the program",
"default": {}
},
"envFile": {
"type": ["string", "array"],
"description": "Path to a file containing environment variable definitions",
"items": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop program after launch",
"default": false
},
"runtimeExecutable": {
"type": ["string", "null"],
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
"default": "node"
},
"runtimeArgs": {
"type": ["array", "null"],
"description": "Arguments passed to the runtime executable",
"items": {
"type": "string"
},
"default": []
},
"outFiles": {
"type": "array",
"description": "Glob patterns for locating generated JavaScript files",
"items": {
"type": "string"
},
"default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
},
"sourceMaps": {
"type": "boolean",
"description": "Use JavaScript source maps if they exist",
"default": true
},
"sourceMapPathOverrides": {
"type": "object",
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
"default": {}
},
"restart": {
"type": ["boolean", "object"],
"description": "Restart session after Node.js has terminated",
"default": false
},
"trace": {
"type": ["boolean", "object"],
"description": "Enables logging of the Debug Adapter",
"default": false
},
"console": {
"type": "string",
"enum": ["internalConsole", "integratedTerminal"],
"description": "Where to launch the debug target",
"default": "internalConsole"
},
// Browser-specific
"url": {
"type": ["string", "null"],
"description": "Will navigate to this URL and attach to it (browser debugging)"
},
"webRoot": {
"type": "string",
"description": "Workspace absolute path to the webserver root",
"default": "${ZED_WORKTREE_ROOT}"
},
"userDataDir": {
"type": ["string", "boolean"],
"description": "Path to a custom Chrome user profile (browser debugging)",
"default": true
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns for files to skip when debugging",
"items": {
"type": "string"
},
"default": ["<node_internals>/**"]
},
"timeout": {
"type": "number",
"description": "Retry for this number of milliseconds to connect to the debug adapter",
"default": 10000
},
"resolveSourceMapLocations": {
"type": ["array", "null"],
"description": "A list of minimatch patterns for source map resolution",
"items": {
"type": "string"
}
}
},
"required": ["program"]
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
"processId": {
"type": ["string", "number"],
"description": "ID of process to attach to (Node.js debugging)"
},
"port": {
"type": "number",
"description": "Debug port to attach to",
"default": 9229
},
"address": {
"type": "string",
"description": "TCP/IP address of the process to be debugged",
"default": "localhost"
},
"restart": {
"type": ["boolean", "object"],
"description": "Restart session after Node.js has terminated",
"default": false
},
"sourceMaps": {
"type": "boolean",
"description": "Use JavaScript source maps if they exist",
"default": true
},
"sourceMapPathOverrides": {
"type": "object",
"description": "Rewrites the locations of source files from what the sourcemap says to their locations on disk",
"default": {}
},
"outFiles": {
"type": "array",
"description": "Glob patterns for locating generated JavaScript files",
"items": {
"type": "string"
},
"default": ["${ZED_WORKTREE_ROOT}/**/*.js", "!**/node_modules/**"]
},
"url": {
"type": "string",
"description": "Will search for a page with this URL and attach to it (browser debugging)"
},
"webRoot": {
"type": "string",
"description": "Workspace absolute path to the webserver root",
"default": "${ZED_WORKTREE_ROOT}"
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns for files to skip when debugging",
"items": {
"type": "string"
},
"default": ["<node_internals>/**"]
},
"timeout": {
"type": "number",
"description": "Retry for this number of milliseconds to connect to the debug adapter",
"default": 10000
},
"resolveSourceMapLocations": {
"type": ["array", "null"],
"description": "A list of minimatch patterns for source map resolution",
"items": {
"type": "string"
}
},
"remoteRoot": {
"type": ["string", "null"],
"description": "Path to the remote directory containing the program"
},
"localRoot": {
"type": ["string", "null"],
"description": "Path to the local directory containing the program"
}
},
"oneOf": [
{ "required": ["processId"] },
{ "required": ["port"] }
]
}
]
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,

View File

@@ -1,5 +1,8 @@
use adapters::latest_github_release;
use anyhow::Context as _;
use anyhow::bail;
use dap::StartDebuggingRequestArguments;
use dap::StartDebuggingRequestArgumentsRequest;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
@@ -18,27 +21,6 @@ impl PhpDebugAdapter {
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
fn request_args(
&self,
config: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
match &config.request {
dap::DebugRequest::Attach(_) => {
anyhow::bail!("php adapter does not support attaching")
}
dap::DebugRequest::Launch(launch_config) => Ok(dap::StartDebuggingRequestArguments {
configuration: json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
}),
request: config.request.to_dap(),
}),
}
}
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -65,10 +47,17 @@ impl PhpDebugAdapter {
})
}
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -86,7 +75,7 @@ impl PhpDebugAdapter {
.context("Couldn't find PHP dap directory")?
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
Ok(DebugAdapterBinary {
@@ -108,15 +97,204 @@ impl PhpDebugAdapter {
host,
timeout,
}),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: self.request_args(config)?,
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
},
})
}
}
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "The request type for the PHP debug adapter, always \"launch\"",
"default": "launch"
},
"hostname": {
"type": "string",
"description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
},
"port": {
"type": "integer",
"description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
"default": 9003
},
"program": {
"type": "string",
"description": "The PHP script to debug (typically a path to a file)",
"default": "${file}"
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program"
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments to pass to the program"
},
"env": {
"type": "object",
"description": "Environment variables to pass to the program",
"additionalProperties": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Whether to break at the beginning of the script",
"default": false
},
"pathMappings": {
"type": "array",
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
"items": {
"type": "object",
"properties": {
"serverPath": {
"type": "string",
"description": "Path on the server"
},
"localPath": {
"type": "string",
"description": "Corresponding path on the local machine"
}
},
"required": ["serverPath", "localPath"]
}
},
"log": {
"type": "boolean",
"description": "Whether to log all communication between editor and the adapter to the debug console",
"default": false
},
"ignore": {
"type": "array",
"description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
"items": {
"type": "string"
}
},
"ignoreExceptions": {
"type": "array",
"description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
"items": {
"type": "string"
}
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
"items": {
"type": "string"
}
},
"skipEntryPaths": {
"type": "array",
"description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
"items": {
"type": "string"
}
},
"maxConnections": {
"type": "integer",
"description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
"default": 1
},
"proxy": {
"type": "object",
"description": "DBGp Proxy settings",
"properties": {
"enable": {
"type": "boolean",
"description": "To enable proxy registration",
"default": false
},
"host": {
"type": "string",
"description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"description": "The port where the adapter will register with the proxy",
"default": 9001
},
"key": {
"type": "string",
"description": "A unique key that allows the proxy to match requests to your editor",
"default": "vsc"
},
"timeout": {
"type": "integer",
"description": "The number of milliseconds to wait before giving up on the connection to proxy",
"default": 3000
},
"allowMultipleSessions": {
"type": "boolean",
"description": "If the proxy should forward multiple sessions/connections at the same time or not",
"default": true
}
}
},
"xdebugSettings": {
"type": "object",
"description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
"properties": {
"max_children": {
"type": "integer",
"description": "Max number of array or object children to initially retrieve"
},
"max_data": {
"type": "integer",
"description": "Max amount of variable data to initially retrieve"
},
"max_depth": {
"type": "integer",
"description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
},
"show_hidden": {
"type": "integer",
"description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
"enum": [0, 1]
},
"breakpoint_include_return_value": {
"type": "boolean",
"description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
}
}
},
"xdebugCloudToken": {
"type": "string",
"description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
},
"stream": {
"type": "object",
"description": "Allows to influence DBGp streams. Xdebug only supports stdout",
"properties": {
"stdout": {
"type": "integer",
"description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
"enum": [0, 1, 2],
"default": 0
}
}
}
},
"required": ["request", "program"]
})
}
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
@@ -125,10 +303,33 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into())
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let obj = match &zed_scenario.request {
dap::DebugRequest::Attach(_) => {
bail!("Php adapter doesn't support attaching")
}
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
}),
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: obj,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
@@ -145,7 +346,7 @@ impl DebugAdapter for PhpDebugAdapter {
}
}
self.get_installed_binary(delegate, &config, user_installed_path, cx)
self.get_installed_binary(delegate, &task_definition, user_installed_path, cx)
.await
}
}

View File

@@ -1,8 +1,13 @@
use crate::*;
use anyhow::Context as _;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use anyhow::{Context as _, anyhow};
use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::LanguageName;
use serde_json::Value;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@@ -17,39 +22,24 @@ impl PythonDebugAdapter {
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
const LANGUAGE_NAME: &'static str = "Python";
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
let mut args = json!({
"request": match config.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &config.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
fn request_args(
&self,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.validate_config(&task_definition.config)?;
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
let mut configuration = task_definition.config.clone();
if let Ok(console) = configuration.dot_get_mut("console") {
// Use built-in Zed terminal if user did not explicitly provide a setting for console.
if console.is_null() {
*console = Value::String("integratedTerminal".into());
}
}
StartDebuggingRequestArguments {
configuration: args,
request: config.request.to_dap(),
}
Ok(StartDebuggingRequestArguments {
configuration,
request,
})
}
async fn fetch_latest_adapter_version(
&self,
@@ -158,9 +148,9 @@ impl PythonDebugAdapter {
port,
timeout,
}),
cwd: None,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: self.request_args(config),
request_args: self.request_args(config)?,
})
}
}
@@ -175,6 +165,397 @@ impl DebugAdapter for PythonDebugAdapter {
Some(SharedString::new_static("Python").into())
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"request": match zed_scenario.request {
DebugRequest::Launch(_) => "launch",
DebugRequest::Attach(_) => "attach",
},
"subProcess": true,
"redirectOutput": true,
});
let map = args.as_object_mut().unwrap();
match &zed_scenario.request {
DebugRequest::Attach(attach) => {
map.insert("processId".into(), attach.process_id.into());
}
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}
if let Some(cwd) = launch.cwd.as_ref() {
map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
}
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: args,
build: None,
tcp_connection: None,
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request is not valid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["attach", "launch"],
"description": "Debug adapter request type"
},
"autoReload": {
"default": {},
"description": "Configures automatic reload of code on edit.",
"properties": {
"enable": {
"default": false,
"description": "Automatically reload code on edit.",
"type": "boolean"
},
"exclude": {
"default": [
"**/.git/**",
"**/.metadata/**",
"**/__pycache__/**",
"**/node_modules/**",
"**/site-packages/**"
],
"description": "Glob patterns of paths to exclude from auto reload.",
"items": {
"type": "string"
},
"type": "array"
},
"include": {
"default": [
"**/*.py",
"**/*.pyw"
],
"description": "Glob patterns of paths to include in auto reload.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"debugAdapterPath": {
"description": "Path (fully qualified) to the python debug adapter executable.",
"type": "string"
},
"django": {
"default": false,
"description": "Django debugging.",
"type": "boolean"
},
"jinja": {
"default": null,
"description": "Jinja template debugging (e.g. Flask).",
"enum": [
false,
null,
true
]
},
"justMyCode": {
"default": true,
"description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.",
"type": "boolean"
},
"logToFile": {
"default": false,
"description": "Enable logging of debugger events to a log file. This file can be found in the debugpy extension install folder.",
"type": "boolean"
},
"pathMappings": {
"default": [],
"items": {
"label": "Path mapping",
"properties": {
"localRoot": {
"default": "${ZED_WORKTREE_ROOT}",
"label": "Local source root.",
"type": "string"
},
"remoteRoot": {
"default": "",
"label": "Remote source root.",
"type": "string"
}
},
"required": [
"localRoot",
"remoteRoot"
],
"type": "object"
},
"label": "Path mappings.",
"type": "array"
},
"redirectOutput": {
"default": true,
"description": "Redirect output.",
"type": "boolean"
},
"showReturnValue": {
"default": true,
"description": "Show return value of functions when stepping.",
"type": "boolean"
},
"subProcess": {
"default": false,
"description": "Whether to enable Sub Process debugging",
"type": "boolean"
},
"consoleName": {
"default": "Python Debug Console",
"description": "Display name of the debug console or terminal",
"type": "string"
},
"clientOS": {
"default": null,
"description": "OS that VS code is using.",
"enum": [
"windows",
null,
"unix"
]
}
},
"required": ["request"],
"allOf": [
{
"if": {
"properties": {
"request": {
"enum": ["attach"]
}
}
},
"then": {
"properties": {
"connect": {
"label": "Attach by connecting to debugpy over a socket.",
"properties": {
"host": {
"default": "127.0.0.1",
"description": "Hostname or IP address to connect to.",
"type": "string"
},
"port": {
"description": "Port to connect to.",
"type": [
"number",
"string"
]
}
},
"required": [
"port"
],
"type": "object"
},
"listen": {
"label": "Attach by listening for incoming socket connection from debugpy",
"properties": {
"host": {
"default": "127.0.0.1",
"description": "Hostname or IP address of the interface to listen on.",
"type": "string"
},
"port": {
"description": "Port to listen on.",
"type": [
"number",
"string"
]
}
},
"required": [
"port"
],
"type": "object"
},
"processId": {
"anyOf": [
{
"default": "${command:pickProcess}",
"description": "Use process picker to select a process to attach, or Process ID as integer.",
"enum": [
"${command:pickProcess}"
]
},
{
"description": "ID of the local process to attach to.",
"type": "integer"
}
]
}
}
}
},
{
"if": {
"properties": {
"request": {
"enum": ["launch"]
}
}
},
"then": {
"properties": {
"args": {
"default": [],
"description": "Command line arguments passed to the program. For string type arguments, it will pass through the shell as is, and therefore all shell variable expansions will apply. But for the array type, the values will be shell-escaped.",
"items": {
"type": "string"
},
"anyOf": [
{
"default": "${command:pickArgs}",
"enum": [
"${command:pickArgs}"
]
},
{
"type": [
"array",
"string"
]
}
]
},
"console": {
"default": "integratedTerminal",
"description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.",
"enum": [
"externalTerminal",
"integratedTerminal",
"internalConsole"
]
},
"cwd": {
"default": "${ZED_WORKTREE_ROOT}",
"description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).",
"type": "string"
},
"autoStartBrowser": {
"default": false,
"description": "Open external browser to launch the application",
"type": "boolean"
},
"env": {
"additionalProperties": {
"type": "string"
},
"default": {},
"description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.",
"type": "object"
},
"envFile": {
"default": "${ZED_WORKTREE_ROOT}/.env",
"description": "Absolute path to a file containing environment variable definitions.",
"type": "string"
},
"gevent": {
"default": false,
"description": "Enable debugging of gevent monkey-patched code.",
"type": "boolean"
},
"module": {
"default": "",
"description": "Name of the module to be debugged.",
"type": "string"
},
"program": {
"default": "${ZED_FILE}",
"description": "Absolute path to the program.",
"type": "string"
},
"purpose": {
"default": [],
"description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.",
"items": {
"enum": [
"debug-test",
"debug-in-terminal"
],
"enumDescriptions": [
"Use this configuration while debugging tests using test view or test debug commands.",
"Use this configuration while debugging a file using debug in terminal button in the editor."
]
},
"type": "array"
},
"pyramid": {
"default": false,
"description": "Whether debugging Pyramid applications.",
"type": "boolean"
},
"python": {
"default": "${command:python.interpreterPath}",
"description": "Absolute path to the Python interpreter executable; overrides workspace configuration if set.",
"type": "string"
},
"pythonArgs": {
"default": [],
"description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".",
"items": {
"type": "string"
},
"type": "array"
},
"stopOnEntry": {
"default": false,
"description": "Automatically stop after launch.",
"type": "boolean"
},
"sudo": {
"default": false,
"description": "Running debug program under elevated permissions (on Unix).",
"type": "boolean"
},
"guiEventLoop": {
"default": "matplotlib",
"description": "The GUI event loop that's going to run. Possible values: \"matplotlib\", \"wx\", \"qt\", \"none\", or a custom function that'll be imported and run.",
"type": "string"
}
}
}
}
]
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,

View File

@@ -3,16 +3,17 @@ use async_trait::async_trait;
use dap::{
DebugRequest, StartDebuggingRequestArguments,
adapters::{
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{path::PathBuf, sync::Arc};
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command;
use crate::ToDap;
#[derive(Default)]
pub(crate) struct RubyDebugAdapter;
@@ -30,6 +31,187 @@ impl DebugAdapter for RubyDebugAdapter {
Some(SharedString::new_static("Ruby").into())
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"required": ["script"],
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
"default": "ruby"
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"useBundler": {
"type": "boolean",
"description": "Execute Ruby programs with `bundle exec` instead of directly",
"default": false
},
"bundlePath": {
"type": "string",
"description": "Location of the bundle executable"
},
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"askParameters": {
"type": "boolean",
"description": "Ask parameters at first."
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"waitLaunchTime": {
"type": "number",
"description": "Wait time before connection in milliseconds"
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"useTerminal": {
"type": "boolean",
"description": "Create a new terminal and then execute commands there",
"default": false
}
}
}
]
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"localfsMap": {
"type": "string",
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the rdbg process",
"default": {}
}
}
}
]
}
]
})
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut config = serde_json::Map::new();
match &zed_scenario.request {
DebugRequest::Launch(launch) => {
config.insert("request".to_string(), json!("launch"));
config.insert("script".to_string(), json!(launch.program));
config.insert("command".to_string(), json!("ruby"));
if !launch.args.is_empty() {
config.insert("args".to_string(), json!(launch.args));
}
if !launch.env.is_empty() {
config.insert("env".to_string(), json!(launch.env));
}
if let Some(cwd) = &launch.cwd {
config.insert("cwd".to_string(), json!(cwd));
}
// Ruby stops on entry so there's no need to handle that case
}
DebugRequest::Attach(attach) => {
config.insert("request".to_string(), json!("attach"));
config.insert("processId".to_string(), json!(attach.process_id));
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: serde_json::Value::Object(config),
tcp_connection: None,
build: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -66,34 +248,25 @@ impl DebugAdapter for RubyDebugAdapter {
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let DebugRequest::Launch(launch) = definition.request.clone() else {
anyhow::bail!("rdbg does not yet support attaching");
};
let mut arguments = vec![
let arguments = vec![
"--open".to_string(),
format!("--port={}", port),
format!("--host={}", host),
];
if delegate.which(launch.program.as_ref()).await.is_some() {
arguments.push("--command".to_string())
}
arguments.push(launch.program);
arguments.extend(launch.args);
Ok(DebugAdapterBinary {
command: rdbg_path.to_string_lossy().to_string(),
arguments,
connection: Some(adapters::TcpArguments {
connection: Some(dap::adapters::TcpArguments {
host,
port,
timeout,
}),
cwd: launch.cwd,
envs: launch.env.into_iter().collect(),
cwd: None,
envs: std::collections::HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration: serde_json::Value::Object(Default::default()),
request: definition.request.to_dap(),
request: self.validate_config(&definition.config)?,
configuration: definition.config.clone(),
},
})
}

View File

@@ -11,6 +11,8 @@ async-trait.workspace = true
dap.workspace = true
extension.workspace = true
gpui.workspace = true
serde_json.workspace = true
task.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[lints]

View File

@@ -7,6 +7,7 @@ use dap::adapters::{
};
use extension::{Extension, WorktreeDelegate};
use gpui::AsyncApp;
use task::{DebugScenario, ZedDebugConfig};
pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>,
@@ -60,6 +61,10 @@ impl DebugAdapter for ExtensionDapAdapter {
self.debug_adapter_name.as_ref().into()
}
async fn dap_schema(&self) -> serde_json::Value {
self.extension.get_dap_schema().await.unwrap_or_default()
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
@@ -76,4 +81,8 @@ impl DebugAdapter for ExtensionDapAdapter {
)
.await
}
fn config_from_zed_format(&self, _zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
Err(anyhow::anyhow!("DAP extensions are not implemented yet"))
}
}

View File

@@ -1,15 +1,15 @@
use dap::DebugRequest;
use dap::adapters::DebugTaskDefinition;
use dap::{DapRegistry, DebugRequest};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use task::ZedDebugConfig;
use util::debug_panic;
use std::sync::Arc;
use sysinfo::System;
use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing};
use util::debug_panic;
use workspace::{ModalView, Workspace};
use crate::debugger_panel::DebugPanel;
@@ -25,7 +25,7 @@ pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
pub(crate) definition: DebugTaskDefinition,
pub(crate) definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
candidates: Arc<[Candidate]>,
}
@@ -33,7 +33,7 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate {
fn new(
workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition,
definition: ZedDebugConfig,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
@@ -54,7 +54,7 @@ pub struct AttachModal {
impl AttachModal {
pub fn new(
definition: DebugTaskDefinition,
definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
modal: bool,
window: &mut Window,
@@ -83,7 +83,7 @@ impl AttachModal {
pub(super) fn with_processes(
workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition,
definition: ZedDebugConfig,
processes: Arc<[Candidate]>,
modal: bool,
window: &mut Window,
@@ -228,7 +228,13 @@ impl PickerDelegate for AttachModalDelegate {
}
}
let scenario = self.definition.to_scenario();
let Some(scenario) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry
.adapter(&self.definition.adapter)
.and_then(|adapter| adapter.config_from_zed_format(self.definition.clone()).ok())
}) else {
return;
};
let panel = self
.workspace

View File

@@ -5,7 +5,7 @@ use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
persistence,
ToggleSessionPicker, ToggleThreadPicker, persistence,
};
use anyhow::{Context as _, Result, anyhow};
use command_palette_hooks::CommandPaletteFilter;
@@ -31,7 +31,7 @@ use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -65,6 +65,8 @@ pub struct DebugPanel {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
}
@@ -77,8 +79,10 @@ impl DebugPanel {
cx.new(|cx| {
let project = workspace.project().clone();
let focus_handle = cx.focus_handle();
let thread_picker_menu_handle = PopoverMenuHandle::default();
let session_picker_menu_handle = PopoverMenuHandle::default();
let debug_panel = Self {
Self {
size: px(300.),
sessions: vec![],
active_session: None,
@@ -87,9 +91,9 @@ impl DebugPanel {
workspace: workspace.weak_handle(),
context_menu: None,
fs: workspace.app_state().fs.clone(),
};
debug_panel
thread_picker_menu_handle,
session_picker_menu_handle,
}
})
}
@@ -269,7 +273,7 @@ impl DebugPanel {
let session = session.clone();
async move |this, cx| {
let debug_session =
Self::register_session(this.clone(), session.clone(), cx).await?;
Self::register_session(this.clone(), session.clone(), true, cx).await?;
let definition = debug_session
.update_in(cx, |debug_session, window, cx| {
debug_session.running_state().update(cx, |running, cx| {
@@ -295,6 +299,7 @@ impl DebugPanel {
cx.spawn(async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
session
.update(cx, |session, cx| {
session
@@ -313,69 +318,21 @@ impl DebugPanel {
pub(crate) async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
focus: bool,
cx: &mut AsyncWindowContext,
) -> Result<Entity<DebugSession>> {
let adapter_name = session.update(cx, |session, _| session.adapter())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |this, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
this.handle_restart_request(session.clone(), window, cx);
}
SessionStateEvent::SpawnChildSession { request } => {
this.handle_start_debugging_request(request, session.clone(), window, cx);
}
_ => {}
},
)
.detach();
})
.ok();
let debug_session = register_session_inner(&this, session, cx).await?;
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let workspace = this.update_in(cx, |this, window, cx| {
if focus {
this.activate_session(debug_session.clone(), window, cx);
}
let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
!session
.read(cx)
.running_state()
.read(cx)
.session()
.read(cx)
.is_terminated()
});
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
cx,
);
// We might want to make this an event subscription and only notify when a new thread is selected
// This is used to filter the command menu correctly
cx.observe(
&debug_session.read(cx).running_state().clone(),
|_, _, cx| cx.notify(),
)
.detach();
this.sessions.push(debug_session.clone());
this.activate_session(debug_session.clone(), window, cx);
(debug_session, this.workspace.clone())
this.workspace.clone()
})?;
workspace.update_in(cx, |workspace, window, cx| {
workspace.focus_panel::<Self>(window, cx);
})?;
Ok(debug_session)
}
@@ -413,7 +370,7 @@ impl DebugPanel {
});
(session, task)
})?;
Self::register_session(this, session, cx).await?;
Self::register_session(this.clone(), session, true, cx).await?;
task.await
})
.detach_and_log_err(cx);
@@ -436,7 +393,6 @@ impl DebugPanel {
let adapter = parent_session.read(cx).adapter().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
cx.spawn_in(window, async move |this, cx| {
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session =
@@ -447,7 +403,7 @@ impl DebugPanel {
});
(session, task)
})?;
Self::register_session(this, session, cx).await?;
Self::register_session(this, session, false, cx).await?;
task.await
})
.detach_and_log_err(cx);
@@ -746,55 +702,6 @@ impl DebugPanel {
}),
)
.child(Divider::vertical())
.child(
IconButton::new(
"debug-enable-breakpoint",
IconName::DebugDisabledBreakpoint,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new(
"debug-disable-breakpoint",
IconName::CircleOff,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(thread_status != ThreadStatus::Stopped),
)
.child(
IconButton::new(
"debug-disable-all-breakpoints",
IconName::BugOff,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Disable all breakpoints",
&ToggleIgnoreBreakpoints,
&focus_handle,
window,
cx,
)
}
}),
)
.child(Divider::vertical())
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
@@ -954,6 +861,21 @@ impl DebugPanel {
}
}
pub(crate) fn activate_session_by_id(
&mut self,
session_id: SessionId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(session) = self
.sessions
.iter()
.find(|session| session.read(cx).session_id(cx) == session_id)
{
self.activate_session(session.clone(), window, cx);
}
}
pub(crate) fn activate_session(
&mut self,
session_item: Entity<DebugSession>,
@@ -967,7 +889,7 @@ impl DebugPanel {
this.go_to_selected_stack_frame(window, cx);
});
});
self.active_session = Some(session_item.clone());
self.active_session = Some(session_item);
cx.notify();
}
@@ -1033,6 +955,75 @@ impl DebugPanel {
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
}
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.session_picker_menu_handle.toggle(window, cx);
}
}
async fn register_session_inner(
this: &WeakEntity<DebugPanel>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
) -> Result<Entity<DebugSession>> {
let adapter_name = session.update(cx, |session, _| session.adapter())?;
this.update_in(cx, |_, window, cx| {
cx.subscribe_in(
&session,
window,
move |this, session, event: &SessionStateEvent, window, cx| match event {
SessionStateEvent::Restart => {
this.handle_restart_request(session.clone(), window, cx);
}
SessionStateEvent::SpawnChildSession { request } => {
this.handle_start_debugging_request(request, session.clone(), window, cx);
}
_ => {}
},
)
.detach();
})
.ok();
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
this.sessions.retain(|session| {
!session
.read(cx)
.running_state()
.read(cx)
.session()
.read(cx)
.is_terminated()
});
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,
cx,
);
// We might want to make this an event subscription and only notify when a new thread is selected
// This is used to filter the command menu correctly
cx.observe(
&debug_session.read(cx).running_state().clone(),
|_, _, cx| cx.notify(),
)
.detach();
this.sessions.push(debug_session.clone());
debug_session
})?;
Ok(debug_session)
}
impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -1249,6 +1240,24 @@ impl Render for DebugPanel {
.ok();
}
})
.on_action({
let this = this.clone();
move |_: &ToggleThreadPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_thread_picker(window, cx);
})
.ok();
}
})
.on_action({
let this = this.clone();
move |_: &ToggleSessionPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_session_picker(window, cx);
})
.ok();
}
})
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,

View File

@@ -45,6 +45,8 @@ actions!(
FocusLoadedSources,
FocusTerminal,
ShowStackTrace,
ToggleThreadPicker,
ToggleSessionPicker,
]
);
@@ -93,6 +95,17 @@ pub fn init(cx: &mut App) {
}
}
})
.register_action(|workspace, _: &Continue, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session()
.map(|session| session.read(cx).running_state().clone())
}) {
active_item.update(cx, |item, cx| item.continue_thread(cx))
}
}
})
.register_action(|workspace, _: &StepInto, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {

View File

@@ -132,7 +132,8 @@ impl DebugPanel {
this
}),
)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
@@ -163,7 +164,7 @@ impl DebugPanel {
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
ContextMenu::build(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
@@ -177,7 +178,8 @@ impl DebugPanel {
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),
)
} else {
None

View File

@@ -10,8 +10,7 @@ use std::{
};
use dap::{
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
};
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -22,7 +21,7 @@ use gpui::{
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, LaunchRequest};
use task::{DebugScenario, LaunchRequest, ZedDebugConfig};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -210,15 +209,16 @@ impl NewSessionModal {
None
};
Some(DebugScenario {
let session_scenario = ZedDebugConfig {
adapter: debugger.to_owned().into(),
label,
request: Some(request),
initialize_args: None,
tcp_connection: None,
request: request,
stop_on_entry,
build: None,
})
};
cx.global::<DapRegistry>()
.adapter(&session_scenario.adapter)
.and_then(|adapter| adapter.config_from_zed_format(session_scenario).ok())
}
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
@@ -242,6 +242,7 @@ impl NewSessionModal {
let Some(task_contexts) = self.task_contexts(cx) else {
return;
};
send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
cx.spawn_in(window, async move |this, cx| {
@@ -264,12 +265,12 @@ impl NewSessionModal {
cx: &mut App,
) {
attach.update(cx, |this, cx| {
if adapter != &this.definition.adapter {
this.definition.adapter = adapter.clone();
if adapter.0 != this.definition.adapter {
this.definition.adapter = adapter.0.clone();
this.attach_picker.update(cx, |this, cx| {
this.picker.update(cx, |this, cx| {
this.delegate.definition.adapter = adapter.clone();
this.delegate.definition.adapter = adapter.0.clone();
this.focus(window, cx);
})
});
@@ -279,8 +280,8 @@ impl NewSessionModal {
})
}
fn task_contexts<'a>(&self, cx: &'a mut Context<Self>) -> Option<&'a TaskContexts> {
self.launch_picker.read(cx).delegate.task_contexts.as_ref()
fn task_contexts(&self, cx: &App) -> Option<Arc<TaskContexts>> {
self.launch_picker.read(cx).delegate.task_contexts.clone()
}
fn adapter_drop_down_menu(
@@ -805,8 +806,6 @@ impl CustomMode {
let args = args.collect::<Vec<_>>();
let (program, path) = resolve_paths(program, path);
task::LaunchRequest {
program,
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
@@ -862,7 +861,7 @@ impl CustomMode {
#[derive(Clone)]
pub(super) struct AttachMode {
pub(super) definition: DebugTaskDefinition,
pub(super) definition: ZedDebugConfig,
pub(super) attach_picker: Entity<AttachModal>,
}
@@ -873,12 +872,10 @@ impl AttachMode {
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
let definition = DebugTaskDefinition {
adapter: debugger.unwrap_or(DebugAdapterName("".into())),
let definition = ZedDebugConfig {
adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
label: "Attach New Session Setup".into(),
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
initialize_args: None,
tcp_connection: None,
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
@@ -905,7 +902,7 @@ pub(super) struct DebugScenarioDelegate {
matches: Vec<StringMatch>,
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
task_contexts: Option<TaskContexts>,
task_contexts: Option<Arc<TaskContexts>>,
divider_index: Option<usize>,
last_used_candidate_index: Option<usize>,
}
@@ -938,27 +935,14 @@ impl DebugScenarioDelegate {
});
let language = language.or_else(|| {
scenario
.request
.as_ref()
.and_then(|request| match request {
DebugRequest::Launch(launch) => launch
.program
.rsplit_once(".")
.and_then(|split| languages.language_name_for_extension(split.1))
.map(|name| TaskSourceKind::Language { name: name.into() }),
_ => None,
})
.or_else(|| {
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
})
})
});
(language, scenario)
@@ -971,7 +955,7 @@ impl DebugScenarioDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.task_contexts = Some(task_contexts);
self.task_contexts = Some(Arc::new(task_contexts));
let (recent, scenarios) = self
.task_store
@@ -1092,7 +1076,7 @@ impl PickerDelegate for DebugScenarioDelegate {
.get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
let Some((_, mut debug_scenario)) = debug_scenario else {
let Some((_, debug_scenario)) = debug_scenario else {
return;
};
@@ -1107,19 +1091,7 @@ impl PickerDelegate for DebugScenarioDelegate {
})
.unwrap_or_default();
if let Some(launch_config) =
debug_scenario
.request
.as_mut()
.and_then(|request| match request {
DebugRequest::Launch(launch) => Some(launch),
_ => None,
})
{
let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
launch_config.program = program;
}
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
self.debug_panel
.update(cx, |panel, cx| {
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
@@ -1173,34 +1145,41 @@ impl PickerDelegate for DebugScenarioDelegate {
}
}
fn resolve_paths(program: String, path: String) -> (String, String) {
let program = if let Some(program) = program.strip_prefix('~') {
format!(
pub(crate) fn resolve_path(path: &mut String) {
if path.starts_with('~') {
let home = paths::home_dir().to_string_lossy().to_string();
let trimmed_path = path.trim().to_owned();
*path = trimmed_path.replacen('~', &home, 1);
} else if let Some(strip_path) = path.strip_prefix(&format!(".{}", std::path::MAIN_SEPARATOR)) {
*path = format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else if !program.starts_with(std::path::MAIN_SEPARATOR) {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else {
program
&strip_path
);
};
let path = if path.starts_with('~') && !path.is_empty() {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&path[1..]
)
} else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
} else {
path
};
(program, path)
}
#[cfg(test)]
mod tests {
use paths::home_dir;
#[test]
fn test_normalize_paths() {
let sep = std::path::MAIN_SEPARATOR;
let home = home_dir().to_string_lossy().to_string();
let resolve_path = |path: &str| -> String {
let mut path = path.to_string();
super::resolve_path(&mut path);
path
};
assert_eq!(resolve_path("bin"), format!("bin"));
assert_eq!(resolve_path(&format!("{sep}foo")), format!("{sep}foo"));
assert_eq!(resolve_path(""), format!(""));
assert_eq!(
resolve_path(&format!("~{sep}blah")),
format!("{home}{sep}blah")
);
assert_eq!(resolve_path("~"), home);
}
}

View File

@@ -7,7 +7,10 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::persistence::{self, DebuggerPaneItem, SerializedLayout};
use crate::{
new_session_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
};
use super::DebugPanelItemEvent;
use anyhow::{Context as _, Result, anyhow};
@@ -15,7 +18,7 @@ use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap};
use console::Console;
use dap::{
Capabilities, RunInTerminalRequestArguments, Thread,
Capabilities, DapRegistry, RunInTerminalRequestArguments, Thread,
adapters::{DebugAdapterName, DebugTaskDefinition},
client::SessionId,
debugger_settings::DebuggerSettings,
@@ -38,8 +41,8 @@ use serde_json::Value;
use settings::Settings;
use stack_frame_list::StackFrameList;
use task::{
BuildTaskDefinition, DebugScenario, LaunchRequest, ShellBuilder, SpawnInTerminal, TaskContext,
substitute_variables_in_map, substitute_variables_in_str,
BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskContext, ZedDebugConfig,
substitute_variables_in_str,
};
use terminal_view::TerminalView;
use ui::{
@@ -96,7 +99,7 @@ impl Render for RunningState {
.find(|pane| pane.read(cx).is_zoomed());
let active = self.panes.panes().into_iter().next();
let x = if let Some(ref zoomed_pane) = zoomed_pane {
let pane = if let Some(ref zoomed_pane) = zoomed_pane {
zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
} else if let Some(active) = active {
self.panes
@@ -122,7 +125,7 @@ impl Render for RunningState {
.size_full()
.key_context("DebugSessionItem")
.track_focus(&self.focus_handle(cx))
.child(h_flex().flex_1().child(x))
.child(h_flex().flex_1().child(pane))
}
}
@@ -519,6 +522,56 @@ impl Focusable for DebugTerminal {
}
impl RunningState {
// todo(debugger) move this to util and make it so you pass a closure to it that converts a string
pub(crate) fn substitute_variables_in_config(
config: &mut serde_json::Value,
context: &TaskContext,
) {
match config {
serde_json::Value::Object(obj) => {
obj.values_mut()
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::String(s) => {
if let Some(substituted) = substitute_variables_in_str(&s, context) {
*s = substituted;
}
}
_ => {}
}
}
pub(crate) fn relativlize_paths(
key: Option<&str>,
config: &mut serde_json::Value,
context: &TaskContext,
) {
match config {
serde_json::Value::Object(obj) => {
obj.iter_mut()
.for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::relativlize_paths(None, value, context));
}
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
resolve_path(s);
if let Some(substituted) = substitute_variables_in_str(&s, context) {
*s = substituted;
}
}
_ => {}
}
}
pub(crate) fn new(
session: Entity<Session>,
project: Entity<Project>,
@@ -561,15 +614,26 @@ impl RunningState {
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
match event {
SessionEvent::Stopped(thread_id) => {
this.workspace
let panel = this
.workspace
.update(cx, |workspace, cx| {
workspace.open_panel::<crate::DebugPanel>(window, cx);
workspace.panel::<crate::DebugPanel>(cx)
})
.log_err();
.log_err()
.flatten();
if let Some(thread_id) = thread_id {
this.select_thread(*thread_id, window, cx);
}
if let Some(panel) = panel {
let id = this.session_id;
window.defer(cx, move |window, cx| {
panel.update(cx, |this, cx| {
this.activate_session_by_id(id, window, cx);
})
})
}
}
SessionEvent::Threads => {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
@@ -628,10 +692,9 @@ impl RunningState {
&workspace,
&stack_frame_list,
&variable_list,
&module_list,
&loaded_source_list,
&console,
&breakpoint_list,
&debug_terminal,
dock_axis,
&mut pane_close_subscriptions,
window,
@@ -705,6 +768,7 @@ impl RunningState {
};
let project = workspace.read(cx).project().clone();
let dap_store = project.read(cx).dap_store().downgrade();
let dap_registry = cx.global::<DapRegistry>().clone();
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
@@ -714,11 +778,19 @@ impl RunningState {
adapter,
label,
build,
request,
initialize_args,
mut config,
tcp_connection,
stop_on_entry,
} = scenario;
Self::relativlize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.and_then(|adapter| adapter.validate_config(&config));
let config_is_valid = request_type.is_ok();
let build_output = if let Some(build) = build {
let (task, locator_name) = match build {
BuildTaskDefinition::Template {
@@ -747,9 +819,9 @@ impl RunningState {
};
let locator_name = if let Some(locator_name) = locator_name {
debug_assert!(request.is_none());
debug_assert!(!config_is_valid);
Some(locator_name)
} else if request.is_none() {
} else if !config_is_valid {
dap_store
.update(cx, |this, cx| {
this.debug_scenario_for_build_task(
@@ -826,63 +898,44 @@ impl RunningState {
} else {
None
};
let request = if let Some(request) = request {
request
if config_is_valid {
// Ok(DebugTaskDefinition {
// label,
// adapter: DebugAdapterName(adapter),
// config,
// tcp_connection,
// })
} else if let Some((task, locator_name)) = build_output {
let locator_name =
locator_name.context("Could not find a valid locator for a build task")?;
dap_store
let request = dap_store
.update(cx, |this, cx| {
this.run_debug_locator(&locator_name, task, cx)
})?
.await?
.await?;
let zed_config = ZedDebugConfig {
label: label.clone(),
adapter: adapter.clone(),
request,
stop_on_entry: None,
};
let scenario = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
config = scenario.config;
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
anyhow::bail!("No request or build provided");
};
let request = match request {
dap::DebugRequest::Launch(launch_request) => {
let cwd = match launch_request.cwd.as_deref().and_then(|path| path.to_str()) {
Some(cwd) => {
let substituted_cwd = substitute_variables_in_str(&cwd, &task_context)
.context("substituting variables in cwd")?;
Some(PathBuf::from(substituted_cwd))
}
None => None,
};
let env = substitute_variables_in_map(
&launch_request.env.into_iter().collect(),
&task_context,
)
.context("substituting variables in env")?
.into_iter()
.collect();
let new_launch_request = LaunchRequest {
program: substitute_variables_in_str(
&launch_request.program,
&task_context,
)
.context("substituting variables in program")?,
args: launch_request
.args
.into_iter()
.map(|arg| substitute_variables_in_str(&arg, &task_context))
.collect::<Option<Vec<_>>>()
.context("substituting variables in args")?,
cwd,
env,
};
dap::DebugRequest::Launch(new_launch_request)
}
request @ dap::DebugRequest::Attach(_) => request, // todo(debugger): We should check that process_id is valid and if not show the modal
};
Ok(DebugTaskDefinition {
label,
adapter: DebugAdapterName(adapter),
request,
initialize_args,
stop_on_entry,
config,
tcp_connection,
})
})
@@ -1468,10 +1521,9 @@ impl RunningState {
workspace: &WeakEntity<Workspace>,
stack_frame_list: &Entity<StackFrameList>,
variable_list: &Entity<VariableList>,
module_list: &Entity<ModuleList>,
loaded_source_list: &Entity<LoadedSourceList>,
console: &Entity<Console>,
breakpoints: &Entity<BreakpointList>,
debug_terminal: &Entity<DebugTerminal>,
dock_axis: Axis,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
@@ -1512,6 +1564,26 @@ impl RunningState {
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
center_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
console.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx,
)),
true,
false,
None,
window,
cx,
);
this.add_item(
Box::new(SubView::new(
variable_list.focus_handle(cx),
@@ -1526,54 +1598,20 @@ impl RunningState {
window,
cx,
);
this.add_item(
Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.add_item(
Box::new(SubView::new(
loaded_source_list.focus_handle(cx),
loaded_source_list.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.activate_item(0, false, false, window, cx);
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
rightmost_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();
this.add_item(
Box::new(SubView::new(
this.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
debug_terminal.focus_handle(cx),
debug_terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,
cx,
)),
true,
false,
false,
None,
window,

View File

@@ -14,7 +14,7 @@ use language::{Buffer, CodeLabel, ToOffset};
use menu::Confirm;
use project::{
Completion,
debugger::session::{CompletionsQuery, OutputToken, Session},
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
};
use settings::Settings;
use std::{cell::RefCell, rc::Rc, usize};
@@ -79,6 +79,11 @@ impl Console {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
if let SessionEvent::ConsoleOutput = event {
this.update_output(window, cx)
}
}),
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window);
@@ -200,12 +205,11 @@ impl Console {
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(&self.query_bar, self.editor_style(cx))
}
}
impl Render for Console {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let session = self.session.clone();
let token = self.last_token;
self.update_output_task = cx.spawn_in(window, async move |this, cx| {
_ = session.update_in(cx, move |session, window, cx| {
let (output, last_processed_token) = session.output(token);
@@ -220,7 +224,11 @@ impl Render for Console {
});
});
});
}
}
impl Render for Console {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.key_context("DebugConsole")

View File

@@ -154,12 +154,15 @@ impl VariableList {
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.subscribe(&session, |this, _, event, _| match event {
cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) => {
this.selection.take();
this.edited_path.take();
this.selected_stack_frame_id.take();
}
SessionEvent::Variables => {
this.build_entries(cx);
}
_ => {}
}),
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
@@ -300,7 +303,7 @@ impl VariableList {
match event {
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
self.selected_stack_frame_id = Some(*stack_frame_id);
cx.notify();
self.build_entries(cx);
}
StackFrameListEvent::BuiltEntries => {}
}
@@ -344,14 +347,14 @@ impl VariableList {
};
entry.is_expanded = !entry.is_expanded;
cx.notify();
self.build_entries(cx);
}
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.first() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
@@ -359,7 +362,7 @@ impl VariableList {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(variable) = self.entries.last() {
self.selection = Some(variable.path.clone());
cx.notify();
self.build_entries(cx);
}
}
@@ -378,7 +381,7 @@ impl VariableList {
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
self.build_entries(cx);
} else {
self.select_last(&SelectLast, window, cx);
}
@@ -402,7 +405,7 @@ impl VariableList {
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
self.build_entries(cx);
} else {
self.select_first(&SelectFirst, window, cx);
}
@@ -464,7 +467,7 @@ impl VariableList {
self.select_prev(&SelectPrevious, window, cx);
} else {
entry_state.is_expanded = false;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -485,7 +488,7 @@ impl VariableList {
self.select_next(&SelectNext, window, cx);
} else {
entry_state.is_expanded = true;
cx.notify();
self.build_entries(cx);
}
}
}
@@ -929,8 +932,6 @@ impl Focusable for VariableList {
impl Render for VariableList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.build_entries(cx);
v_flex()
.track_focus(&self.focus_handle)
.key_context("VariableList")
@@ -946,7 +947,6 @@ impl Render for VariableList {
.on_action(cx.listener(Self::collapse_selected_entry))
.on_action(cx.listener(Self::cancel_variable_edit))
.on_action(cx.listener(Self::confirm_variable_edit))
//
.child(
uniform_list(
cx.entity().clone(),

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{Context as _, Result};
use dap::adapters::DebugTaskDefinition;
use dap::{DebugRequest, client::DebugAdapterClient};
use dap::client::DebugAdapterClient;
use gpui::{Entity, TestAppContext, WindowHandle};
use project::{Project, debugger::session::Session};
use settings::SettingsStore;
@@ -25,6 +25,9 @@ mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
#[cfg(not(windows))]
mod new_session_modal;
#[cfg(test)]
mod persistence;
#[cfg(test)]
mod stack_frame_list;
@@ -136,16 +139,18 @@ pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
cx: &mut gpui::TestAppContext,
configure: T,
) -> Result<Entity<Session>> {
use serde_json::json;
start_debug_session_with(
workspace,
cx,
DebugTaskDefinition {
adapter: "fake-adapter".into(),
request: DebugRequest::Launch(Default::default()),
label: "test".into(),
initialize_args: None,
config: json!({
"request": "launch"
}),
tcp_connection: None,
stop_on_entry: None,
},
configure,
)

View File

@@ -5,7 +5,7 @@ use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::{AttachRequest, TcpArgumentsTemplate};
use task::AttachRequest;
use tests::{init_test, init_test_workspace};
use util::path;
@@ -32,13 +32,12 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
cx,
DebugTaskDefinition {
adapter: "fake-adapter".into(),
request: dap::DebugRequest::Attach(AttachRequest {
process_id: Some(10),
}),
label: "label".into(),
initialize_args: None,
config: json!({
"request": "attach",
"process_id": 10,
}),
tcp_connection: None,
stop_on_entry: None,
},
|client| {
client.on_request::<dap::requests::Attach, _>(move |_, args| {
@@ -107,13 +106,10 @@ async fn test_show_attach_modal_and_select_process(
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
DebugTaskDefinition {
task::ZedDebugConfig {
adapter: FakeAdapter::ADAPTER_NAME.into(),
request: dap::DebugRequest::Attach(AttachRequest::default()),
label: "attach example".into(),
initialize_args: None,
tcp_connection: Some(TcpArgumentsTemplate::default()),
stop_on_entry: None,
},
vec![

View File

@@ -24,14 +24,12 @@ use project::{
};
use serde_json::json;
use std::{
collections::HashMap,
path::Path,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use task::LaunchRequest;
use terminal_view::terminal_panel::TerminalPanel;
use tests::{active_debug_session_panel, init_test, init_test_workspace};
use util::path;
@@ -425,6 +423,13 @@ async fn test_handle_start_debugging_request(
}
});
let sessions = workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel.read(cx).sessions()
})
.unwrap();
assert_eq!(sessions.len(), 1);
client
.fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
@@ -437,20 +442,42 @@ async fn test_handle_start_debugging_request(
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
// Active session does not change on spawn.
let active_session = debug_panel
.read(cx)
.active_session()
.unwrap()
.read(cx)
.session(cx);
let parent_session = active_session.read(cx).parent_session().unwrap();
assert_eq!(active_session, sessions[0].read(cx).session(cx));
assert!(active_session.read(cx).parent_session().is_none());
let current_sessions = debug_panel.read(cx).sessions();
assert_eq!(current_sessions.len(), 2);
assert_eq!(current_sessions[0], sessions[0]);
let parent_session = current_sessions[1]
.read(cx)
.session(cx)
.read(cx)
.parent_session()
.unwrap();
assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
// We should preserve the original binary (params to spawn process etc.) except for launch params
// (as they come from reverse spawn request).
let mut original_binary = parent_session.read(cx).binary().clone();
original_binary.request_args = StartDebuggingRequestArguments {
request: StartDebuggingRequestArgumentsRequest::Launch,
configuration: fake_config.clone(),
};
assert_eq!(active_session.read(cx).binary(), &original_binary);
assert_eq!(
current_sessions[1].read(cx).session(cx).read(cx).binary(),
&original_binary
);
})
.unwrap();
@@ -1388,16 +1415,15 @@ async fn test_we_send_arguments_from_user_config(
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let debug_definition = DebugTaskDefinition {
adapter: "fake-adapter".into(),
request: dap::DebugRequest::Launch(LaunchRequest {
program: "main.rs".to_owned(),
args: vec!["arg1".to_owned(), "arg2".to_owned()],
cwd: Some(path!("/Random_path").into()),
env: HashMap::from_iter(vec![("KEY".to_owned(), "VALUE".to_owned())]),
config: json!({
"request": "launch",
"program": "main.rs".to_owned(),
"args": vec!["arg1".to_owned(), "arg2".to_owned()],
"cwd": path!("/Random_path"),
"env": json!({ "KEY": "VALUE" }),
}),
label: "test".into(),
initialize_args: None,
tcp_connection: None,
stop_on_entry: None,
};
let launch_handler_called = Arc::new(AtomicBool::new(false));

View File

@@ -1,5 +1,6 @@
use crate::{
debugger_panel::DebugPanel,
persistence::DebuggerPaneItem,
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
@@ -110,7 +111,8 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
running_state.update_in(cx, |this, window, cx| {
this.activate_item(crate::persistence::DebuggerPaneItem::Modules, window, cx);
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});

View File

@@ -0,0 +1,157 @@
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugScenario, TaskContext, VariableName};
use util::path;
use crate::tests::{init_test, init_test_workspace};
// todo(tasks) figure out why task replacement is broken on windows
#[gpui::test]
async fn test_debug_session_substitutes_variables_and_relativizes_paths(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Set up task variables to simulate a real environment
let test_variables = vec![(
VariableName::WorktreeRoot,
"/test/worktree/path".to_string(),
)]
.into_iter()
.collect();
let task_context = TaskContext {
cwd: None,
task_variables: test_variables,
project_env: Default::default(),
};
let home_dir = paths::home_dir();
let sep = std::path::MAIN_SEPARATOR;
// Test cases for different path formats
let test_cases: Vec<(Arc<String>, Arc<String>)> = vec![
// Absolute path - should not be relativized
(
Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)),
Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)),
),
// Relative path - should be prefixed with worktree root
(
Arc::from(format!(".{0}src{0}program", sep)),
Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)),
),
// Home directory path - should be prefixed with worktree root
(
Arc::from(format!("~{0}src{0}program", sep)),
Arc::from(format!(
"{1}{0}src{0}program",
sep,
home_dir.to_string_lossy()
)),
),
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
(
Arc::from(format!("$ZED_WORKTREE_ROOT{0}src{0}program", sep)),
Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)),
),
];
let called_launch = Arc::new(AtomicBool::new(false));
for (input_path, expected_path) in test_cases {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
let called_launch = called_launch.clone();
let input_path = input_path.clone();
let expected_path = expected_path.clone();
move |client| {
client.on_request::<dap::requests::Launch, _>({
let called_launch = called_launch.clone();
let input_path = input_path.clone();
let expected_path = expected_path.clone();
move |_, args| {
let config = args.raw.as_object().unwrap();
// Verify the program path was substituted correctly
assert_eq!(
config["program"].as_str().unwrap(),
expected_path.as_str(),
"Program path was not correctly substituted for input: {}",
input_path.as_str()
);
// Verify the cwd path was substituted correctly
assert_eq!(
config["cwd"].as_str().unwrap(),
expected_path.as_str(),
"CWD path was not correctly substituted for input: {}",
input_path.as_str()
);
// Verify that otherField was substituted but not relativized
// It should still have $ZED_WORKTREE_ROOT substituted if present
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
input_path.replace("$ZED_WORKTREE_ROOT", "/test/worktree/path")
} else {
input_path.to_string()
};
assert_eq!(
config["otherField"].as_str().unwrap(),
expected_other_field,
"Other field was incorrectly modified for input: {}",
input_path
);
called_launch.store(true, Ordering::SeqCst);
Ok(())
}
});
}
});
let scenario = DebugScenario {
adapter: "fake-adapter".into(),
label: "test-debug-session".into(),
build: None,
config: json!({
"request": "launch",
"program": input_path,
"cwd": input_path,
"otherField": input_path
}),
tcp_connection: None,
};
workspace
.update(cx, |workspace, window, cx| {
workspace.start_debug_session(scenario, task_context.clone(), None, window, cx)
})
.unwrap();
cx.run_until_parked();
assert!(called_launch.load(Ordering::SeqCst));
called_launch.store(false, Ordering::SeqCst);
}
}

View File

@@ -5,6 +5,7 @@ use std::sync::{
use crate::{
DebugPanel,
persistence::DebuggerPaneItem,
session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
@@ -706,7 +707,13 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
cx.focus_self(window);
let running = item.running_state().clone();
let variable_list = running.read_with(cx, |state, _| state.variable_list().clone());
let variable_list = running.update(cx, |state, cx| {
// have to do this because the variable list pane should be shown/active
// for testing keyboard navigation
state.activate_item(DebuggerPaneItem::Variables, window, cx);
state.variable_list().clone()
});
variable_list.update(cx, |_, cx| cx.focus_self(window));
running
});

View File

@@ -37,6 +37,7 @@ clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
convert_case.workspace = true
dap.workspace = true
db.workspace = true
buffer_diff.workspace = true
emojis.workspace = true

View File

@@ -59,6 +59,7 @@ use client::{Collaborator, ParticipantIndex};
use clock::{AGENT_REPLICA_ID, ReplicaId};
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
use dap::TelemetrySpawnLocation;
use display_map::*;
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
pub use editor_settings::{
@@ -3964,15 +3965,18 @@ impl Editor {
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
let (delimiter, trimmed_len) =
delimiters.iter().find_map(|delimiter| {
let trimmed = delimiter.trim_end();
if comment_candidate.starts_with(trimmed) {
Some((delimiter, trimmed.len()))
let (delimiter, trimmed_len) = delimiters
.iter()
.filter_map(|delimiter| {
let prefix = delimiter.trim_end();
if comment_candidate.starts_with(prefix) {
Some((delimiter, prefix.len()))
} else {
None
}
})?;
})
.max_by_key(|(_, len)| *len)?;
let cursor_is_placed_after_comment_marker =
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
@@ -4040,31 +4044,29 @@ impl Editor {
};
let cursor_is_before_end_tag_if_exists = {
let num_of_whitespaces_rev = snapshot
.reversed_chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let mut line_iter = snapshot
.reversed_chars_for_range(range)
.skip(num_of_whitespaces_rev);
let end_tag_exists = end_tag
.chars()
.rev()
.all(|char| line_iter.next() == Some(char));
if end_tag_exists {
let max_point = snapshot.line_len(start_point.row) as usize;
let ordering = (num_of_whitespaces_rev
+ end_tag.len()
+ start_point.column as usize)
.cmp(&max_point);
let mut char_position = 0u32;
let mut end_tag_offset = None;
'outer: for chunk in snapshot.text_for_range(range.clone()) {
if let Some(byte_pos) = chunk.find(&**end_tag) {
let chars_before_match =
chunk[..byte_pos].chars().count() as u32;
end_tag_offset =
Some(char_position + chars_before_match);
break 'outer;
}
char_position += chunk.chars().count() as u32;
}
if let Some(end_tag_offset) = end_tag_offset {
let cursor_is_before_end_tag =
ordering != Ordering::Greater;
start_point.column <= end_tag_offset;
if cursor_is_after_start_tag {
if cursor_is_before_end_tag {
insert_extra_newline = true;
}
let cursor_is_at_start_of_end_tag =
ordering == Ordering::Equal;
start_point.column == end_tag_offset;
if cursor_is_at_start_of_end_tag {
indent_on_extra_newline.len = (*len).into();
}
@@ -5616,6 +5618,7 @@ impl Editor {
let context = actions_menu.actions.context.clone();
workspace.update(cx, |workspace, cx| {
dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx);
workspace.start_debug_session(scenario, context, Some(buffer), window, cx);
});
Some(Task::ready(Ok(())))
@@ -7263,24 +7266,22 @@ impl Editor {
..Default::default()
};
let primary_action_text = if breakpoint.is_disabled() {
"enable"
"Enable breakpoint"
} else if is_phantom && !collides_with_existing {
"set"
"Set breakpoint"
} else {
"unset"
"Unset breakpoint"
};
let mut primary_text = format!("Click to {primary_action_text}");
if collides_with_existing && !breakpoint.is_disabled() {
use std::fmt::Write;
write!(primary_text, ", {alt_as_text}-click to disable").ok();
}
let primary_text = SharedString::from(primary_text);
let focus_handle = self.focus_handle.clone();
let meta = if is_rejected {
"No executable code is associated with this line."
SharedString::from("No executable code is associated with this line.")
} else if collides_with_existing && !breakpoint.is_disabled() {
SharedString::from(format!(
"{alt_as_text}-click to disable,\nright-click for more options."
))
} else {
"Right-click for more options."
SharedString::from("Right-click for more options.")
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
@@ -7319,7 +7320,14 @@ impl Editor {
);
}))
.tooltip(move |window, cx| {
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
Tooltip::with_meta_in(
primary_action_text,
Some(&ToggleBreakpoint),
meta.clone(),
&focus_handle,
window,
cx,
)
})
}
@@ -20236,6 +20244,7 @@ impl SemanticsProvider for Entity<Project> {
fn inline_values(
&self,
buffer_handle: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {

View File

@@ -2820,6 +2820,42 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["// ".into(), "/// ".into()],
..LanguageConfig::default()
},
None,
));
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
//ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
//
// ˇ
"});
cx.set_state(indoc! {"
///ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
///
/// ˇ
"});
}
}
#[gpui::test]
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {
@@ -2975,6 +3011,32 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
*/
ˇ
"});
// Ensure that inline comment followed by code
// doesn't add comment prefix on newline
cx.set_state(indoc! {"
/** */ textˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/** */ text
ˇ
"});
// Ensure that text after comment end tag
// doesn't add comment prefix on newline
cx.set_state(indoc! {"
/**
*
*/ˇtext
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
*
*/
ˇtext
"});
}
// Ensure that comment continuations can be disabled.
update_test_language_settings(cx, |settings| {

View File

@@ -143,6 +143,8 @@ pub trait Extension: Send + Sync + 'static {
user_installed_path: Option<PathBuf>,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<DebugAdapterBinary>;
async fn get_dap_schema(&self) -> Result<serde_json::Value>;
}
pub fn parse_wasm_extension_version(

View File

@@ -20,7 +20,7 @@ pub use wit::{
make_file_executable,
zed::extension::context_server::ContextServerConfiguration,
zed::extension::dap::{
DebugAdapterBinary, DebugRequest, DebugTaskDefinition, StartDebuggingRequestArguments,
DebugAdapterBinary, DebugTaskDefinition, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
resolve_tcp_template,
},
@@ -203,6 +203,10 @@ pub trait Extension: Send + Sync {
) -> Result<DebugAdapterBinary, String> {
Err("`get_dap_binary` not implemented".to_string())
}
fn dap_schema(&mut self) -> Result<serde_json::Value, String> {
Err("`dap_schema` not implemented".to_string())
}
}
/// Registers the provided type as a Zed extension.
@@ -396,6 +400,10 @@ impl wit::Guest for Component {
) -> Result<wit::DebugAdapterBinary, String> {
extension().get_dap_binary(adapter_name, config, user_installed_path, worktree)
}
fn dap_schema() -> Result<String, String> {
extension().dap_schema().map(|schema| schema.to_string())
}
}
/// The ID of a language server.

View File

@@ -35,9 +35,7 @@ interface dap {
record debug-task-definition {
label: string,
adapter: string,
request: debug-request,
initialize-args: option<string>,
stop-on-entry: option<bool>,
config: string,
tcp-connection: option<tcp-arguments-template>,
}

View File

@@ -134,6 +134,7 @@ world extension {
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Returns the completions that should be shown when completing the provided slash command with the given query.
export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
@@ -158,4 +159,6 @@ world extension {
/// Returns a configured debug adapter binary for a given debug task.
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>;
/// Get a debug adapter's configuration schema
export dap-schema: func() -> result<string, string>;
}

View File

@@ -398,6 +398,20 @@ impl extension::Extension for WasmExtension {
})
.await
}
async fn get_dap_schema(&self) -> Result<serde_json::Value> {
self.call(|extension, store| {
async move {
extension
.call_dap_schema(store)
.await
.and_then(|schema| serde_json::to_value(schema).map_err(|err| err.to_string()))
.map_err(|err| anyhow!(err.to_string()))
}
.boxed()
})
.await
}
}
pub struct WasmState {
@@ -710,100 +724,3 @@ impl CacheStore for IncrementalCompilationCache {
true
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use extension::{
ExtensionCapability, ExtensionLibraryKind, LanguageServerManifestEntry, LibManifestEntry,
SchemaVersion,
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
};
use gpui::TestAppContext;
use reqwest_client::ReqwestClient;
use super::*;
#[gpui::test]
fn test_cache_size_for_test_extension(cx: &TestAppContext) {
let cache_store = cache_store();
let engine = wasm_engine();
let wasm_bytes = wasm_bytes(cx, &mut manifest());
Component::new(&engine, wasm_bytes).unwrap();
cache_store.cache.run_pending_tasks();
let size: usize = cache_store
.cache
.iter()
.map(|(k, v)| k.len() + v.len())
.sum();
// If this assertion fails, it means extensions got larger and we may want to
// reconsider our cache size.
assert!(size < 512 * 1024);
}
fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest) -> Vec<u8> {
let extension_builder = extension_builder();
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("extensions/test-extension");
cx.executor()
.block(extension_builder.compile_extension(
&path,
manifest,
CompileExtensionOptions { release: true },
))
.unwrap();
std::fs::read(path.join("extension.wasm")).unwrap()
}
fn extension_builder() -> ExtensionBuilder {
let user_agent = format!(
"Zed Extension CLI/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH
);
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap());
// Local dir so that we don't have to download it on every run
let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build");
ExtensionBuilder::new(http_client, build_dir)
}
fn manifest() -> ExtensionManifest {
ExtensionManifest {
id: "test-extension".into(),
name: "Test Extension".into(),
version: "0.1.0".into(),
schema_version: SchemaVersion(1),
description: Some("An extension for use in tests.".into()),
authors: Vec::new(),
repository: None,
themes: Default::default(),
icon_themes: Vec::new(),
lib: LibManifestEntry {
kind: Some(ExtensionLibraryKind::Rust),
version: Some(SemanticVersion::new(0, 1, 0)),
},
languages: Vec::new(),
grammars: BTreeMap::default(),
language_servers: [("gleam".into(), LanguageServerManifestEntry::default())]
.into_iter()
.collect(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![ExtensionCapability::ProcessExec {
command: "echo".into(),
args: vec!["hello!".into()],
}],
debug_adapters: Vec::new(),
}
}
}

View File

@@ -922,6 +922,20 @@ impl Extension {
_ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"),
}
}
pub async fn call_dap_schema(&self, store: &mut Store<WasmState>) -> Result<String, String> {
match self {
Extension::V0_6_0(ext) => {
let schema = ext
.call_dap_schema(store)
.await
.map_err(|err| err.to_string())?;
schema
}
_ => Err("`get_dap_binary` not available prior to v0.6.0".to_string()),
}
}
}
trait ToWasmtimeResult<T> {

View File

@@ -15,6 +15,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::archive::extract_zip;
use util::maybe;
use wasmtime::component::{Linker, Resource};
@@ -543,9 +544,9 @@ impl ExtensionImports for WasmState {
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
.with_context(|| format!("unzipping {path:?} archive"))?;
}
}

View File

@@ -1,7 +1,7 @@
use crate::wasm_host::wit::since_v0_6_0::{
dap::{
AttachRequest, DebugRequest, LaunchRequest, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, TcpArguments,
TcpArgumentsTemplate,
},
slash_command::SlashCommandOutputSection,
};
@@ -27,7 +27,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use util::{archive::extract_zip, maybe};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
@@ -79,17 +79,6 @@ impl From<Command> for extension::Command {
}
}
impl From<extension::LaunchRequest> for LaunchRequest {
fn from(value: extension::LaunchRequest) -> Self {
Self {
program: value.program,
cwd: value.cwd.map(|path| path.to_string_lossy().into_owned()),
envs: value.env.into_iter().collect(),
args: value.args,
}
}
}
impl From<StartDebuggingRequestArgumentsRequest>
for extension::StartDebuggingRequestArgumentsRequest
{
@@ -129,32 +118,14 @@ impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
}
}
}
impl From<extension::AttachRequest> for AttachRequest {
fn from(value: extension::AttachRequest) -> Self {
Self {
process_id: value.process_id,
}
}
}
impl From<extension::DebugRequest> for DebugRequest {
fn from(value: extension::DebugRequest) -> Self {
match value {
extension::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
extension::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
}
}
}
impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
type Error = anyhow::Error;
fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> {
let initialize_args = value.initialize_args.map(|s| s.to_string());
Ok(Self {
label: value.label.to_string(),
adapter: value.adapter.to_string(),
request: value.request.into(),
initialize_args,
stop_on_entry: value.stop_on_entry,
config: value.config.to_string(),
tcp_connection: value.tcp_connection.map(Into::into),
})
}
@@ -906,9 +877,9 @@ impl ExtensionImports for WasmState {
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
.with_context(|| format!("unzipping {path:?} archive"))?;
}
}

View File

@@ -16,7 +16,6 @@ use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
use http_client::http::{HeaderMap, HeaderValue};
use icons::IconName;
use parking_lot::Mutex;
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::fmt;
@@ -48,15 +47,6 @@ pub fn init_settings(cx: &mut App) {
registry::init(cx);
}
/// The availability of a [`LanguageModel`].
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum LanguageModelAvailability {
/// The language model is available to the general public.
Public,
/// The language model is available to users on the indicated plan.
RequiresPlan(Plan),
}
/// Configuration for caching language model messages.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageModelCacheConfiguration {
@@ -242,11 +232,6 @@ pub trait LanguageModel: Send + Sync {
None
}
/// Returns the availability of this language model.
fn availability(&self) -> LanguageModelAvailability {
LanguageModelAvailability::Public
}
/// Whether this model supports images
fn supports_images(&self) -> bool;
@@ -263,6 +248,10 @@ pub trait LanguageModel: Send + Sync {
}
const MAX_MODE_CAPABLE_MODELS: &[CloudModel] = &[
CloudModel::Anthropic(anthropic::Model::ClaudeOpus4),
CloudModel::Anthropic(anthropic::Model::ClaudeOpus4Thinking),
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4),
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4Thinking),
CloudModel::Anthropic(anthropic::Model::Claude3_7Sonnet),
CloudModel::Anthropic(anthropic::Model::Claude3_7SonnetThinking),
];

View File

@@ -13,7 +13,7 @@ use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
use strum::EnumIter;
use thiserror::Error;
use crate::{LanguageModelAvailability, LanguageModelToolSchemaFormat};
use crate::LanguageModelToolSchemaFormat;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "provider", rename_all = "lowercase")]
@@ -60,59 +60,6 @@ impl CloudModel {
}
}
/// Returns the availability of this model.
pub fn availability(&self) -> LanguageModelAvailability {
match self {
Self::Anthropic(model) => match model {
anthropic::Model::Claude3_5Sonnet
| anthropic::Model::Claude3_7Sonnet
| anthropic::Model::Claude3_7SonnetThinking => {
LanguageModelAvailability::RequiresPlan(Plan::Free)
}
anthropic::Model::Claude3Opus
| anthropic::Model::Claude3Sonnet
| anthropic::Model::Claude3Haiku
| anthropic::Model::Claude3_5Haiku
| anthropic::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}
},
Self::OpenAi(model) => match model {
open_ai::Model::ThreePointFiveTurbo
| open_ai::Model::Four
| open_ai::Model::FourTurbo
| open_ai::Model::FourOmni
| open_ai::Model::FourOmniMini
| open_ai::Model::FourPointOne
| open_ai::Model::FourPointOneMini
| open_ai::Model::FourPointOneNano
| open_ai::Model::O1Mini
| open_ai::Model::O1Preview
| open_ai::Model::O1
| open_ai::Model::O3Mini
| open_ai::Model::O3
| open_ai::Model::O4Mini
| open_ai::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}
},
Self::Google(model) => match model {
google_ai::Model::Gemini15Pro
| google_ai::Model::Gemini15Flash
| google_ai::Model::Gemini20Pro
| google_ai::Model::Gemini20Flash
| google_ai::Model::Gemini20FlashThinking
| google_ai::Model::Gemini20FlashLite
| google_ai::Model::Gemini25ProExp0325
| google_ai::Model::Gemini25ProPreview0325
| google_ai::Model::Gemini25FlashPreview0417
| google_ai::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}
},
}
}
pub fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
match self {
Self::Anthropic(_) | Self::OpenAi(_) => LanguageModelToolSchemaFormat::JsonSchema,

View File

@@ -240,8 +240,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
[
anthropic::Model::Claude3_7Sonnet,
anthropic::Model::Claude3_7SonnetThinking,
anthropic::Model::ClaudeSonnet4,
anthropic::Model::ClaudeSonnet4Thinking,
]
.into_iter()
.map(|model| self.create_language_model(model))

View File

@@ -19,8 +19,8 @@ use language_model::{
ZED_CLOUD_PROVIDER_ID,
};
use language_model::{
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
PaymentRequiredError, RefreshLlmTokenListener,
LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken, PaymentRequiredError,
RefreshLlmTokenListener,
};
use proto::Plan;
use release_channel::AppVersion;
@@ -278,7 +278,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let llm_api_token = self.state.read(cx).llm_api_token.clone();
let model = CloudModel::Anthropic(anthropic::Model::Claude3_7Sonnet);
let model = CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4);
Some(self.create_language_model(model, llm_api_token))
}
@@ -291,8 +291,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let llm_api_token = self.state.read(cx).llm_api_token.clone();
[
CloudModel::Anthropic(anthropic::Model::Claude3_7Sonnet),
CloudModel::Anthropic(anthropic::Model::Claude3_7SonnetThinking),
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4),
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4Thinking),
]
.into_iter()
.map(|model| self.create_language_model(model, llm_api_token.clone()))
@@ -331,6 +331,14 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
anthropic::Model::Claude3_7SonnetThinking.id().to_string(),
CloudModel::Anthropic(anthropic::Model::Claude3_7SonnetThinking),
);
models.insert(
anthropic::Model::ClaudeSonnet4.id().to_string(),
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4),
);
models.insert(
anthropic::Model::ClaudeSonnet4Thinking.id().to_string(),
CloudModel::Anthropic(anthropic::Model::ClaudeSonnet4Thinking),
);
}
let llm_closed_beta_models = if cx.has_flag::<LlmClosedBetaFeatureFlag>() {
@@ -699,10 +707,6 @@ impl LanguageModel for CloudLanguageModel {
format!("zed.dev/{}", self.model.id())
}
fn availability(&self) -> LanguageModelAvailability {
self.model.availability()
}
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
self.model.tool_input_format()
}

View File

@@ -39,6 +39,7 @@ async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true

View File

@@ -7,9 +7,9 @@ pub use language::*;
use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
use project::lsp_store::clangd_ext;
use serde_json::json;
use smol::fs::{self, File};
use smol::{fs, io::BufReader};
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
pub struct CLspAdapter;
@@ -32,7 +32,7 @@ impl super::LspAdapter for CLspAdapter {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: vec![],
arguments: Vec::new(),
env: None,
})
}
@@ -69,7 +69,6 @@ impl super::LspAdapter for CLspAdapter {
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
let version_dir = container_dir.join(format!("clangd_{}", version.name));
let binary_path = version_dir.join("bin/clangd");
@@ -79,28 +78,31 @@ impl super::LspAdapter for CLspAdapter {
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path).await?;
anyhow::ensure!(
response.status().is_success(),
"download failed with status {}",
response.status().to_string()
);
futures::io::copy(response.body_mut(), &mut file).await?;
let unzip_status = util::command::new_smol_command("unzip")
.current_dir(&container_dir)
.arg(&zip_path)
.output()
.await?
.status;
anyhow::ensure!(unzip_status.success(), "failed to unzip clangd archive");
extract_zip(&container_dir, BufReader::new(response.body_mut()))
.await
.with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?;
remove_matching(&container_dir, |entry| entry != version_dir).await;
// todo("windows")
#[cfg(not(windows))]
{
fs::set_permissions(
&binary_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
}
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
arguments: Vec::new(),
})
}
@@ -306,7 +308,7 @@ impl super::LspAdapter for CLspAdapter {
.map(move |diag| {
let range =
language::range_to_lsp(diag.range.to_point_utf16(&snapshot)).unwrap();
let mut tags = vec![];
let mut tags = Vec::with_capacity(1);
if diag.diagnostic.is_unnecessary {
tags.push(DiagnosticTag::UNNECESSARY);
}
@@ -344,7 +346,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
Ok(LanguageServerBinary {
path: clangd_bin,
env: None,
arguments: vec![],
arguments: Vec::new(),
})
})
.await

View File

@@ -3,6 +3,7 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
use dap::DapRegistry;
use futures::StreamExt;
use gpui::{App, AsyncApp};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
@@ -25,8 +26,8 @@ use std::{
str::FromStr,
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@@ -75,7 +76,11 @@ impl JsonLspAdapter {
}
}
fn get_workspace_config(language_names: Vec<String>, cx: &mut App) -> Value {
fn get_workspace_config(
language_names: Vec<String>,
adapter_schemas: AdapterSchemas,
cx: &mut App,
) -> Value {
let keymap_schema = KeymapFile::generate_json_schema_for_registered_actions(cx);
let font_names = &cx.text_system().all_font_names();
let settings_schema = cx.global::<SettingsStore>().json_schema(
@@ -85,8 +90,9 @@ impl JsonLspAdapter {
},
cx,
);
let tasks_schema = task::TaskTemplates::generate_json_schema();
let debug_schema = task::DebugTaskFile::generate_json_schema();
let debug_schema = task::DebugTaskFile::generate_json_schema(&adapter_schemas);
let snippets_schema = snippet_provider::format::VsSnippetsFile::generate_json_schema();
let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
@@ -160,8 +166,15 @@ impl JsonLspAdapter {
}
}
let mut writer = self.workspace_config.write().await;
let config =
cx.update(|cx| Self::get_workspace_config(self.languages.language_names(), cx))?;
let adapter_schemas = cx
.read_global::<DapRegistry, _>(|dap_registry, _| dap_registry.to_owned())?
.adapters_schema()
.await;
let config = cx.update(|cx| {
Self::get_workspace_config(self.languages.language_names().clone(), adapter_schemas, cx)
})?;
writer.replace(config.clone());
return Ok(config);
}
@@ -429,7 +442,7 @@ impl LspAdapter for NodeVersionAdapter {
.await
.context("downloading release")?;
if version.url.ends_with(".zip") {
node_runtime::extract_zip(
extract_zip(
&destination_container_path,
BufReader::new(response.body_mut()),
)

View File

@@ -22,6 +22,7 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
@@ -215,14 +216,11 @@ impl LspAdapter for RustLspAdapter {
})?;
}
AssetKind::Zip => {
node_runtime::extract_zip(
&destination_path,
BufReader::new(response.body_mut()),
)
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
extract_zip(&destination_path, BufReader::new(response.body_mut()))
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
};

View File

@@ -19,6 +19,7 @@ use std::{
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::archive::extract_zip;
use util::{ResultExt, fs::remove_matching, maybe};
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
@@ -514,14 +515,11 @@ impl LspAdapter for EsLintLspAdapter {
})?;
}
AssetKind::Zip => {
node_runtime::extract_zip(
&destination_path,
BufReader::new(response.body_mut()),
)
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
extract_zip(&destination_path, BufReader::new(response.body_mut()))
.await
.with_context(|| {
format!("unzipping {} to {:?}", version.url, destination_path)
})?;
}
}

View File

@@ -58,6 +58,8 @@ pub enum Model {
OpenMistralNemo,
#[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")]
OpenCodestralMamba,
#[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")]
DevstralSmallLatest,
#[serde(rename = "custom")]
Custom {
@@ -96,6 +98,7 @@ impl Model {
Self::MistralSmallLatest => "mistral-small-latest",
Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralSmallLatest => "devstral-small-latest",
Self::Custom { name, .. } => name,
}
}
@@ -108,6 +111,7 @@ impl Model {
Self::MistralSmallLatest => "mistral-small-latest",
Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralSmallLatest => "devstral-small-latest",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -122,6 +126,7 @@ impl Model {
Self::MistralSmallLatest => 32000,
Self::OpenMistralNemo => 131000,
Self::OpenCodestralMamba => 256000,
Self::DevstralSmallLatest => 262144,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -142,7 +147,8 @@ impl Model {
| Self::MistralMediumLatest
| Self::MistralSmallLatest
| Self::OpenMistralNemo
| Self::OpenCodestralMamba => true,
| Self::OpenCodestralMamba
| Self::DevstralSmallLatest => true,
Self::Custom { supports_tools, .. } => supports_tools.unwrap_or(false),
}
}

View File

@@ -13,7 +13,7 @@ path = "src/node_runtime.rs"
doctest = false
[features]
test-support = ["tempfile"]
test-support = []
[dependencies]
anyhow.workspace = true
@@ -21,7 +21,6 @@ async-compression.workspace = true
async-watch.workspace = true
async-tar.workspace = true
async-trait.workspace = true
async_zip.workspace = true
futures.workspace = true
http_client.workspace = true
log.workspace = true
@@ -30,14 +29,9 @@ semver.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tempfile = { workspace = true, optional = true }
util.workspace = true
walkdir = "2.5.0"
which.workspace = true
workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
tempfile.workspace = true

View File

@@ -1,7 +1,4 @@
mod archive;
use anyhow::{Context as _, Result, anyhow, bail};
pub use archive::extract_zip;
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};
@@ -19,6 +16,7 @@ use std::{
sync::Arc,
};
use util::ResultExt;
use util::archive::extract_zip;
const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS";
@@ -353,7 +351,7 @@ impl ManagedNodeRuntime {
let archive = Archive::new(decompressed_bytes);
archive.unpack(&node_containing_dir).await?;
}
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?,
}
}

View File

@@ -54,9 +54,8 @@ fn get_max_tokens(name: &str) -> usize {
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
| "dolphin-mixtral" => 32768,
"llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r"
| "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder" => {
128000
}
| "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder"
| "devstral" => 128000,
_ => DEFAULT_TOKENS,
}
.clamp(1, MAXIMUM_TOKENS)

View File

@@ -195,10 +195,7 @@ impl DapStore {
.and_then(|s| s.binary.as_ref().map(PathBuf::from));
let delegate = self.delegate(&worktree, console, cx);
let cwd: Arc<Path> = definition
.cwd()
.unwrap_or(worktree.read(cx).abs_path().as_ref())
.into();
let cwd: Arc<Path> = worktree.read(cx).abs_path().as_ref().into();
cx.spawn(async move |this, cx| {
let mut binary = adapter
@@ -416,9 +413,7 @@ impl DapStore {
})?
.await?;
if let Some(args) = definition.initialize_args {
merge_json_value_into(args, &mut binary.request_args.configuration);
}
merge_json_value_into(definition.config, &mut binary.request_args.configuration);
session
.update(cx, |session, cx| {

View File

@@ -82,10 +82,8 @@ impl DapLocator for CargoLocator {
task_template,
locator_name: Some(self.name()),
}),
request: None,
initialize_args: None,
config: serde_json::Value::Null,
tcp_connection: None,
stop_on_entry: None,
})
}

View File

@@ -24,7 +24,7 @@ use dap::{
messages::{Events, Message},
};
use dap::{
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory,
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
RunInTerminalRequestArguments, StartDebuggingRequestArguments,
};
use futures::channel::{mpsc, oneshot};
@@ -674,6 +674,7 @@ pub enum SessionEvent {
request: RunInTerminalRequestArguments,
sender: mpsc::Sender<Result<u32>>,
},
ConsoleOutput,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -863,7 +864,7 @@ impl Session {
pub fn binary(&self) -> &DebugAdapterBinary {
let Mode::Running(local_mode) = &self.mode else {
panic!("Session is not local");
panic!("Session is not running");
};
&local_mode.binary
}
@@ -885,9 +886,8 @@ impl Session {
cx.spawn(async move |this, cx| {
while let Some(output) = rx.next().await {
this.update(cx, |this, _| {
this.output_token.0 += 1;
this.output.push_back(dap::OutputEvent {
this.update(cx, |this, cx| {
let event = dap::OutputEvent {
category: None,
output,
group: None,
@@ -897,7 +897,8 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
this.push_output(event, cx);
})?;
}
anyhow::Ok(())
@@ -1266,8 +1267,7 @@ impl Session {
return;
}
self.output.push_back(event);
self.output_token.0 += 1;
self.push_output(event, cx);
cx.notify();
}
Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| {
@@ -1445,6 +1445,12 @@ impl Session {
});
}
fn push_output(&mut self, event: OutputEvent, cx: &mut Context<Self>) {
self.output.push_back(event);
self.output_token.0 += 1;
cx.emit(SessionEvent::ConsoleOutput);
}
pub fn any_stopped_thread(&self) -> bool {
self.thread_states.any_stopped_thread()
}
@@ -2063,8 +2069,7 @@ impl Session {
source: Option<Source>,
cx: &mut Context<Self>,
) -> Task<()> {
self.output_token.0 += 1;
self.output.push_back(dap::OutputEvent {
let event = dap::OutputEvent {
category: None,
output: format!("> {expression}"),
group: None,
@@ -2074,7 +2079,8 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
self.push_output(event, cx);
let request = self.mode.request_dap(EvaluateCommand {
expression,
context,
@@ -2086,8 +2092,7 @@ impl Session {
this.update(cx, |this, cx| {
match response {
Ok(response) => {
this.output_token.0 += 1;
this.output.push_back(dap::OutputEvent {
let event = dap::OutputEvent {
category: None,
output: format!("< {}", &response.result),
group: None,
@@ -2097,11 +2102,11 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
this.push_output(event, cx);
}
Err(e) => {
this.output_token.0 += 1;
this.output.push_back(dap::OutputEvent {
let event = dap::OutputEvent {
category: None,
output: format!("{}", e),
group: None,
@@ -2111,7 +2116,8 @@ impl Session {
column: None,
data: None,
location_reference: None,
});
};
this.push_output(event, cx);
}
};
this.invalidate_command_type::<ScopesCommand>();

View File

@@ -348,11 +348,11 @@ impl LocalLspStore {
delegate.update_status(
adapter.name(),
BinaryStatus::Failed {
error: format!("{err}\n-- stderr--\n{}", log),
error: format!("{err}\n-- stderr--\n{log}"),
},
);
log::error!("Failed to start language server {server_name:?}: {err}");
log::error!("server stderr: {:?}", log);
log::error!("Failed to start language server {server_name:?}: {err:#?}");
log::error!("server stderr: {log}");
None
}
}

View File

@@ -15,7 +15,7 @@ use anyhow::Result;
use collections::HashMap;
use fs::Fs;
use gpui::{App, AppContext as _, Context, Entity, Task};
use util::ResultExt;
use util::{ResultExt, archive::extract_zip};
pub(crate) struct YarnPathStore {
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
@@ -131,7 +131,7 @@ fn zip_path(path: &Path) -> Option<&Path> {
async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
let contents = fs.load_bytes(&path).await?;
node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
Ok(dir)
}

View File

@@ -461,13 +461,8 @@ message DapModule {
message DebugTaskDefinition {
string adapter = 1;
string label = 2;
oneof request {
DebugLaunchRequest debug_launch_request = 3;
DebugAttachRequest debug_attach_request = 4;
}
optional string initialize_args = 5;
optional TcpHost tcp_connection = 6;
optional bool stop_on_entry = 7;
string config = 3;
optional TcpHost tcp_connection = 4;
}
message TcpHost {

View File

@@ -0,0 +1,62 @@
use anyhow::Result;
use gpui::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
/// Represents a schema for a specific adapter
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct AdapterSchema {
/// The adapter name identifier
pub adapter: SharedString,
/// The JSON schema for this adapter's configuration
pub schema: serde_json::Value,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct AdapterSchemas(pub Vec<AdapterSchema>);
impl AdapterSchemas {
pub fn generate_json_schema(&self) -> Result<serde_json_lenient::Value> {
let adapter_conditions = self
.0
.iter()
.map(|adapter_schema| {
let adapter_name = adapter_schema.adapter.to_string();
json!({
"if": {
"properties": {
"adapter": { "const": adapter_name }
}
},
"then": adapter_schema.schema
})
})
.collect::<Vec<_>>();
let schema = serde_json_lenient::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Debug Adapter Configurations",
"description": "Configuration for debug adapters. Schema changes based on the selected adapter.",
"type": "array",
"items": {
"type": "object",
"required": ["adapter", "label"],
"properties": {
"adapter": {
"type": "string",
"description": "The name of the debug adapter"
},
"label": {
"type": "string",
"description": "The name of the debug configuration"
},
},
"allOf": adapter_conditions
}
});
Ok(serde_json_lenient::to_value(schema)?)
}
}

View File

@@ -1,12 +1,12 @@
use anyhow::{Context as _, Result};
use collections::FxHashMap;
use gpui::SharedString;
use schemars::{JsonSchema, r#gen::SchemaSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;
use std::path::PathBuf;
use std::{net::Ipv4Addr, path::Path};
use crate::TaskTemplate;
use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
/// Represents the host information of the debug adapter
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
@@ -106,7 +106,7 @@ impl LaunchRequest {
/// Represents the type that will determine which request to call on the debug adapter
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "lowercase", untagged)]
#[serde(rename_all = "lowercase", tag = "request")]
pub enum DebugRequest {
/// Call the `launch` request on the debug adapter
Launch(LaunchRequest),
@@ -193,8 +193,30 @@ pub enum BuildTaskDefinition {
locator_name: Option<SharedString>,
},
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
pub enum Request {
Launch,
Attach,
}
/// This struct represent a user created debug task from the new session modal
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ZedDebugConfig {
/// Name of the debug task
pub label: SharedString,
/// The debug adapter to use
pub adapter: SharedString,
#[serde(flatten)]
pub request: DebugRequest,
/// Whether to tell the debug adapter to stop on entry
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>,
}
/// This struct represent a user created debug task
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct DebugScenario {
pub adapter: SharedString,
@@ -203,11 +225,9 @@ pub struct DebugScenario {
/// A task to run prior to spawning the debuggee.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build: Option<BuildTaskDefinition>,
#[serde(flatten)]
pub request: Option<DebugRequest>,
/// Additional initialization arguments to be sent on DAP initialization
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initialize_args: Option<serde_json::Value>,
/// The main arguments to be sent to the debug adapter
#[serde(default, flatten)]
pub config: serde_json::Value,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
@@ -215,19 +235,6 @@ pub struct DebugScenario {
/// that is already running or is started by another process.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tcp_connection: Option<TcpArgumentsTemplate>,
/// Whether to tell the debug adapter to stop on entry
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>,
}
impl DebugScenario {
pub fn cwd(&self) -> Option<&Path> {
if let Some(DebugRequest::Launch(config)) = &self.request {
config.cwd.as_ref().map(Path::new)
} else {
None
}
}
}
/// A group of Debug Tasks defined in a JSON file.
@@ -237,32 +244,15 @@ pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile {
/// Generates JSON schema of Tasks JSON template format.
pub fn generate_json_schema() -> serde_json_lenient::Value {
let schema = SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<Self>();
serde_json_lenient::to_value(schema).unwrap()
pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
schemas.generate_json_schema().unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use crate::{DebugRequest, DebugScenario, LaunchRequest};
#[test]
fn test_can_deserialize_non_attach_task() {
let deserialized: DebugRequest =
serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
assert_eq!(
deserialized,
DebugRequest::Launch(LaunchRequest {
program: "cafebabe".to_owned(),
..Default::default()
})
);
}
use crate::DebugScenario;
use serde_json::json;
#[test]
fn test_empty_scenario_has_none_request() {
@@ -273,7 +263,10 @@ mod tests {
}"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
assert_eq!(deserialized.request, None);
assert_eq!(json!({}), deserialized.config);
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
assert_eq!("Build & debug rust", deserialized.label.as_ref());
}
#[test]
@@ -281,18 +274,19 @@ mod tests {
let json = r#"{
"label": "Launch program",
"adapter": "CodeLLDB",
"request": "launch",
"program": "target/debug/myapp",
"args": ["--test"]
}"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
match deserialized.request {
Some(DebugRequest::Launch(launch)) => {
assert_eq!(launch.program, "target/debug/myapp");
assert_eq!(launch.args, vec!["--test"]);
}
_ => panic!("Expected Launch request"),
}
assert_eq!(
json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }),
deserialized.config
);
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
assert_eq!("Launch program", deserialized.label.as_ref());
}
#[test]
@@ -300,15 +294,17 @@ mod tests {
let json = r#"{
"label": "Attach to process",
"adapter": "CodeLLDB",
"process_id": 1234
"process_id": 1234,
"request": "attach"
}"#;
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
match deserialized.request {
Some(DebugRequest::Attach(attach)) => {
assert_eq!(attach.process_id, Some(1234));
}
_ => panic!("Expected Attach request"),
}
assert_eq!(
json!({ "request": "attach", "process_id": 1234 }),
deserialized.config
);
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
assert_eq!("Attach to process", deserialized.label.as_ref());
}
}

View File

@@ -1,5 +1,6 @@
//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic.
mod adapter_schema;
mod debug_format;
mod serde_helpers;
pub mod static_source;
@@ -15,14 +16,14 @@ use std::borrow::Cow;
use std::path::PathBuf;
use std::str::FromStr;
pub use adapter_schema::{AdapterSchema, AdapterSchemas};
pub use debug_format::{
AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest,
TcpArgumentsTemplate,
Request, TcpArgumentsTemplate, ZedDebugConfig,
};
pub use task_template::{
DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
substitute_all_template_variables_in_str, substitute_variables_in_map,
substitute_variables_in_str,
substitute_variables_in_map, substitute_variables_in_str,
};
pub use vscode_debug_format::VsCodeDebugTaskFile;
pub use vscode_format::VsCodeTaskFile;

View File

@@ -315,7 +315,7 @@ pub fn substitute_variables_in_str(template_str: &str, context: &TaskContext) ->
&mut substituted_variables,
)
}
pub fn substitute_all_template_variables_in_str<A: AsRef<str>>(
fn substitute_all_template_variables_in_str<A: AsRef<str>>(
template_str: &str,
task_variables: &HashMap<String, A>,
variable_names: &HashMap<String, VariableName>,

View File

@@ -1,14 +1,10 @@
use std::path::PathBuf;
use anyhow::Context as _;
use collections::HashMap;
use gpui::SharedString;
use serde::Deserialize;
use util::ResultExt as _;
use crate::{
AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, EnvVariableReplacer, LaunchRequest,
TcpArgumentsTemplate, VariableName,
DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName,
};
#[derive(Clone, Debug, Deserialize, PartialEq)]
@@ -40,7 +36,7 @@ struct VsCodeDebugTaskDefinition {
#[serde(default)]
stop_on_entry: Option<bool>,
#[serde(flatten)]
other_attributes: HashMap<String, serde_json_lenient::Value>,
other_attributes: serde_json::Value,
}
impl VsCodeDebugTaskDefinition {
@@ -50,33 +46,6 @@ impl VsCodeDebugTaskDefinition {
let definition = DebugScenario {
label,
build: None,
request: match self.request {
Request::Launch => {
let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd)));
let program = self
.program
.context("vscode debug launch configuration does not define a program")?;
let program = replacer.replace(&program);
let args = self
.args
.into_iter()
.map(|arg| replacer.replace(&arg))
.collect();
let env = self
.env
.into_iter()
.filter_map(|(k, v)| v.map(|v| (k, v)))
.collect();
DebugRequest::Launch(LaunchRequest {
program,
cwd,
args,
env,
})
.into()
}
Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }).into(),
},
adapter: task_type_to_adapter_name(&self.r#type),
// TODO host?
tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
@@ -84,9 +53,7 @@ impl VsCodeDebugTaskDefinition {
host: None,
timeout: None,
}),
stop_on_entry: self.stop_on_entry,
// TODO
initialize_args: None,
config: self.other_attributes,
};
Ok(definition)
}
@@ -135,10 +102,9 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
#[cfg(test)]
mod tests {
use serde_json::json;
use collections::FxHashMap;
use crate::{DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate};
use crate::{DebugScenario, DebugTaskFile, TcpArgumentsTemplate};
use super::VsCodeDebugTaskFile;
@@ -173,19 +139,14 @@ mod tests {
DebugTaskFile(vec![DebugScenario {
label: "Debug my JS app".into(),
adapter: "JavaScript".into(),
stop_on_entry: Some(true),
initialize_args: None,
config: json!({
"showDevDebugOutput": false,
}),
tcp_connection: Some(TcpArgumentsTemplate {
port: Some(17),
host: None,
timeout: None,
}),
request: Some(DebugRequest::Launch(LaunchRequest {
program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
env: FxHashMap::from_iter([("X".into(), "Y".into())])
})),
build: None
}])
);

View File

@@ -154,7 +154,6 @@ pub struct ContextMenu {
key_context: SharedString,
_on_blur_subscription: Subscription,
keep_open_on_confirm: bool,
eager: bool,
documentation_aside: Option<(usize, DocumentationAside)>,
fixed_width: Option<DefiniteLength>,
}
@@ -207,7 +206,6 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
eager: false,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -250,43 +248,6 @@ impl ContextMenu {
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: true,
eager: false,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
},
window,
cx,
)
})
}
pub fn build_eager(
window: &mut Window,
cx: &mut App,
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
let _on_blur_subscription = cx.on_blur(
&focus_handle,
window,
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
);
window.refresh();
f(
Self {
builder: None,
items: Default::default(),
focus_handle,
action_context: None,
selected_index: None,
delayed: false,
clicked: false,
key_context: "menu".into(),
_on_blur_subscription,
keep_open_on_confirm: false,
eager: true,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -327,7 +288,6 @@ impl ContextMenu {
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
),
keep_open_on_confirm: false,
eager: false,
documentation_aside: None,
fixed_width: None,
end_slot_action: None,
@@ -634,10 +594,7 @@ impl ContextMenu {
..
})
| ContextMenuItem::CustomEntry { handler, .. },
) = self
.selected_index
.and_then(|ix| self.items.get(ix))
.filter(|_| !self.eager)
) = self.selected_index.and_then(|ix| self.items.get(ix))
{
(handler)(context, window, cx)
}
@@ -740,10 +697,9 @@ impl ContextMenu {
fn select_index(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let context = self.action_context.as_ref();
self.documentation_aside = None;
let item = self.items.get(ix)?;
if item.is_selectable() {
@@ -752,9 +708,6 @@ impl ContextMenu {
if let Some(callback) = &entry.documentation_aside {
self.documentation_aside = Some((ix, callback.clone()));
}
if self.eager && !entry.disabled {
(entry.handler)(context, window, cx)
}
}
}
Some(ix)

View File

@@ -2,6 +2,8 @@ use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
use crate::{ContextMenu, PopoverMenu, prelude::*};
use super::PopoverMenuHandle;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DropdownStyle {
#[default]
@@ -22,6 +24,7 @@ pub struct DropdownMenu {
menu: Entity<ContextMenu>,
full_width: bool,
disabled: bool,
handle: Option<PopoverMenuHandle<ContextMenu>>,
}
impl DropdownMenu {
@@ -37,6 +40,7 @@ impl DropdownMenu {
menu,
full_width: false,
disabled: false,
handle: None,
}
}
@@ -52,6 +56,7 @@ impl DropdownMenu {
menu,
full_width: false,
disabled: false,
handle: None,
}
}
@@ -64,6 +69,11 @@ impl DropdownMenu {
self.full_width = full_width;
self
}
pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
self.handle = Some(handle);
self
}
}
impl Disableable for DropdownMenu {
@@ -85,6 +95,7 @@ impl RenderOnce for DropdownMenu {
.style(self.style),
)
.attach(Corner::BottomLeft)
.when_some(self.handle.clone(), |el, handle| el.with_handle(handle))
}
}
@@ -159,17 +170,11 @@ pub struct DropdownTriggerStyle {
impl DropdownTriggerStyle {
pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
let colors = cx.theme().colors();
if style == DropdownStyle::Solid {
Self {
// why is this editor_background?
bg: colors.editor_background,
}
} else {
Self {
bg: colors.ghost_element_background,
}
}
let bg = match style {
DropdownStyle::Solid => colors.editor_background,
DropdownStyle::Ghost => colors.ghost_element_background,
};
Self { bg }
}
}

View File

@@ -18,6 +18,7 @@ test-support = ["tempfile", "git2", "rand", "util_macros"]
[dependencies]
anyhow.workspace = true
async-fs.workspace = true
async_zip.workspace = true
collections.workspace = true
dirs.workspace = true
futures-lite.workspace = true
@@ -31,11 +32,13 @@ regex.workspace = true
rust-embed.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
smol.workspace = true
take-until.workspace = true
tempfile = { workspace = true, optional = true }
unicase.workspace = true
util_macros = { workspace = true, optional = true }
walkdir.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]

View File

@@ -1,4 +1,5 @@
pub mod arc_cow;
pub mod archive;
pub mod command;
pub mod fs;
pub mod markdown;
@@ -400,6 +401,31 @@ pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {
}
}
pub fn merge_json_lenient_value_into(
source: serde_json_lenient::Value,
target: &mut serde_json_lenient::Value,
) {
match (source, target) {
(serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => {
for (key, value) in source {
if let Some(target) = target.get_mut(&key) {
merge_json_lenient_value_into(value, target);
} else {
target.insert(key, value);
}
}
}
(serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => {
for value in source {
target.push(value);
}
}
(source, target) => *target = source,
}
}
pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
use serde_json::Value;

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.188.0"
version = "0.188.1"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -14,7 +14,7 @@ use license_detection::LICENSE_FILES_TO_CHECK;
pub use license_detection::is_license_eligible_for_data_collection;
pub use rate_completion_modal::*;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use arrayvec::ArrayVec;
use client::{Client, UserStore};
use collections::{HashMap, HashSet, VecDeque};
@@ -23,7 +23,7 @@ use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, SemanticVersion,
Subscription, Task, WeakEntity, actions,
};
use http_client::{HttpClient, Method};
use http_client::{AsyncBody, HttpClient, Method, Request, Response};
use input_excerpt::excerpt_for_cursor_position;
use language::{
Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint, text_diff,
@@ -54,8 +54,8 @@ use workspace::Workspace;
use workspace::notifications::{ErrorMessagePrompt, NotificationId};
use worktree::Worktree;
use zed_llm_client::{
EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, PredictEditsBody,
PredictEditsResponse, ZED_VERSION_HEADER_NAME,
AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME,
PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME,
};
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
@@ -823,6 +823,74 @@ and then another
}
}
fn accept_edit_prediction(
&mut self,
request_id: InlineCompletionId,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let llm_token = self.llm_token.clone();
let app_version = AppVersion::global(cx);
cx.spawn(async move |this, cx| {
let http_client = client.http_client();
let mut response = llm_token_retry(&llm_token, &client, |token| {
let request_builder = http_client::Request::builder().method(Method::POST);
let request_builder =
if let Ok(accept_prediction_url) = std::env::var("ZED_ACCEPT_PREDICTION_URL") {
request_builder.uri(accept_prediction_url)
} else {
request_builder.uri(
http_client
.build_zed_llm_url("/predict_edits/accept", &[])?
.as_ref(),
)
};
Ok(request_builder
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", token))
.header(ZED_VERSION_HEADER_NAME, app_version.to_string())
.body(
serde_json::to_string(&AcceptEditPredictionBody {
request_id: request_id.0,
})?
.into(),
)?)
})
.await?;
if let Some(minimum_required_version) = response
.headers()
.get(MINIMUM_REQUIRED_VERSION_HEADER_NAME)
.and_then(|version| SemanticVersion::from_str(version.to_str().ok()?).ok())
{
if app_version < minimum_required_version {
return Err(anyhow!(ZedUpdateRequiredError {
minimum_version: minimum_required_version
}));
}
}
if response.status().is_success() {
if let Some(usage) = EditPredictionUsage::from_headers(response.headers()).ok() {
this.update(cx, |this, cx| {
this.last_usage = Some(usage);
cx.notify();
})?;
}
Ok(())
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Err(anyhow!(
"error accepting edit prediction.\nStatus: {:?}\nBody: {}",
response.status(),
body
))
}
})
}
fn process_completion_response(
prediction_response: PredictEditsResponse,
buffer: Entity<Buffer>,
@@ -1381,6 +1449,34 @@ impl ProviderDataCollection {
}
}
async fn llm_token_retry(
llm_token: &LlmApiToken,
client: &Arc<Client>,
build_request: impl Fn(String) -> Result<Request<AsyncBody>>,
) -> Result<Response<AsyncBody>> {
let mut did_retry = false;
let http_client = client.http_client();
let mut token = llm_token.acquire(client).await?;
loop {
let request = build_request(token.clone())?;
let response = http_client.send(request).await?;
if !did_retry
&& !response.status().is_success()
&& response
.headers()
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
.is_some()
{
did_retry = true;
token = llm_token.refresh(client).await?;
continue;
}
return Ok(response);
}
}
pub struct ZetaInlineCompletionProvider {
zeta: Entity<Zeta>,
pending_completions: ArrayVec<PendingCompletion, 2>,
@@ -1597,7 +1693,18 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
// Right now we don't support cycling.
}
fn accept(&mut self, _cx: &mut Context<Self>) {
fn accept(&mut self, cx: &mut Context<Self>) {
let completion_id = self
.current_completion
.as_ref()
.map(|completion| completion.completion.id);
if let Some(completion_id) = completion_id {
self.zeta
.update(cx, |zeta, cx| {
zeta.accept_edit_prediction(completion_id, cx)
})
.detach();
}
self.pending_completions.clear();
}

View File

@@ -32,6 +32,7 @@
- [Channels](./channels.md)
- [Collaboration](./collaboration.md)
- [Git](./git.md)
- [Debugging (Beta)](./debuggers.md)
- [Tasks](./tasks.md)
- [Remote Development](./remote-development.md)
- [Environment Variables](./environment.md)

View File

@@ -1,4 +1,4 @@
# Debugger
# Debugger (Beta)
Zed uses the Debug Adapter Protocol (DAP) to provide debugging functionality across multiple programming languages.
DAP is a standardized protocol that defines how debuggers, editors, and IDEs communicate with each other.
@@ -22,187 +22,58 @@ Zed supports a variety of debug adapters for different programming languages:
- PHP (xdebug): Provides debugging and profiling capabilities for PHP applications, including remote debugging and code coverage analysis.
- Custom: Allows you to configure any debug adapter that supports the Debug Adapter Protocol, enabling debugging for additional languages or specialized environments not natively supported by Zed.
- Ruby (rdbg): Provides debugging capabilities for Ruby applications
These adapters enable Zed to provide a consistent debugging experience across multiple languages while leveraging the specific features and capabilities of each debugger.
## How To Get Started
## Getting Started
To start a debug session, we added few default debug configurations for each supported language that supports generic configuration options. To see all the available debug configurations, you can use the command palette `debugger: start` action, this should list all the available debug configurations.
For basic debugging you can set up a new configuration by opening up the `New Session Modal` either by the `debugger: start` (default: f4) or clicking the plus icon at the top right of the debug panel. Once the `New Session Modal` is open you can click custom on the bottom left to open a view that allows you to create a custom debug configuration. Once you have created a configuration you can save it to your workspace's `.zed/debug.json` by clicking on the save button on the bottom left.
For more advance use cases you can create debug configurations by directly editing the `.zed/debug.json` file in your project root directory. Once you fill out the adapter and label fields completions will show you the available options of the selected debug adapter.
You can then use the `New Session Modal` to select a configuration then start debugging.
### Configuration
To create a custom debug configuration you have to create a `.zed/debug.json` file in your project root directory. This file should contain an array of debug configurations, each with a unique label and adapter the other option are optional/required based on the adapter.
While configuration fields are debug adapter dependent, most adapters support the follow fields.
```json
[
{
// The label for the debug configuration and used to identify the debug session inside the debug panel
// The label for the debug configuration and used to identify the debug session inside the debug panel & new session modal
"label": "Example Start debugger config",
// The debug adapter that Zed should use to debug the program
"adapter": "custom",
// Request: defaults to launch
"adapter": "Example adapter name",
// Request:
// - launch: Zed will launch the program if specified or shows a debug terminal with the right configuration
// - attach: Zed will attach to a running program to debug it or when the process_id is not specified we will show a process picker (only supported for node currently)
"request": "launch",
// cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT)
// this field also supports task variables e.g. $ZED_WORKTREE_ROOT
"cwd": "$ZED_WORKTREE_ROOT",
// program: The program that you want to debug
// this fields also support task variables e.g. $ZED_FILE
// Note: this field should only contain the path to the program you want to debug
// This field supports path resolution with ~ or . symbols
"program": "path_to_program",
// initialize_args: This field should contain all the adapter specific initialization arguments that are directly send to the debug adapter
"initialize_args": {
// "stopOnEntry": true // e.g. to stop on the first line of the program (These args are DAP specific)
},
// connection: the connection that a custom debugger should use
"connection": "stdio",
// The cli command used to start the debug adapter e.g. `python3`, `node` or the adapter binary
"command": "path_to_cli"
// cwd: defaults to the current working directory of your project ($ZED_WORKTREE_ROOT)
"cwd": "$ZED_WORKTREE_ROOT"
}
]
```
### Using Attach [WIP]
#### Task Variables
Only javascript and lldb supports starting a debug session using attach.
When using the attach request with a process ID the syntax is as follows:
```json
{
"label": "Attach to Process",
"adapter": "javascript",
"request": {
"attach": {
"process_id": "12345"
}
}
}
```
Without process ID the syntax is as follows:
```json
{
"label": "Attach to Process",
"adapter": "javascript",
"request": {
"attach": {}
}
}
```
#### JavaScript Configuration
##### Debug Active File
This configuration allows you to debug a JavaScript file in your project.
```json
{
"label": "JavaScript: Debug Active File",
"adapter": "javascript",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
##### Debug Terminal
This configuration will spawn a debug terminal where you could start you program by typing `node test.js`, and the debug adapter will automatically attach to the process.
```json
{
"label": "JavaScript: Debug Terminal",
"adapter": "javascript",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
// "program": "$ZED_FILE", // optional if you pass this in, you will see the output inside the terminal itself
"initialize_args": {
"console": "integratedTerminal"
}
}
```
#### PHP Configuration
##### Debug Active File
This configuration allows you to debug a PHP file in your project.
```json
{
"label": "PHP: Debug Active File",
"adapter": "php",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
#### Python Configuration
##### Debug Active File
This configuration allows you to debug a Python file in your project.
```json
{
"label": "Python: Debug Active File",
"adapter": "python",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
#### GDB Configuration
**NOTE:** This configuration is for Linux systems only & intel macbooks.
##### Debug Program
This configuration allows you to debug a program using GDB e.g. Zed itself.
```json
{
"label": "GDB: Debug program",
"adapter": "gdb",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
#### LLDB Configuration
##### Debug Program
This configuration allows you to debug a program using LLDB e.g. Zed itself.
```json
{
"label": "LLDB: Debug program",
"adapter": "lldb",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
}
```
All configuration fields support task variables. See [Tasks](./tasks.md)
## Breakpoints
Zed currently supports these types of breakpoints
- Log Breakpoints: Output a log message instead of stopping at the breakpoint when it's hit
- Standard Breakpoints: Stop at the breakpoint when it's hit
- Log Breakpoints: Output a log message instead of stopping at the breakpoint when it's hit
- Conditional Breakpoints: Stop at the breakpoint when it's hit if the condition is met
- Hit Breakpoints: Stop at the breakpoint when it's hit a certain number of times
Standard breakpoints can be toggled by left clicking on the editor gutter or using the Toggle Breakpoint action. Right clicking on a breakpoint, code action symbol, or code runner symbol brings up the breakpoint context menu. That has options for toggling breakpoints and editing log breakpoints.
Standard breakpoints can be toggled by left clicking on the editor gutter or using the Toggle Breakpoint action. Right clicking on a breakpoint, right clicking on a code runner symbol brings up the breakpoint context menu. That has options for toggling breakpoints and editing log breakpoints.
Log breakpoints can also be edited/added through the edit log breakpoint action
Other kinds of breakpoints can be toggled/edited by right clicking on the breakpoint icon in the gutter and selecting the desired option.
## Settings

View File

@@ -19,6 +19,7 @@ ahash = { version = "0.8", features = ["serde"] }
aho-corasick = { version = "1" }
anstream = { version = "0.6" }
arrayvec = { version = "0.7", features = ["serde"] }
async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
async-std = { version = "1", features = ["attributes", "unstable"] }
async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
aws-config = { version = "1", features = ["behavior-version-latest"] }
@@ -145,6 +146,7 @@ ahash = { version = "0.8", features = ["serde"] }
aho-corasick = { version = "1" }
anstream = { version = "0.6" }
arrayvec = { version = "0.7", features = ["serde"] }
async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
async-std = { version = "1", features = ["attributes", "unstable"] }
async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
aws-config = { version = "1", features = ["behavior-version-latest"] }