mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-27 08:34:11 +00:00
TODO:
- [x] merge main
- [x] nonshrinking `set_excerpts_for_path`
- [x] Test-drive potential problem areas in the app
- [x] prepare cloud side
- [x] test collaboration
- [ ] docstrings
- [ ] ???
## Context
### Background
Currently, a multibuffer consists of an arbitrary list of
anchor-delimited excerpts from individual buffers. Excerpt ranges for a
fixed buffer are permitted to overlap, and can appear in any order in
the multibuffer, possibly separated by excerpts from other buffers.
However, in practice all code that constructs multibuffers does so using
the APIs defined in the `path_key` submodule of the `multi_buffer` crate
(`set_excerpts_for_path` etc.) If you only use these APIs, the resulting
multibuffer will maintain the following invariants:
- All excerpts for the same buffer appear contiguously in the
multibuffer
- Excerpts for the same buffer cannot overlap
- Excerpts for the same buffer appear in order
- The placement of the excerpts for a specific buffer in the multibuffer
are determined by the `PathKey` passed to `set_excerpts_for_path`. There
is exactly one `PathKey` per buffer in the multibuffer
### Purpose of this PR
This PR changes the multibuffer so that the invariants maintained by the
`path_key` APIs *always* hold. It's no longer possible to construct a
multibuffer with overlapping excerpts, etc. The APIs that permitted
this, like `insert_excerpts_with_ids_after`, have been removed in favor
of the `path_key` suite.
The main upshot of this is that given a `text::Anchor` and a
multibuffer, it's possible to efficiently figure out the unique excerpt
that includes that anchor, if any:
```
impl MultiBufferSnapshot {
fn buffer_anchor_to_anchor(&self, anchor: text::Anchor) -> Option<multi_buffer::Anchor>;
}
```
And in the other direction, given a `multi_buffer::Anchor`, we can look
at its `text::Anchor` to locate the excerpt that contains it. That means
we don't need an `ExcerptId` to create or resolve
`multi_buffer::Anchor`, and in fact we can delete `ExcerptId` entirely,
so that excerpts no longer have any identity outside their
`Range<text::Anchor>`.
There are a large number of changes to `editor` and other downstream
crates as a result of removing `ExcerptId` and multibuffer APIs that
assumed it.
### Other changes
There are some other improvements that are not immediate consequences of
that big change, but helped make it smoother. Notably:
- The `buffer_id` field of `text::Anchor` is no longer optional.
`text::Anchor::{MIN, MAX}` have been removed in favor of
`min_for_buffer`, etc.
- `multi_buffer::Anchor` is now a three-variant enum (inlined slightly):
```
enum Anchor {
Min,
Excerpt {
text_anchor: text::Anchor,
path_key_index: PathKeyIndex,
diff_base_anchor: Option<text::Anchor>,
},
Max,
}
```
That means it's no longer possible to unconditionally access the
`text_anchor` field, which is good because most of the places that were
doing that were buggy for min/max! Instead, we have a new API that
correctly resolves min/max to the start of the first excerpt or the end
of the last excerpt:
```
impl MultiBufferSnapshot {
fn anchor_to_buffer_anchor(&self, anchor: multi_buffer::Anchor) -> Option<text::Anchor>;
}
```
- `MultiBufferExcerpt` has been removed in favor of a new
`map_excerpt_ranges` API directly on `MultiBufferSnapshot`.
## Self-Review Checklist
<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
---------
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
Co-authored-by: Conrad <conrad@zed.dev>
446 lines
18 KiB
Rust
446 lines
18 KiB
Rust
use std::any::TypeId;
|
|
|
|
use debugger_panel::DebugPanel;
|
|
use editor::{Editor, MultiBufferOffsetUtf16};
|
|
use gpui::{Action, App, DispatchPhase, EntityInputHandler, actions};
|
|
use new_process_modal::{NewProcessModal, NewProcessMode};
|
|
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use session::DebugSession;
|
|
|
|
use tasks_ui::{Spawn, TaskOverrides};
|
|
use ui::{FluentBuilder, InteractiveElement};
|
|
use util::maybe;
|
|
use workspace::{ShutdownDebugAdapters, Workspace};
|
|
use zed_actions::debug_panel::{Toggle, ToggleFocus};
|
|
|
|
pub mod attach_modal;
|
|
pub mod debugger_panel;
|
|
mod dropdown_menus;
|
|
mod new_process_modal;
|
|
mod persistence;
|
|
pub(crate) mod session;
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub mod tests;
|
|
|
|
// Let's see the diff-test in action.
|
|
actions!(
|
|
debugger,
|
|
[
|
|
/// Starts a new debugging session.
|
|
Start,
|
|
/// Continues execution until the next breakpoint.
|
|
Continue,
|
|
/// Detaches the debugger from the running process.
|
|
Detach,
|
|
/// Pauses the currently running program.
|
|
Pause,
|
|
/// Restarts the current debugging session.
|
|
Restart,
|
|
/// Reruns the current debugging session with the same configuration.
|
|
RerunSession,
|
|
/// Steps into the next function call.
|
|
StepInto,
|
|
/// Steps over the current line.
|
|
StepOver,
|
|
/// Steps out of the current function.
|
|
StepOut,
|
|
/// Steps back to the previous statement.
|
|
StepBack,
|
|
/// Stops the debugging session.
|
|
Stop,
|
|
/// Toggles whether to ignore all breakpoints.
|
|
ToggleIgnoreBreakpoints,
|
|
/// Clears all breakpoints in the project.
|
|
ClearAllBreakpoints,
|
|
/// Focuses on the debugger console panel.
|
|
FocusConsole,
|
|
/// Focuses on the variables panel.
|
|
FocusVariables,
|
|
/// Focuses on the breakpoint list panel.
|
|
FocusBreakpointList,
|
|
/// Focuses on the call stack frames panel.
|
|
FocusFrames,
|
|
/// Focuses on the loaded modules panel.
|
|
FocusModules,
|
|
/// Focuses on the loaded sources panel.
|
|
FocusLoadedSources,
|
|
/// Focuses on the terminal panel.
|
|
FocusTerminal,
|
|
/// Toggles the thread picker dropdown.
|
|
ToggleThreadPicker,
|
|
/// Toggles the session picker dropdown.
|
|
ToggleSessionPicker,
|
|
/// Reruns the last debugging session.
|
|
#[action(deprecated_aliases = ["debugger::RerunLastSession"])]
|
|
Rerun,
|
|
/// Toggles expansion of the selected item in the debugger UI.
|
|
ToggleExpandItem,
|
|
/// Toggle the user frame filter in the stack frame list
|
|
/// When toggled on, only frames from the user's code are shown
|
|
/// When toggled off, all frames are shown
|
|
ToggleUserFrames,
|
|
]
|
|
);
|
|
|
|
/// Set a data breakpoint on the selected variable or memory region.
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = debugger)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ToggleDataBreakpoint {
|
|
/// The type of data breakpoint
|
|
/// Read & Write
|
|
/// Read
|
|
/// Write
|
|
#[serde(default)]
|
|
pub access_type: Option<dap::DataBreakpointAccessType>,
|
|
}
|
|
|
|
actions!(
|
|
dev,
|
|
[
|
|
/// Copies debug adapter launch arguments to clipboard.
|
|
CopyDebugAdapterArguments
|
|
]
|
|
);
|
|
|
|
pub fn init(cx: &mut App) {
|
|
workspace::FollowableViewRegistry::register::<DebugSession>(cx);
|
|
|
|
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
|
workspace
|
|
.register_action(spawn_task_or_modal)
|
|
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
|
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
|
})
|
|
.register_action(|workspace, _: &Toggle, window, cx| {
|
|
if !workspace.toggle_panel_focus::<DebugPanel>(window, cx) {
|
|
workspace.close_panel::<DebugPanel>(window, cx);
|
|
}
|
|
})
|
|
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
|
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
|
})
|
|
.register_action(|workspace: &mut Workspace, _: &Rerun, window, cx| {
|
|
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
|
return;
|
|
};
|
|
|
|
debug_panel.update(cx, |debug_panel, cx| {
|
|
debug_panel.rerun_last_session(workspace, window, cx);
|
|
})
|
|
})
|
|
.register_action(
|
|
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
|
workspace.project().update(cx, |project, cx| {
|
|
project.dap_store().update(cx, |store, cx| {
|
|
store.shutdown_sessions(cx).detach();
|
|
})
|
|
})
|
|
},
|
|
)
|
|
.register_action_renderer(|div, workspace, _, cx| {
|
|
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
|
return div;
|
|
};
|
|
let Some(active_item) = debug_panel
|
|
.read(cx)
|
|
.active_session()
|
|
.map(|session| session.read(cx).running_state().clone())
|
|
else {
|
|
return div;
|
|
};
|
|
let running_state = active_item.read(cx);
|
|
if running_state.session().read(cx).is_terminated() {
|
|
return div;
|
|
}
|
|
|
|
let caps = running_state.capabilities(cx);
|
|
let supports_step_back = caps.supports_step_back.unwrap_or_default();
|
|
let supports_detach = running_state.session().read(cx).is_attached();
|
|
let status = running_state.thread_status(cx);
|
|
|
|
let active_item = active_item.downgrade();
|
|
div.when(status == Some(ThreadStatus::Running), |div| {
|
|
let active_item = active_item.clone();
|
|
div.on_action(move |_: &Pause, _, cx| {
|
|
active_item
|
|
.update(cx, |item, cx| item.pause_thread(cx))
|
|
.ok();
|
|
})
|
|
})
|
|
.when(status == Some(ThreadStatus::Stopped), |div| {
|
|
div.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &StepInto, _, cx| {
|
|
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
|
|
}
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &StepOver, _, cx| {
|
|
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
|
|
}
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &StepOut, _, cx| {
|
|
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
|
|
}
|
|
})
|
|
.when(supports_step_back, |div| {
|
|
let active_item = active_item.clone();
|
|
div.on_action(move |_: &StepBack, _, cx| {
|
|
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
|
|
})
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &Continue, _, cx| {
|
|
active_item
|
|
.update(cx, |item, cx| item.continue_thread(cx))
|
|
.ok();
|
|
}
|
|
})
|
|
})
|
|
.when(supports_detach, |div| {
|
|
let active_item = active_item.clone();
|
|
div.on_action(move |_: &Detach, _, cx| {
|
|
active_item
|
|
.update(cx, |item, cx| item.detach_client(cx))
|
|
.ok();
|
|
})
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &Restart, _, cx| {
|
|
active_item
|
|
.update(cx, |item, cx| item.restart_session(cx))
|
|
.ok();
|
|
}
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &RerunSession, window, cx| {
|
|
active_item
|
|
.update(cx, |item, cx| item.rerun_session(window, cx))
|
|
.ok();
|
|
}
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &Stop, _, cx| {
|
|
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
|
|
}
|
|
})
|
|
.on_action({
|
|
let active_item = active_item.clone();
|
|
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
|
active_item
|
|
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
|
.ok();
|
|
}
|
|
})
|
|
.on_action(move |_: &ToggleUserFrames, _, cx| {
|
|
if let Some((thread_status, stack_frame_list)) = active_item
|
|
.read_with(cx, |item, cx| {
|
|
(item.thread_status(cx), item.stack_frame_list().clone())
|
|
})
|
|
.ok()
|
|
{
|
|
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
|
stack_frame_list.toggle_frame_filter(thread_status, cx);
|
|
})
|
|
}
|
|
})
|
|
});
|
|
})
|
|
.detach();
|
|
|
|
cx.observe_new({
|
|
move |editor: &mut Editor, _, _| {
|
|
editor
|
|
.register_action_renderer(move |editor, window, cx| {
|
|
let Some(workspace) = editor.workspace() else {
|
|
return;
|
|
};
|
|
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
|
|
return;
|
|
};
|
|
let Some(active_session) =
|
|
debug_panel.update(cx, |panel, _| panel.active_session())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let session = active_session
|
|
.read(cx)
|
|
.running_state
|
|
.read(cx)
|
|
.session()
|
|
.read(cx);
|
|
|
|
if session.is_terminated() {
|
|
return;
|
|
}
|
|
|
|
let editor = cx.entity().downgrade();
|
|
|
|
window.on_action_when(
|
|
session.any_stopped_thread(),
|
|
TypeId::of::<editor::actions::RunToCursor>(),
|
|
{
|
|
let editor = editor.clone();
|
|
let active_session = active_session.clone();
|
|
move |_, phase, _, cx| {
|
|
if phase != DispatchPhase::Bubble {
|
|
return;
|
|
}
|
|
maybe!({
|
|
let (buffer, position) = editor
|
|
.update(cx, |editor, cx| {
|
|
let cursor_point: language::Point = editor
|
|
.selections
|
|
.newest(&editor.display_snapshot(cx))
|
|
.head();
|
|
|
|
editor
|
|
.buffer()
|
|
.read(cx)
|
|
.point_to_buffer_point(cursor_point, cx)
|
|
})
|
|
.ok()??;
|
|
|
|
let path =
|
|
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
|
|
&buffer, cx,
|
|
)?;
|
|
|
|
let source_breakpoint = SourceBreakpoint {
|
|
row: position.row,
|
|
path,
|
|
message: None,
|
|
condition: None,
|
|
hit_condition: None,
|
|
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
|
};
|
|
|
|
active_session.update(cx, |session, cx| {
|
|
session.running_state().update(cx, |state, cx| {
|
|
if let Some(thread_id) = state.selected_thread_id() {
|
|
state.session().update(cx, |session, cx| {
|
|
session.run_to_position(
|
|
source_breakpoint,
|
|
thread_id,
|
|
cx,
|
|
);
|
|
})
|
|
}
|
|
});
|
|
});
|
|
|
|
Some(())
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
window.on_action(
|
|
TypeId::of::<editor::actions::EvaluateSelectedText>(),
|
|
move |_, _, window, cx| {
|
|
let status = maybe!({
|
|
let text = editor
|
|
.update(cx, |editor, cx| {
|
|
let range = editor
|
|
.selections
|
|
.newest::<MultiBufferOffsetUtf16>(
|
|
&editor.display_snapshot(cx),
|
|
)
|
|
.range();
|
|
editor.text_for_range(
|
|
range.start.0.0..range.end.0.0,
|
|
&mut None,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.ok()??;
|
|
|
|
active_session.update(cx, |session, cx| {
|
|
session.running_state().update(cx, |state, cx| {
|
|
let stack_id = state.selected_stack_frame_id(cx);
|
|
|
|
state.session().update(cx, |session, cx| {
|
|
session
|
|
.evaluate(
|
|
text,
|
|
Some(dap::EvaluateArgumentsContext::Repl),
|
|
stack_id,
|
|
None,
|
|
cx,
|
|
)
|
|
.detach();
|
|
});
|
|
});
|
|
});
|
|
|
|
Some(())
|
|
});
|
|
if status.is_some() {
|
|
cx.stop_propagation();
|
|
}
|
|
},
|
|
);
|
|
})
|
|
.detach();
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn spawn_task_or_modal(
|
|
workspace: &mut Workspace,
|
|
action: &Spawn,
|
|
window: &mut ui::Window,
|
|
cx: &mut ui::Context<Workspace>,
|
|
) {
|
|
match action {
|
|
Spawn::ByName {
|
|
task_name,
|
|
reveal_target,
|
|
} => {
|
|
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
|
reveal_target: Some(reveal_target),
|
|
});
|
|
let name = task_name.clone();
|
|
tasks_ui::spawn_tasks_filtered(
|
|
move |(_, task)| task.label.eq(&name),
|
|
overrides,
|
|
window,
|
|
cx,
|
|
)
|
|
.detach_and_log_err(cx)
|
|
}
|
|
Spawn::ByTag {
|
|
task_tag,
|
|
reveal_target,
|
|
} => {
|
|
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
|
reveal_target: Some(reveal_target),
|
|
});
|
|
let tag = task_tag.clone();
|
|
tasks_ui::spawn_tasks_filtered(
|
|
move |(_, task)| task.tags.contains(&tag),
|
|
overrides,
|
|
window,
|
|
cx,
|
|
)
|
|
.detach_and_log_err(cx)
|
|
}
|
|
Spawn::ViaModal { reveal_target } => {
|
|
NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
|
|
}
|
|
}
|
|
}
|