Compare commits

...

35 Commits

Author SHA1 Message Date
Joseph T. Lyons
ff435e1b2f v0.187.x stable 2025-05-21 11:14:16 -04:00
Anthony Eid
339c1d0f34 Fix project search panic (#31089)
The panic occurred when querying a second search in the project search
multibuffer while there were dirty buffers.

The panic only happened in Nightly so there's no release notes 

Release Notes:

- N/A
2025-05-21 11:09:34 -04:00
gcp-cherry-pick-bot[bot]
2529cc7a16 Don't pass -z flag to git-cat-file (cherry-pick #31053) (#31093)
Cherry-picked Don't pass `-z` flag to git-cat-file (#31053)

Closes #30972 

Release Notes:

- Fixed a bug that prevented the `copy permalink to line` action from
working on systems with older versions of git.

Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-21 11:06:58 -04:00
Peter Tripp
2c4b30faee sublime: Don't map editor::FindNextMatch by default (#31029)
Closes: https://github.com/zed-industries/zed/issues/29535

Broken in: https://github.com/zed-industries/zed/pull/28559/files

Removes `editor::FindNextMatch` and `editor::FindPreviousMatch` from the
default sublime mappings. If you would like to use this, you will have
to add them to your user keymap. Reverts the previous behavior where
cmd-g / cmd-shift-g relies on the base keymap.

Linux:
```json
  {
    "context": "Editor && mode == full",
    "bindings": {
      "f3": "editor::FindNextMatch",
      "shift-f3": "editor::FindPreviousMatch"
    }
  }
```

MacOS:
```json
  {
    "context": "Editor && mode == full",
    "bindings": {
      "cmd-g": "editor::FindNextMatch",
      "cmd-shift-g": "editor::FindPreviousMatch"
    }
  },
```


Release Notes:

- Fixed a regression in Sublime Text keymap for find next/previous in
the search bar
2025-05-20 13:52:36 -04:00
Erik Funder Carstensen
80651c4cff Remove alt-. keybinding from terminal on macOS (#30827)
Closes: #30730
It conflicts with the `>` key on the Czech keyboard layout  
If you want the previous behavior, add `"alt-.": ["terminal::SendText",
"\u001b."]` to your keymap under the `Terminal` context.

Release Notes: 

- Improved the default terminal keybind to not conflict on Czech
keyboards

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-20 13:40:30 -04:00
Aleksei Gusev
a6e579db72 Fix ctrl-delete in terminal (#30720)
Closes #30719

Release Notes:

- Fixed `ctrl-delete` in terminal, now it deletes a word forward
2025-05-20 13:40:30 -04:00
gcp-cherry-pick-bot[bot]
a01127904f Revert "linux(x11): Add support for pasting images from clipboard (#29387)" (cherry-pick #31033) (#31041)
Cherry-picked Revert "linux(x11): Add support for pasting images from
clipboard (#29387)" (#31033)

Closes: #30523

Release Notes:

- linux: Reverted the ability to paste images on X11, as the change
broke pasting from some external applications

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-05-20 13:30:32 -04:00
gcp-cherry-pick-bot[bot]
9b75288558 Project Search: Don't prompt to save edited buffers in project search results if buffers open elsewhere (cherry-pick #31026) (#31028)
Cherry-picked Project Search: Don't prompt to save edited buffers in
project search results if buffers open elsewhere (#31026)

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-05-20 13:06:31 -04:00
Piotr Osiewicz
b2edefd6e1 extension/dap: Add resolve_tcp_template function (#31010)
Extensions cannot look up available port themselves, hence the new API.
With this I'm able to port our Ruby implementation into an extension.

Release Notes:

- N/A
2025-05-20 12:34:40 -04:00
Marshall Bowers
f810584d19 zed_extension_api: Format dap.wit (#30701)
This PR formats the `dap.wit` file.

Release Notes:

- N/A
2025-05-20 12:34:23 -04:00
Piotr Osiewicz
ac2afad2cb extension: Add debug_adapters to extension manifest (#30676)
Also pass worktree to the get_dap_binary.

Release Notes:

- N/A
2025-05-20 12:33:38 -04:00
Piotr Osiewicz
a69b020339 debugger: Surface validity of breakpoints (#30380)
We now show on the breakpoint itself whether it can ever be hit.

![image](https://github.com/user-attachments/assets/148d7712-53c9-4a0a-9fc0-4ff80dec5fb1)

Release Notes:

- N/A

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: peppidesu <bakker.pepijn@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
Co-authored-by: Jens Krause <47693+sectore@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Max Nordlund <max.nordlund@gmail.com>
Co-authored-by: Finn Evers <dev@bahn.sh>
Co-authored-by: tidely <43219534+tidely@users.noreply.github.com>
Co-authored-by: Sergei Kartsev <kartsevsb@gmail.com>
Co-authored-by: Shardul Vaidya <31039336+5herlocked@users.noreply.github.com>
Co-authored-by: Chris Kelly <amateurhuman@gmail.com>
Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Umesh Yadav <23421535+imumesh18@users.noreply.github.com>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: william341 <wwokwilliam@gmail.com>
Co-authored-by: Liam <33645555+lj3954@users.noreply.github.com>
Co-authored-by: AidanV <aidanvanduyne@gmail.com>
Co-authored-by: imumesh18 <umesh4257@gmail.com>
Co-authored-by: d1y <chenhonzhou@gmail.com>
Co-authored-by: AidanV <84053180+AidanV@users.noreply.github.com>
Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: 张小白 <364772080@qq.com>
Co-authored-by: THELOSTSOUL <1095533751@qq.com>
Co-authored-by: Ron Harel <55725807+ronharel02@users.noreply.github.com>
Co-authored-by: Tristan Hume <tristan@anthropic.com>
Co-authored-by: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
Co-authored-by: Thomas David Baker <bakert@gmail.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Rob McBroom <github@skurfer.com>
Co-authored-by: CharlesChen0823 <yongchen0823@gmail.com>
2025-05-20 12:21:57 -04:00
Cole Miller
5570248fa7 debugger: Remember focused item (#30722)
Release Notes:

- Debugger Beta: the `debug panel: toggle focus` action now preserves
the debug panel's focused item.
2025-05-20 12:21:20 -04:00
gcp-cherry-pick-bot[bot]
1c999287e2 Add minimap vscode settings import (cherry-pick #30997) (#30999)
Cherry-picked Add minimap vscode settings import (#30997)

Looks like we missed these when adding the minimap.

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-05-20 11:57:06 +03:00
Andres Suarez
31e3cd3726 title_bar: Fix config merging to respect priority (#30980)
This is a follow-up to #30450 so that _global_ `title_bar` configs
shadow _defaults_. The way `SettingsSources::json_merge` works is by
considering non-json-nulls as values to propagate. So it's important
that configs be `Option<T>` so any intent in overriding values is
captured.

This PR follows the same `*Settings<FileContent = *SettingsContent>`
pattern used throughout to keep the `Option`s in the "settings content"
type with the finalized values in the "settings" type.

Release Notes:

- N/A
2025-05-20 11:43:57 +03:00
Mikayla Maki
96c3fb7d67 zed 0.187.4 2025-05-19 17:28:10 -07:00
Mikayla Maki
af4d39efce Add end of service notifications (#30982)
Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-19 17:26:42 -07:00
Oleksiy Syvokon
b2f32c53c9 agent: Fix OpenAI models not getting first message (#30941)
Closes #30733

Release Notes:

- N/A
2025-05-19 10:42:25 -04:00
Oleksiy Syvokon
186660ed20 agent: Fix path checks in edit_file (#30909)
- Fixed bug where creating a file failed when the root path wasn't
provided

- Many new checks for the edit_file path

Closes #30706

Release Notes:

- N/A
2025-05-19 10:42:15 -04:00
Oleksiy Syvokon
6b0d58da9d agent: Fix unnecessary "tool result too long" (#30798)
Release Notes:

- N/A
2025-05-19 10:41:31 -04:00
gcp-cherry-pick-bot[bot]
853b70687d project_settings: Fix default settings values for DiagnosticsSettings (cherry-pick #30686) (#30879) 2025-05-17 17:39:53 +02:00
gcp-cherry-pick-bot[bot]
75b8203566 Fix project search unsaved edits (cherry-pick #30864) (#30865)
Cherry-picked Fix project search unsaved edits (#30864)

Closes #30820

Release Notes:

- Fixed an issue where entering a new search in the project search would
drop unsaved edits in the project search buffer

---------

Co-authored-by: Mark Janssen <20283+praseodym@users.noreply.github.com>

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Mark Janssen <20283+praseodym@users.noreply.github.com>
2025-05-17 06:26:00 -04:00
Joseph T. Lyons
1d0b4df24f zed 0.187.3 2025-05-16 16:26:22 +02:00
Remco Smits
2c861184fa debugger: Prevent pane context menu from showing on secondary mouse click in list entries (#30781)
This PR prevents the debug panel pane context menu from showing when you
click your secondary mouse button in **stackframe**, **breakpoint** and
**module** list entries.

Release Notes:

- N/A
2025-05-16 16:24:41 +02:00
Anthony Eid
7c0c5bd516 debugger: Fix inline values panic when selecting stack frames (#30821)
Release Notes:

- debugger beta: Fix panic that could occur when selecting a stack frame
- debugger beta: Fix inline values not showing in stack trace view

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-16 16:24:41 +02:00
Joseph T. Lyons
8f9b217f79 zed 0.187.2 2025-05-15 12:44:38 +02:00
Cole Miller
de30643e74 debugger: Make the stack frame list and module list keyboard-navigable (#30682)
- Switch stack frame list and module list to `UniformList` to access
scrolling behavior
- Implement `menu::` navigation actions

Release Notes:

- Debugger Beta: Added support for menu navigation actions (`ctrl-n`,
`ctrl-p`, etc.) in the stack frame list and module list.
2025-05-15 11:53:55 +02:00
gcp-cherry-pick-bot[bot]
235fd06adc workspace: Remove default keybindings for close active dock (cherry-pick #30691) (#30736)
Cherry-picked workspace: Remove default keybindings for close active
dock (#30691)

Release Notes:

- N/A

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-15 11:53:00 +02:00
gcp-cherry-pick-bot[bot]
c408200f9b Fix rejecting overwritten files if the agent previously edited them (cherry-pick #30744) (#30745)
Cherry-picked Fix rejecting overwritten files if the agent previously
edited them (#30744)

Release Notes:

- Fixed rejecting overwritten files if the agent had previously edited
them.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-15 11:48:18 +02:00
Joseph T. Lyons
53faf0d78d zed 0.187.1 2025-05-14 16:15:22 +02:00
Nate Butler
f2050dfe2b debugger: Tidy up dropdown menus (#30679)
Before
![CleanShot 2025-05-14 at 13 22
44@2x](https://github.com/user-attachments/assets/c6c06c5c-571d-4913-a691-161f44bba27c)

After
![CleanShot 2025-05-14 at 13 22
17@2x](https://github.com/user-attachments/assets/0a25a053-81a3-4b96-8963-4b770b1e5b45)

Release Notes:

- N/A
2025-05-14 16:13:53 +02:00
Danilo Leal
4cfc49e2d4 agent: Fix Markdown codeblock header buttons (#30645)
Closes https://github.com/zed-industries/zed/issues/30592

Release Notes:

- agent: Fixed Markdown codeblock header buttons being pushed by long
paths/file names.
2025-05-14 16:00:01 +02:00
Oleksiy Syvokon
72426a9608 agent: Fix tool use in Gemini (#30689)
Thread doesn't run pending tools when `stop_reason` is not `ToolUse`.
Perhaps we should change that so that it always runs pending tools if
there are some, but for now this change just fixes setting `stop_reason`
for Google models.

Release Notes:

- N/A
2025-05-14 15:59:18 +02:00
Oleksiy Syvokon
1c638a1309 agent: Don't duplicate recommended models in all models list (#30692)
Release Notes:

- N/A
2025-05-14 15:58:58 +02:00
Joseph T. Lyons
249597a4a8 v0.187.x preview 2025-05-14 12:18:41 +02:00
85 changed files with 2475 additions and 1235 deletions

View File

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

5
Cargo.lock generated
View File

@@ -4953,6 +4953,7 @@ dependencies = [
"clap",
"client",
"collections",
"debug_adapter_extension",
"dirs 4.0.0",
"dotenv",
"env_logger 0.11.8",
@@ -11861,6 +11862,7 @@ dependencies = [
"clock",
"dap",
"dap_adapters",
"debug_adapter_extension",
"env_logger 0.11.8",
"extension",
"extension_host",
@@ -18540,7 +18542,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.187.0"
version = "0.187.4"
dependencies = [
"activity_indicator",
"agent",
@@ -18572,6 +18574,7 @@ dependencies = [
"dap",
"dap_adapters",
"db",
"debug_adapter_extension",
"debugger_tools",
"debugger_ui",
"diagnostics",

View File

@@ -538,7 +538,6 @@
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-w": "workspace::CloseActiveDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
@@ -929,6 +928,7 @@
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -608,7 +608,6 @@
"cmd-b": "workspace::ToggleLeftDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"cmd-w": "workspace::CloseActiveDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
@@ -1012,7 +1011,7 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],

View File

@@ -51,9 +51,7 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
"shift-f3": "editor::FindPreviousMatch"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{

View File

@@ -53,9 +53,7 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-g": "editor::FindNextMatch",
"cmd-shift-g": "editor::FindPreviousMatch"
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{

View File

@@ -383,18 +383,25 @@ fn render_markdown_code_block(
)
} else {
let content = if let Some(parent) = path_range.path.parent() {
let file_name = file_name.to_string_lossy().to_string();
let path = parent.to_string_lossy().to_string();
let path_and_file = format!("{}/{}", path, file_name);
h_flex()
.id(("code-block-header-label", ix))
.ml_1()
.gap_1()
.child(
Label::new(file_name.to_string_lossy().to_string())
.size(LabelSize::Small),
)
.child(
Label::new(parent.to_string_lossy().to_string())
.color(Color::Muted)
.size(LabelSize::Small),
)
.child(Label::new(file_name).size(LabelSize::Small))
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Jump to File",
None,
path_and_file.clone(),
window,
cx,
)
})
.into_any_element()
} else {
Label::new(path_range.path.to_string_lossy().to_string())
@@ -404,7 +411,7 @@ fn render_markdown_code_block(
};
h_flex()
.id(("code-block-header-label", ix))
.id(("code-block-header-button", ix))
.w_full()
.max_w_full()
.px_1()
@@ -412,7 +419,6 @@ fn render_markdown_code_block(
.cursor_pointer()
.rounded_sm()
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.gap_0p5()
@@ -462,10 +468,87 @@ fn render_markdown_code_block(
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
let control_buttons = h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.absolute()
.top_0()
.right_0()
.h_full()
.bg(codeblock_header_bg)
.rounded_tr_md()
.px_1()
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code = parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
});
let codeblock_header = h_flex()
.py_1()
.pl_1p5()
.pr_1()
.relative()
.p_1()
.gap_1()
.justify_between()
.border_b_1()
@@ -473,79 +556,7 @@ fn render_markdown_code_block(
.bg(codeblock_header_bg)
.rounded_t_md()
.children(label)
.child(
h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code =
parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
}),
);
.child(control_buttons);
v_flex()
.group(CODEBLOCK_CONTAINER_GROUP)

View File

@@ -85,6 +85,7 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
ResetTrialEndUpsell,
]
);

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use db::kvp::KEY_VALUE_STORE;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use markdown::Markdown;
use serde::{Deserialize, Serialize};
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
ToggleNavigationMenu, ToggleOptionsMenu,
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -157,7 +157,10 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
set_trial_upsell_dismissed(false, cx);
TrialUpsell::set_dismissed(false, cx);
})
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
TrialEndUpsell::set_dismissed(false, cx);
});
},
)
@@ -1911,12 +1914,23 @@ impl AgentPanel {
}
}
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
if TrialEndUpsell::dismissed() {
return false;
}
let plan = self.user_store.read(cx).current_plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || dismissed_trial_upsell() {
if self.hide_trial_upsell || TrialUpsell::dismissed() {
return false;
}
@@ -1962,125 +1976,115 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
set_trial_upsell_dismissed(toggle_state_bool, cx);
TrialUpsell::set_dismissed(toggle_state_bool, cx);
},
);
Some(
div().p_2().child(
v_flex()
let contents = div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
.size(LabelSize::Small),
)
.child(
Label::new(
"Use your own API keys or enable usage-based billing once you hit the cap.",
)
.color(Color::Muted),
)
.child(
h_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.child(
div()
h_flex()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
)
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_trial_upsell = true;
cx.notify();
});
}
}),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
)
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
))
)
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
);
Some(self.render_upsell_container(cx, contents))
}
fn render_trial_end_upsell(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if !self.should_render_trial_end_upsell(cx) {
return None;
}
Some(
self.render_upsell_container(
cx,
div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
)
.child(
Label::new("You've been automatically reset to the free plan.")
.size(LabelSize::Small),
)
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(div())
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.gap_2()
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(
cx,
|this, cx| {
let hidden =
this.hide_trial_upsell;
println!("hidden: {}", hidden);
this.hide_trial_upsell = true;
let new_hidden =
this.hide_trial_upsell;
println!(
"new_hidden: {}",
new_hidden
);
cx.notify();
},
);
}
}),
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
Button::new("dismiss-button", "Stay on Free")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |_this, cx| {
TrialEndUpsell::set_dismissed(true, cx);
cx.notify();
});
}
}),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
),
),
@@ -2088,6 +2092,91 @@ impl AgentPanel {
)
}
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
div().p_2().child(
v_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(
VectorName::Grid,
rems_from_px(441.),
rems_from_px(167.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(
VectorName::AiGrid,
rems_from_px(400.),
rems_from_px(92.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(
cx.theme().colors().panel_background.alpha(0.01),
1.0,
),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
)),
)
.child(content),
),
)
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -2805,6 +2894,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
@@ -2992,25 +3082,14 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
struct TrialUpsell;
fn dismissed_trial_upsell() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
.log_err()
.map_or(false, |s| s.is_some())
impl Dismissable for TrialUpsell {
const KEY: &'static str = "dismissed-trial-upsell";
}
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
.await
}
})
struct TrialEndUpsell;
impl Dismissable for TrialEndUpsell {
const KEY: &'static str = "dismissed-trial-end-upsell";
}

View File

@@ -11,6 +11,7 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -33,7 +34,6 @@ use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
if RateLimitNotice::dismissed() {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
ui::ToggleState::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)
RateLimitNotice::set_dismissed(is_dismissed, cx);
},
))
.child(
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !dismissed_rate_limit_notice()
&& !RateLimitNotice::dismissed()
{
self.show_rate_limit_notice = true;
cx.notify();
@@ -1180,27 +1180,10 @@ impl PromptEditor<TerminalCodegen> {
}
}
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
struct RateLimitNotice;
fn dismissed_rate_limit_notice() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
.await
}
})
impl Dismissable for RateLimitNotice {
const KEY: &'static str = "dismissed-rate-limit-notice";
}
pub enum CodegenStatus {

View File

@@ -425,16 +425,17 @@ impl ToolUseState {
let content = match tool_result {
ToolResultContent::Text(text) => {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
LanguageModelToolResultContent::Text(
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
.into(),
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {

View File

@@ -49,6 +49,37 @@ impl ActionLog {
is_created: bool,
cx: &mut Context<Self>,
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
} => TrackedBufferStatus::Created {
existing_file_content,
},
TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
TrackedBufferStatus::Created {
existing_file_content: Some(tracked.diff_base),
}
}
}
} else if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
TrackedBufferStatus::Created {
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
}
} else {
TrackedBufferStatus::Created {
existing_file_content: None,
}
}
} else {
TrackedBufferStatus::Modified
};
let tracked_buffer = self
.tracked_buffers
.entry(buffer.clone())
@@ -60,36 +91,21 @@ impl ActionLog {
let text_snapshot = buffer.read(cx).text_snapshot();
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let base_text;
let status;
let diff_base;
let unreviewed_changes;
if is_created {
let existing_file_content = if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
Some(text_snapshot.as_rope().clone())
} else {
None
};
base_text = Rope::default();
status = TrackedBufferStatus::Created {
existing_file_content,
};
diff_base = Rope::default();
unreviewed_changes = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
base_text = buffer.read(cx).as_rope().clone();
status = TrackedBufferStatus::Modified;
diff_base = buffer.read(cx).as_rope().clone();
unreviewed_changes = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
base_text,
diff_base,
unreviewed_changes,
snapshot: text_snapshot.clone(),
status,
@@ -184,7 +200,7 @@ impl ActionLog {
.context("buffer not tracked")?;
let rebase = cx.background_spawn({
let mut base_text = tracked_buffer.base_text.clone();
let mut base_text = tracked_buffer.diff_base.clone();
let old_snapshot = tracked_buffer.snapshot.clone();
let new_snapshot = buffer_snapshot.clone();
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
@@ -210,7 +226,7 @@ impl ActionLog {
))
})??;
let (new_base_text, new_base_text_rope) = rebase.await;
let (new_base_text, new_diff_base) = rebase.await;
let diff_snapshot = BufferDiff::update_diff(
diff.clone(),
buffer_snapshot.clone(),
@@ -229,24 +245,23 @@ impl ActionLog {
.background_spawn({
let diff_snapshot = diff_snapshot.clone();
let buffer_snapshot = buffer_snapshot.clone();
let new_base_text_rope = new_base_text_rope.clone();
let new_diff_base = new_diff_base.clone();
async move {
let mut unreviewed_changes = Patch::default();
for hunk in diff_snapshot.hunks_intersecting_range(
Anchor::MIN..Anchor::MAX,
&buffer_snapshot,
) {
let old_range = new_base_text_rope
let old_range = new_diff_base
.offset_to_point(hunk.diff_base_byte_range.start)
..new_base_text_rope
.offset_to_point(hunk.diff_base_byte_range.end);
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
let new_range = hunk.range.start..hunk.range.end;
unreviewed_changes.push(point_to_row_edit(
Edit {
old: old_range,
new: new_range,
},
&new_base_text_rope,
&new_diff_base,
&buffer_snapshot.as_rope(),
));
}
@@ -264,7 +279,7 @@ impl ActionLog {
.tracked_buffers
.get_mut(&buffer)
.context("buffer not tracked")?;
tracked_buffer.base_text = new_base_text_rope;
tracked_buffer.diff_base = new_diff_base;
tracked_buffer.snapshot = buffer_snapshot;
tracked_buffer.unreviewed_changes = unreviewed_changes;
cx.notify();
@@ -283,7 +298,6 @@ impl ActionLog {
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer.clone(), true, cx);
}
@@ -346,11 +360,11 @@ impl ActionLog {
true
} else {
let old_range = tracked_buffer
.base_text
.diff_base
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.base_text.point_to_offset(cmp::min(
..tracked_buffer.diff_base.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.base_text.max_point(),
tracked_buffer.diff_base.max_point(),
));
let new_range = tracked_buffer
.snapshot
@@ -359,7 +373,7 @@ impl ActionLog {
Point::new(edit.new.end, 0),
tracked_buffer.snapshot.max_point(),
));
tracked_buffer.base_text.replace(
tracked_buffer.diff_base.replace(
old_range,
&tracked_buffer
.snapshot
@@ -417,7 +431,7 @@ impl ActionLog {
}
TrackedBufferStatus::Deleted => {
buffer.update(cx, |buffer, cx| {
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
});
let save = self
.project
@@ -464,14 +478,14 @@ impl ActionLog {
if revert {
let old_range = tracked_buffer
.base_text
.diff_base
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.base_text.point_to_offset(cmp::min(
..tracked_buffer.diff_base.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.base_text.max_point(),
tracked_buffer.diff_base.max_point(),
));
let old_text = tracked_buffer
.base_text
.diff_base
.chunks_in_range(old_range)
.collect::<String>();
edits_to_revert.push((new_range, old_text));
@@ -492,7 +506,7 @@ impl ActionLog {
TrackedBufferStatus::Deleted => false,
_ => {
tracked_buffer.unreviewed_changes.clear();
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
true
}
@@ -655,7 +669,7 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
base_text: Rope,
diff_base: Rope,
unreviewed_changes: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
@@ -1094,6 +1108,86 @@ mod tests {
);
}
#[gpui::test(iterations = 10)]
async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"file1": "Lorem ipsum dolor"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 37),
diff_status: DiffHunkStatusKind::Modified,
old_text: "Lorem ipsum dolor".into(),
}],
)]
);
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 9),
diff_status: DiffHunkStatusKind::Added,
old_text: "".into(),
}],
)]
);
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
assert_eq!(
buffer.read_with(cx, |buffer, _cx| buffer.text()),
"Lorem ipsum dolor"
);
}
#[gpui::test(iterations = 10)]
async fn test_deleting_files(cx: &mut TestAppContext) {
init_test(cx);
@@ -1601,7 +1695,7 @@ mod tests {
cx.run_until_parked();
action_log.update(cx, |log, cx| {
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
let mut old_text = tracked_buffer.base_text.clone();
let mut old_text = tracked_buffer.diff_base.clone();
let new_text = buffer.read(cx).as_rope();
for edit in tracked_buffer.unreviewed_changes.edits() {
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));

View File

@@ -22,7 +22,7 @@ use language::{
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -86,7 +86,7 @@ pub struct EditFileToolInput {
pub mode: EditFileMode,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditFileMode {
Edit,
@@ -171,12 +171,9 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let card = window.and_then(|window| {
@@ -199,20 +196,6 @@ impl Tool for EditFileTool {
})?
.await?;
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
let create_or_overwrite = match input.mode {
EditFileMode::Create | EditFileMode::Overwrite => true,
_ => false,
};
if !create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
@@ -221,15 +204,15 @@ impl Tool for EditFileTool {
})
.await;
let (output, mut events) = if create_or_overwrite {
edit_agent.overwrite(
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.edit(
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
@@ -349,6 +332,72 @@ impl Tool for EditFileTool {
}
}
/// Validate that the file path is valid, meaning:
///
/// - For `edit` and `overwrite`, the path must point to an existing file.
/// - For `create`, the file must not already exist, but it's parent dir must exist.
fn resolve_path(
input: &EditFileToolInput,
project: Entity<Project>,
cx: &mut App,
) -> Result<ProjectPath> {
let project = project.read(cx);
match input.mode {
EditFileMode::Edit | EditFileMode::Overwrite => {
let path = project
.find_project_path(&input.path, cx)
.ok_or_else(|| anyhow!("Can't edit file: path not found"))?;
let entry = project
.entry_for_path(&path, cx)
.ok_or_else(|| anyhow!("Can't edit file: path not found"))?;
if !entry.is_file() {
return Err(anyhow!("Can't edit file: path is a directory"));
}
Ok(path)
}
EditFileMode::Create => {
if let Some(path) = project.find_project_path(&input.path, cx) {
if project.entry_for_path(&path, cx).is_some() {
return Err(anyhow!("Can't create file: file already exists"));
}
}
let parent_path = input
.path
.parent()
.ok_or_else(|| anyhow!("Can't create file: incorrect path"))?;
let parent_project_path = project.find_project_path(&parent_path, cx);
let parent_entry = parent_project_path
.as_ref()
.and_then(|path| project.entry_for_path(&path, cx))
.ok_or_else(|| anyhow!("Can't create file: parent directory doesn't exist"))?;
if !parent_entry.is_dir() {
return Err(anyhow!("Can't create file: parent is not a directory"));
}
let file_name = input
.path
.file_name()
.ok_or_else(|| anyhow!("Can't create file: invalid filename"))?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
..parent
});
new_file_path.ok_or_else(|| anyhow!("Can't create file"))
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
@@ -868,7 +917,10 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use std::result::Result;
use super::*;
use client::TelemetrySettings;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
@@ -908,10 +960,102 @@ mod tests {
.await;
assert_eq!(
result.unwrap_err().to_string(),
"root/nonexistent_file.txt not found"
"Can't edit file: path not found"
);
}
#[gpui::test]
async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, "dir/new.txt");
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: file already exists"
);
let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: parent directory doesn't exist"
);
}
#[gpui::test]
async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Edit;
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path not found"
);
let result = test_resolve_path(mode, "root/dir", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path is a directory"
);
}
async fn test_resolve_path(
mode: &EditFileMode,
path: &str,
cx: &mut TestAppContext,
) -> Result<ProjectPath, anyhow::Error> {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"existing.txt": "hello"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let input = EditFileToolInput {
display_description: "Some edit".into(),
path: path.into(),
mode: mode.clone(),
};
let result = cx.update(|cx| resolve_path(&input, project, cx));
result
}
fn assert_resolved_path_eq(path: Result<ProjectPath, anyhow::Error>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
}
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
@@ -984,6 +1128,7 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
Project::init_settings(cx);
});
}

