Compare commits

...

18 Commits

Author SHA1 Message Date
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
32 changed files with 1384 additions and 659 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
}

2
Cargo.lock generated
View File

@@ -18540,7 +18540,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.187.0"
version = "0.187.3"
dependencies = [
"activity_indicator",
"agent",

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",

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 }],

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

@@ -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

@@ -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,
@@ -87,7 +88,20 @@ impl DebugPanel {
})
}
fn filter_action_types(&self, cx: &mut App) {
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| {
@@ -273,7 +287,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 +356,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 +430,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 +489,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 +541,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 +585,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 +601,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 +628,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 +652,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 +676,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 +703,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 +753,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 +776,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 +798,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 +832,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 +866,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 +916,7 @@ impl DebugPanel {
}
}
fn activate_item(
pub(crate) fn activate_item(
&mut self,
item: DebuggerPaneItem,
window: &mut Window,
@@ -985,7 +931,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;

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,12 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
}
impl RunningState {
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
}
impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let zoomed_pane = self
@@ -515,7 +520,7 @@ impl Focusable for DebugTerminal {
}
impl RunningState {
pub fn new(
pub(crate) fn new(
session: Entity<Session>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
@@ -1214,10 +1219,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 +1245,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 +1322,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 +1464,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;
@@ -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

@@ -17918,10 +17918,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 +17945,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

@@ -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

@@ -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

@@ -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,61 @@ 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);
let should_confirm_save = !will_autosave && is_dirty;
cx.spawn_in(window, async move |this, cx| {
let should_search = if should_confirm_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 +1561,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 +1630,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 +1701,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 +1954,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 +2241,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 +2286,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 +2307,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,50 +2,48 @@ 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, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(default)]
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,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct TitleBarSettingsContent {
/// Whether to show the branch icon beside branch switcher in the title bar.
///
/// Default: false
pub show_branch_icon: Option<bool>,
pub show_branch_icon: bool,
/// Whether to show onboarding banners in the title bar.
///
/// Default: true
pub show_onboarding_banner: Option<bool>,
pub show_onboarding_banner: bool,
/// Whether to show user avatar in the title bar.
///
/// Default: true
pub show_user_picture: Option<bool>,
pub show_user_picture: bool,
/// Whether to show the branch name button in the titlebar.
///
/// Default: true
pub show_branch_name: Option<bool>,
pub show_branch_name: bool,
/// Whether to show the project host and name in the titlebar.
///
/// Default: true
pub show_project_items: Option<bool>,
pub show_project_items: bool,
}
impl Default for TitleBarSettings {
fn default() -> Self {
Self {
show_branch_icon: false,
show_onboarding_banner: true,
show_user_picture: true,
show_branch_name: true,
show_project_items: true,
}
}
}
impl Settings for TitleBarSettings {
const KEY: Option<&'static str> = Some("title_bar");
type FileContent = TitleBarSettingsContent;
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> anyhow::Result<Self>
where

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

@@ -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

@@ -1857,7 +1857,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 +1945,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 +1955,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

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

View File

@@ -1 +1 @@
dev
preview