View File

@@ -2517,7 +2517,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2526,7 +2526,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2550,7 +2550,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2559,7 +2559,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2583,7 +2583,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2592,7 +2592,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -2616,7 +2616,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
@@ -2625,7 +2625,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
.clone()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});

View File

@@ -32,15 +32,17 @@ pub enum DapStatus {
Failed { error: String },
}
#[async_trait(?Send)]
pub trait DapDelegate {
#[async_trait]
pub trait DapDelegate: Send + Sync + 'static {
fn worktree_id(&self) -> WorktreeId;
fn worktree_root_path(&self) -> &Path;
fn http_client(&self) -> Arc<dyn HttpClient>;
fn node_runtime(&self) -> NodeRuntime;
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
fn fs(&self) -> Arc<dyn Fs>;
fn output_to_console(&self, msg: String);
fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn read_text_file(&self, path: PathBuf) -> Result<String>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
@@ -413,7 +415,7 @@ pub trait DebugAdapter: 'static + Send + Sync {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -472,7 +474,7 @@ impl DebugAdapter for FakeAdapter {
async fn get_binary(
&self,
_: &dyn DapDelegate,
_: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
_: Option<PathBuf>,
_: &mut AsyncApp,

View File

@@ -6,6 +6,8 @@ pub mod proto_conversions;
mod registry;
pub mod transport;
use std::net::Ipv4Addr;
pub use dap_types::*;
pub use registry::{DapLocator, DapRegistry};
pub use task::DebugRequest;
@@ -16,3 +18,19 @@ pub type StackFrameId = u64;
#[cfg(any(test, feature = "test-support"))]
pub use adapters::FakeAdapter;
use task::TcpArgumentsTemplate;
pub async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
) -> anyhow::Result<(Ipv4Addr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
let port = if let Some(port) = tcp_connection.port {
port
} else {
transport::TcpTransport::port(&tcp_connection).await?
};
Ok((host, port, timeout))
}

View File

@@ -54,10 +54,6 @@ impl DapRegistry {
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
let name = adapter.name();
let _previous_value = self.0.write().adapters.insert(name, adapter);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new debug adapter when one is already registered"
);
}
pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {

View File

@@ -61,7 +61,7 @@ impl CodeLldbDebugAdapter {
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release =
latest_github_release("vadimcn/codelldb", true, false, delegate.http_client()).await?;
@@ -111,7 +111,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
@@ -129,7 +129,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
self.name(),
version.clone(),
adapters::DownloadedFileType::Vsix,
delegate,
delegate.as_ref(),
)
.await?;
let version_path =

View File

@@ -6,7 +6,7 @@ mod php;
mod python;
mod ruby;
use std::{net::Ipv4Addr, sync::Arc};
use std::sync::Arc;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
@@ -17,6 +17,7 @@ use dap::{
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
GithubRepo,
},
configure_tcp_connection,
inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
};
use gdb::GdbDebugAdapter;
@@ -27,7 +28,6 @@ use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter;
use serde_json::{Value, json};
use task::TcpArgumentsTemplate;
pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
@@ -45,21 +45,6 @@ pub fn init(cx: &mut App) {
})
}
pub(crate) async fn configure_tcp_connection(
tcp_connection: TcpArgumentsTemplate,
) -> Result<(Ipv4Addr, u16, Option<u64>)> {
let host = tcp_connection.host();
let timeout = tcp_connection.timeout;
let port = if let Some(port) = tcp_connection.port {
port
} else {
dap::transport::TcpTransport::port(&tcp_connection).await?
};
Ok((host, port, timeout))
}
trait ToDap {
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
}

View File

@@ -65,7 +65,7 @@ impl DebugAdapter for GdbDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<std::path::PathBuf>,
_: &mut AsyncApp,
@@ -76,6 +76,7 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = delegate
.which(OsStr::new("gdb"))
.await
.and_then(|p| p.to_str().map(|s| s.to_string()))
.ok_or(anyhow!("Could not find gdb in path"));

View File

@@ -50,13 +50,14 @@ impl DebugAdapter for GoDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let delve_path = delegate
.which(OsStr::new("dlv"))
.await
.and_then(|p| p.to_str().map(|p| p.to_string()))
.ok_or(anyhow!("Dlv not found in path"))?;

View File

@@ -56,7 +56,7 @@ impl JsDebugAdapter {
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
@@ -82,7 +82,7 @@ impl JsDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
@@ -139,7 +139,7 @@ impl DebugAdapter for JsDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -151,7 +151,7 @@ impl DebugAdapter for JsDebugAdapter {
self.name(),
version,
adapters::DownloadedFileType::GzipTar,
delegate,
delegate.as_ref(),
)
.await?;
}

View File

@@ -40,7 +40,7 @@ impl PhpDebugAdapter {
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
@@ -66,7 +66,7 @@ impl PhpDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_: &mut AsyncApp,
@@ -126,7 +126,7 @@ impl DebugAdapter for PhpDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -138,7 +138,7 @@ impl DebugAdapter for PhpDebugAdapter {
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate,
delegate.as_ref(),
)
.await?;
}

View File

@@ -52,26 +52,26 @@ impl PythonDebugAdapter {
}
async fn fetch_latest_adapter_version(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let github_repo = GithubRepo {
repo_name: Self::ADAPTER_PACKAGE_NAME.into(),
repo_owner: "microsoft".into(),
};
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await
}
async fn install_binary(
&self,
version: AdapterVersion,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
) -> Result<()> {
let version_path = adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Zip,
delegate,
delegate.as_ref(),
)
.await?;
@@ -93,7 +93,7 @@ impl PythonDebugAdapter {
async fn get_installed_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
@@ -128,14 +128,18 @@ impl PythonDebugAdapter {
let python_path = if let Some(toolchain) = toolchain {
Some(toolchain.path.to_string())
} else {
BINARY_NAMES
.iter()
.filter_map(|cmd| {
delegate
.which(OsStr::new(cmd))
.map(|path| path.to_string_lossy().to_string())
})
.find(|_| true)
let mut name = None;
for cmd in BINARY_NAMES {
name = delegate
.which(OsStr::new(cmd))
.await
.map(|path| path.to_string_lossy().to_string());
if name.is_some() {
break;
}
}
name
};
Ok(DebugAdapterBinary {
@@ -172,7 +176,7 @@ impl DebugAdapter for PythonDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,

View File

@@ -8,7 +8,7 @@ use dap::{
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};
use util::command::new_smol_command;
use crate::ToDap;
@@ -32,7 +32,7 @@ impl DebugAdapter for RubyDebugAdapter {
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
@@ -40,7 +40,7 @@ impl DebugAdapter for RubyDebugAdapter {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let mut rdbg_path = adapter_path.join("rdbg");
if !delegate.fs().is_file(&rdbg_path).await {
match delegate.which("rdbg".as_ref()) {
match delegate.which("rdbg".as_ref()).await {
Some(path) => rdbg_path = path,
None => {
delegate.output_to_console(
@@ -76,7 +76,7 @@ impl DebugAdapter for RubyDebugAdapter {
format!("--port={}", port),
format!("--host={}", host),
];
if delegate.which(launch.program.as_ref()).is_some() {
if delegate.which(launch.program.as_ref()).await.is_some() {
arguments.push("--command".to_string())
}
arguments.push(launch.program);

View File

@@ -1,6 +1,8 @@
use gpui::App;
use sqlez_macros::sql;
use util::ResultExt as _;
use crate::{define_connection, query};
use crate::{define_connection, query, write_and_log};
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
&[sql!(
@@ -11,6 +13,29 @@ define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
)];
);
pub trait Dismissable {
const KEY: &'static str;
fn dismissed() -> bool {
KEY_VALUE_STORE
.read_kvp(Self::KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_dismissed(is_dismissed: bool, cx: &mut App) {
write_and_log(cx, move || async move {
if is_dismissed {
KEY_VALUE_STORE
.write_kvp(Self::KEY.into(), "1".into())
.await
} else {
KEY_VALUE_STORE.delete_kvp(Self::KEY.into()).await
}
})
}
}
impl KeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {

View File

@@ -5,7 +5,7 @@ use async_trait::async_trait;
use dap::adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
};
use extension::Extension;
use extension::{Extension, WorktreeDelegate};
use gpui::AsyncApp;
pub(crate) struct ExtensionDapAdapter {
@@ -25,6 +25,35 @@ impl ExtensionDapAdapter {
}
}
/// An adapter that allows an [`dap::adapters::DapDelegate`] to be used as a [`WorktreeDelegate`].
struct WorktreeDelegateAdapter(pub Arc<dyn DapDelegate>);
#[async_trait]
impl WorktreeDelegate for WorktreeDelegateAdapter {
fn id(&self) -> u64 {
self.0.worktree_id().to_proto()
}
fn root_path(&self) -> String {
self.0.worktree_root_path().to_string_lossy().to_string()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
self.0.read_text_file(path).await
}
async fn which(&self, binary_name: String) -> Option<String> {
self.0
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string())
}
async fn shell_env(&self) -> Vec<(String, String)> {
self.0.shell_env().await.into_iter().collect()
}
}
#[async_trait(?Send)]
impl DebugAdapter for ExtensionDapAdapter {
fn name(&self) -> DebugAdapterName {
@@ -33,7 +62,7 @@ impl DebugAdapter for ExtensionDapAdapter {
async fn get_binary(
&self,
_: &dyn DapDelegate,
delegate: &Arc<dyn DapDelegate>,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
@@ -43,6 +72,7 @@ impl DebugAdapter for ExtensionDapAdapter {
self.debug_adapter_name.clone(),
config.clone(),
user_installed_path,
Arc::new(WorktreeDelegateAdapter(delegate.clone())),
)
.await
}

View File

@@ -1,5 +1,6 @@
use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
@@ -30,7 +31,7 @@ use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -68,15 +69,20 @@ pub struct DebugPanel {
}
impl DebugPanel {
pub fn new(workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
pub fn new(
workspace: &Workspace,
_window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
cx.new(|cx| {
let project = workspace.project().clone();
let focus_handle = cx.focus_handle();
let debug_panel = Self {
size: px(300.),
sessions: vec![],
active_session: None,
focus_handle: cx.focus_handle(),
focus_handle,
project,
workspace: workspace.weak_handle(),
context_menu: None,
@@ -87,7 +93,38 @@ impl DebugPanel {
})
}
fn filter_action_types(&self, cx: &mut App) {
pub(crate) fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(session) = self.active_session.clone() else {
return;
};
let Some(active_pane) = session
.read(cx)
.running_state()
.read(cx)
.active_pane()
.cloned()
else {
return;
};
active_pane.update(cx, |pane, cx| {
pane.focus_active_item(window, cx);
});
}
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
self.sessions.clone()
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
self.active_session()
.map(|session| session.read(cx).running_state().clone())
}
pub(crate) fn filter_action_types(&self, cx: &mut App) {
let (has_active_session, supports_restart, support_step_back, status) = self
.active_session()
.map(|item| {
@@ -168,8 +205,8 @@ impl DebugPanel {
cx: &mut AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
workspace.update(cx, |workspace, cx| {
let debug_panel = DebugPanel::new(workspace, cx);
workspace.update_in(cx, |workspace, window, cx| {
let debug_panel = DebugPanel::new(workspace, window, cx);
workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
workspace.project().read(cx).breakpoint_store().update(
@@ -273,7 +310,7 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
async fn register_session(
pub(crate) async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
@@ -342,7 +379,7 @@ impl DebugPanel {
Ok(debug_session)
}
fn handle_restart_request(
pub(crate) fn handle_restart_request(
&mut self,
mut curr_session: Entity<Session>,
window: &mut Window,
@@ -416,11 +453,12 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn close_session(
&mut self,
entity_id: EntityId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(session) = self
.sessions
.iter()
@@ -474,93 +512,8 @@ impl DebugPanel {
})
.detach();
}
fn sessions_drop_down_menu(
&self,
active_session: &Entity<DebugSession>,
window: &mut Window,
cx: &mut Context<Self>,
) -> DropdownMenu {
let sessions = self.sessions.clone();
let weak = cx.weak_entity();
let label = active_session.read(cx).label_element(cx);
DropdownMenu::new_with_element(
"debugger-session-list",
label,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session.session_id(cx).0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
}
fn deploy_context_menu(
pub(crate) fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
window: &mut Window,
@@ -611,7 +564,11 @@ impl DebugPanel {
}
}
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
pub(crate) fn top_controls_strip(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Div> {
let active_session = self.active_session.clone();
let focus_handle = self.focus_handle.clone();
let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
@@ -651,12 +608,12 @@ impl DebugPanel {
active_session
.as_ref()
.map(|session| session.read(cx).running_state()),
|this, running_session| {
|this, running_state| {
let thread_status =
running_session.read(cx).thread_status(cx).unwrap_or(
running_state.read(cx).thread_status(cx).unwrap_or(
project::debugger::session::ThreadStatus::Exited,
);
let capabilities = running_session.read(cx).capabilities(cx);
let capabilities = running_state.read(cx).capabilities(cx);
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
@@ -667,7 +624,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.pause_thread(cx);
},
@@ -694,7 +651,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| this.continue_thread(cx),
))
.disabled(thread_status != ThreadStatus::Stopped)
@@ -718,7 +675,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.step_over(cx);
},
@@ -742,7 +699,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.step_out(cx);
},
@@ -769,7 +726,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
@@ -819,7 +776,7 @@ impl DebugPanel {
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
@@ -842,7 +799,7 @@ impl DebugPanel {
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.restart_session(cx);
},
@@ -864,7 +821,7 @@ impl DebugPanel {
IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.stop_thread(cx);
},
@@ -898,7 +855,7 @@ impl DebugPanel {
IconButton::new("debug-disconnect", IconName::DebugDetach)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
@@ -932,30 +889,42 @@ impl DebugPanel {
.as_ref()
.map(|session| session.read(cx).running_state())
.cloned(),
|this, session| {
this.child(
session.update(cx, |this, cx| {
this.thread_dropdown(window, cx)
}),
)
|this, running_state| {
this.children({
let running_state = running_state.clone();
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
session
.update(cx, |session, cx| session.threads(cx))
});
self.render_thread_dropdown(
&running_state,
threads,
window,
cx,
)
})
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
},
),
)
.child(
h_flex()
.when_some(active_session.as_ref(), |this, session| {
let context_menu =
self.sessions_drop_down_menu(session, window, cx);
this.child(context_menu).gap_2().child(Divider::vertical())
})
.children(self.render_session_menu(
self.active_session(),
self.running_state(cx),
window,
cx,
))
.when(!is_side, |this| this.child(new_session_button())),
),
),
)
}
fn activate_pane_in_direction(
pub(crate) fn activate_pane_in_direction(
&mut self,
direction: SplitDirection,
window: &mut Window,
@@ -970,7 +939,7 @@ impl DebugPanel {
}
}
fn activate_item(
pub(crate) fn activate_item(
&mut self,
item: DebuggerPaneItem,
window: &mut Window,
@@ -985,7 +954,7 @@ impl DebugPanel {
}
}
fn activate_session(
pub(crate) fn activate_session(
&mut self,
session_item: Entity<DebugSession>,
window: &mut Window,

View File

@@ -13,6 +13,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal;
mod persistence;
pub(crate) mod session;
@@ -59,7 +60,16 @@ pub fn init(cx: &mut App) {
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
workspace
.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
let did_focus_panel = workspace.toggle_panel_focus::<DebugPanel>(window, cx);
if !did_focus_panel {
return;
};
let Some(panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.focus_active_item(window, cx);
})
})
.register_action(|workspace, _: &Pause, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {

View File

@@ -0,0 +1,186 @@
use gpui::Entity;
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
Label::new(label).size(LabelSize::Small)
}
pub fn render_session_menu(
&mut self,
active_session: Option<Entity<DebugSession>>,
running_state: Option<Entity<RunningState>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(running_state) = running_state {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
let trigger = h_flex()
.gap_2()
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.justify_between()
.child(
DebugPanel::dropdown_label(label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
Some(
DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString = format!(
"debug-session-{}",
session.session_id(cx).0
)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
pub(crate) fn render_thread_dropdown(
&self,
running_state: &Entity<RunningState>,
threads: Vec<(dap::Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<DropdownMenu> {
let running_state = running_state.clone();
let running_state_read = running_state.read(cx);
let thread_id = running_state_read.thread_id();
let session = running_state_read.session();
let session_id = session.read(cx).session_id();
let session_terminated = session.read(cx).is_terminated();
let selected_thread_name = threads
.iter()
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone());
if let Some(selected_thread_name) = selected_thread_name {
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
Some(
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
running_state.update(cx, |running_state, cx| {
running_state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
}

View File

@@ -1,7 +1,6 @@
pub mod running;
use std::{cell::OnceCell, sync::OnceLock};
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@@ -11,14 +10,13 @@ use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
use rpc::proto;
use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock};
use ui::{Indicator, prelude::*};
use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item},
};
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>,
@@ -159,7 +157,11 @@ impl DebugSession {
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(Label::new(label).when(is_terminated, |this| this.strikethrough()))
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element()
}
}

View File

@@ -43,11 +43,10 @@ use task::{
};
use terminal_view::TerminalView;
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement,
IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div,
h_flex, v_flex,
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
};
use util::ResultExt;
use variable_list::VariableList;
@@ -78,6 +77,16 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
}
impl RunningState {
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
pub(crate) fn active_pane(&self) -> Option<&Entity<Pane>> {
self.active_pane.as_ref()
}
}
impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let zoomed_pane = self
@@ -497,25 +506,20 @@ impl DebugTerminal {
impl gpui::Render for DebugTerminal {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
if let Some(terminal) = self.terminal.clone() {
terminal.into_any_element()
} else {
div().track_focus(&self.focus_handle).into_any_element()
}
div()
.size_full()
.track_focus(&self.focus_handle)
.children(self.terminal.clone())
}
}
impl Focusable for DebugTerminal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
if let Some(terminal) = self.terminal.as_ref() {
return terminal.focus_handle(cx);
} else {
self.focus_handle.clone()
}
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl RunningState {
pub fn new(
pub(crate) fn new(
session: Entity<Session>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
@@ -1214,10 +1218,16 @@ impl RunningState {
}
}
pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
if self.thread_id.is_some() {
self.stack_frame_list
.update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
.update(cx, |list, cx| {
let Some(stack_frame_id) = list.opened_stack_frame_id() else {
return Task::ready(Ok(()));
};
list.go_to_stack_frame(stack_frame_id, window, cx)
})
.detach();
}
}
@@ -1234,7 +1244,7 @@ impl RunningState {
}
pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
self.stack_frame_list.read(cx).selected_stack_frame_id()
self.stack_frame_list.read(cx).opened_stack_frame_id()
}
pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
@@ -1311,7 +1321,12 @@ impl RunningState {
.map(|id| self.session().read(cx).thread_status(id))
}
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn select_thread(
&mut self,
thread_id: ThreadId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread_id.is_some_and(|id| id == thread_id) {
return;
}
@@ -1448,38 +1463,6 @@ impl RunningState {
});
}
pub(crate) fn thread_dropdown(
&self,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
) -> DropdownMenu {
let state = cx.entity();
let session_terminated = self.session.read(cx).is_terminated();
let threads = self.session.update(cx, |this, cx| this.threads(cx));
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
}
fn default_pane_layout(
project: Entity<Project>,
workspace: &WeakEntity<Workspace>,

View File

@@ -21,8 +21,8 @@ use project::{
use ui::{
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
h_flex, px, v_flex,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
div, h_flex, px, v_flex,
};
use util::{ResultExt, maybe};
use workspace::Workspace;
@@ -148,7 +148,7 @@ impl Render for BreakpointList {
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_breakpoints(cx);
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
let breakpoints = breakpoints.into_iter().flat_map(|(path, mut breakpoints)| {
@@ -259,6 +259,11 @@ impl LineBreakpoint {
dir, name, line
)))
.cursor_pointer()
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
"Disable Breakpoint"
} else {
"Enable Breakpoint"
}))
.on_click({
let weak = weak.clone();
let path = path.clone();
@@ -290,6 +295,9 @@ impl LineBreakpoint {
)))
.start_slot(indicator)
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.end_hover_slot(
IconButton::new(
SharedString::from(format!(
@@ -423,12 +431,20 @@ impl ExceptionBreakpoint {
self.id
)))
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.start_slot(
div()
.id(SharedString::from(format!(
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(Tooltip::text(if self.is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
}))
.on_click(move |_, _, cx| {
list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| {

View File

@@ -162,7 +162,7 @@ impl Console {
.evaluate(
expression,
Some(dap::EvaluateArgumentsContext::Repl),
self.stack_frame_list.read(cx).selected_stack_frame_id(),
self.stack_frame_list.read(cx).opened_stack_frame_id(),
None,
cx,
)
@@ -389,7 +389,7 @@ impl ConsoleQueryBarCompletionProvider {
) -> Task<Result<Option<Vec<Completion>>>> {
let completion_task = console.update(cx, |console, cx| {
console.session.update(cx, |state, cx| {
let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
state.completions(
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),

View File

@@ -1,7 +1,8 @@
use anyhow::anyhow;
use dap::Module;
use gpui::{
AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, WeakEntity, list,
AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
};
use project::{
ProjectItem as _, ProjectPath,
@@ -9,16 +10,17 @@ use project::{
};
use std::{path::Path, sync::Arc};
use ui::{Scrollbar, ScrollbarState, prelude::*};
use util::maybe;
use workspace::Workspace;
pub struct ModuleList {
list: ListState,
invalidate: bool,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
scrollbar_state: ScrollbarState,
entries: Vec<Module>,
_rebuild_task: Task<()>,
_subscription: Subscription,
}
@@ -28,38 +30,43 @@ impl ModuleList {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
.unwrap_or(div().into_any())
},
);
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::Modules => {
this.invalidate = true;
cx.notify();
this.schedule_rebuild(cx);
}
_ => {}
});
Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
session,
workspace,
focus_handle,
entries: Vec::new(),
selected_ix: None,
_subscription,
invalidate: true,
}
_rebuild_task: Task::ready(()),
};
this.schedule_rebuild(cx);
this
}
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
self._rebuild_task = cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
let modules = this
.session
.update(cx, |session, cx| session.modules(cx).to_owned());
this.entries = modules;
cx.notify();
})
.ok();
});
}
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
@@ -111,36 +118,40 @@ impl ModuleList {
anyhow::Ok(())
})
.detach_and_log_err(cx);
.detach();
}
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let Some(module) = maybe!({
self.session
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
}) else {
return Empty.into_any();
};
let module = self.entries[ix].clone();
v_flex()
.rounded_md()
.w_full()
.group("")
.id(("module-list", ix))
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.when(module.path.is_some(), |this| {
this.on_click({
let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
let path = module
.path
.as_deref()
.map(|path| Arc::<Path>::from(Path::new(path)));
cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
if let Some(path) = path.as_ref() {
this.open_module(path.clone(), window, cx);
} else {
log::error!("Wasn't able to find module path, but was still able to click on module list entry");
}
cx.notify();
})
})
})
.p_1()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when(Some(ix) == self.selected_ix, |s| {
s.bg(cx.theme().colors().element_hover)
})
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
.child(
h_flex()
@@ -188,6 +199,96 @@ impl ModuleList {
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else { return };
let Some(entry) = self.entries.get(ix) else {
return;
};
let Some(path) = entry.path.as_deref() else {
return;
};
let path = Arc::from(Path::new(path));
self.open_module(path, window, cx);
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.entries.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.entries.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.entries.len() > 0 {
Some(self.entries.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"module-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
}
}
impl Focusable for ModuleList {
@@ -197,21 +298,17 @@ impl Focusable for ModuleList {
}
impl Render for ModuleList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.invalidate {
let len = self
.session
.update(cx, |session, cx| session.modules(cx).len());
self.list.reset(len);
self.invalidate = false;
cx.notify();
}
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::confirm))
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
.child(self.render_list(window, cx))
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -5,20 +5,18 @@ use std::time::Duration;
use anyhow::{Result, anyhow};
use dap::StackFrameId;
use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, Task, WeakEntity, list,
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
};
use crate::StackTraceView;
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::ResultExt;
use workspace::{ItemHandle, Workspace};
use crate::StackTraceView;
use super::RunningState;
#[derive(Debug)]
@@ -28,15 +26,16 @@ pub enum StackFrameListEvent {
}
pub struct StackFrameList {
list: ListState,
focus_handle: FocusHandle,
_subscription: Subscription,
session: Entity<Session>,
state: WeakEntity<RunningState>,
entries: Vec<StackFrameEntry>,
workspace: WeakEntity<Workspace>,
selected_stack_frame_id: Option<StackFrameId>,
selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle,
_refresh_task: Task<()>,
}
@@ -55,22 +54,8 @@ impl StackFrameList {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|stack_frame_list| {
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
})
.unwrap_or(div().into_any())
},
);
let scroll_handle = UniformListScrollHandle::new();
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -84,15 +69,16 @@ impl StackFrameList {
});
let mut this = Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
session,
workspace,
focus_handle,
state,
_subscription,
entries: Default::default(),
selected_stack_frame_id: None,
selected_ix: None,
opened_stack_frame_id: None,
scroll_handle,
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
@@ -123,7 +109,7 @@ impl StackFrameList {
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
self.state
.read_with(cx, |state, _| state.thread_id)
.log_err()
.ok()
.flatten()
.map(|thread_id| {
self.session
@@ -140,27 +126,8 @@ impl StackFrameList {
.collect()
}
pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
self.selected_stack_frame_id
}
pub(crate) fn select_stack_frame_id(
&mut self,
id: StackFrameId,
window: &Window,
cx: &mut Context<Self>,
) {
if !self.entries.iter().any(|entry| match entry {
StackFrameEntry::Normal(entry) => entry.id == id,
StackFrameEntry::Collapsed(stack_frames) => {
stack_frames.iter().any(|frame| frame.id == id)
}
}) {
return;
}
self.selected_stack_frame_id = Some(id);
self.go_to_selected_stack_frame(window, cx);
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
self.opened_stack_frame_id
}
pub(super) fn schedule_refresh(
@@ -193,13 +160,22 @@ impl StackFrameList {
pub fn build_entries(
&mut self,
select_first_stack_frame: bool,
open_first_stack_frame: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let old_selected_frame_id = self
.selected_ix
.and_then(|ix| self.entries.get(ix))
.and_then(|entry| match entry {
StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
StackFrameEntry::Collapsed(stack_frames) => {
stack_frames.first().map(|stack_frame| stack_frame.id)
}
});
let mut entries = Vec::new();
let mut collapsed_entries = Vec::new();
let mut current_stack_frame = None;
let mut first_stack_frame = None;
let stack_frames = self.stack_frames(cx);
for stack_frame in &stack_frames {
@@ -213,7 +189,7 @@ impl StackFrameList {
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
}
current_stack_frame.get_or_insert(&stack_frame.dap);
first_stack_frame.get_or_insert(entries.len());
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
}
}
@@ -225,69 +201,60 @@ impl StackFrameList {
}
std::mem::swap(&mut self.entries, &mut entries);
self.list.reset(self.entries.len());
if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
{
self.select_stack_frame(current_stack_frame, true, window, cx)
.detach_and_log_err(cx);
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
self.select_ix(Some(ix), cx);
self.activate_selected_entry(window, cx);
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
let ix = self.entries.iter().position(|entry| match entry {
StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
StackFrameEntry::Collapsed(frames) => {
frames.iter().any(|frame| frame.id == old_selected_frame_id)
}
});
self.selected_ix = ix;
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
let frame = self
.entries
.iter()
.find_map(|entry| match entry {
StackFrameEntry::Normal(dap) => {
if dap.id == selected_stack_frame_id {
Some(dap)
} else {
None
}
}
StackFrameEntry::Collapsed(daps) => {
daps.iter().find(|dap| dap.id == selected_stack_frame_id)
}
})
.cloned();
if let Some(frame) = frame.as_ref() {
self.select_stack_frame(frame, true, window, cx)
.detach_and_log_err(cx);
}
}
}
pub fn select_stack_frame(
pub fn go_to_stack_frame(
&mut self,
stack_frame: &dap::StackFrame,
go_to_stack_frame: bool,
window: &Window,
stack_frame_id: StackFrameId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.selected_stack_frame_id = Some(stack_frame.id);
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame.id,
));
cx.notify();
if !go_to_stack_frame {
return Task::ready(Ok(()));
let Some(stack_frame) = self
.entries
.iter()
.flat_map(|entry| match entry {
StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
})
.find(|stack_frame| stack_frame.id == stack_frame_id)
.cloned()
else {
return Task::ready(Err(anyhow!("No stack frame for ID")));
};
self.go_to_stack_frame_inner(stack_frame, window, cx)
}
let row = (stack_frame.line.saturating_sub(1)) as u32;
fn go_to_stack_frame_inner(
&mut self,
stack_frame: dap::StackFrame,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let stack_frame_id = stack_frame.id;
self.opened_stack_frame_id = Some(stack_frame_id);
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
let stack_frame_id = stack_frame.id;
let row = stack_frame.line.saturating_sub(1) as u32;
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame_id,
));
cx.spawn_in(window, async move |this, cx| {
let (worktree, relative_path) = this
.update(cx, |this, cx| {
@@ -386,11 +353,12 @@ impl StackFrameList {
fn render_normal_entry(
&self,
ix: usize,
stack_frame: &dap::StackFrame,
cx: &mut Context<Self>,
) -> AnyElement {
let source = stack_frame.source.clone();
let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
let is_selected_frame = Some(ix) == self.selected_ix;
let path = source.clone().and_then(|s| s.path.or(s.name));
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
@@ -426,12 +394,12 @@ impl StackFrameList {
.when(is_selected_frame, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_click(cx.listener({
let stack_frame = stack_frame.clone();
move |this, _, window, cx| {
this.select_stack_frame(&stack_frame, true, window, cx)
.detach_and_log_err(cx);
}
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
@@ -486,20 +454,15 @@ impl StackFrameList {
.into_any()
}
pub fn expand_collapsed_entry(
&mut self,
ix: usize,
stack_frames: &Vec<dap::StackFrame>,
cx: &mut Context<Self>,
) {
self.entries.splice(
ix..ix + 1,
stack_frames
.iter()
.map(|frame| StackFrameEntry::Normal(frame.clone())),
);
self.list.reset(self.entries.len());
cx.notify();
pub(crate) fn expand_collapsed_entry(&mut self, ix: usize) {
let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
return;
};
let entries = std::mem::take(stack_frames)
.into_iter()
.map(StackFrameEntry::Normal);
self.entries.splice(ix..ix + 1, entries);
self.selected_ix = Some(ix);
}
fn render_collapsed_entry(
@@ -509,6 +472,7 @@ impl StackFrameList {
cx: &mut Context<Self>,
) -> AnyElement {
let first_stack_frame = &stack_frames[0];
let is_selected = Some(ix) == self.selected_ix;
h_flex()
.rounded_md()
@@ -517,11 +481,15 @@ impl StackFrameList {
.group("")
.id(("stack-frame", first_stack_frame.id))
.p_1()
.on_click(cx.listener({
let stack_frames = stack_frames.clone();
move |this, _, _window, cx| {
this.expand_collapsed_entry(ix, &stack_frames, cx);
}
.when(is_selected, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
@@ -544,7 +512,7 @@ impl StackFrameList {
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
match &self.entries[ix] {
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
StackFrameEntry::Collapsed(stack_frames) => {
self.render_collapsed_entry(ix, stack_frames, cx)
}
@@ -583,15 +551,120 @@ impl StackFrameList {
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = self.selected_ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.entries.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.entries.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.entries.len() > 0 {
Some(self.entries.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else {
return;
};
let Some(entry) = self.entries.get_mut(ix) else {
return;
};
match entry {
StackFrameEntry::Normal(stack_frame) => {
let stack_frame = stack_frame.clone();
self.go_to_stack_frame_inner(stack_frame, window, cx)
.detach_and_log_err(cx)
}
StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix),
}
cx.notify();
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
self.activate_selected_entry(window, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"stack-frame-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
}
}
impl Render for StackFrameList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.child(self.render_list(window, cx))
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -69,7 +69,7 @@ impl StackTraceView {
.filter(|id| Some(**id) != this.selected_stack_frame_id)
{
this.stack_frame_list.update(cx, |list, cx| {
list.select_stack_frame_id(*stack_frame_id, window, cx);
list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
});
}
}
@@ -82,7 +82,7 @@ impl StackTraceView {
|this, stack_frame_list, event, window, cx| match event {
StackFrameListEvent::BuiltEntries => {
this.selected_stack_frame_id =
stack_frame_list.read(cx).selected_stack_frame_id();
stack_frame_list.read(cx).opened_stack_frame_id();
this.update_excerpts(window, cx);
}
StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {

View File

@@ -168,7 +168,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
});
@@ -373,14 +373,14 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
.unwrap();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
// select second stack frame
stack_frame_list
.update_in(cx, |stack_frame_list, window, cx| {
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
})
.await
.unwrap();
@@ -388,7 +388,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
cx.run_until_parked();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
@@ -718,11 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
1,
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
cx,
);
stack_frame_list.expand_collapsed_entry(1);
assert_eq!(
&vec![
@@ -739,11 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
4,
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
cx,
);
stack_frame_list.expand_collapsed_entry(4);
assert_eq!(
&vec![

View File

@@ -190,7 +190,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
assert_eq!(stack_frames, stack_frame_list);
@@ -431,7 +431,7 @@ async fn test_fetch_variables_for_multiple_scopes(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
assert_eq!(Some(1), stack_frame_id);
@@ -1452,7 +1452,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
assert_eq!(Some(1), stack_frame_id);
@@ -1734,7 +1734,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);
@@ -1745,7 +1745,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state
.stack_frame_list()
.read(cx)
.selected_stack_frame_id(),
.opened_stack_frame_id(),
Some(1)
);
@@ -1778,7 +1778,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state
.stack_frame_list()
.update(cx, |stack_frame_list, cx| {
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
})
})
.await
@@ -1789,7 +1789,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);

View File

@@ -122,10 +122,11 @@ use markdown::Markdown;
use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
ProjectPath,
BreakpointWithPosition, ProjectPath,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent,
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
BreakpointStoreEvent,
},
session::{Session, SessionEvent},
},
@@ -198,7 +199,7 @@ use theme::{
};
use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Key, Tooltip, h_flex, prelude::*,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
@@ -6848,7 +6849,7 @@ impl Editor {
range: Range<DisplayRow>,
window: &mut Window,
cx: &mut Context<Self>,
) -> HashMap<DisplayRow, (Anchor, Breakpoint)> {
) -> HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)> {
let mut breakpoint_display_points = HashMap::default();
let Some(breakpoint_store) = self.breakpoint_store.clone() else {
@@ -6882,15 +6883,17 @@ impl Editor {
buffer_snapshot,
cx,
);
for (anchor, breakpoint) in breakpoints {
for (breakpoint, state) in breakpoints {
let multi_buffer_anchor =
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor);
Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position);
let position = multi_buffer_anchor
.to_point(&multi_buffer_snapshot)
.to_display_point(&snapshot);
breakpoint_display_points
.insert(position.row(), (multi_buffer_anchor, breakpoint.clone()));
breakpoint_display_points.insert(
position.row(),
(multi_buffer_anchor, breakpoint.bp.clone(), state),
);
}
}
@@ -7065,8 +7068,10 @@ impl Editor {
position: Anchor,
row: DisplayRow,
breakpoint: &Breakpoint,
state: Option<BreakpointSessionState>,
cx: &mut Context<Self>,
) -> IconButton {
let is_rejected = state.is_some_and(|s| !s.verified);
// Is it a breakpoint that shows up when hovering over gutter?
let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or(
(false, false),
@@ -7092,6 +7097,8 @@ impl Editor {
let color = if is_phantom {
Color::Hint
} else if is_rejected {
Color::Disabled
} else {
Color::Debugger
};
@@ -7119,9 +7126,18 @@ impl Editor {
}
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."
} else {
"Right-click for more options."
};
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.when(is_rejected, |this| {
this.indicator(Indicator::icon(Icon::new(IconName::Warning)).color(Color::Warning))
})
.icon_color(color)
.style(ButtonStyle::Transparent)
.on_click(cx.listener({
@@ -7153,14 +7169,7 @@ impl Editor {
);
}))
.tooltip(move |window, cx| {
Tooltip::with_meta_in(
primary_text.clone(),
None,
"Right-click for more options",
&focus_handle,
window,
cx,
)
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
})
}
@@ -7300,11 +7309,11 @@ impl Editor {
_style: &EditorStyle,
is_active: bool,
row: DisplayRow,
breakpoint: Option<(Anchor, Breakpoint)>,
breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
cx: &mut Context<Self>,
) -> IconButton {
let color = Color::Muted;
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
.shape(ui::IconButtonShape::Square)
@@ -9484,16 +9493,16 @@ impl Editor {
cx,
)
.next()
.and_then(|(anchor, bp)| {
.and_then(|(bp, _)| {
let breakpoint_row = buffer_snapshot
.summary_for_anchor::<text::PointUtf16>(anchor)
.summary_for_anchor::<text::PointUtf16>(&bp.position)
.row;
if breakpoint_row == row {
snapshot
.buffer_snapshot
.anchor_in_excerpt(enclosing_excerpt, *anchor)
.map(|anchor| (anchor, bp.clone()))
.anchor_in_excerpt(enclosing_excerpt, bp.position)
.map(|position| (position, bp.bp.clone()))
} else {
None
}
@@ -9656,7 +9665,10 @@ impl Editor {
breakpoint_store.update(cx, |breakpoint_store, cx| {
breakpoint_store.toggle_breakpoint(
buffer,
(breakpoint_position.text_anchor, breakpoint),
BreakpointWithPosition {
position: breakpoint_position.text_anchor,
bp: breakpoint,
},
edit_action,
cx,
);
@@ -17918,10 +17930,6 @@ impl Editor {
.and_then(|lines| lines.last().map(|line| line.range.start));
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
let snapshot = editor
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
.ok()?;
let inline_values = editor
.update(cx, |editor, cx| {
let Some(current_execution_position) = current_execution_position else {
@@ -17949,22 +17957,40 @@ impl Editor {
.context("refreshing debugger inlays")
.log_err()?;
let (excerpt_id, buffer_id) = snapshot
.excerpts()
.next()
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
let mut buffer_inline_values: HashMap<BufferId, Vec<InlayHint>> = HashMap::default();
for (buffer_id, inline_value) in inline_values
.into_iter()
.filter_map(|hint| Some((hint.position.buffer_id?, hint)))
{
buffer_inline_values
.entry(buffer_id)
.or_default()
.push(inline_value);
}
editor
.update(cx, |editor, cx| {
let new_inlays = inline_values
.into_iter()
.map(|debugger_value| {
Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
debugger_value.text(),
)
})
.collect::<Vec<_>>();
let snapshot = editor.buffer.read(cx).snapshot(cx);
let mut new_inlays = Vec::default();
for (excerpt_id, buffer_snapshot, _) in snapshot.excerpts() {
let buffer_id = buffer_snapshot.remote_id();
buffer_inline_values
.get(&buffer_id)
.into_iter()
.flatten()
.for_each(|hint| {
let inlay = Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
);
new_inlays.push(inlay);
});
}
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);

View File

@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings};
use util::serde::default_true;
/// Imports from the VSCode settings at
/// https://code.visualstudio.com/docs/reference/default-settings
#[derive(Deserialize, Clone)]
pub struct EditorSettings {
pub cursor_blink: bool,
@@ -539,7 +541,7 @@ pub struct ScrollbarContent {
}
/// Minimap related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct MinimapContent {
/// When to show the minimap in the editor.
///
@@ -770,5 +772,32 @@ impl Settings for EditorSettings {
let search = current.search.get_or_insert_default();
search.include_ignored = use_ignored;
}
let mut minimap = MinimapContent::default();
let minimap_enabled = vscode.read_bool("editor.minimap.enabled").unwrap_or(true);
let autohide = vscode.read_bool("editor.minimap.autohide");
if minimap_enabled {
if let Some(false) = autohide {
minimap.show = Some(ShowMinimap::Always);
} else {
minimap.show = Some(ShowMinimap::Auto);
}
} else {
minimap.show = Some(ShowMinimap::Never);
}
vscode.enum_setting(
"editor.minimap.showSlider",
&mut minimap.thumb,
|s| match s {
"always" => Some(MinimapThumb::Always),
"mouseover" => Some(MinimapThumb::Hover),
_ => None,
},
);
if minimap != MinimapContent::default() {
current.minimap = Some(minimap)
}
}
}

View File

@@ -18522,7 +18522,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18547,7 +18547,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18569,7 +18569,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18636,7 +18636,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18657,7 +18657,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18677,7 +18677,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18700,7 +18700,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18723,7 +18723,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18816,7 +18816,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18848,7 +18848,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});
@@ -18884,7 +18884,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
.as_ref()
.unwrap()
.read(cx)
.all_breakpoints(cx)
.all_source_breakpoints(cx)
.clone()
});

View File

@@ -62,7 +62,7 @@ use multi_buffer::{
use project::{
ProjectPath,
debugger::breakpoint_store::Breakpoint,
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
};
use settings::Settings;
@@ -2320,7 +2320,7 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint)>,
breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
row_infos: &[RowInfo],
window: &mut Window,
cx: &mut App,
@@ -2328,7 +2328,7 @@ impl EditorElement {
self.editor.update(cx, |editor, cx| {
breakpoints
.into_iter()
.filter_map(|(display_row, (text_anchor, bp))| {
.filter_map(|(display_row, (text_anchor, bp, state))| {
if row_infos
.get((display_row.0.saturating_sub(range.start.0)) as usize)
.is_some_and(|row_info| {
@@ -2351,7 +2351,7 @@ impl EditorElement {
return None;
}
let button = editor.render_breakpoint(text_anchor, display_row, &bp, cx);
let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
let button = prepaint_gutter_button(
button,
@@ -2381,7 +2381,7 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
snapshot: &EditorSnapshot,
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint)>,
breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
@@ -7454,8 +7454,10 @@ impl Element for EditorElement {
editor.active_breakpoints(start_row..end_row, window, cx)
});
if cx.has_flag::<DebuggerFeatureFlag>() {
for display_row in breakpoint_rows.keys() {
active_rows.entry(*display_row).or_default().breakpoint = true;
for (display_row, (_, bp, state)) in &breakpoint_rows {
if bp.is_enabled() && state.is_none_or(|s| s.verified) {
active_rows.entry(*display_row).or_default().breakpoint = true;
}
}
}
@@ -7495,7 +7497,7 @@ impl Element for EditorElement {
let breakpoint = Breakpoint::new_standard();
phantom_breakpoint.collides_with_existing_breakpoint =
false;
(position, breakpoint)
(position, breakpoint, None)
});
}
})

View File

@@ -30,6 +30,7 @@ chrono.workspace = true
clap.workspace = true
client.workspace = true
collections.workspace = true
debug_adapter_extension.workspace = true
dirs.workspace = true
dotenv.workspace = true
env_logger.workspace = true

View File

@@ -422,6 +422,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
let extension_host_proxy = ExtensionHostProxy::global(cx);
language::init(cx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
language_extension::init(extension_host_proxy.clone(), languages.clone());
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);

View File

@@ -141,6 +141,7 @@ pub trait Extension: Send + Sync + 'static {
dap_name: Arc<str>,
config: DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<DebugAdapterBinary>;
}

View File

@@ -87,6 +87,8 @@ pub struct ExtensionManifest {
pub snippets: Option<PathBuf>,
#[serde(default)]
pub capabilities: Vec<ExtensionCapability>,
#[serde(default)]
pub debug_adapters: Vec<Arc<str>>,
}
impl ExtensionManifest {
@@ -274,6 +276,7 @@ fn manifest_from_old_manifest(
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: vec![],
}
}
@@ -301,6 +304,7 @@ mod tests {
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![],
debug_adapters: Default::default(),
}
}

View File

@@ -19,6 +19,11 @@ pub use wit::{
KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file,
make_file_executable,
zed::extension::context_server::ContextServerConfiguration,
zed::extension::dap::{
DebugAdapterBinary, DebugRequest, DebugTaskDefinition, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
resolve_tcp_template,
},
zed::extension::github::{
GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name,
latest_github_release,
@@ -194,6 +199,7 @@ pub trait Extension: Send + Sync {
_adapter_name: String,
_config: DebugTaskDefinition,
_user_provided_path: Option<String>,
_worktree: &Worktree,
) -> Result<DebugAdapterBinary, String> {
Err("`get_dap_binary` not implemented".to_string())
}
@@ -386,8 +392,9 @@ impl wit::Guest for Component {
adapter_name: String,
config: DebugTaskDefinition,
user_installed_path: Option<String>,
) -> Result<DebugAdapterBinary, String> {
extension().get_dap_binary(adapter_name, config, user_installed_path)
worktree: &Worktree,
) -> Result<wit::DebugAdapterBinary, String> {
extension().get_dap_binary(adapter_name, config, user_installed_path, worktree)
}
}

View File

@@ -1,5 +1,9 @@
interface dap {
use common.{env-vars};
/// Resolves a specified TcpArgumentsTemplate into TcpArguments
resolve-tcp-template: func(template: tcp-arguments-template) -> result<tcp-arguments, string>;
record launch-request {
program: string,
cwd: option<string>,
@@ -27,6 +31,7 @@ interface dap {
host: option<u32>,
timeout: option<u64>,
}
record debug-task-definition {
label: string,
adapter: string,
@@ -40,11 +45,12 @@ interface dap {
launch,
attach,
}
record start-debugging-request-arguments {
configuration: string,
request: start-debugging-request-arguments-request,
}
record debug-adapter-binary {
command: string,
arguments: list<string>,

View File

@@ -11,7 +11,7 @@ world extension {
use common.{env-vars, range};
use context-server.{context-server-configuration};
use dap.{debug-adapter-binary, debug-task-definition};
use dap.{debug-adapter-binary, debug-task-definition, debug-request};
use lsp.{completion, symbol};
use process.{command};
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
@@ -157,5 +157,5 @@ world extension {
export index-docs: func(provider-name: string, package-name: string, database: borrow<key-value-store>) -> result<_, string>;
/// 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>) -> result<debug-adapter-binary, string>;
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>;
}

View File

@@ -14,9 +14,10 @@ use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
pub use extension::ExtensionManifest;
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::{
ExtensionContextServerProxy, ExtensionEvents, ExtensionGrammarProxy, ExtensionHostProxy,
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents,
ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy,
ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy,
ExtensionSnippetProxy, ExtensionThemeProxy,
};
use fs::{Fs, RemoveOptions};
use futures::{
@@ -1328,6 +1329,11 @@ impl ExtensionStore {
this.proxy
.register_indexed_docs_provider(extension.clone(), provider_id.clone());
}
for debug_adapter in &manifest.debug_adapters {
this.proxy
.register_debug_adapter(extension.clone(), debug_adapter.clone());
}
}
this.wasm_extensions.extend(wasm_extensions);

View File

@@ -164,6 +164,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
}),
dev: false,
},
@@ -193,6 +194,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
}),
dev: false,
},
@@ -367,6 +369,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
}),
dev: false,
},

View File

@@ -379,11 +379,13 @@ impl extension::Extension for WasmExtension {
dap_name: Arc<str>,
config: DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<DebugAdapterBinary> {
self.call(|extension, store| {
async move {
let resource = store.data_mut().table().push(worktree)?;
let dap_binary = extension
.call_get_dap_binary(store, dap_name, config, user_installed_path)
.call_get_dap_binary(store, dap_name, config, user_installed_path, resource)
.await?
.map_err(|err| anyhow!("{err:?}"))?;
let dap_binary = dap_binary.try_into()?;

View File

@@ -903,6 +903,7 @@ impl Extension {
adapter_name: Arc<str>,
task: DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<DebugAdapterBinary, String>> {
match self {
Extension::V0_6_0(ext) => {
@@ -912,6 +913,7 @@ impl Extension {
&adapter_name,
&task.try_into()?,
user_installed_path.as_ref().and_then(|p| p.to_str()),
resource,
)
.await?
.map_err(|e| anyhow!("{e:?}"))?;

View File

@@ -48,7 +48,7 @@ wasmtime::component::bindgen!({
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs"));
include!(concat!(env!("OUT_DIR"), "/since_v0.6.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
@@ -729,8 +729,29 @@ impl slash_command::Host for WasmState {}
#[async_trait]
impl context_server::Host for WasmState {}
#[async_trait]
impl dap::Host for WasmState {}
impl dap::Host for WasmState {
async fn resolve_tcp_template(
&mut self,
template: TcpArgumentsTemplate,
) -> wasmtime::Result<Result<TcpArguments, String>> {
maybe!(async {
let (host, port, timeout) =
::dap::configure_tcp_connection(task::TcpArgumentsTemplate {
port: template.port,
host: template.host.map(Ipv4Addr::from_bits),
timeout: template.timeout,
})
.await?;
Ok(TcpArguments {
port,
host: host.to_bits(),
timeout,
})
})
.await
.to_wasmtime_result()
}
}
impl ExtensionImports for WasmState {
async fn get_settings(

View File

@@ -748,7 +748,6 @@ impl GitRepository for RealGitRepository {
"--no-optional-locks",
"cat-file",
"--batch-check=%(objectname)",
"-z",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -761,7 +760,7 @@ impl GitRepository for RealGitRepository {
.ok_or_else(|| anyhow!("no stdin for git cat-file subprocess"))?;
let mut stdin = BufWriter::new(stdin);
for rev in &revs {
write!(&mut stdin, "{rev}\0")?;
write!(&mut stdin, "{rev}\n")?;
}
drop(stdin);

View File

@@ -1,5 +1,4 @@
mod client;
mod clipboard;
mod display;
mod event;
mod window;

View File

@@ -1,3 +1,4 @@
use crate::platform::scap_screen_capture::scap_screen_sources;
use core::str;
use std::{
cell::RefCell,
@@ -40,9 +41,8 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE};
use super::{
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail,
clipboard::{self, Clipboard},
get_valuator_axis_index, modifiers_from_state, pressed_button_from_mask,
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail, get_valuator_axis_index,
modifiers_from_state, pressed_button_from_mask,
};
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
use super::{XimCallbackEvent, XimHandler};
@@ -56,7 +56,6 @@ use crate::platform::{
reveal_path_internal,
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
},
scap_screen_capture::scap_screen_sources,
};
use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
@@ -202,7 +201,7 @@ pub struct X11ClientState {
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
pub(crate) common: LinuxCommon,
pub(crate) clipboard: Clipboard,
pub(crate) clipboard: x11_clipboard::Clipboard,
pub(crate) clipboard_item: Option<ClipboardItem>,
pub(crate) xdnd_state: Xdnd,
}
@@ -389,7 +388,7 @@ impl X11Client {
.reply()
.unwrap();
let clipboard = Clipboard::new().unwrap();
let clipboard = x11_clipboard::Clipboard::new().unwrap();
let xcb_connection = Rc::new(xcb_connection);
@@ -1497,36 +1496,39 @@ impl LinuxClient for X11Client {
let state = self.0.borrow_mut();
state
.clipboard
.set_text(
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
clipboard::ClipboardKind::Primary,
clipboard::WaitConfig::None,
.store(
state.clipboard.setter.atoms.primary,
state.clipboard.setter.atoms.utf8_string,
item.text().unwrap_or_default().as_bytes(),
)
.context("Failed to write to clipboard (primary)")
.log_with_level(log::Level::Debug);
.ok();
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
let mut state = self.0.borrow_mut();
state
.clipboard
.set_text(
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
clipboard::ClipboardKind::Clipboard,
clipboard::WaitConfig::None,
.store(
state.clipboard.setter.atoms.clipboard,
state.clipboard.setter.atoms.utf8_string,
item.text().unwrap_or_default().as_bytes(),
)
.context("Failed to write to clipboard (clipboard)")
.log_with_level(log::Level::Debug);
.ok();
state.clipboard_item.replace(item);
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
let state = self.0.borrow_mut();
return state
state
.clipboard
.get_any(clipboard::ClipboardKind::Primary)
.context("Failed to read from clipboard (primary)")
.log_with_level(log::Level::Debug);
.load(
state.clipboard.getter.atoms.primary,
state.clipboard.getter.atoms.utf8_string,
state.clipboard.getter.atoms.property,
Duration::from_secs(3),
)
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
.ok()
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
@@ -1535,15 +1537,26 @@ impl LinuxClient for X11Client {
// which has metadata attached.
if state
.clipboard
.is_owner(clipboard::ClipboardKind::Clipboard)
.setter
.connection
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
.ok()
.and_then(|r| r.reply().ok())
.map(|reply| reply.owner == state.clipboard.setter.window)
.unwrap_or(false)
{
return state.clipboard_item.clone();
}
return state
state
.clipboard
.get_any(clipboard::ClipboardKind::Clipboard)
.context("Failed to read from clipboard (clipboard)")
.log_with_level(log::Level::Debug);
.load(
state.clipboard.getter.atoms.clipboard,
state.clipboard.getter.atoms.utf8_string,
state.clipboard.getter.atoms.property,
Duration::from_secs(3),
)
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
.ok()
}
fn run(&self) {

View File

@@ -27,6 +27,19 @@ pub trait FluentBuilder {
self.map(|this| if condition { then(this) } else { this })
}
/// Conditionally modify self with the given closure.
fn when_else(
self,
condition: bool,
then: impl FnOnce(Self) -> Self,
else_fn: impl FnOnce(Self) -> Self,
) -> Self
where
Self: Sized,
{
self.map(|this| if condition { then(this) } else { else_fn(this) })
}
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where

View File

@@ -83,6 +83,13 @@ impl EditPredictionUsage {
Ok(Self { limit, amount })
}
pub fn over_limit(&self) -> bool {
match self.limit {
UsageLimit::Limited(limit) => self.amount >= limit,
UsageLimit::Unlimited => false,
}
}
}
pub trait EditPredictionProvider: 'static + Sized {

View File

@@ -33,7 +33,7 @@ use workspace::{
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
notifications::NotificationId,
};
use zed_actions::OpenBrowser;
use zed_actions::{OpenBrowser, OpenZedUrl};
use zed_llm_client::UsageLimit;
use zeta::RateCompletions;
@@ -277,14 +277,31 @@ impl Render for InlineCompletionButton {
);
}
let mut over_limit = false;
if let Some(usage) = self
.edit_prediction_provider
.as_ref()
.and_then(|provider| provider.usage(cx))
{
over_limit = usage.over_limit()
}
let show_editor_predictions = self.editor_show_predictions;
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square)
.when(enabled && !show_editor_predictions, |this| {
this.indicator(Indicator::dot().color(Color::Muted))
.when(
enabled && (!show_editor_predictions || over_limit),
|this| {
this.indicator(Indicator::dot().when_else(
over_limit,
|dot| dot.color(Color::Error),
|dot| dot.color(Color::Muted),
))
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
})
},
)
.when(!self.popover_menu_handle.is_deployed(), |element| {
element.tooltip(move |window, cx| {
if enabled {
@@ -440,6 +457,16 @@ impl InlineCompletionButton {
},
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
)
.when(usage.over_limit(), |menu| -> ContextMenu {
menu.entry("Subscribe to increase your limit", None, |window, cx| {
window.dispatch_action(
Box::new(OpenZedUrl {
url: zed_urls::account_url(cx),
}),
cx,
);
})
})
.separator();
}

View File

@@ -51,7 +51,7 @@ pub trait ToolchainLister: Send + Sync {
}
#[async_trait(?Send)]
pub trait LanguageToolchainStore {
pub trait LanguageToolchainStore: Send + Sync + 'static {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,

View File

@@ -326,8 +326,14 @@ struct GroupedModels {
impl GroupedModels {
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in other {
if recommended_ids.contains(&model.model.id()) {
continue;
}
let provider = model.model.provider_id();
if let Some(models) = other_by_provider.get_mut(&provider) {
models.push(model);
@@ -889,4 +895,26 @@ mod tests {
let results = matcher.fuzzy_search("z4n");
assert_models_eq(results, vec!["zed/gpt-4.1-nano"]);
}
#[gpui::test]
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![
("zed", "claude"), // Should be filtered out from "other"
("zed", "gemini"),
("copilot", "o3"),
]);
let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_other_models = grouped_models
.other
.values()
.flatten()
.cloned()
.collect::<Vec<_>>();
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
}
}

View File

@@ -628,6 +628,7 @@ impl GoogleEventMapper {
// responds with `finish_reason: STOP`
if wants_to_use_tool {
self.stop_reason = StopReason::ToolUse;
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
}
events
}

View File

@@ -477,14 +477,14 @@ fn add_message_content_part(
_ => {
messages.push(match role {
Role::User => open_ai::RequestMessage::User {
content: open_ai::MessageContent::empty(),
content: open_ai::MessageContent::from(vec![new_part]),
},
Role::Assistant => open_ai::RequestMessage::Assistant {
content: open_ai::MessageContent::empty(),
content: open_ai::MessageContent::from(vec![new_part]),
tool_calls: Vec::new(),
},
Role::System => open_ai::RequestMessage::System {
content: open_ai::MessageContent::empty(),
content: open_ai::MessageContent::from(vec![new_part]),
},
});
}

View File

@@ -2,8 +2,9 @@
//!
//! Breakpoints are separate from a session because they're not associated with any particular debug session. They can also be set up without a session running.
use anyhow::{Result, anyhow};
use breakpoints_in_file::BreakpointsInFile;
use collections::BTreeMap;
pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition};
use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint};
use collections::{BTreeMap, HashMap};
use dap::{StackFrameId, client::SessionId};
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
use itertools::Itertools;
@@ -14,21 +15,54 @@ use rpc::{
};
use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32};
use text::{Point, PointUtf16};
use util::maybe;
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
use super::session::ThreadId;
mod breakpoints_in_file {
use collections::HashMap;
use language::{BufferEvent, DiskState};
use super::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BreakpointWithPosition {
pub position: text::Anchor,
pub bp: Breakpoint,
}
/// A breakpoint with per-session data about it's state (as seen by the Debug Adapter).
#[derive(Clone, Debug)]
pub struct StatefulBreakpoint {
pub bp: BreakpointWithPosition,
pub session_state: HashMap<SessionId, BreakpointSessionState>,
}
impl StatefulBreakpoint {
pub(super) fn new(bp: BreakpointWithPosition) -> Self {
Self {
bp,
session_state: Default::default(),
}
}
pub(super) fn position(&self) -> &text::Anchor {
&self.bp.position
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct BreakpointSessionState {
/// Session-specific identifier for the breakpoint, as assigned by Debug Adapter.
pub id: u64,
pub verified: bool,
}
#[derive(Clone)]
pub(super) struct BreakpointsInFile {
pub(super) buffer: Entity<Buffer>,
// TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons
pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>,
pub(super) breakpoints: Vec<StatefulBreakpoint>,
_subscription: Arc<Subscription>,
}
@@ -199,9 +233,26 @@ impl BreakpointStore {
.breakpoints
.into_iter()
.filter_map(|breakpoint| {
let anchor = language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
let position =
language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
let session_state = breakpoint
.session_state
.iter()
.map(|(session_id, state)| {
let state = BreakpointSessionState {
id: state.id,
verified: state.verified,
};
(SessionId::from_proto(*session_id), state)
})
.collect();
let breakpoint = Breakpoint::from_proto(breakpoint)?;
Some((anchor, breakpoint))
let bp = BreakpointWithPosition {
position,
bp: breakpoint,
};
Some(StatefulBreakpoint { bp, session_state })
})
.collect();
@@ -231,7 +282,7 @@ impl BreakpointStore {
.payload
.breakpoint
.ok_or_else(|| anyhow!("Breakpoint not present in RPC payload"))?;
let anchor = language::proto::deserialize_anchor(
let position = language::proto::deserialize_anchor(
breakpoint
.position
.clone()
@@ -244,7 +295,10 @@ impl BreakpointStore {
breakpoints.update(&mut cx, |this, cx| {
this.toggle_breakpoint(
buffer,
(anchor, breakpoint),
BreakpointWithPosition {
position,
bp: breakpoint,
},
BreakpointEditAction::Toggle,
cx,
);
@@ -261,13 +315,76 @@ impl BreakpointStore {
breakpoints: breakpoint_set
.breakpoints
.iter()
.filter_map(|(anchor, bp)| bp.to_proto(&path, anchor))
.filter_map(|breakpoint| {
breakpoint.bp.bp.to_proto(
&path,
&breakpoint.position(),
&breakpoint.session_state,
)
})
.collect(),
});
}
}
}
pub(crate) fn update_session_breakpoint(
&mut self,
session_id: SessionId,
_: dap::BreakpointEventReason,
breakpoint: dap::Breakpoint,
) {
maybe!({
let event_id = breakpoint.id?;
let state = self
.breakpoints
.values_mut()
.find_map(|breakpoints_in_file| {
breakpoints_in_file
.breakpoints
.iter_mut()
.find_map(|state| {
let state = state.session_state.get_mut(&session_id)?;
if state.id == event_id {
Some(state)
} else {
None
}
})
})?;
state.verified = breakpoint.verified;
Some(())
});
}
pub(super) fn mark_breakpoints_verified(
&mut self,
session_id: SessionId,
abs_path: &Path,
it: impl Iterator<Item = (BreakpointWithPosition, BreakpointSessionState)>,
) {
maybe!({
let breakpoints = self.breakpoints.get_mut(abs_path)?;
for (breakpoint, state) in it {
if let Some(to_update) = breakpoints
.breakpoints
.iter_mut()
.find(|bp| *bp.position() == breakpoint.position)
{
to_update
.session_state
.entry(session_id)
.insert_entry(state);
}
}
Some(())
});
}
pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
worktree::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
@@ -277,7 +394,7 @@ impl BreakpointStore {
pub fn toggle_breakpoint(
&mut self,
buffer: Entity<Buffer>,
mut breakpoint: (text::Anchor, Breakpoint),
mut breakpoint: BreakpointWithPosition,
edit_action: BreakpointEditAction,
cx: &mut Context<Self>,
) {
@@ -295,54 +412,57 @@ impl BreakpointStore {
let len_before = breakpoint_set.breakpoints.len();
breakpoint_set
.breakpoints
.retain(|value| &breakpoint != value);
.retain(|value| breakpoint != value.bp);
if len_before == breakpoint_set.breakpoints.len() {
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
}
BreakpointEditAction::InvertState => {
if let Some((_, bp)) = breakpoint_set
if let Some(bp) = breakpoint_set
.breakpoints
.iter_mut()
.find(|value| breakpoint == **value)
.find(|value| breakpoint == value.bp)
{
let bp = &mut bp.bp.bp;
if bp.is_enabled() {
bp.state = BreakpointState::Disabled;
} else {
bp.state = BreakpointState::Enabled;
}
} else {
breakpoint.1.state = BreakpointState::Disabled;
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint.bp.state = BreakpointState::Disabled;
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
}
BreakpointEditAction::EditLogMessage(log_message) => {
if !log_message.is_empty() {
let found_bp =
breakpoint_set
.breakpoints
.iter_mut()
.find_map(|(other_pos, other_bp)| {
if breakpoint.0 == *other_pos {
Some(other_bp)
} else {
None
}
});
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {
if breakpoint.position == *bp.position() {
Some(&mut bp.bp.bp)
} else {
None
}
});
if let Some(found_bp) = found_bp {
found_bp.message = Some(log_message.clone());
} else {
breakpoint.1.message = Some(log_message.clone());
breakpoint.bp.message = Some(log_message.clone());
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
} else if breakpoint.1.message.is_some() {
} else if breakpoint.bp.message.is_some() {
if let Some(position) = breakpoint_set
.breakpoints
.iter()
.find_position(|(pos, bp)| &breakpoint.0 == pos && bp == &breakpoint.1)
.find_position(|other| breakpoint == other.bp)
.map(|res| res.0)
{
breakpoint_set.breakpoints.remove(position);
@@ -353,30 +473,28 @@ impl BreakpointStore {
}
BreakpointEditAction::EditHitCondition(hit_condition) => {
if !hit_condition.is_empty() {
let found_bp =
breakpoint_set
.breakpoints
.iter_mut()
.find_map(|(other_pos, other_bp)| {
if breakpoint.0 == *other_pos {
Some(other_bp)
} else {
None
}
});
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
if breakpoint.position == *other.position() {
Some(&mut other.bp.bp)
} else {
None
}
});
if let Some(found_bp) = found_bp {
found_bp.hit_condition = Some(hit_condition.clone());
} else {
breakpoint.1.hit_condition = Some(hit_condition.clone());
breakpoint.bp.hit_condition = Some(hit_condition.clone());
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()))
}
} else if breakpoint.1.hit_condition.is_some() {
} else if breakpoint.bp.hit_condition.is_some() {
if let Some(position) = breakpoint_set
.breakpoints
.iter()
.find_position(|(pos, bp)| &breakpoint.0 == pos && bp == &breakpoint.1)
.find_position(|bp| breakpoint == bp.bp)
.map(|res| res.0)
{
breakpoint_set.breakpoints.remove(position);
@@ -387,30 +505,28 @@ impl BreakpointStore {
}
BreakpointEditAction::EditCondition(condition) => {
if !condition.is_empty() {
let found_bp =
breakpoint_set
.breakpoints
.iter_mut()
.find_map(|(other_pos, other_bp)| {
if breakpoint.0 == *other_pos {
Some(other_bp)
} else {
None
}
});
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
if breakpoint.position == *other.position() {
Some(&mut other.bp.bp)
} else {
None
}
});
if let Some(found_bp) = found_bp {
found_bp.condition = Some(condition.clone());
} else {
breakpoint.1.condition = Some(condition.clone());
breakpoint.bp.condition = Some(condition.clone());
// We did not remove any breakpoint, hence let's toggle one.
breakpoint_set.breakpoints.push(breakpoint.clone());
breakpoint_set
.breakpoints
.push(StatefulBreakpoint::new(breakpoint.clone()));
}
} else if breakpoint.1.condition.is_some() {
} else if breakpoint.bp.condition.is_some() {
if let Some(position) = breakpoint_set
.breakpoints
.iter()
.find_position(|(pos, bp)| &breakpoint.0 == pos && bp == &breakpoint.1)
.find_position(|bp| breakpoint == bp.bp)
.map(|res| res.0)
{
breakpoint_set.breakpoints.remove(position);
@@ -425,7 +541,11 @@ impl BreakpointStore {
self.breakpoints.remove(&abs_path);
}
if let BreakpointStoreMode::Remote(remote) = &self.mode {
if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
if let Some(breakpoint) =
breakpoint
.bp
.to_proto(&abs_path, &breakpoint.position, &HashMap::default())
{
cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
project_id: remote._upstream_project_id,
path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
@@ -441,7 +561,11 @@ impl BreakpointStore {
breakpoint_set
.breakpoints
.iter()
.filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
.filter_map(|bp| {
bp.bp
.bp
.to_proto(&abs_path, bp.position(), &bp.session_state)
})
.collect()
})
.unwrap_or_default();
@@ -485,21 +609,31 @@ impl BreakpointStore {
range: Option<Range<text::Anchor>>,
buffer_snapshot: &'a BufferSnapshot,
cx: &App,
) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
{
let abs_path = Self::abs_path_from_buffer(buffer, cx);
let active_session_id = self
.active_stack_frame
.as_ref()
.map(|frame| frame.session_id);
abs_path
.and_then(|path| self.breakpoints.get(&path))
.into_iter()
.flat_map(move |file_breakpoints| {
file_breakpoints.breakpoints.iter().filter({
file_breakpoints.breakpoints.iter().filter_map({
let range = range.clone();
move |(position, _)| {
move |bp| {
if let Some(range) = &range {
position.cmp(&range.start, buffer_snapshot).is_ge()
&& position.cmp(&range.end, buffer_snapshot).is_le()
} else {
true
if bp.position().cmp(&range.start, buffer_snapshot).is_lt()
|| bp.position().cmp(&range.end, buffer_snapshot).is_gt()
{
return None;
}
}
let session_state = active_session_id
.and_then(|id| bp.session_state.get(&id))
.copied();
Some((&bp.bp, session_state))
}
})
})
@@ -549,34 +683,46 @@ impl BreakpointStore {
path: &Path,
row: u32,
cx: &App,
) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
self.breakpoints.get(path).and_then(|breakpoints| {
let snapshot = breakpoints.buffer.read(cx).text_snapshot();
breakpoints
.breakpoints
.iter()
.find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
.map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
.find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
.map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
})
}
pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
self.breakpoints
.get(path)
.map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
.unwrap_or_default()
}
pub fn source_breakpoints_from_path(
&self,
path: &Arc<Path>,
cx: &App,
) -> Vec<SourceBreakpoint> {
self.breakpoints
.get(path)
.map(|bp| {
let snapshot = bp.buffer.read(cx).snapshot();
bp.breakpoints
.iter()
.map(|(position, breakpoint)| {
let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
.map(|bp| {
let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
let bp = &bp.bp;
SourceBreakpoint {
row: position,
path: path.clone(),
state: breakpoint.state,
message: breakpoint.message.clone(),
condition: breakpoint.condition.clone(),
hit_condition: breakpoint.hit_condition.clone(),
state: bp.bp.state,
message: bp.bp.message.clone(),
condition: bp.bp.condition.clone(),
hit_condition: bp.bp.hit_condition.clone(),
}
})
.collect()
@@ -584,7 +730,18 @@ impl BreakpointStore {
.unwrap_or_default()
}
pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
self.breakpoints
.iter()
.map(|(path, bp)| {
(
path.clone(),
bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
)
})
.collect()
}
pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
self.breakpoints
.iter()
.map(|(path, bp)| {
@@ -593,15 +750,18 @@ impl BreakpointStore {
path.clone(),
bp.breakpoints
.iter()
.map(|(position, breakpoint)| {
let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
.map(|breakpoint| {
let position = snapshot
.summary_for_anchor::<PointUtf16>(&breakpoint.position())
.row;
let breakpoint = &breakpoint.bp;
SourceBreakpoint {
row: position,
path: path.clone(),
message: breakpoint.message.clone(),
state: breakpoint.state,
hit_condition: breakpoint.hit_condition.clone(),
condition: breakpoint.condition.clone(),
message: breakpoint.bp.message.clone(),
state: breakpoint.bp.state,
hit_condition: breakpoint.bp.hit_condition.clone(),
condition: breakpoint.bp.condition.clone(),
}
})
.collect(),
@@ -656,15 +816,17 @@ impl BreakpointStore {
continue;
}
let position = snapshot.anchor_after(point);
breakpoints_for_file.breakpoints.push((
position,
Breakpoint {
message: bp.message,
state: bp.state,
condition: bp.condition,
hit_condition: bp.hit_condition,
},
))
breakpoints_for_file
.breakpoints
.push(StatefulBreakpoint::new(BreakpointWithPosition {
position,
bp: Breakpoint {
message: bp.message,
state: bp.state,
condition: bp.condition,
hit_condition: bp.hit_condition,
},
}))
}
new_breakpoints.insert(path, breakpoints_for_file);
}
@@ -755,7 +917,7 @@ impl BreakpointState {
pub struct Breakpoint {
pub message: Option<BreakpointMessage>,
/// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
pub hit_condition: Option<BreakpointMessage>,
pub hit_condition: Option<Arc<str>>,
pub condition: Option<BreakpointMessage>,
pub state: BreakpointState,
}
@@ -788,7 +950,12 @@ impl Breakpoint {
}
}
fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
fn to_proto(
&self,
_path: &Path,
position: &text::Anchor,
session_states: &HashMap<SessionId, BreakpointSessionState>,
) -> Option<client::proto::Breakpoint> {
Some(client::proto::Breakpoint {
position: Some(serialize_text_anchor(position)),
state: match self.state {
@@ -801,6 +968,18 @@ impl Breakpoint {
.hit_condition
.as_ref()
.map(|s| String::from(s.as_ref())),
session_state: session_states
.iter()
.map(|(session_id, state)| {
(
session_id.to_proto(),
proto::BreakpointSessionState {
id: state.id,
verified: state.verified,
},
)
})
.collect(),
})
}

View File

@@ -10,13 +10,15 @@ use crate::{
terminals::{SshCommand, wrap_for_ssh},
worktree_store::WorktreeStore,
};
use anyhow::{Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use dap::{
Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
adapters::{DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments},
adapters::{
DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
},
client::SessionId,
inline_value::VariableLookupKind,
messages::Message,
@@ -489,14 +491,14 @@ impl DapStore {
worktree: &Entity<Worktree>,
console: UnboundedSender<String>,
cx: &mut App,
) -> DapAdapterDelegate {
) -> Arc<dyn DapDelegate> {
let Some(local_store) = self.as_local() else {
unimplemented!("Starting session on remote side");
};
DapAdapterDelegate::new(
Arc::new(DapAdapterDelegate::new(
local_store.fs.clone(),
worktree.read(cx).id(),
worktree.read(cx).snapshot(),
console,
local_store.node_runtime.clone(),
local_store.http_client.clone(),
@@ -504,7 +506,7 @@ impl DapStore {
local_store.environment.update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
}),
)
))
}
pub fn evaluate(
@@ -812,7 +814,7 @@ impl DapStore {
pub struct DapAdapterDelegate {
fs: Arc<dyn Fs>,
console: mpsc::UnboundedSender<String>,
worktree_id: WorktreeId,
worktree: worktree::Snapshot,
node_runtime: NodeRuntime,
http_client: Arc<dyn HttpClient>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
@@ -822,7 +824,7 @@ pub struct DapAdapterDelegate {
impl DapAdapterDelegate {
pub fn new(
fs: Arc<dyn Fs>,
worktree_id: WorktreeId,
worktree: worktree::Snapshot,
status: mpsc::UnboundedSender<String>,
node_runtime: NodeRuntime,
http_client: Arc<dyn HttpClient>,
@@ -832,7 +834,7 @@ impl DapAdapterDelegate {
Self {
fs,
console: status,
worktree_id,
worktree,
http_client,
node_runtime,
toolchain_store,
@@ -841,12 +843,15 @@ impl DapAdapterDelegate {
}
}
#[async_trait(?Send)]
#[async_trait]
impl dap::adapters::DapDelegate for DapAdapterDelegate {
fn worktree_id(&self) -> WorktreeId {
self.worktree_id
self.worktree.id()
}
fn worktree_root_path(&self) -> &Path {
&self.worktree.abs_path()
}
fn http_client(&self) -> Arc<dyn HttpClient> {
self.http_client.clone()
}
@@ -863,7 +868,7 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
self.console.unbounded_send(msg).ok();
}
fn which(&self, command: &OsStr) -> Option<PathBuf> {
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
which::which(command).ok()
}
@@ -875,4 +880,16 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
self.toolchain_store.clone()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
let entry = self
.worktree
.entry_for_path(&path)
.with_context(|| format!("no worktree entry for path {path:?}"))?;
let abs_path = self
.worktree
.absolutize(&entry.path)
.with_context(|| format!("cannot absolutize path {path:?}"))?;
self.fs.load(&abs_path).await
}
}

View File

@@ -1,3 +1,5 @@
use crate::debugger::breakpoint_store::BreakpointSessionState;
use super::breakpoint_store::{
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
};
@@ -218,25 +220,55 @@ impl LocalMode {
breakpoint_store: &Entity<BreakpointStore>,
cx: &mut App,
) -> Task<()> {
let breakpoints = breakpoint_store
.read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx))
let breakpoints =
breakpoint_store
.read_with(cx, |store, cx| {
store.source_breakpoints_from_path(&abs_path, cx)
})
.into_iter()
.filter(|bp| bp.state.is_enabled())
.chain(self.tmp_breakpoint.iter().filter_map(|breakpoint| {
breakpoint.path.eq(&abs_path).then(|| breakpoint.clone())
}))
.map(Into::into)
.collect();
let raw_breakpoints = breakpoint_store
.read(cx)
.breakpoints_from_path(&abs_path)
.into_iter()
.filter(|bp| bp.state.is_enabled())
.chain(self.tmp_breakpoint.clone())
.map(Into::into)
.collect();
.filter(|bp| bp.bp.state.is_enabled())
.collect::<Vec<_>>();
let task = self.request(dap_command::SetBreakpoints {
source: client_source(&abs_path),
source_modified: Some(matches!(reason, BreakpointUpdatedReason::FileSaved)),
breakpoints,
});
cx.background_spawn(async move {
match task.await {
Ok(_) => {}
Err(err) => log::warn!("Set breakpoints request failed for path: {}", err),
let session_id = self.client.id();
let breakpoint_store = breakpoint_store.downgrade();
cx.spawn(async move |cx| match cx.background_spawn(task).await {
Ok(breakpoints) => {
let breakpoints =
breakpoints
.into_iter()
.zip(raw_breakpoints)
.filter_map(|(dap_bp, zed_bp)| {
Some((
zed_bp,
BreakpointSessionState {
id: dap_bp.id?,
verified: dap_bp.verified,
},
))
});
breakpoint_store
.update(cx, |this, _| {
this.mark_breakpoints_verified(session_id, &abs_path, breakpoints);
})
.ok();
}
Err(err) => log::warn!("Set breakpoints request failed for path: {}", err),
})
}
@@ -271,8 +303,11 @@ impl LocalMode {
cx: &App,
) -> Task<HashMap<Arc<Path>, anyhow::Error>> {
let mut breakpoint_tasks = Vec::new();
let breakpoints = breakpoint_store.read_with(cx, |store, cx| store.all_breakpoints(cx));
let breakpoints =
breakpoint_store.read_with(cx, |store, cx| store.all_source_breakpoints(cx));
let mut raw_breakpoints = breakpoint_store.read_with(cx, |this, _| this.all_breakpoints());
debug_assert_eq!(raw_breakpoints.len(), breakpoints.len());
let session_id = self.client.id();
for (path, breakpoints) in breakpoints {
let breakpoints = if ignore_breakpoints {
vec![]
@@ -284,14 +319,46 @@ impl LocalMode {
.collect()
};
breakpoint_tasks.push(
self.request(dap_command::SetBreakpoints {
let raw_breakpoints = raw_breakpoints
.remove(&path)
.unwrap_or_default()
.into_iter()
.filter(|bp| bp.bp.state.is_enabled());
let error_path = path.clone();
let send_request = self
.request(dap_command::SetBreakpoints {
source: client_source(&path),
source_modified: Some(false),
breakpoints,
})
.map(|result| result.map_err(|e| (path, e))),
);
.map(|result| result.map_err(move |e| (error_path, e)));
let task = cx.spawn({
let breakpoint_store = breakpoint_store.downgrade();
async move |cx| {
let breakpoints = cx.background_spawn(send_request).await?;
let breakpoints = breakpoints.into_iter().zip(raw_breakpoints).filter_map(
|(dap_bp, zed_bp)| {
Some((
zed_bp,
BreakpointSessionState {
id: dap_bp.id?,
verified: dap_bp.verified,
},
))
},
);
breakpoint_store
.update(cx, |this, _| {
this.mark_breakpoints_verified(session_id, &path, breakpoints);
})
.ok();
Ok(())
}
});
breakpoint_tasks.push(task);
}
cx.background_spawn(async move {
@@ -1204,7 +1271,9 @@ impl Session {
self.output_token.0 += 1;
cx.notify();
}
Events::Breakpoint(_) => {}
Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| {
store.update_session_breakpoint(self.session_id(), event.reason, event.breakpoint);
}),
Events::Module(event) => {
match event.reason {
dap::ModuleEventReason::New => {

View File

@@ -47,6 +47,7 @@ use dap::{DapRegistry, client::DebugAdapterClient};
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
pub use debugger::breakpoint_store::BreakpointWithPosition;
use debugger::{
breakpoint_store::{ActiveStackFrame, BreakpointStore},
dap_store::{DapStore, DapStoreEvent},

View File

@@ -118,22 +118,19 @@ pub enum DirenvSettings {
Direct,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct DiagnosticsSettings {
/// Whether to show the project diagnostics button in the status bar.
#[serde(default = "default_true")]
pub button: bool,
/// Whether or not to include warning diagnostics.
#[serde(default = "default_true")]
pub include_warnings: bool,
/// Settings for showing inline diagnostics.
#[serde(default)]
pub inline: InlineDiagnosticsSettings,
/// Configuration, related to Rust language diagnostics.
#[serde(default)]
pub cargo: Option<CargoDiagnosticsSettings>,
}
@@ -146,33 +143,29 @@ impl DiagnosticsSettings {
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct InlineDiagnosticsSettings {
/// Whether or not to show inline diagnostics
///
/// Default: false
#[serde(default)]
pub enabled: bool,
/// Whether to only show the inline diagnostics after a delay after the
/// last editor event.
///
/// Default: 150
#[serde(default = "default_inline_diagnostics_debounce_ms")]
pub update_debounce_ms: u64,
/// The amount of padding between the end of the source line and the start
/// of the inline diagnostic in units of columns.
///
/// Default: 4
#[serde(default = "default_inline_diagnostics_padding")]
pub padding: u32,
/// The minimum column to display inline diagnostics. This setting can be
/// used to horizontally align inline diagnostics at some position. Lines
/// longer than this value will still push diagnostics further to the right.
///
/// Default: 0
#[serde(default)]
pub min_column: u32,
#[serde(default)]
pub max_severity: Option<DiagnosticSeverity>,
}
@@ -211,24 +204,27 @@ impl DiagnosticSeverity {
}
}
impl Default for InlineDiagnosticsSettings {
impl Default for DiagnosticsSettings {
fn default() -> Self {
Self {
enabled: false,
update_debounce_ms: default_inline_diagnostics_debounce_ms(),
padding: default_inline_diagnostics_padding(),
min_column: 0,
max_severity: None,
button: true,
include_warnings: true,
inline: Default::default(),
cargo: Default::default(),
}
}
}
fn default_inline_diagnostics_debounce_ms() -> u64 {
150
}
fn default_inline_diagnostics_padding() -> u32 {
4
impl Default for InlineDiagnosticsSettings {
fn default() -> Self {
Self {
enabled: false,
update_debounce_ms: 150,
padding: 4,
min_column: 0,
max_severity: None,
}
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

View File

@@ -3,7 +3,6 @@ fn main() {
build
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute("ProjectPath", "#[derive(Hash, Eq)]")
.type_attribute("Breakpoint", "#[derive(Hash, Eq)]")
.type_attribute("Anchor", "#[derive(Hash, Eq)]")
.compile_protos(&["proto/zed.proto"], &["proto"])
.unwrap();

View File

@@ -16,6 +16,12 @@ message Breakpoint {
optional string message = 4;
optional string condition = 5;
optional string hit_condition = 6;
map<uint64, BreakpointSessionState> session_state = 7;
}
message BreakpointSessionState {
uint64 id = 1;
bool verified = 2;
}
message BreakpointsForFile {
@@ -30,63 +36,6 @@ message ToggleBreakpoint {
Breakpoint breakpoint = 3;
}
enum DebuggerThreadItem {
Console = 0;
LoadedSource = 1;
Modules = 2;
Variables = 3;
}
message DebuggerSetVariableState {
string name = 1;
DapScope scope = 2;
string value = 3;
uint64 stack_frame_id = 4;
optional string evaluate_name = 5;
uint64 parent_variables_reference = 6;
}
message VariableListOpenEntry {
oneof entry {
DebuggerOpenEntryScope scope = 1;
DebuggerOpenEntryVariable variable = 2;
}
}
message DebuggerOpenEntryScope {
string name = 1;
}
message DebuggerOpenEntryVariable {
string scope_name = 1;
string name = 2;
uint64 depth = 3;
}
message VariableListEntrySetState {
uint64 depth = 1;
DebuggerSetVariableState state = 2;
}
message VariableListEntryVariable {
uint64 depth = 1;
DapScope scope = 2;
DapVariable variable = 3;
bool has_children = 4;
uint64 container_reference = 5;
}
message DebuggerScopeVariableIndex {
repeated uint64 fetched_ids = 1;
repeated DebuggerVariableContainer variables = 2;
}
message DebuggerVariableContainer {
uint64 container_reference = 1;
DapVariable variable = 2;
uint64 depth = 3;
}
enum DapThreadStatus {
Running = 0;
Stopped = 1;
@@ -94,18 +43,6 @@ enum DapThreadStatus {
Ended = 3;
}
message VariableListScopes {
uint64 stack_frame_id = 1;
repeated DapScope scopes = 2;
}
message VariableListVariables {
uint64 stack_frame_id = 1;
uint64 scope_id = 2;
DebuggerScopeVariableIndex variables = 3;
}
enum VariablesArgumentsFilter {
Indexed = 0;
Named = 1;

View File

@@ -30,6 +30,7 @@ chrono.workspace = true
clap.workspace = true
client.workspace = true
dap_adapters.workspace = true
debug_adapter_extension.workspace = true
env_logger.workspace = true
extension.workspace = true
extension_host.workspace = true

View File

@@ -76,6 +76,7 @@ impl HeadlessProject {
}: HeadlessAppState,
cx: &mut Context<Self>,
) -> Self {
debug_adapter_extension::init(proxy.clone(), cx);
language_extension::init(proxy.clone(), languages.clone());
languages::init(languages.clone(), node_runtime.clone(), cx);

View File

@@ -37,7 +37,7 @@ use ui::{
Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, LabelCommon, LabelSize,
Toggleable, Tooltip, h_flex, prelude::*, utils::SearchInputWidth, v_flex,
};
use util::paths::PathMatcher;
use util::{ResultExt as _, paths::PathMatcher};
use workspace::{
DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace, WorkspaceId,
@@ -72,15 +72,18 @@ pub fn init(cx: &mut App) {
);
register_workspace_action(
workspace,
move |search_bar, _: &ToggleCaseSensitive, _, cx| {
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
move |search_bar, _: &ToggleCaseSensitive, window, cx| {
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
},
);
register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, _, cx| {
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
});
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, _, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, cx);
register_workspace_action(
workspace,
move |search_bar, _: &ToggleWholeWord, window, cx| {
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
},
);
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, window, cx);
});
register_workspace_action(
workspace,
@@ -1032,6 +1035,69 @@ impl ProjectSearchView {
});
}
fn prompt_to_save_if_dirty_then_search(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
use workspace::AutosaveSetting;
let project = self.entity.read(cx).project.clone();
let can_autosave = self.results_editor.can_autosave(cx);
let autosave_setting = self.results_editor.workspace_settings(cx).autosave;
let will_autosave = can_autosave
&& matches!(
autosave_setting,
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
);
let is_dirty = self.is_dirty(cx);
cx.spawn_in(window, async move |this, cx| {
let skip_save_on_close = this
.read_with(cx, |this, cx| {
this.workspace.read_with(cx, |workspace, cx| {
workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx)
})
})?
.unwrap_or(false);
let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
let should_search = if should_prompt_to_save {
let options = &["Save", "Don't Save", "Cancel"];
let result_channel = this.update_in(cx, |_, window, cx| {
window.prompt(
gpui::PromptLevel::Warning,
"Project search buffer contains unsaved edits. Do you want to save it?",
None,
options,
cx,
)
})?;
let result = result_channel.await?;
let should_save = result == 0;
if should_save {
this.update_in(cx, |this, window, cx| this.save(true, project, window, cx))?
.await
.log_err();
}
let should_search = result != 2;
should_search
} else {
true
};
if should_search {
this.update(cx, |this, cx| {
this.search(cx);
})?;
}
anyhow::Ok(())
})
}
fn search(&mut self, cx: &mut Context<Self>) {
if let Some(query) = self.build_search_query(cx) {
self.entity.update(cx, |model, cx| model.search(query, cx));
@@ -1503,7 +1569,9 @@ impl ProjectSearchBar {
.is_focused(window)
{
cx.stop_propagation();
search_view.search(cx);
search_view
.prompt_to_save_if_dirty_then_search(window, cx)
.detach_and_log_err(cx);
}
});
}
@@ -1570,19 +1638,39 @@ impl ProjectSearchBar {
});
}
fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context<Self>) -> bool {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
search_view.toggle_search_option(option, cx);
if search_view.entity.read(cx).active_query.is_some() {
search_view.search(cx);
}
});
cx.notify();
true
} else {
false
fn toggle_search_option(
&mut self,
option: SearchOptions,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
if self.active_project_search.is_none() {
return false;
}
cx.spawn_in(window, async move |this, cx| {
let task = this.update_in(cx, |this, window, cx| {
let search_view = this.active_project_search.as_ref()?;
search_view.update(cx, |search_view, cx| {
search_view.toggle_search_option(option, cx);
search_view
.entity
.read(cx)
.active_query
.is_some()
.then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
})
})?;
if let Some(task) = task {
task.await?;
}
this.update(cx, |_, cx| {
cx.notify();
})?;
anyhow::Ok(())
})
.detach();
true
}
fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
@@ -1621,19 +1709,33 @@ impl ProjectSearchBar {
}
fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
search_view.toggle_opened_only(window, cx);
if search_view.entity.read(cx).active_query.is_some() {
search_view.search(cx);
}
});
cx.notify();
true
} else {
false
if self.active_project_search.is_none() {
return false;
}
cx.spawn_in(window, async move |this, cx| {
let task = this.update_in(cx, |this, window, cx| {
let search_view = this.active_project_search.as_ref()?;
search_view.update(cx, |search_view, cx| {
search_view.toggle_opened_only(window, cx);
search_view
.entity
.read(cx)
.active_query
.is_some()
.then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx))
})
})?;
if let Some(task) = task {
task.await?;
}
this.update(cx, |_, cx| {
cx.notify();
})?;
anyhow::Ok(())
})
.detach();
true
}
fn is_opened_only_enabled(&self, cx: &App) -> bool {
@@ -1860,22 +1962,22 @@ impl Render for ProjectSearchBar {
.child(SearchOptions::CASE_SENSITIVE.as_button(
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
focus_handle.clone(),
cx.listener(|this, _, _, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
}),
))
.child(SearchOptions::WHOLE_WORD.as_button(
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
focus_handle.clone(),
cx.listener(|this, _, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
}),
))
.child(SearchOptions::REGEX.as_button(
self.is_option_enabled(SearchOptions::REGEX, cx),
focus_handle.clone(),
cx.listener(|this, _, _, cx| {
this.toggle_search_option(SearchOptions::REGEX, cx);
cx.listener(|this, _, window, cx| {
this.toggle_search_option(SearchOptions::REGEX, window, cx);
}),
)),
);
@@ -2147,8 +2249,12 @@ impl Render for ProjectSearchBar {
.search_options
.contains(SearchOptions::INCLUDE_IGNORED),
focus_handle.clone(),
cx.listener(|this, _, _, cx| {
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
cx.listener(|this, _, window, cx| {
this.toggle_search_option(
SearchOptions::INCLUDE_IGNORED,
window,
cx,
);
}),
),
),
@@ -2188,11 +2294,11 @@ impl Render for ProjectSearchBar {
.on_action(cx.listener(|this, action, window, cx| {
this.toggle_replace(action, window, cx);
}))
.on_action(cx.listener(|this, _: &ToggleWholeWord, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
.on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx);
}))
.on_action(cx.listener(|this, _: &ToggleCaseSensitive, _, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
.on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx);
}))
.on_action(cx.listener(|this, action, window, cx| {
if let Some(search) = this.active_project_search.as_ref() {
@@ -2209,8 +2315,8 @@ impl Render for ProjectSearchBar {
}
}))
.when(search.filters_enabled, |this| {
this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, _, cx| {
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, cx);
this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| {
this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx);
}))
})
.on_action(cx.listener(Self::select_next_match))

View File

@@ -2,23 +2,17 @@ use db::anyhow;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use util::serde::default_true;
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
#[derive(Copy, Clone, Deserialize, Debug)]
pub struct TitleBarSettings {
#[serde(default)]
pub show_branch_icon: bool,
#[serde(default = "default_true")]
pub show_branch_name: bool,
#[serde(default = "default_true")]
pub show_project_items: bool,
#[serde(default = "default_true")]
pub show_onboarding_banner: bool,
#[serde(default = "default_true")]
pub show_user_picture: bool,
pub show_branch_name: bool,
pub show_project_items: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct TitleBarSettingsContent {
/// Whether to show the branch icon beside branch switcher in the title bar.
///

View File

@@ -1,7 +1,14 @@
use gpui::{ClickEvent, Corner, CursorStyle, Entity, MouseButton};
use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
use crate::{ContextMenu, PopoverMenu, prelude::*};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DropdownStyle {
#[default]
Solid,
Ghost,
}
enum LabelKind {
Text(SharedString),
Element(AnyElement),
@@ -11,6 +18,7 @@ enum LabelKind {
pub struct DropdownMenu {
id: ElementId,
label: LabelKind,
style: DropdownStyle,
menu: Entity<ContextMenu>,
full_width: bool,
disabled: bool,
@@ -25,6 +33,7 @@ impl DropdownMenu {
Self {
id: id.into(),
label: LabelKind::Text(label.into()),
style: DropdownStyle::default(),
menu,
full_width: false,
disabled: false,
@@ -39,12 +48,18 @@ impl DropdownMenu {
Self {
id: id.into(),
label: LabelKind::Element(label),
style: DropdownStyle::default(),
menu,
full_width: false,
disabled: false,
}
}
pub fn style(mut self, style: DropdownStyle) -> Self {
self.style = style;
self
}
pub fn full_width(mut self, full_width: bool) -> Self {
self.full_width = full_width;
self
@@ -66,7 +81,8 @@ impl RenderOnce for DropdownMenu {
.trigger(
DropdownMenuTrigger::new(self.label)
.full_width(self.full_width)
.disabled(self.disabled),
.disabled(self.disabled)
.style(self.style),
)
.attach(Corner::BottomLeft)
}
@@ -135,12 +151,35 @@ impl Component for DropdownMenu {
}
}
#[derive(Debug, Clone, Copy)]
pub struct DropdownTriggerStyle {
pub bg: Hsla,
}
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,
}
}
}
}
#[derive(IntoElement)]
struct DropdownMenuTrigger {
label: LabelKind,
full_width: bool,
selected: bool,
disabled: bool,
style: DropdownStyle,
cursor_style: CursorStyle,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
@@ -152,6 +191,7 @@ impl DropdownMenuTrigger {
full_width: false,
selected: false,
disabled: false,
style: DropdownStyle::default(),
cursor_style: CursorStyle::default(),
on_click: None,
}
@@ -161,6 +201,11 @@ impl DropdownMenuTrigger {
self.full_width = full_width;
self
}
pub fn style(mut self, style: DropdownStyle) -> Self {
self.style = style;
self
}
}
impl Disableable for DropdownMenuTrigger {
@@ -193,11 +238,13 @@ impl RenderOnce for DropdownMenuTrigger {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let disabled = self.disabled;
let style = DropdownTriggerStyle::for_style(self.style, cx);
h_flex()
.id("dropdown-menu-trigger")
.justify_between()
.rounded_sm()
.bg(cx.theme().colors().editor_background)
.bg(style.bg)
.pl_2()
.pr_1p5()
.py_0p5()

View File

@@ -13,6 +13,7 @@ pub struct ProgressBar {
value: f32,
max_value: f32,
bg_color: Hsla,
over_color: Hsla,
fg_color: Hsla,
}
@@ -23,6 +24,7 @@ impl ProgressBar {
value,
max_value,
bg_color: cx.theme().colors().background,
over_color: cx.theme().status().error,
fg_color: cx.theme().status().info,
}
}
@@ -50,6 +52,12 @@ impl ProgressBar {
self.fg_color = color;
self
}
/// Sets the over limit color of the progress bar.
pub fn over_color(mut self, color: Hsla) -> Self {
self.over_color = color;
self
}
}
impl RenderOnce for ProgressBar {
@@ -74,7 +82,8 @@ impl RenderOnce for ProgressBar {
div()
.h_full()
.rounded_full()
.bg(self.fg_color)
.when(self.value > self.max_value, |div| div.bg(self.over_color))
.when(self.value <= self.max_value, |div| div.bg(self.fg_color))
.w(relative(fill_width)),
)
}

View File

@@ -564,6 +564,10 @@ pub trait ItemHandle: 'static + Send {
fn preserve_preview(&self, cx: &App) -> bool;
fn include_in_nav_history(&self) -> bool;
fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App);
fn can_autosave(&self, cx: &App) -> bool {
let is_deleted = self.project_entry_ids(cx).is_empty();
self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted
}
}
pub trait WeakItemHandle: Send + Sync {

View File

@@ -1440,10 +1440,7 @@ impl Pane {
}
});
if dirty_project_item_ids.is_empty() {
if item.is_singleton(cx) && item.is_dirty(cx) {
return false;
}
return true;
return !(item.is_singleton(cx) && item.is_dirty(cx));
}
for open_item in workspace.items(cx) {
@@ -1456,11 +1453,7 @@ impl Pane {
let other_project_item_ids = open_item.project_item_model_ids(cx);
dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id));
}
if dirty_project_item_ids.is_empty() {
return true;
}
false
return dirty_project_item_ids.is_empty();
}
pub(super) fn file_names_for_prompt(
@@ -1857,7 +1850,7 @@ impl Pane {
matches!(
item.workspace_settings(cx).autosave,
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
) && Self::can_autosave_item(item, cx)
) && item.can_autosave(cx)
})?;
if !will_autosave {
let item_id = item.item_id();
@@ -1945,11 +1938,6 @@ impl Pane {
})
}
fn can_autosave_item(item: &dyn ItemHandle, cx: &App) -> bool {
let is_deleted = item.project_entry_ids(cx).is_empty();
item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
}
pub fn autosave_item(
item: &dyn ItemHandle,
project: Entity<Project>,
@@ -1960,7 +1948,7 @@ impl Pane {
item.workspace_settings(cx).autosave,
AutosaveSetting::AfterDelay { .. }
);
if Self::can_autosave_item(item, cx) {
if item.can_autosave(cx) {
item.save(format, project, window, cx)
} else {
Task::ready(Ok(()))

View File

@@ -2775,10 +2775,17 @@ impl Workspace {
/// Focus the panel of the given type if it isn't already focused. If it is
/// already focused, then transfer focus back to the workspace center.
pub fn toggle_panel_focus<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub fn toggle_panel_focus<T: Panel>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let mut did_focus_panel = false;
self.focus_or_unfocus_panel::<T>(window, cx, |panel, window, cx| {
!panel.panel_focus_handle(cx).contains_focused(window, cx)
did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
did_focus_panel
});
did_focus_panel
}
pub fn activate_panel_for_proto_id(
@@ -2812,7 +2819,7 @@ impl Workspace {
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
should_focus: impl Fn(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
mut should_focus: impl FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
) -> Option<Arc<dyn PanelHandle>> {
let mut result_panel = None;
let mut serialize = false;
@@ -4987,7 +4994,10 @@ impl Workspace {
if let Some(location) = self.serialize_workspace_location(cx) {
let breakpoints = self.project.update(cx, |project, cx| {
project.breakpoint_store().read(cx).all_breakpoints(cx)
project
.breakpoint_store()
.read(cx)
.all_source_breakpoints(cx)
});
let center_group = build_serialized_pane_group(&self.center.root, window, cx);

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.187.0"
version = "0.187.4"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -45,6 +45,7 @@ dap_adapters.workspace = true
debugger_ui.workspace = true
debugger_tools.workspace = true
db.workspace = true
debug_adapter_extension.workspace = true
diagnostics.workspace = true
editor.workspace = true
env_logger.workspace = true

View File

@@ -1 +1 @@
dev
stable

View File

@@ -419,6 +419,7 @@ fn main() {
.detach();
let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
language::init(cx);
language_extension::init(extension_host_proxy.clone(), languages.clone());
languages::init(languages.clone(), node_runtime.clone(), cx);

View File

@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
use anyhow::Context as _;
use client::{Client, UserStore, zed_urls};
use client::{Client, UserStore};
use db::kvp::KEY_VALUE_STORE;
use fs::Fs;
use gpui::{
@@ -384,47 +384,29 @@ impl Render for ZedPredictModal {
} else {
(IconName::ChevronDown, IconName::ChevronUp)
};
let plan = plan.unwrap_or(proto::Plan::Free);
base.child(Label::new(copy).color(Color::Muted))
.child(h_flex().map(|parent| {
if let Some(plan) = plan {
parent.child(
Checkbox::new("plan", ToggleState::Selected)
.fill()
.disabled(true)
.label(format!(
"You get {} edit predictions through your {}.",
if plan == proto::Plan::Free {
"2,000"
} else {
"unlimited"
},
match plan {
proto::Plan::Free => "Zed Free plan",
proto::Plan::ZedPro => "Zed Pro plan",
proto::Plan::ZedProTrial => "Zed Pro trial",
}
)),
)
} else {
parent
.child(
Checkbox::new("plan-required", ToggleState::Unselected)
.fill()
.disabled(true)
.label("To get started with edit prediction"),
)
.child(
Button::new("subscribe", "choose a plan")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.on_click(|_event, _window, cx| {
cx.open_url(&zed_urls::account_url(cx));
}),
)
}
}))
.child(
h_flex().child(
Checkbox::new("plan", ToggleState::Selected)
.fill()
.disabled(true)
.label(format!(
"You get {} edit predictions through your {}.",
if plan == proto::Plan::Free {
"2,000"
} else {
"unlimited"
},
match plan {
proto::Plan::Free => "Zed Free plan",
proto::Plan::ZedPro => "Zed Pro plan",
proto::Plan::ZedProTrial => "Zed Pro trial",
}
)),
),
)
.child(
h_flex()
.child(
@@ -495,7 +477,7 @@ impl Render for ZedPredictModal {
.w_full()
.child(
Button::new("accept-tos", "Enable Edit Prediction")
.disabled(plan.is_none() || !self.terms_of_service)
.disabled(!self.terms_of_service)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::accept_and_enable)),