Compare commits
149 Commits
ex
...
debugger-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc5999d79 | ||
|
|
74931bd472 | ||
|
|
7f8c28877f | ||
|
|
1ff23477de | ||
|
|
d28950c633 | ||
|
|
6ff5e00740 | ||
|
|
b70acdfa4a | ||
|
|
403ae10087 | ||
|
|
9a8a54109e | ||
|
|
11c740b47a | ||
|
|
1a421db92d | ||
|
|
9c60884771 | ||
|
|
ee3323d12a | ||
|
|
b88fd3e0c5 | ||
|
|
2e10853b34 | ||
|
|
d08e28f4e0 | ||
|
|
ef67321ff2 | ||
|
|
4c777ad140 | ||
|
|
923ae5473a | ||
|
|
a48166e5a0 | ||
|
|
ef098c028b | ||
|
|
7ce1b8dc76 | ||
|
|
e350417a33 | ||
|
|
ea9e0755df | ||
|
|
6aced1b3aa | ||
|
|
99e01fc608 | ||
|
|
fe899c9164 | ||
|
|
f8b9937e51 | ||
|
|
dc5928374e | ||
|
|
0deb3cc606 | ||
|
|
ba4a70d7ae | ||
|
|
65a790e4ca | ||
|
|
b6e677eb06 | ||
|
|
ffa0609f8d | ||
|
|
77a314350f | ||
|
|
737b03c928 | ||
|
|
1b42dd5865 | ||
|
|
c9074b1c25 | ||
|
|
00379280f3 | ||
|
|
4e2d0351cc | ||
|
|
a583efd9b9 | ||
|
|
6237c29a42 | ||
|
|
9ea9b41e73 | ||
|
|
8d99f9b7d2 | ||
|
|
014ffbce2e | ||
|
|
68dd3c90c2 | ||
|
|
4a6f6151f0 | ||
|
|
f108d4c705 | ||
|
|
5ab95f1e1a | ||
|
|
49da08ffa4 | ||
|
|
f287c897a4 | ||
|
|
d15ff2d06f | ||
|
|
648daa3237 | ||
|
|
33e127de09 | ||
|
|
5a9b279039 | ||
|
|
cce58570dc | ||
|
|
361bbec3a0 | ||
|
|
a87409813c | ||
|
|
da84aa1ac2 | ||
|
|
817760688a | ||
|
|
13e56010c1 | ||
|
|
b827a35e44 | ||
|
|
9006e8fdff | ||
|
|
ce8ec033f4 | ||
|
|
9678cc9bc3 | ||
|
|
e87c4ddadc | ||
|
|
3f8581a2fb | ||
|
|
00c5b83384 | ||
|
|
4aedc1cd0b | ||
|
|
d238675c1a | ||
|
|
dcf6f6ca30 | ||
|
|
f4606bd951 | ||
|
|
12bef0830a | ||
|
|
953a2b376c | ||
|
|
ac3b9f7a4c | ||
|
|
ef5990d427 | ||
|
|
23a81d5d70 | ||
|
|
515122c54d | ||
|
|
f4eacca987 | ||
|
|
003fb7c81e | ||
|
|
7d2f63ebbd | ||
|
|
93e0bbb833 | ||
|
|
8c5f6a0be7 | ||
|
|
d6cafb8315 | ||
|
|
a24b76b30b | ||
|
|
11d74ea4ec | ||
|
|
93f4775cf6 | ||
|
|
a93913b9a3 | ||
|
|
08afbc6b58 | ||
|
|
b869465f00 | ||
|
|
2debea8115 | ||
|
|
cae295ff65 | ||
|
|
c51206e980 | ||
|
|
1baa5aea94 | ||
|
|
3e022a5565 | ||
|
|
3dd769be94 | ||
|
|
7936a4bee3 | ||
|
|
09aabe481c | ||
|
|
2ea1e4fa85 | ||
|
|
8699dad0e3 | ||
|
|
47a5f0c620 | ||
|
|
854ff68bac | ||
|
|
01d384e676 | ||
|
|
ddd893a795 | ||
|
|
3d7cd5dac7 | ||
|
|
a6fdfb5191 | ||
|
|
73a68d560f | ||
|
|
b4eeb25f55 | ||
|
|
fc991ab273 | ||
|
|
ab58d14559 | ||
|
|
3a0b311378 | ||
|
|
b81065fe63 | ||
|
|
a67f28dba2 | ||
|
|
99b2472e83 | ||
|
|
be45d5aa73 | ||
|
|
0508df9e7b | ||
|
|
153efab377 | ||
|
|
8015fb70e3 | ||
|
|
79d23aa4fe | ||
|
|
d5dae425fc | ||
|
|
d9e09c4a66 | ||
|
|
331625e876 | ||
|
|
61949fb348 | ||
|
|
c7f4e09496 | ||
|
|
5442e116ce | ||
|
|
11a4fc8b02 | ||
|
|
d303ebd46e | ||
|
|
e1de8dc50e | ||
|
|
5fe110c1dd | ||
|
|
89b203d03a | ||
|
|
0f4f8abbaa | ||
|
|
0d97e9e579 | ||
|
|
14b913fb4b | ||
|
|
9f1cd2bdb5 | ||
|
|
547c40e332 | ||
|
|
9cff6d5aa5 | ||
|
|
7e438bc1f3 | ||
|
|
944a52ce91 | ||
|
|
95a814ed41 | ||
|
|
6b9295b6c4 | ||
|
|
1128fce61a | ||
|
|
7c355fdb0f | ||
|
|
0e2a0b9edc | ||
|
|
c130f9c2f2 | ||
|
|
08300c6e90 | ||
|
|
7b71119094 | ||
|
|
c18db76862 | ||
|
|
c0dd152509 | ||
|
|
f402a4e5ce |
1
.gitignore
vendored
@@ -29,3 +29,4 @@ DerivedData/
|
||||
.vscode
|
||||
.wrangler
|
||||
.flatpak-builder
|
||||
.zed/debug.json
|
||||
|
||||
55
Cargo.lock
generated
@@ -3224,6 +3224,37 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||
|
||||
[[package]]
|
||||
name = "dap"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
"dap-types",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"smol",
|
||||
"task",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types#e715437f1193d5da6d7de6a71abdd1ac1fbf4c9d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
@@ -3273,6 +3304,27 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dap",
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"picker",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.8"
|
||||
@@ -7962,6 +8014,7 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"dap",
|
||||
"dev_server_projects",
|
||||
"env_logger",
|
||||
"fs",
|
||||
@@ -10672,6 +10725,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"sha2 0.10.7",
|
||||
"shellexpand 2.1.2",
|
||||
@@ -13627,6 +13681,7 @@ dependencies = [
|
||||
"command_palette",
|
||||
"copilot",
|
||||
"db",
|
||||
"debugger_ui",
|
||||
"dev_server_projects",
|
||||
"diagnostics",
|
||||
"editor",
|
||||
|
||||
@@ -21,6 +21,8 @@ members = [
|
||||
"crates/command_palette_hooks",
|
||||
"crates/completion",
|
||||
"crates/copilot",
|
||||
"crates/dap",
|
||||
"crates/debugger_ui",
|
||||
"crates/db",
|
||||
"crates/dev_server_projects",
|
||||
"crates/diagnostics",
|
||||
@@ -183,7 +185,9 @@ command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
completion = { path = "crates/completion" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
dap = { path = "crates/dap" }
|
||||
db = { path = "crates/db" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
|
||||
1
assets/icons/debug-continue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 2H4v12H2.5V2zm4.936.39L6.25 3v10l1.186.61 7-5V7.39l-7-5zM12.71 8l-4.96 3.543V4.457L12.71 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
1
assets/icons/debug-pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M4.5 3H6v10H4.5V3zm7 0v10H10V3h1.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 156 B |
1
assets/icons/debug-restart.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 8a4.5 4.5 0 0 1-8.61 1.834l-1.391.565A6.001 6.001 0 0 0 14.25 8 6 6 0 0 0 3.5 4.334V2.5H2v4l.75.75h3.5v-1.5H4.352A4.5 4.5 0 0 1 12.75 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
1
assets/icons/debug-step-into.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 9.532h.542l3.905-3.905-1.061-1.06-2.637 2.61V1H7.251v6.177l-2.637-2.61-1.061 1.06 3.905 3.905H8zm1.956 3.481a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
1
assets/icons/debug-step-out.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 1h-.542L3.553 4.905l1.061 1.06 2.637-2.61v6.177h1.498V3.355l2.637 2.61 1.061-1.06L8.542 1H8zm1.956 12.013a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
1
assets/icons/debug-step-over.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 5.75v-4h-1.5v2.542c-1.145-1.359-2.911-2.209-4.84-2.209-3.177 0-5.92 2.307-6.16 5.398l-.02.269h1.501l.022-.226c.212-2.195 2.202-3.94 4.656-3.94 1.736 0 3.244.875 4.05 2.166h-2.83v1.5h4.163l.962-.975V5.75h-.004zM8 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>
|
||||
|
After Width: | Height: | Size: 411 B |
1
assets/icons/debug-stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 1.99976L14 2.99976V12.9998L13 13.9998H3L2 12.9998L2 2.99976L3 1.99976H13ZM12.7461 3.25057L3.25469 3.25057L3.25469 12.7504H12.7461V3.25057Z"/></svg>
|
||||
|
After Width: | Height: | Size: 303 B |
@@ -314,7 +314,7 @@ impl AssistantPanel {
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
Some(NewFile.boxed_clone()),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
|
||||
27
crates/dap/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "dap"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-std = "1.12.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types" }
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
646
crates/dap/src/client.rs
Normal file
@@ -0,0 +1,646 @@
|
||||
use crate::transport::{Events, Payload, Response, Transport};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use dap_types::{
|
||||
requests::{
|
||||
Attach, ConfigurationDone, Continue, Disconnect, Initialize, Launch, Next, Pause, Request,
|
||||
Restart, RunInTerminal, SetBreakpoints, StartDebugging, StepBack, StepIn, StepOut,
|
||||
},
|
||||
AttachRequestArguments, ConfigurationDoneArguments, ContinueArguments, ContinueResponse,
|
||||
DisconnectArguments, InitializeRequestArgumentsPathFormat, LaunchRequestArguments,
|
||||
NextArguments, PauseArguments, RestartArguments, RunInTerminalRequestArguments,
|
||||
RunInTerminalResponse, Scope, SetBreakpointsArguments, SetBreakpointsResponse, Source,
|
||||
SourceBreakpoint, StackFrame, StartDebuggingRequestArguments, StepBackArguments,
|
||||
StepInArguments, StepOutArguments, SteppingGranularity, Variable,
|
||||
};
|
||||
use futures::{AsyncBufRead, AsyncReadExt, AsyncWrite};
|
||||
use gpui::{AppContext, AsyncAppContext};
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use serde_json::Value;
|
||||
use smol::{
|
||||
channel::{bounded, unbounded, Receiver, Sender},
|
||||
io::BufReader,
|
||||
net::{TcpListener, TcpStream},
|
||||
process::{self, Child},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
path::PathBuf,
|
||||
process::Stdio,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use task::{DebugAdapterConfig, DebugConnectionType, DebugRequestType, TCPHost};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ThreadStatus {
|
||||
#[default]
|
||||
Running,
|
||||
Stopped,
|
||||
Exited,
|
||||
Ended,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct DebugAdapterClientId(pub usize);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ThreadState {
|
||||
pub status: ThreadStatus,
|
||||
pub stack_frames: Vec<StackFrame>,
|
||||
pub scopes: HashMap<u64, Vec<Scope>>, // stack_frame_id -> scopes
|
||||
pub variables: HashMap<u64, Vec<Variable>>, // scope.variable_reference -> variables
|
||||
pub current_stack_frame_id: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct DebugAdapterClient {
|
||||
id: DebugAdapterClientId,
|
||||
_process: Option<Child>,
|
||||
server_tx: Sender<Payload>,
|
||||
sequence_count: AtomicU64,
|
||||
capabilities: Arc<Mutex<Option<dap_types::Capabilities>>>,
|
||||
config: DebugAdapterConfig,
|
||||
thread_states: Arc<Mutex<HashMap<u64, ThreadState>>>, // thread_id -> thread_state
|
||||
sub_client: Arc<Mutex<Option<Arc<Self>>>>,
|
||||
}
|
||||
|
||||
impl DebugAdapterClient {
|
||||
/// Creates & returns a new debug adapter client
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
pub async fn new<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
match config.connection.clone() {
|
||||
DebugConnectionType::TCP(host) => {
|
||||
Self::create_tcp_client(
|
||||
id,
|
||||
config,
|
||||
host,
|
||||
command,
|
||||
args,
|
||||
project_path,
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
DebugConnectionType::STDIO => {
|
||||
Self::create_stdio_client(
|
||||
id,
|
||||
config,
|
||||
command,
|
||||
args,
|
||||
project_path,
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a debug client that connects to an adapter through tcp
|
||||
///
|
||||
/// TCP clients don't have an error communication stream with an adapter
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn create_tcp_client<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
host: TCPHost,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let mut port = host.port;
|
||||
if port.is_none() {
|
||||
port = Self::get_port().await;
|
||||
}
|
||||
|
||||
let mut command = process::Command::new(command);
|
||||
command
|
||||
.current_dir(project_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
|
||||
if let Some(delay) = host.delay {
|
||||
// some debug adapters need some time to start the TCP server
|
||||
// so we have to wait few milliseconds before we can connect to it
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(delay))
|
||||
.await;
|
||||
}
|
||||
|
||||
let address = SocketAddrV4::new(
|
||||
host.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)),
|
||||
port.unwrap(),
|
||||
);
|
||||
|
||||
let (rx, tx) = TcpStream::connect(address).await?.split();
|
||||
|
||||
Self::handle_transport(
|
||||
id,
|
||||
config,
|
||||
Box::new(BufReader::new(rx)),
|
||||
Box::new(tx),
|
||||
None,
|
||||
Some(process),
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get an open port to use with the tcp client when not supplied by debug config
|
||||
async fn get_port() -> Option<u16> {
|
||||
Some(
|
||||
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 0))
|
||||
.await
|
||||
.ok()?
|
||||
.local_addr()
|
||||
.ok()?
|
||||
.port(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a debug client that connects to an adapter through std input/output
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `id`: The id that [`Project`](project::Project) uses to keep track of specific clients
|
||||
/// - `config`: The adapter specific configurations from debugger task that is starting
|
||||
/// - `command`: The command that starts the debugger
|
||||
/// - `args`: Arguments of the command that starts the debugger
|
||||
/// - `project_path`: The absolute path of the project that is being debugged
|
||||
/// - `cx`: The context that the new client belongs too
|
||||
async fn create_stdio_client<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
command: &str,
|
||||
args: Vec<&str>,
|
||||
project_path: PathBuf,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let mut command = process::Command::new(command);
|
||||
command
|
||||
.current_dir(project_path)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn command.")?;
|
||||
|
||||
let stdin = process
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdin"))?;
|
||||
let stdout = process
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdout"))?;
|
||||
let stderr = process
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stderr"))?;
|
||||
|
||||
let stdin = Box::new(stdin);
|
||||
let stdout = Box::new(BufReader::new(stdout));
|
||||
let stderr = Box::new(BufReader::new(stderr));
|
||||
|
||||
Self::handle_transport(
|
||||
id,
|
||||
config,
|
||||
stdout,
|
||||
stdin,
|
||||
Some(stderr),
|
||||
Some(process),
|
||||
event_handler,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn handle_transport<F>(
|
||||
id: DebugAdapterClientId,
|
||||
config: DebugAdapterConfig,
|
||||
rx: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
tx: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||
process: Option<Child>,
|
||||
event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
let (server_rx, server_tx) = Transport::start(rx, tx, err, cx);
|
||||
let (client_tx, client_rx) = unbounded::<Payload>();
|
||||
|
||||
let client = Arc::new(Self {
|
||||
id,
|
||||
config,
|
||||
_process: process,
|
||||
sub_client: Default::default(),
|
||||
server_tx: server_tx.clone(),
|
||||
capabilities: Default::default(),
|
||||
thread_states: Default::default(),
|
||||
sequence_count: AtomicU64::new(1),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_recv(server_rx, client_tx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|mut cx| async move {
|
||||
Self::handle_events(client, client_rx, server_tx, event_handler, &mut cx).await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Set's up a client's event handler.
|
||||
///
|
||||
/// This function should only be called once or else errors will arise
|
||||
/// # Parameters
|
||||
/// `client`: A pointer to the client to pass the event handler too
|
||||
/// `event_handler`: The function that is called to handle events
|
||||
/// should be DebugPanel::handle_debug_client_events
|
||||
/// `cx`: The context that this task will run in
|
||||
pub async fn handle_events<F>(
|
||||
this: Arc<Self>,
|
||||
client_rx: Receiver<Payload>,
|
||||
server_tx: Sender<Payload>,
|
||||
mut event_handler: F,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnMut(Events, &mut AppContext) + 'static + Send + Sync + Clone,
|
||||
{
|
||||
while let Ok(payload) = client_rx.recv().await {
|
||||
match payload {
|
||||
Payload::Event(event) => cx.update(|cx| event_handler(*event, cx))?,
|
||||
Payload::Request(request) => {
|
||||
if RunInTerminal::COMMAND == request.command {
|
||||
Self::handle_run_in_terminal_request(request, &server_tx).await?;
|
||||
} else if StartDebugging::COMMAND == request.command {
|
||||
Self::handle_start_debugging_request(&this, request, cx).await?;
|
||||
} else {
|
||||
unreachable!("Unknown reverse request {}", request.command);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_run_in_terminal_request(
|
||||
request: crate::transport::Request,
|
||||
server_tx: &Sender<Payload>,
|
||||
) -> Result<()> {
|
||||
let arguments: RunInTerminalRequestArguments =
|
||||
serde_json::from_value(request.arguments.unwrap_or_default())?;
|
||||
|
||||
let mut args = arguments.args.clone();
|
||||
let mut command = process::Command::new(args.remove(0));
|
||||
|
||||
let envs = arguments.env.as_ref().and_then(|e| e.as_object()).map(|e| {
|
||||
e.iter()
|
||||
.map(|(key, value)| (key.clone(), value.clone().to_string()))
|
||||
.collect::<HashMap<String, String>>()
|
||||
});
|
||||
|
||||
if let Some(envs) = envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
let process = command
|
||||
.current_dir(arguments.cwd)
|
||||
.args(args)
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn run in terminal command.")?;
|
||||
|
||||
server_tx
|
||||
.send(Payload::Response(Response {
|
||||
request_seq: request.seq,
|
||||
success: true,
|
||||
command: RunInTerminal::COMMAND.into(),
|
||||
message: None,
|
||||
body: Some(serde_json::to_value(RunInTerminalResponse {
|
||||
process_id: Some(process.id() as u64),
|
||||
shell_process_id: None,
|
||||
})?),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_start_debugging_request(
|
||||
this: &Arc<Self>,
|
||||
request: crate::transport::Request,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
dbg!(&request);
|
||||
let arguments: StartDebuggingRequestArguments =
|
||||
serde_json::from_value(request.arguments.clone().unwrap_or_default())?;
|
||||
|
||||
let sub_client = DebugAdapterClient::new(
|
||||
DebugAdapterClientId(1),
|
||||
this.config.clone(),
|
||||
"node",
|
||||
vec![
|
||||
"/Users/remcosmits/Downloads/js-debug/src/dapDebugServer.js",
|
||||
"8134",
|
||||
"127.0.0.1",
|
||||
],
|
||||
PathBuf::from("/Users/remcosmits/Documents/code/prettier-test"),
|
||||
|event, _cx| {
|
||||
dbg!(event);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
dbg!(&arguments);
|
||||
|
||||
let res = sub_client.launch(request.arguments).await?;
|
||||
dbg!(res);
|
||||
|
||||
*this.sub_client.lock() = Some(sub_client);
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn handle_recv(server_rx: Receiver<Payload>, client_tx: Sender<Payload>) -> Result<()> {
|
||||
while let Ok(payload) = server_rx.recv().await {
|
||||
match payload {
|
||||
Payload::Event(ev) => client_tx.send(Payload::Event(ev)).await?,
|
||||
Payload::Response(_) => unreachable!(),
|
||||
Payload::Request(req) => client_tx.send(Payload::Request(req)).await?,
|
||||
};
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
/// Send a request to an adapter and get a response back
|
||||
/// Note: This function will block until a response is sent back from the adapter
|
||||
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
|
||||
let serialized_arguments = serde_json::to_value(arguments)?;
|
||||
|
||||
let (callback_tx, callback_rx) = bounded::<Result<Response>>(1);
|
||||
|
||||
let request = crate::transport::Request {
|
||||
back_ch: Some(callback_tx),
|
||||
seq: self.next_sequence_id(),
|
||||
command: R::COMMAND.to_string(),
|
||||
arguments: Some(serialized_arguments),
|
||||
};
|
||||
|
||||
self.server_tx.send(Payload::Request(request)).await?;
|
||||
|
||||
let response = callback_rx.recv().await??;
|
||||
|
||||
match response.success {
|
||||
true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
|
||||
false => Err(anyhow!("Request failed")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> DebugAdapterClientId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn config(&self) -> DebugAdapterConfig {
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
pub fn request_type(&self) -> DebugRequestType {
|
||||
self.config.request.clone()
|
||||
}
|
||||
|
||||
pub fn capabilities(&self) -> dap_types::Capabilities {
|
||||
self.capabilities.lock().clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the next sequence id to be used in a request
|
||||
/// # Side Effect
|
||||
/// This function also increment's client's sequence count by one
|
||||
pub fn next_sequence_id(&self) -> u64 {
|
||||
self.sequence_count.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn update_thread_state_status(&self, thread_id: u64, status: ThreadStatus) {
|
||||
if let Some(thread_state) = self.thread_states().get_mut(&thread_id) {
|
||||
thread_state.status = status;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn thread_states(&self) -> MutexGuard<HashMap<u64, ThreadState>> {
|
||||
self.thread_states.lock()
|
||||
}
|
||||
|
||||
pub fn thread_state_by_id(&self, thread_id: u64) -> ThreadState {
|
||||
self.thread_states.lock().get(&thread_id).cloned().unwrap()
|
||||
}
|
||||
|
||||
pub async fn initialize(&self) -> Result<dap_types::Capabilities> {
|
||||
let args = dap_types::InitializeRequestArguments {
|
||||
client_id: Some("zed".to_owned()),
|
||||
client_name: Some("Zed".to_owned()),
|
||||
adapter_id: self.config.id.clone(),
|
||||
locale: Some("en-us".to_owned()),
|
||||
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
|
||||
supports_variable_type: Some(true),
|
||||
supports_variable_paging: Some(false),
|
||||
supports_run_in_terminal_request: Some(false), // TODO: we should support this
|
||||
supports_memory_references: Some(true),
|
||||
supports_progress_reporting: Some(true),
|
||||
supports_invalidated_event: Some(false),
|
||||
lines_start_at1: Some(true),
|
||||
columns_start_at1: Some(true),
|
||||
supports_memory_event: Some(true),
|
||||
supports_args_can_be_interpreted_by_shell: None,
|
||||
supports_start_debugging_request: Some(true),
|
||||
};
|
||||
|
||||
let capabilities = self.request::<Initialize>(args).await?;
|
||||
|
||||
*self.capabilities.lock() = Some(capabilities.clone());
|
||||
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
pub async fn launch(&self, args: Option<Value>) -> Result<()> {
|
||||
self.request::<Launch>(LaunchRequestArguments {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn attach(&self, args: Option<Value>) -> Result<()> {
|
||||
self.request::<Attach>(AttachRequestArguments {
|
||||
raw: args.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn resume(&self, thread_id: u64) -> Result<ContinueResponse> {
|
||||
self.request::<Continue>(ContinueArguments {
|
||||
thread_id,
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_over(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<Next>(NextArguments {
|
||||
thread_id,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_in(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepIn>(StepInArguments {
|
||||
thread_id,
|
||||
target_id: None,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_out(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepOut>(StepOutArguments {
|
||||
thread_id,
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
single_thread: Some(true),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn step_back(&self, thread_id: u64) -> Result<()> {
|
||||
self.request::<StepBack>(StepBackArguments {
|
||||
thread_id,
|
||||
single_thread: Some(true),
|
||||
granularity: Some(SteppingGranularity::Statement),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn restart(&self) {
|
||||
self.request::<Restart>(RestartArguments {
|
||||
raw: self
|
||||
.config
|
||||
.request_args
|
||||
.as_ref()
|
||||
.map(|v| v.args.clone())
|
||||
.unwrap_or(Value::Null),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn pause(&self, thread_id: u64) {
|
||||
self.request::<Pause>(PauseArguments { thread_id })
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
self.request::<Disconnect>(DisconnectArguments {
|
||||
restart: Some(false),
|
||||
terminate_debuggee: Some(false),
|
||||
suspend_debuggee: Some(false),
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub async fn set_breakpoints(
|
||||
&self,
|
||||
path: PathBuf,
|
||||
breakpoints: Option<Vec<SourceBreakpoint>>,
|
||||
) -> Result<SetBreakpointsResponse> {
|
||||
let adapter_data = self.config.request_args.clone().map(|c| c.args);
|
||||
|
||||
self.request::<SetBreakpoints>(SetBreakpointsArguments {
|
||||
source: Source {
|
||||
path: Some(String::from(path.to_string_lossy())),
|
||||
name: None,
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data,
|
||||
checksums: None,
|
||||
},
|
||||
breakpoints,
|
||||
source_modified: None,
|
||||
lines: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn configuration_done(&self) -> Result<()> {
|
||||
self.request::<ConfigurationDone>(ConfigurationDoneArguments)
|
||||
.await
|
||||
}
|
||||
}
|
||||
3
crates/dap/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod transport;
|
||||
pub use dap_types::*;
|
||||
283
crates/dap/src/transport.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dap_types::{
|
||||
BreakpointEvent, Capabilities, CapabilitiesEvent, ContinuedEvent, ExitedEvent,
|
||||
InvalidatedEvent, LoadedSourceEvent, MemoryEvent, ModuleEvent, OutputEvent, ProcessEvent,
|
||||
ProgressEndEvent, ProgressStartEvent, ProgressUpdateEvent, StoppedEvent, TerminatedEvent,
|
||||
ThreadEvent,
|
||||
};
|
||||
use futures::{AsyncBufRead, AsyncWrite};
|
||||
use gpui::AsyncAppContext;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::{
|
||||
channel::{unbounded, Receiver, Sender},
|
||||
io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt},
|
||||
lock::Mutex,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum Payload {
|
||||
Event(Box<Events>),
|
||||
Response(Response),
|
||||
Request(Request),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(tag = "event", content = "body")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Events {
|
||||
Initialized(Option<Capabilities>),
|
||||
Stopped(StoppedEvent),
|
||||
Continued(ContinuedEvent),
|
||||
Exited(ExitedEvent),
|
||||
Terminated(Option<TerminatedEvent>),
|
||||
Thread(ThreadEvent),
|
||||
Output(OutputEvent),
|
||||
Breakpoint(BreakpointEvent),
|
||||
Module(ModuleEvent),
|
||||
LoadedSource(LoadedSourceEvent),
|
||||
Process(ProcessEvent),
|
||||
Capabilities(CapabilitiesEvent),
|
||||
ProgressStart(ProgressStartEvent),
|
||||
ProgressUpdate(ProgressUpdateEvent),
|
||||
ProgressEnd(ProgressEndEvent),
|
||||
Invalidated(InvalidatedEvent),
|
||||
Memory(MemoryEvent),
|
||||
#[serde(untagged)]
|
||||
Other(HashMap<String, Value>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(skip)]
|
||||
pub back_ch: Option<Sender<Result<Response>>>,
|
||||
pub seq: u64,
|
||||
pub command: String,
|
||||
pub arguments: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||
pub struct Response {
|
||||
pub request_seq: u64,
|
||||
pub success: bool,
|
||||
pub command: String,
|
||||
pub message: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_empty_object")]
|
||||
pub body: Option<Value>,
|
||||
}
|
||||
|
||||
fn deserialize_empty_object<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
if value == Value::Object(serde_json::Map::new()) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
pub fn start(
|
||||
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> (Receiver<Payload>, Sender<Payload>) {
|
||||
let (client_tx, server_rx) = unbounded::<Payload>();
|
||||
let (server_tx, client_rx) = unbounded::<Payload>();
|
||||
|
||||
let transport = Arc::new(Self {
|
||||
pending_requests: Mutex::new(HashMap::default()),
|
||||
});
|
||||
|
||||
let _ = cx.update(|cx| {
|
||||
let transport = transport.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::receive(transport.clone(), server_stdout, client_tx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::send(transport.clone(), server_stdin, client_rx))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if let Some(stderr) = server_stderr {
|
||||
cx.background_executor()
|
||||
.spawn(Self::err(stderr))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
|
||||
(server_rx, server_tx)
|
||||
}
|
||||
|
||||
async fn recv_server_message(
|
||||
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
buffer: &mut String,
|
||||
) -> Result<Payload> {
|
||||
let mut content_length = None;
|
||||
loop {
|
||||
buffer.truncate(0);
|
||||
|
||||
if reader
|
||||
.read_line(buffer)
|
||||
.await
|
||||
.with_context(|| "reading a message from server")?
|
||||
== 0
|
||||
{
|
||||
return Err(anyhow!("reader stream closed"));
|
||||
};
|
||||
|
||||
if buffer == "\r\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts = buffer.trim().split_once(": ");
|
||||
|
||||
match parts {
|
||||
Some(("Content-Length", value)) => {
|
||||
content_length = Some(value.parse().context("invalid content length")?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content_length.context("missing content length")?;
|
||||
|
||||
let mut content = vec![0; content_length];
|
||||
reader
|
||||
.read_exact(&mut content)
|
||||
.await
|
||||
.with_context(|| "reading after a loop")?;
|
||||
|
||||
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
|
||||
|
||||
Ok(serde_json::from_str::<Payload>(msg)?)
|
||||
}
|
||||
|
||||
async fn recv_server_error(
|
||||
err: &mut (impl AsyncBufRead + Unpin + Send),
|
||||
buffer: &mut String,
|
||||
) -> Result<()> {
|
||||
buffer.truncate(0);
|
||||
if err.read_line(buffer).await? == 0 {
|
||||
return Err(anyhow!("error stream closed"));
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_payload_to_server(
|
||||
&self,
|
||||
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||
mut payload: Payload,
|
||||
) -> Result<()> {
|
||||
if let Payload::Request(request) = &mut payload {
|
||||
if let Some(back) = request.back_ch.take() {
|
||||
self.pending_requests.lock().await.insert(request.seq, back);
|
||||
}
|
||||
}
|
||||
self.send_string_to_server(server_stdin, serde_json::to_string(&payload)?)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_string_to_server(
|
||||
&self,
|
||||
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
|
||||
request: String,
|
||||
) -> Result<()> {
|
||||
server_stdin
|
||||
.write_all(format!("Content-Length: {}\r\n\r\n{}", request.len(), request).as_bytes())
|
||||
.await?;
|
||||
|
||||
server_stdin.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_response(response: Response) -> Result<Response> {
|
||||
if response.success {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(anyhow!("Received failed response"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_server_message(
|
||||
&self,
|
||||
client_tx: &Sender<Payload>,
|
||||
payload: Payload,
|
||||
) -> Result<()> {
|
||||
match payload {
|
||||
Payload::Response(res) => {
|
||||
if let Some(tx) = self.pending_requests.lock().await.remove(&res.request_seq) {
|
||||
|
||||
if !tx.is_closed() {
|
||||
tx.send(Self::process_response(res)).await?;
|
||||
} else {
|
||||
log::warn!(
|
||||
"Response stream associated with request seq: {} is closed",
|
||||
&res.request_seq
|
||||
); // TODO: Fix this case so it never happens
|
||||
}
|
||||
} else {
|
||||
client_tx.send(Payload::Response(res)).await?;
|
||||
};
|
||||
}
|
||||
|
||||
Payload::Request(_) => {
|
||||
client_tx.send(payload).await?;
|
||||
}
|
||||
Payload::Event(_) => {
|
||||
client_tx.send(payload).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
|
||||
client_tx: Sender<Payload>,
|
||||
) -> Result<()> {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
transport
|
||||
.process_server_message(
|
||||
&client_tx,
|
||||
Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?,
|
||||
)
|
||||
.await
|
||||
.context("Process server message failed in transport::receive")?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
|
||||
client_rx: Receiver<Payload>,
|
||||
) -> Result<()> {
|
||||
while let Ok(payload) = client_rx.recv().await {
|
||||
transport
|
||||
.send_payload_to_server(&mut server_stdin, payload)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn err(mut server_stderr: Box<dyn AsyncBufRead + Unpin + Send>) -> Result<()> {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
crates/debugger_ui/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
602
crates/debugger_ui/src/debugger_panel.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use anyhow::Result;
|
||||
use dap::client::{DebugAdapterClientId, ThreadState, ThreadStatus};
|
||||
use dap::requests::{Disconnect, Scopes, StackTrace, Variables};
|
||||
use dap::{client::DebugAdapterClient, transport::Events};
|
||||
use dap::{
|
||||
Capabilities, ContinuedEvent, DisconnectArguments, ExitedEvent, Scope, ScopesArguments,
|
||||
StackFrame, StackTraceArguments, StoppedEvent, TerminatedEvent, ThreadEvent, ThreadEventReason,
|
||||
Variable, VariablesArguments,
|
||||
};
|
||||
use editor::Editor;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
|
||||
Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use task::DebugRequestType;
|
||||
use ui::prelude::*;
|
||||
use workspace::Pane;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::debugger_panel_item::DebugPanelItem;
|
||||
|
||||
enum DebugCurrentRowHighlight {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DebugPanelEvent {
|
||||
Stopped((DebugAdapterClientId, StoppedEvent)),
|
||||
Thread((DebugAdapterClientId, ThreadEvent)),
|
||||
}
|
||||
|
||||
actions!(debug_panel, [ToggleFocus]);
|
||||
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
pane: View<Pane>,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakView<Workspace>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||
cx.new_view(|cx| {
|
||||
let pane = cx.new_view(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
pane.set_can_navigate(true, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
|
||||
pane
|
||||
});
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(&project, {
|
||||
move |this: &mut Self, _, event, cx| match event {
|
||||
project::Event::DebugClientEvent { event, client_id } => {
|
||||
Self::handle_debug_client_events(
|
||||
this,
|
||||
this.debug_client_by_id(*client_id, cx),
|
||||
event,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
project::Event::DebugClientStarted(client_id) => {
|
||||
let client = this.debug_client_by_id(*client_id, cx);
|
||||
cx.spawn(|_, _| async move {
|
||||
client.initialize().await?;
|
||||
let request_args = client.config().request_args.map(|a| a.args);
|
||||
|
||||
// send correct request based on adapter config
|
||||
match client.config().request {
|
||||
DebugRequestType::Launch => client.launch(request_args).await,
|
||||
DebugRequestType::Attach => client.attach(request_args).await,
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})];
|
||||
|
||||
Self {
|
||||
pane,
|
||||
size: px(300.),
|
||||
_subscriptions,
|
||||
focus_handle: cx.focus_handle(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| DebugPanel::new(workspace, cx))
|
||||
})
|
||||
}
|
||||
|
||||
fn debug_client_by_id(
|
||||
&self,
|
||||
client_id: DebugAdapterClientId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Arc<DebugAdapterClient> {
|
||||
self.workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.project()
|
||||
.read(cx)
|
||||
.debug_adapter_by_id(client_id)
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn handle_debug_client_events(
|
||||
this: &mut Self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &Events,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
Events::Initialized(event) => Self::handle_initialized_event(client, event, cx),
|
||||
Events::Stopped(event) => Self::handle_stopped_event(client, event, cx),
|
||||
Events::Continued(event) => Self::handle_continued_event(client, event, cx),
|
||||
Events::Exited(event) => Self::handle_exited_event(client, event, cx),
|
||||
Events::Terminated(event) => Self::handle_terminated_event(this, client, event, cx),
|
||||
Events::Thread(event) => Self::handle_thread_event(client, event, cx),
|
||||
Events::Output(_) => {}
|
||||
Events::Breakpoint(_) => {}
|
||||
Events::Module(_) => {}
|
||||
Events::LoadedSource(_) => {}
|
||||
Events::Capabilities(_) => {}
|
||||
Events::Memory(_) => {}
|
||||
Events::Process(_) => {}
|
||||
Events::ProgressEnd(_) => {}
|
||||
Events::ProgressStart(_) => {}
|
||||
Events::ProgressUpdate(_) => {}
|
||||
Events::Invalidated(_) => {}
|
||||
Events::Other(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn go_to_stack_frame(
|
||||
workspace: WeakView<Workspace>,
|
||||
stack_frame: StackFrame,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
clear_highlights: bool,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
let column = (stack_frame.column.saturating_sub(1)) as u32;
|
||||
|
||||
if clear_highlights {
|
||||
Self::remove_highlights(workspace.clone(), client, cx.clone()).await?;
|
||||
}
|
||||
|
||||
let task = workspace.update(&mut cx, |workspace, cx| {
|
||||
let project_path = workspace.project().read_with(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
workspace.open_path_preview(project_path, None, false, true, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"No project path found for path: {}",
|
||||
path
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
let editor = task.await?.downcast::<Editor>().unwrap();
|
||||
|
||||
workspace.update(&mut cx, |_, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_line::<DebugCurrentRowHighlight>(
|
||||
row,
|
||||
column,
|
||||
Some(cx.theme().colors().editor_debugger_active_line_background),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn remove_highlights(
|
||||
workspace: WeakView<Workspace>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut tasks = Vec::new();
|
||||
for thread_state in client.thread_states().values() {
|
||||
for stack_frame in thread_state.stack_frames.clone() {
|
||||
tasks.push(Self::remove_editor_highlight(
|
||||
workspace.clone(),
|
||||
stack_frame,
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !tasks.is_empty() {
|
||||
try_join_all(tasks).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn remove_highlights_for_thread(
|
||||
workspace: WeakView<Workspace>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
thread_id: u64,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let mut tasks = Vec::new();
|
||||
if let Some(thread_state) = client.thread_states().get(&thread_id) {
|
||||
for stack_frame in thread_state.stack_frames.clone() {
|
||||
tasks.push(Self::remove_editor_highlight(
|
||||
workspace.clone(),
|
||||
stack_frame.clone(),
|
||||
cx.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !tasks.is_empty() {
|
||||
try_join_all(tasks).await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn remove_editor_highlight(
|
||||
workspace: WeakView<Workspace>,
|
||||
stack_frame: StackFrame,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let path = stack_frame.clone().source.unwrap().path.unwrap().clone();
|
||||
|
||||
let task = workspace.update(&mut cx, |workspace, cx| {
|
||||
let project_path = workspace.project().read_with(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&Path::new(&path), cx)
|
||||
});
|
||||
|
||||
if let Some(project_path) = project_path {
|
||||
workspace.open_path(project_path, None, false, cx)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"No project path found for path: {}",
|
||||
path
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
let editor = task.await?.downcast::<Editor>().unwrap();
|
||||
|
||||
editor.update(&mut cx, |editor, _| {
|
||||
editor.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_initialized_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_: &Option<Capabilities>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
let client = client.clone();
|
||||
|
||||
project.send_breakpoints(client, cx)
|
||||
})
|
||||
})
|
||||
})??;
|
||||
|
||||
task.await?;
|
||||
|
||||
client.configuration_done().await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_continued_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &ContinuedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let all_threads = event.all_threads_continued.unwrap_or(false);
|
||||
|
||||
if all_threads {
|
||||
for thread in client.thread_states().values_mut() {
|
||||
thread.status = ThreadStatus::Running;
|
||||
}
|
||||
} else {
|
||||
client.update_thread_state_status(event.thread_id, ThreadStatus::Running);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_stopped_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &StoppedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(thread_id) = event.thread_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let client_id = client.id();
|
||||
cx.spawn({
|
||||
let event = event.clone();
|
||||
|this, mut cx| async move {
|
||||
let stack_trace_response = client
|
||||
.request::<StackTrace>(StackTraceArguments {
|
||||
thread_id,
|
||||
start_frame: None,
|
||||
levels: None,
|
||||
format: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let current_stack_frame =
|
||||
stack_trace_response.stack_frames.first().unwrap().clone();
|
||||
let mut scope_tasks = Vec::new();
|
||||
for stack_frame in stack_trace_response.stack_frames.clone().into_iter() {
|
||||
let frame_id = stack_frame.id;
|
||||
let client = client.clone();
|
||||
scope_tasks.push(async move {
|
||||
anyhow::Ok((
|
||||
frame_id,
|
||||
client
|
||||
.request::<Scopes>(ScopesArguments { frame_id })
|
||||
.await?,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
let mut scopes: HashMap<u64, Vec<Scope>> = HashMap::new();
|
||||
let mut variables: HashMap<u64, Vec<Variable>> = HashMap::new();
|
||||
|
||||
let mut variable_tasks = Vec::new();
|
||||
for (thread_id, response) in try_join_all(scope_tasks).await? {
|
||||
scopes.insert(thread_id, response.scopes.clone());
|
||||
|
||||
for scope in response.scopes {
|
||||
let scope_reference = scope.variables_reference;
|
||||
let client = client.clone();
|
||||
variable_tasks.push(async move {
|
||||
anyhow::Ok((
|
||||
scope_reference,
|
||||
client
|
||||
.request::<Variables>(VariablesArguments {
|
||||
variables_reference: scope_reference,
|
||||
filter: None,
|
||||
start: None,
|
||||
count: None,
|
||||
format: None,
|
||||
})
|
||||
.await?,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (scope_reference, response) in try_join_all(variable_tasks).await? {
|
||||
variables.insert(scope_reference, response.variables.clone());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut thread_state = client.thread_states();
|
||||
let thread_state = thread_state
|
||||
.entry(thread_id)
|
||||
.or_insert(ThreadState::default());
|
||||
|
||||
thread_state.current_stack_frame_id = Some(current_stack_frame.clone().id);
|
||||
thread_state.stack_frames = stack_trace_response.stack_frames;
|
||||
thread_state.scopes = scopes;
|
||||
thread_state.variables = variables;
|
||||
thread_state.status = ThreadStatus::Stopped;
|
||||
|
||||
let existing_item = this
|
||||
.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugPanelItem>())
|
||||
.any(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.client().id() == client_id && item.thread_id() == thread_id
|
||||
});
|
||||
|
||||
if !existing_item {
|
||||
let debug_panel = cx.view().clone();
|
||||
this.pane.update(cx, |this, cx| {
|
||||
let tab = cx.new_view(|cx| {
|
||||
DebugPanelItem::new(debug_panel, client.clone(), thread_id, cx)
|
||||
});
|
||||
|
||||
this.add_item(Box::new(tab.clone()), false, false, None, cx)
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(DebugPanelEvent::Stopped((client_id, event)));
|
||||
|
||||
if let Some(item) = this.pane.read(cx).active_item() {
|
||||
if let Some(pane) = item.downcast::<DebugPanelItem>() {
|
||||
let pane = pane.read(cx);
|
||||
if pane.thread_id() == thread_id && pane.client().id() == client_id {
|
||||
let workspace = this.workspace.clone();
|
||||
let client = client.clone();
|
||||
return cx.spawn(|_, cx| async move {
|
||||
Self::go_to_stack_frame(
|
||||
workspace,
|
||||
current_stack_frame.clone(),
|
||||
client,
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(anyhow::Ok(()))
|
||||
})?
|
||||
.await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let thread_id = event.thread_id;
|
||||
|
||||
if event.reason == ThreadEventReason::Started {
|
||||
client
|
||||
.thread_states()
|
||||
.insert(thread_id, ThreadState::default());
|
||||
} else {
|
||||
client.update_thread_state_status(thread_id, ThreadStatus::Ended);
|
||||
|
||||
// TODO: we want to figure out for witch clients/threads we should remove the highlights
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
|this, mut cx| async move {
|
||||
let workspace = this.update(&mut cx, |this, _| this.workspace.clone())?;
|
||||
|
||||
Self::remove_highlights_for_thread(workspace, client, thread_id, cx).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.emit(DebugPanelEvent::Thread((client.id(), event.clone())));
|
||||
}
|
||||
|
||||
fn handle_exited_event(
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_: &ExitedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.spawn(|_, _| async move {
|
||||
for thread_state in client.thread_states().values_mut() {
|
||||
thread_state.status = ThreadStatus::Exited;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_terminated_event(
|
||||
this: &mut Self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
event: &Option<TerminatedEvent>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let restart_args = event.clone().and_then(|e| e.restart);
|
||||
let workspace = this.workspace.clone();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
let should_restart = restart_args.is_some();
|
||||
|
||||
Self::remove_highlights(workspace, client.clone(), cx).await?;
|
||||
|
||||
client
|
||||
.request::<Disconnect>(DisconnectArguments {
|
||||
restart: Some(should_restart),
|
||||
terminate_debuggee: None,
|
||||
suspend_debuggee: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if should_restart {
|
||||
match client.request_type() {
|
||||
DebugRequestType::Launch => client.launch(restart_args).await,
|
||||
DebugRequestType::Attach => client.attach(restart_args).await,
|
||||
}
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl FocusableView for DebugPanel {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for DebugPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"DebugPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position == DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, _cx: &mut ViewContext<Self>) {
|
||||
self.size = size.unwrap();
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
None
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn starts_open(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
}
|
||||
|
||||
impl Render for DebugPanel {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("DebugPanel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.child(self.pane.clone())
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
514
crates/debugger_ui/src/debugger_panel_item.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
use crate::debugger_panel::{DebugPanel, DebugPanelEvent};
|
||||
use anyhow::Result;
|
||||
use dap::client::{DebugAdapterClient, DebugAdapterClientId, ThreadState, ThreadStatus};
|
||||
use dap::{Scope, StackFrame, StoppedEvent, ThreadEvent, Variable};
|
||||
use gpui::{
|
||||
actions, list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
||||
FocusableView, ListState, Subscription, View, WeakView,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::WindowContext;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use workspace::item::{Item, ItemEvent};
|
||||
|
||||
pub struct DebugPanelItem {
|
||||
thread_id: u64,
|
||||
focus_handle: FocusHandle,
|
||||
stack_frame_list: ListState,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
current_stack_frame_id: Option<u64>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
debug_panel_item,
|
||||
[Continue, StepOver, StepIn, StepOut, Restart, Pause, Stop]
|
||||
);
|
||||
|
||||
impl DebugPanelItem {
|
||||
pub fn new(
|
||||
debug_panel: View<DebugPanel>,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
thread_id: u64,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let weakview = cx.view().downgrade();
|
||||
let stack_frame_list =
|
||||
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
|
||||
if let Some(view) = weakview.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.render_stack_frame(ix, cx).into_any_element()
|
||||
})
|
||||
} else {
|
||||
div().into_any()
|
||||
}
|
||||
});
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(&debug_panel, {
|
||||
move |this: &mut Self, _, event: &DebugPanelEvent, cx| {
|
||||
match event {
|
||||
DebugPanelEvent::Stopped((client_id, event)) => {
|
||||
Self::handle_stopped_event(this, client_id, event, cx)
|
||||
}
|
||||
DebugPanelEvent::Thread((client_id, event)) => {
|
||||
Self::handle_thread_event(this, client_id, event, cx)
|
||||
}
|
||||
};
|
||||
}
|
||||
})];
|
||||
|
||||
Self {
|
||||
client,
|
||||
thread_id,
|
||||
focus_handle,
|
||||
_subscriptions,
|
||||
stack_frame_list,
|
||||
current_stack_frame_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
thread_id: u64,
|
||||
) -> bool {
|
||||
thread_id != this.thread_id || *client_id != this.client.id()
|
||||
}
|
||||
|
||||
fn handle_stopped_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
event: &StoppedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if Self::should_skip_event(this, client_id, event.thread_id.unwrap_or_default()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stack_frame_list
|
||||
.reset(this.current_thread_state().stack_frames.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
this: &mut Self,
|
||||
client_id: &DebugAdapterClientId,
|
||||
event: &ThreadEvent,
|
||||
_: &mut ViewContext<Self>,
|
||||
) {
|
||||
if Self::should_skip_event(this, client_id, event.thread_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: handle thread event
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for DebugPanelItem {}
|
||||
|
||||
impl FocusableView for DebugPanelItem {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for DebugPanelItem {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
params: workspace::item::TabContentParams,
|
||||
_: &WindowContext,
|
||||
) -> AnyElement {
|
||||
Label::new(format!(
|
||||
"{} - Thread {}",
|
||||
self.client.config().id,
|
||||
self.thread_id
|
||||
))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(SharedString::from(format!(
|
||||
"{} Thread {} - {:?}",
|
||||
self.client.config().id,
|
||||
self.thread_id,
|
||||
self.current_thread_state().status
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugPanelItem {
|
||||
pub fn client(&self) -> Arc<DebugAdapterClient> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn thread_id(&self) -> u64 {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
fn stack_frame_for_index(&self, ix: usize) -> StackFrame {
|
||||
self.client
|
||||
.thread_state_by_id(self.thread_id)
|
||||
.stack_frames
|
||||
.get(ix)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn current_thread_state(&self) -> ThreadState {
|
||||
self.client
|
||||
.thread_states()
|
||||
.get(&self.thread_id)
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn render_stack_frames(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_1_3()
|
||||
.gap_3()
|
||||
.h_full()
|
||||
.child(list(self.stack_frame_list.clone()).size_full())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_stack_frame(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let stack_frame = self.stack_frame_for_index(ix);
|
||||
|
||||
let source = stack_frame.source.clone();
|
||||
let selected_frame_id = self.current_stack_frame_id;
|
||||
let is_selected_frame = Some(stack_frame.id) == selected_frame_id;
|
||||
|
||||
let formatted_path = format!(
|
||||
"{}:{}",
|
||||
source.clone().and_then(|s| s.name).unwrap_or_default(),
|
||||
stack_frame.line,
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.group("")
|
||||
.id(("stack-frame", stack_frame.id))
|
||||
.tooltip({
|
||||
let formatted_path = formatted_path.clone();
|
||||
move |cx| Tooltip::text(formatted_path.clone(), cx)
|
||||
})
|
||||
.p_1()
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let stack_frame = stack_frame.clone();
|
||||
move |this, _, _| {
|
||||
this.current_stack_frame_id = Some(stack_frame.id);
|
||||
|
||||
// let client = this.client();
|
||||
// DebugPanel::go_to_stack_frame(&stack_frame, client, true, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
|
||||
// TODO:
|
||||
// this.go_to_stack_frame(&stack_frame, this.client.clone(), false, cx)
|
||||
// .detach_and_log_err(cx);
|
||||
// cx.notify();
|
||||
}
|
||||
}))
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.child(stack_frame.name.clone())
|
||||
.child(formatted_path),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scopes(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let thread_state = self.current_thread_state();
|
||||
let Some(scopes) = thread_state
|
||||
.current_stack_frame_id
|
||||
.and_then(|id| thread_state.scopes.get(&id))
|
||||
else {
|
||||
return div().child("No scopes for this thread yet").into_any();
|
||||
};
|
||||
|
||||
div()
|
||||
.w_3_4()
|
||||
.gap_3()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
scopes
|
||||
.iter()
|
||||
.map(|scope| self.render_scope(&thread_state, scope, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scope(
|
||||
&self,
|
||||
thread_state: &ThreadState,
|
||||
scope: &Scope,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(("scope", scope.variables_reference))
|
||||
.p_1()
|
||||
.text_ui_sm(cx)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(scope.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.ml_2()
|
||||
.child(self.render_variables(thread_state, scope, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variables(
|
||||
&self,
|
||||
thread_state: &ThreadState,
|
||||
scope: &Scope,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let Some(variables) = thread_state.variables.get(&scope.variables_reference) else {
|
||||
return div().child("No variables for this thread yet").into_any();
|
||||
};
|
||||
|
||||
div()
|
||||
.gap_3()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
variables
|
||||
.iter()
|
||||
.map(|variable| self.render_variable(variable, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variable(&self, variable: &Variable, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(("variable", variable.variables_reference))
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.text_ui_sm(cx)
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(variable.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(variable.value.clone()),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
// if the debug adapter does not send the continued event,
|
||||
// and the status of the thread did not change we have to assume the thread is running
|
||||
// so we have to update the thread state status to running
|
||||
fn update_thread_state(
|
||||
this: WeakView<Self>,
|
||||
previous_status: ThreadStatus,
|
||||
all_threads_continued: Option<bool>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if previous_status == this.current_thread_state().status {
|
||||
if all_threads_continued.unwrap_or(false) {
|
||||
for thread in this.client.thread_states().values_mut() {
|
||||
thread.status = ThreadStatus::Running;
|
||||
}
|
||||
} else {
|
||||
this.client
|
||||
.update_thread_state_status(this.thread_id, ThreadStatus::Running);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_continue_action(&mut self, _: &Continue, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = client.resume(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, response.all_threads_continued, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_over_action(&mut self, _: &StepOver, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_over(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_in_action(&mut self, _: &StepIn, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_in(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_step_out_action(&mut self, _: &StepOut, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
let previous_status = self.current_thread_state().status;
|
||||
|
||||
cx.spawn(|this, cx| async move {
|
||||
client.step_out(thread_id).await?;
|
||||
|
||||
Self::update_thread_state(this, previous_status, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_restart_action(&mut self, _: &Restart, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.restart().await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_pause_action(&mut self, _: &Pause, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let thread_id = self.thread_id;
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.pause(thread_id).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_stop_action(&mut self, _: &Stop, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move { client.stop().await })
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugPanelItem {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let thread_status = self.current_thread_state().status;
|
||||
|
||||
v_flex()
|
||||
.key_context("DebugPanelItem")
|
||||
.track_focus(&self.focus_handle)
|
||||
.capture_action(cx.listener(Self::handle_continue_action))
|
||||
.capture_action(cx.listener(Self::handle_step_over_action))
|
||||
.capture_action(cx.listener(Self::handle_step_in_action))
|
||||
.capture_action(cx.listener(Self::handle_step_out_action))
|
||||
.capture_action(cx.listener(Self::handle_restart_action))
|
||||
.capture_action(cx.listener(Self::handle_pause_action))
|
||||
.capture_action(cx.listener(Self::handle_stop_action))
|
||||
.p_2()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if self.current_thread_state().status == ThreadStatus::Running {
|
||||
this.child(
|
||||
IconButton::new("debug-pause", IconName::DebugPause)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Pause))),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Pause program", cx)),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
IconButton::new("debug-continue", IconName::DebugContinue)
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.dispatch_action(Box::new(Continue))
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Continue program", cx)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::DebugStepOver)
|
||||
.on_click(
|
||||
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOver))),
|
||||
)
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step over", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-in", IconName::DebugStepInto)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepIn))))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step in", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::DebugStepOut)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(StepOut))))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |cx| Tooltip::text("Step out", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Restart))))
|
||||
.disabled(
|
||||
!self
|
||||
.client
|
||||
.capabilities()
|
||||
.supports_restart_request
|
||||
.unwrap_or_default()
|
||||
|| thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Restart", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-stop", IconName::DebugStop)
|
||||
.on_click(cx.listener(|_, _, cx| cx.dispatch_action(Box::new(Stop))))
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text("Stop", cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.child(self.render_stack_frames(cx))
|
||||
.child(self.render_scopes(cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
18
crates/debugger_ui/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use gpui::AppContext;
|
||||
use workspace::{StartDebugger, Workspace};
|
||||
|
||||
pub mod debugger_panel;
|
||||
mod debugger_panel_item;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(cx);
|
||||
});
|
||||
workspace.register_action(|workspace: &mut Workspace, _: &StartDebugger, cx| {
|
||||
tasks_ui::toggle_modal(workspace, cx, task::TaskType::Debug).detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -293,6 +293,7 @@ gpui::actions!(
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleBreakpoint,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
|
||||
@@ -449,6 +449,13 @@ struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Breakpoint {
|
||||
row: MultiBufferRow,
|
||||
_line: BufferRow,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct MultiBufferOffset(usize);
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
@@ -568,6 +575,7 @@ pub struct Editor {
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
breakpoints: BTreeMap<(BufferId, BufferRow), Breakpoint>,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
file_header_size: u8,
|
||||
breadcrumb_header: Option<String>,
|
||||
@@ -1883,6 +1891,7 @@ impl Editor {
|
||||
blame_subscription: None,
|
||||
file_header_size,
|
||||
tasks: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -5132,6 +5141,17 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_breakpoint(&self, row: DisplayRow, cx: &mut ViewContext<Self>) -> IconButton {
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener(move |editor, _e, cx| {
|
||||
editor.focus(cx);
|
||||
editor.toggle_breakpoint_at_row(row.0, cx) //TODO handle folded
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_run_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
@@ -5950,6 +5970,38 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint(&mut self, _: &ToggleBreakpoint, cx: &mut ViewContext<Self>) {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
self.toggle_breakpoint_at_row(cursor_position.row, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint_at_row(&mut self, row: u32, cx: &mut ViewContext<Self>) {
|
||||
let Some(project) = &self.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let key = (buffer_id, row);
|
||||
|
||||
if self.breakpoints.remove(&key).is_none() {
|
||||
self.breakpoints.insert(
|
||||
key,
|
||||
Breakpoint {
|
||||
row: MultiBufferRow(row),
|
||||
_line: row,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.update_breakpoint(buffer, row + 1, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn gather_revert_changes(
|
||||
&mut self,
|
||||
selections: &[Selection<Anchor>],
|
||||
@@ -8926,6 +8978,31 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_line<T: 'static>(
|
||||
&mut self,
|
||||
row: u32,
|
||||
column: u32,
|
||||
highlight_color: Option<Hsla>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(cx).display_snapshot;
|
||||
let point = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(row, column), Bias::Left);
|
||||
let anchor = snapshot.buffer_snapshot.anchor_before(point);
|
||||
self.clear_row_highlights::<T>();
|
||||
self.highlight_rows::<T>(
|
||||
anchor..=anchor,
|
||||
Some(
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center(), cx);
|
||||
}
|
||||
|
||||
fn seek_in_direction(
|
||||
&mut self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
|
||||
@@ -400,7 +400,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::accept_partial_inline_completion);
|
||||
register_action(view, cx, Editor::accept_inline_completion);
|
||||
register_action(view, cx, Editor::revert_selected_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal)
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal);
|
||||
register_action(view, cx, Editor::toggle_breakpoint);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
|
||||
@@ -1554,6 +1555,45 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_breakpoints(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.breakpoints
|
||||
.iter()
|
||||
.filter_map(|(_, breakpoint)| {
|
||||
if snapshot.is_line_folded(breakpoint.row) {
|
||||
return None;
|
||||
}
|
||||
let display_row = Point::new(breakpoint.row.0, 0)
|
||||
.to_display_point(snapshot)
|
||||
.row();
|
||||
let button = editor.render_breakpoint(display_row, cx);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
display_row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -3252,6 +3292,9 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(cx);
|
||||
}
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(cx);
|
||||
}
|
||||
@@ -5392,6 +5435,16 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let breakpoints = self.layout_breakpoints(
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
let test_indicators = if gutter_settings.runnables {
|
||||
self.layout_run_indicators(
|
||||
line_height,
|
||||
@@ -5527,6 +5580,7 @@ impl Element for EditorElement {
|
||||
selections,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
breakpoints,
|
||||
close_indicators,
|
||||
code_actions_indicator,
|
||||
gutter_fold_toggles,
|
||||
@@ -5669,6 +5723,7 @@ pub struct EditorLayout {
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
breakpoints: Vec<AnyElement>,
|
||||
close_indicators: Vec<AnyElement>,
|
||||
gutter_fold_toggles: Vec<Option<AnyElement>>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
|
||||
@@ -249,3 +249,15 @@ pub fn local_tasks_file_relative_path() -> &'static Path {
|
||||
pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
|
||||
Path::new(".vscode/tasks.json")
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `launch.json` file within a project.
|
||||
pub fn local_debug_file_relative_path() -> &'static Path {
|
||||
static LOCAL_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
|
||||
LOCAL_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/debug.json"))
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `.vscode/launch.json` file within a project.
|
||||
pub fn local_vscode_launch_file_relative_path() -> &'static Path {
|
||||
static LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
|
||||
LOCAL_VSCODE_LAUNCH_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".vscode/launch.json"))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ async-trait.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
dev_server_projects.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -24,6 +24,11 @@ use client::{
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use dap::{
|
||||
client::{DebugAdapterClient, DebugAdapterClientId},
|
||||
transport::Events,
|
||||
SourceBreakpoint,
|
||||
};
|
||||
use debounced_delay::DebouncedDelay;
|
||||
use futures::{
|
||||
channel::mpsc::{self, UnboundedReceiver},
|
||||
@@ -51,8 +56,8 @@ use language::{
|
||||
deserialize_anchor, deserialize_version, serialize_anchor, serialize_line_ending,
|
||||
serialize_version, split_operations,
|
||||
},
|
||||
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel,
|
||||
ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
|
||||
range_from_lsp, Bias, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, Capability,
|
||||
CodeLabel, ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
|
||||
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||
LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
|
||||
ToPointUtf16, Transaction, Unclipped,
|
||||
@@ -68,7 +73,8 @@ use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use paths::{
|
||||
local_settings_file_relative_path, local_tasks_file_relative_path,
|
||||
local_debug_file_relative_path, local_settings_file_relative_path,
|
||||
local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
|
||||
local_vscode_tasks_file_relative_path,
|
||||
};
|
||||
use postage::watch;
|
||||
@@ -175,6 +181,8 @@ pub struct Project {
|
||||
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
|
||||
language_servers: HashMap<LanguageServerId, LanguageServerState>,
|
||||
language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
|
||||
debug_adapters: HashMap<DebugAdapterClientId, DebugAdapterClientState>,
|
||||
breakpoints: HashMap<BufferId, Vec<Breakpoint>>,
|
||||
language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
|
||||
last_formatting_failure: Option<String>,
|
||||
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
|
||||
@@ -183,6 +191,7 @@ pub struct Project {
|
||||
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
|
||||
client: Arc<client::Client>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
next_debugger_id: AtomicUsize,
|
||||
join_project_response_message_id: u32,
|
||||
next_diagnostic_group_id: usize,
|
||||
diagnostic_summaries:
|
||||
@@ -306,6 +315,10 @@ impl PartialEq for LanguageServerPromptRequest {
|
||||
}
|
||||
}
|
||||
|
||||
struct Breakpoint {
|
||||
row: BufferRow,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Event {
|
||||
LanguageServerAdded(LanguageServerId),
|
||||
@@ -314,6 +327,11 @@ pub enum Event {
|
||||
Notification(String),
|
||||
LanguageServerPrompt(LanguageServerPromptRequest),
|
||||
LanguageNotFound(Model<Buffer>),
|
||||
DebugClientStarted(DebugAdapterClientId),
|
||||
DebugClientEvent {
|
||||
client_id: DebugAdapterClientId,
|
||||
event: Events,
|
||||
},
|
||||
ActiveEntryChanged(Option<ProjectEntryId>),
|
||||
ActivateProjectPanel,
|
||||
WorktreeAdded,
|
||||
@@ -379,6 +397,11 @@ pub struct LanguageServerProgress {
|
||||
pub last_update_at: Instant,
|
||||
}
|
||||
|
||||
pub enum DebugAdapterClientState {
|
||||
Starting(Task<Option<Arc<DebugAdapterClient>>>),
|
||||
Running(Arc<DebugAdapterClient>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct ProjectPath {
|
||||
pub worktree_id: WorktreeId,
|
||||
@@ -800,11 +823,14 @@ impl Project {
|
||||
fs,
|
||||
ssh_session: None,
|
||||
next_entry_id: Default::default(),
|
||||
next_debugger_id: Default::default(),
|
||||
next_diagnostic_group_id: Default::default(),
|
||||
diagnostics: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
supplementary_language_servers: HashMap::default(),
|
||||
language_servers: Default::default(),
|
||||
debug_adapters: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
language_server_ids: HashMap::default(),
|
||||
language_server_statuses: Default::default(),
|
||||
last_formatting_failure: None,
|
||||
@@ -962,6 +988,7 @@ impl Project {
|
||||
fs,
|
||||
ssh_session: None,
|
||||
next_entry_id: Default::default(),
|
||||
next_debugger_id: Default::default(),
|
||||
next_diagnostic_group_id: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
diagnostics: Default::default(),
|
||||
@@ -997,6 +1024,8 @@ impl Project {
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
debug_adapters: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
last_formatting_failure: None,
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
language_server_watched_paths: HashMap::default(),
|
||||
@@ -1106,6 +1135,177 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn running_debug_adapters(&self) -> impl Iterator<Item = Arc<DebugAdapterClient>> + '_ {
|
||||
self.debug_adapters
|
||||
.values()
|
||||
.filter_map(|state| match state {
|
||||
DebugAdapterClientState::Starting(_) => None,
|
||||
DebugAdapterClientState::Running(client) => Some(client.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn debug_adapter_by_id(&self, id: DebugAdapterClientId) -> Option<Arc<DebugAdapterClient>> {
|
||||
self.debug_adapters.get(&id).and_then(|state| match state {
|
||||
DebugAdapterClientState::Starting(_) => None,
|
||||
DebugAdapterClientState::Running(client) => Some(client.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_breakpoints(
|
||||
&self,
|
||||
client: Arc<DebugAdapterClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|project, mut cx| async move {
|
||||
let task = project.update(&mut cx, |project, cx| {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for (buffer_id, breakpoints) in project.breakpoints.iter() {
|
||||
let res = maybe!({
|
||||
let buffer = project.buffer_for_id(*buffer_id, cx)?;
|
||||
|
||||
let project_path = buffer.read(cx).project_path(cx)?;
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
let path = worktree.read(cx).absolutize(&project_path.path).ok()?;
|
||||
|
||||
Some((path, breakpoints))
|
||||
});
|
||||
|
||||
if let Some((path, breakpoints)) = res {
|
||||
tasks.push(
|
||||
client.set_breakpoints(
|
||||
path,
|
||||
Some(
|
||||
breakpoints
|
||||
.iter()
|
||||
.map(|b| SourceBreakpoint {
|
||||
line: b.row as u64,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
log_message: None,
|
||||
column: None,
|
||||
mode: None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try_join_all(tasks)
|
||||
})?;
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_debug_adapter_client(
|
||||
&mut self,
|
||||
debug_task: task::ResolvedTask,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let id = DebugAdapterClientId(self.next_debugger_id());
|
||||
let debug_template = debug_task.original_task();
|
||||
let cwd = debug_template
|
||||
.cwd
|
||||
.clone()
|
||||
.expect("Debug tasks need to know what directory to open");
|
||||
let adapter_config = debug_task
|
||||
.debug_adapter_config()
|
||||
.expect("Debug tasks need to specify adapter configuration");
|
||||
|
||||
let command = debug_template.command.clone();
|
||||
let args = debug_template.args.clone();
|
||||
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
let project = this.clone();
|
||||
let client = DebugAdapterClient::new(
|
||||
id,
|
||||
adapter_config.clone(),
|
||||
&command,
|
||||
args.iter().map(|ele| &ele[..]).collect(),
|
||||
cwd.into(),
|
||||
move |event, cx| {
|
||||
project
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(Event::DebugClientEvent {
|
||||
client_id: id,
|
||||
event,
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
},
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
.log_err()?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let handle = this
|
||||
.debug_adapters
|
||||
.get_mut(&id)
|
||||
.with_context(|| "Failed to find debug adapter with given id")?;
|
||||
*handle = DebugAdapterClientState::Running(client.clone());
|
||||
|
||||
cx.emit(Event::DebugClientStarted(id));
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Some(client)
|
||||
});
|
||||
|
||||
self.debug_adapters
|
||||
.insert(id, DebugAdapterClientState::Starting(task));
|
||||
}
|
||||
|
||||
pub fn update_breakpoint(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
row: BufferRow,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let breakpoints_for_buffer = self
|
||||
.breakpoints
|
||||
.entry(buffer.read(cx).remote_id())
|
||||
.or_insert(Vec::new());
|
||||
|
||||
if let Some(ix) = breakpoints_for_buffer
|
||||
.iter()
|
||||
.position(|breakpoint| breakpoint.row == row)
|
||||
{
|
||||
breakpoints_for_buffer.remove(ix);
|
||||
} else {
|
||||
breakpoints_for_buffer.push(Breakpoint { row });
|
||||
}
|
||||
|
||||
let clients = self
|
||||
.debug_adapters
|
||||
.iter()
|
||||
.filter_map(|(_, state)| match state {
|
||||
DebugAdapterClientState::Starting(_) => None,
|
||||
DebugAdapterClientState::Running(client) => Some(client.clone()),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for client in clients {
|
||||
tasks.push(self.send_breakpoints(client, cx));
|
||||
}
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
try_join_all(tasks).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn shutdown_language_servers(
|
||||
&mut self,
|
||||
_cx: &mut ModelContext<Self>,
|
||||
@@ -8123,17 +8323,35 @@ impl Project {
|
||||
abs_path,
|
||||
id_base: "local_vscode_tasks_for_worktree".into(),
|
||||
},
|
||||
|tx, cx| {
|
||||
StaticSource::new(TrackedFile::new_convertible::<
|
||||
task::VsCodeTaskFile,
|
||||
>(
|
||||
tasks_file_rx, tx, cx
|
||||
))
|
||||
},
|
||||
|tx, cx| StaticSource::new(TrackedFile::new(tasks_file_rx, tx, cx)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
} else if path.ends_with(local_debug_file_relative_path()) {
|
||||
self.task_inventory().update(cx, |task_inventory, cx| {
|
||||
if removed {
|
||||
task_inventory.remove_local_static_source(&abs_path);
|
||||
} else {
|
||||
let fs = self.fs.clone();
|
||||
let debug_task_file_rx =
|
||||
watch_config_file(&cx.background_executor(), fs, abs_path.clone());
|
||||
|
||||
task_inventory.add_source(
|
||||
TaskSourceKind::Worktree {
|
||||
id: remote_worktree_id,
|
||||
abs_path,
|
||||
id_base: "local_debug_file_for_worktree".into(),
|
||||
},
|
||||
|tx, cx| {
|
||||
StaticSource::new(TrackedFile::new(debug_task_file_rx, tx, cx))
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if path.ends_with(local_vscode_launch_file_relative_path()) {
|
||||
// TODO: handle vscode launch file (.vscode/launch.json)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8357,6 +8575,37 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_path_for_absolute_path(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
cx: &AppContext,
|
||||
) -> Option<ProjectPath> {
|
||||
self.find_local_worktree(abs_path, cx)
|
||||
.map(|(worktree, relative_path)| ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: relative_path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_local_worktree(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
cx: &AppContext,
|
||||
) -> Option<(Model<Worktree>, PathBuf)> {
|
||||
let trees = self.worktrees(cx);
|
||||
|
||||
for tree in trees {
|
||||
if let Some(relative_path) = tree
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
|
||||
{
|
||||
return Some((tree.clone(), relative_path.into()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_workspace_root(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
@@ -10253,6 +10502,10 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_debugger_id(&mut self) -> usize {
|
||||
self.next_debugger_id.fetch_add(1, SeqCst)
|
||||
}
|
||||
|
||||
pub fn task_context_for_location(
|
||||
&self,
|
||||
captured_variables: TaskVariables,
|
||||
@@ -10434,6 +10687,7 @@ impl Project {
|
||||
hide,
|
||||
shell,
|
||||
tags: proto_template.tags,
|
||||
..Default::default()
|
||||
};
|
||||
Some((task_source_kind, task_template))
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ hex.workspace = true
|
||||
parking_lot.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
sha2.workspace = true
|
||||
shellexpand.workspace = true
|
||||
|
||||
24
crates/task/src/debug_format.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use anyhow::bail;
|
||||
use collections::HashMap;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{TaskTemplate, TaskTemplates, VariableName};
|
||||
|
||||
struct ZedDebugTaskFile {}
|
||||
|
||||
impl ZedDebugTaskFile {
|
||||
fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {}
|
||||
}
|
||||
impl TryFrom<ZedDebugTaskFile> for TaskTemplates {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: ZedDebugTaskFile) -> Result<Self, Self::Error> {
|
||||
let templates = value
|
||||
.tasks
|
||||
.into_iter()
|
||||
.filter_map(|debug_task_file| debug_task_file.to_zed_format(&replacer).log_err())
|
||||
.collect();
|
||||
Ok(Self(templates))
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,10 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
|
||||
pub use task_template::{
|
||||
DebugAdapterConfig, DebugConnectionType, DebugRequestType, RevealStrategy, TCPHost,
|
||||
TaskTemplate, TaskTemplates, TaskType, HideStrategy,
|
||||
};
|
||||
pub use vscode_format::VsCodeTaskFile;
|
||||
|
||||
/// Task identifier, unique within the application.
|
||||
@@ -111,6 +114,17 @@ impl ResolvedTask {
|
||||
&self.original_task
|
||||
}
|
||||
|
||||
/// Get the task type that determines what this task is used for
|
||||
/// And where is it shown in the UI
|
||||
pub fn task_type(&self) -> TaskType {
|
||||
self.original_task.task_type.clone()
|
||||
}
|
||||
|
||||
/// Get the configuration for the debug adapter that should be used for this task.
|
||||
pub fn debug_adapter_config(&self) -> Option<DebugAdapterConfig> {
|
||||
self.original_task.debug_adapter.clone()
|
||||
}
|
||||
|
||||
/// Variables that were substituted during the task template resolution.
|
||||
pub fn substituted_variables(&self) -> &HashSet<VariableName> {
|
||||
&self.substituted_variables
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{net::Ipv4Addr, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -51,6 +51,14 @@ pub struct TaskTemplate {
|
||||
/// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`.
|
||||
#[serde(default)]
|
||||
pub hide: HideStrategy,
|
||||
/// If this task should start a debugger or not
|
||||
#[serde(default)]
|
||||
pub task_type: TaskType,
|
||||
/// Specific configuration for the debug adapter
|
||||
/// This is only used if `task_type` is `Debug`
|
||||
#[serde(default)]
|
||||
pub debug_adapter: Option<DebugAdapterConfig>,
|
||||
|
||||
/// Represents the tags which this template attaches to. Adding this removes this task from other UI.
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
@@ -59,6 +67,80 @@ pub struct TaskTemplate {
|
||||
pub shell: Shell,
|
||||
}
|
||||
|
||||
/// Represents the type of task that is being ran
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskType {
|
||||
/// Act like a typically task that runs commands
|
||||
#[default]
|
||||
Script,
|
||||
/// This task starts the debugger for a language
|
||||
Debug,
|
||||
}
|
||||
|
||||
/// Represents the type of the debugger adapter connection
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase", tag = "connection")]
|
||||
pub enum DebugConnectionType {
|
||||
/// Connect to the debug adapter via TCP
|
||||
TCP(TCPHost),
|
||||
/// Connect to the debug adapter via STDIO
|
||||
STDIO,
|
||||
}
|
||||
|
||||
impl Default for DebugConnectionType {
|
||||
fn default() -> Self {
|
||||
DebugConnectionType::TCP(TCPHost::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the host information of the debug adapter
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
pub struct TCPHost {
|
||||
/// The port that the debug adapter is listening on
|
||||
pub port: Option<u16>,
|
||||
/// The host that the debug adapter is listening too
|
||||
pub host: Option<Ipv4Addr>,
|
||||
/// The delay in ms between starting and connecting to the debug adapter
|
||||
pub delay: Option<u64>,
|
||||
}
|
||||
|
||||
/// Represents the type that will determine which request to call on the debug adapter
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DebugRequestType {
|
||||
/// Call the `launch` request on the debug adapter
|
||||
#[default]
|
||||
Launch,
|
||||
/// Call the `attach` request on the debug adapter
|
||||
Attach,
|
||||
}
|
||||
|
||||
/// Represents the configuration for the debug adapter
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DebugAdapterConfig {
|
||||
/// Unique id of for the debug adapter,
|
||||
/// that will be send with the `initialize` request
|
||||
pub id: String,
|
||||
/// The type of connection the adapter should use
|
||||
#[serde(default, flatten)]
|
||||
pub connection: DebugConnectionType,
|
||||
/// The type of request that should be called on the debug adapter
|
||||
#[serde(default)]
|
||||
pub request: DebugRequestType,
|
||||
/// The configuration options that are send with the `launch` or `attach` request
|
||||
/// to the debug adapter
|
||||
pub request_args: Option<DebugRequestArgs>,
|
||||
}
|
||||
|
||||
/// Represents the configuration for the debug adapter that is send with the launch request
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(transparent)]
|
||||
pub struct DebugRequestArgs {
|
||||
pub args: serde_json::Value,
|
||||
}
|
||||
|
||||
/// What to do with the terminal pane and tab, after the command was started.
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
||||
@@ -3,6 +3,7 @@ use editor::{tasks::task_context, Editor};
|
||||
use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
|
||||
use modal::TasksModal;
|
||||
use project::{Location, WorktreeId};
|
||||
use task::TaskType;
|
||||
use workspace::tasks::schedule_task;
|
||||
use workspace::{tasks::schedule_resolved_task, Workspace};
|
||||
|
||||
@@ -70,7 +71,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toggle_modal(workspace, cx).detach();
|
||||
toggle_modal(workspace, cx, TaskType::Script).detach();
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -81,11 +82,15 @@ pub fn init(cx: &mut AppContext) {
|
||||
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
|
||||
match &action.task_name {
|
||||
Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx),
|
||||
None => toggle_modal(workspace, cx).detach(),
|
||||
None => toggle_modal(workspace, cx, task::TaskType::Script).detach(),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
|
||||
pub fn toggle_modal(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<'_, Workspace>,
|
||||
task_type: TaskType,
|
||||
) -> AsyncTask<()> {
|
||||
let project = workspace.project().clone();
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let context_task = task_context(workspace, cx);
|
||||
@@ -97,7 +102,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>)
|
||||
project.is_local() || project.ssh_connection_string(cx).is_some()
|
||||
}) {
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
TasksModal::new(project, task_context, workspace_handle, cx)
|
||||
TasksModal::new(project, task_context, workspace_handle, cx, task_type)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{
|
||||
};
|
||||
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
|
||||
use project::{Project, TaskSourceKind};
|
||||
use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate};
|
||||
use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskType};
|
||||
use ui::{
|
||||
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
|
||||
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
|
||||
@@ -73,6 +73,8 @@ pub(crate) struct TasksModalDelegate {
|
||||
prompt: String,
|
||||
task_context: TaskContext,
|
||||
placeholder_text: Arc<str>,
|
||||
/// If this delegate is responsible for running a scripting task or a debugger
|
||||
task_type: TaskType,
|
||||
}
|
||||
|
||||
impl TasksModalDelegate {
|
||||
@@ -80,6 +82,7 @@ impl TasksModalDelegate {
|
||||
project: Model<Project>,
|
||||
task_context: TaskContext,
|
||||
workspace: WeakView<Workspace>,
|
||||
task_type: TaskType,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
@@ -92,6 +95,7 @@ impl TasksModalDelegate {
|
||||
prompt: String::default(),
|
||||
task_context,
|
||||
placeholder_text: Arc::from("Find a task, or run a command"),
|
||||
task_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +147,11 @@ impl TasksModal {
|
||||
task_context: TaskContext,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
task_type: TaskType,
|
||||
) -> Self {
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::uniform_list(
|
||||
TasksModalDelegate::new(project, task_context, workspace),
|
||||
TasksModalDelegate::new(project, task_context, workspace, task_type),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -203,12 +208,13 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
query: String,
|
||||
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let task_type = self.task_type.clone();
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let Some(candidates_task) = picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
match &mut picker.delegate.candidates {
|
||||
Some(candidates) => {
|
||||
Task::ready(Ok(string_match_candidates(candidates.iter())))
|
||||
Task::ready(Ok(string_match_candidates(candidates.iter(), task_type)))
|
||||
}
|
||||
None => {
|
||||
let Ok((worktree, location)) =
|
||||
@@ -252,6 +258,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
let (used, current) = resolved_task.await;
|
||||
picker.update(&mut cx, |picker, _| {
|
||||
@@ -264,7 +271,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
let mut new_candidates = used;
|
||||
new_candidates.extend(current);
|
||||
let match_candidates =
|
||||
string_match_candidates(new_candidates.iter());
|
||||
string_match_candidates(new_candidates.iter(), task_type);
|
||||
let _ = picker.delegate.candidates.insert(new_candidates);
|
||||
match_candidates
|
||||
})
|
||||
@@ -334,7 +341,20 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
|
||||
match task.task_type() {
|
||||
TaskType::Script => schedule_resolved_task(
|
||||
workspace,
|
||||
task_source_kind,
|
||||
task,
|
||||
omit_history_entry,
|
||||
cx,
|
||||
),
|
||||
// TODO: Should create a schedule_resolved_debug_task function
|
||||
// This would allow users to access to debug history and other issues
|
||||
TaskType::Debug => workspace.project().update(cx, |project, cx| {
|
||||
project.start_debug_adapter_client(task, cx)
|
||||
}),
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
cx.emit(DismissEvent);
|
||||
@@ -473,9 +493,23 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
let Some((task_source_kind, task)) = self.spawn_oneshot() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
|
||||
match task.task_type() {
|
||||
TaskType::Script => schedule_resolved_task(
|
||||
workspace,
|
||||
task_source_kind,
|
||||
task,
|
||||
omit_history_entry,
|
||||
cx,
|
||||
),
|
||||
// TODO: Should create a schedule_resolved_debug_task function
|
||||
// This would allow users to access to debug history and other issues
|
||||
TaskType::Debug => workspace.project().update(cx, |project, cx| {
|
||||
project.start_debug_adapter_client(task, cx)
|
||||
}),
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
cx.emit(DismissEvent);
|
||||
@@ -582,9 +616,11 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
|
||||
fn string_match_candidates<'a>(
|
||||
candidates: impl Iterator<Item = &'a (TaskSourceKind, ResolvedTask)> + 'a,
|
||||
task_type: TaskType,
|
||||
) -> Vec<StringMatchCandidate> {
|
||||
candidates
|
||||
.enumerate()
|
||||
.filter(|(_, (_, candidate))| candidate.task_type() == task_type)
|
||||
.map(|(index, (_, candidate))| StringMatchCandidate {
|
||||
id: index,
|
||||
char_bag: candidate.resolved_label.chars().collect(),
|
||||
|
||||
@@ -78,7 +78,7 @@ impl TerminalPanel {
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
NewTerminal.boxed_clone(),
|
||||
Some(NewTerminal.boxed_clone()),
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(false, cx);
|
||||
|
||||
@@ -71,6 +71,7 @@ impl ThemeColors {
|
||||
editor_subheader_background: neutral().light().step_2(),
|
||||
editor_active_line_background: neutral().light_alpha().step_3(),
|
||||
editor_highlighted_line_background: neutral().light_alpha().step_3(),
|
||||
editor_debugger_active_line_background: neutral().light().step_8(),
|
||||
editor_line_number: neutral().light().step_10(),
|
||||
editor_active_line_number: neutral().light().step_11(),
|
||||
editor_invisible: neutral().light().step_10(),
|
||||
@@ -169,6 +170,7 @@ impl ThemeColors {
|
||||
editor_subheader_background: neutral().dark().step_3(),
|
||||
editor_active_line_background: neutral().dark_alpha().step_3(),
|
||||
editor_highlighted_line_background: neutral().dark_alpha().step_4(),
|
||||
editor_debugger_active_line_background: neutral().light_alpha().step_5(),
|
||||
editor_line_number: neutral().dark_alpha().step_10(),
|
||||
editor_active_line_number: neutral().dark_alpha().step_12(),
|
||||
editor_invisible: neutral().dark_alpha().step_4(),
|
||||
|
||||
@@ -88,6 +88,12 @@ pub(crate) fn one_dark() -> Theme {
|
||||
editor_subheader_background: bg,
|
||||
editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0),
|
||||
editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1),
|
||||
editor_debugger_active_line_background: hsla(
|
||||
207.8 / 360.,
|
||||
81. / 100.,
|
||||
66. / 100.,
|
||||
0.2,
|
||||
),
|
||||
editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
|
||||
editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0),
|
||||
editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
|
||||
|
||||
@@ -375,6 +375,10 @@ pub struct ThemeColorsContent {
|
||||
#[serde(rename = "editor.highlighted_line.background")]
|
||||
pub editor_highlighted_line_background: Option<String>,
|
||||
|
||||
/// Background of active line of debugger
|
||||
#[serde(rename = "editor.debugger_active_line.background")]
|
||||
pub editor_debugger_active_line_background: Option<String>,
|
||||
|
||||
/// Text Color. Used for the text of the line number in the editor gutter.
|
||||
#[serde(rename = "editor.line_number")]
|
||||
pub editor_line_number: Option<String>,
|
||||
@@ -756,6 +760,10 @@ impl ThemeColorsContent {
|
||||
.editor_highlighted_line_background
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
editor_debugger_active_line_background: self
|
||||
.editor_debugger_active_line_background
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
editor_line_number: self
|
||||
.editor_line_number
|
||||
.as_ref()
|
||||
|
||||
@@ -147,6 +147,8 @@ pub struct ThemeColors {
|
||||
pub editor_subheader_background: Hsla,
|
||||
pub editor_active_line_background: Hsla,
|
||||
pub editor_highlighted_line_background: Hsla,
|
||||
/// Line color of the line a debugger is currently stopped at
|
||||
pub editor_debugger_active_line_background: Hsla,
|
||||
/// Text Color. Used for the text of the line number in the editor gutter.
|
||||
pub editor_line_number: Hsla,
|
||||
/// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted.
|
||||
|
||||
@@ -148,6 +148,13 @@ pub enum IconName {
|
||||
Copy,
|
||||
CountdownTimer,
|
||||
Dash,
|
||||
DebugPause,
|
||||
DebugContinue,
|
||||
DebugStepOver,
|
||||
DebugStepInto,
|
||||
DebugStepOut,
|
||||
DebugRestart,
|
||||
DebugStop,
|
||||
Delete,
|
||||
Disconnected,
|
||||
Download,
|
||||
@@ -299,6 +306,13 @@ impl IconName {
|
||||
IconName::Copy => "icons/copy.svg",
|
||||
IconName::CountdownTimer => "icons/countdown_timer.svg",
|
||||
IconName::Dash => "icons/dash.svg",
|
||||
IconName::DebugPause => "icons/debug-pause.svg",
|
||||
IconName::DebugContinue => "icons/debug-continue.svg",
|
||||
IconName::DebugStepOver => "icons/debug-step-over.svg",
|
||||
IconName::DebugStepInto => "icons/debug-step-into.svg",
|
||||
IconName::DebugStepOut => "icons/debug-step-out.svg",
|
||||
IconName::DebugRestart => "icons/debug-restart.svg",
|
||||
IconName::DebugStop => "icons/debug-stop.svg",
|
||||
IconName::Delete => "icons/delete.svg",
|
||||
IconName::Disconnected => "icons/disconnected.svg",
|
||||
IconName::Download => "icons/download.svg",
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::{insert::NormalBefore, motion, state::Mode, ModeIndicator};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
println!("TEST");
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.simulate_keystrokes("h j k l");
|
||||
cx.assert_editor_state("hjklˇ");
|
||||
|
||||
@@ -242,7 +242,7 @@ pub struct Pane {
|
||||
/// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
|
||||
/// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed.
|
||||
display_nav_history_buttons: Option<bool>,
|
||||
double_click_dispatch_action: Box<dyn Action>,
|
||||
double_click_dispatch_action: Option<Box<dyn Action>>,
|
||||
save_modals_spawned: HashSet<EntityId>,
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ impl Pane {
|
||||
project: Model<Project>,
|
||||
next_timestamp: Arc<AtomicUsize>,
|
||||
can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>>,
|
||||
double_click_dispatch_action: Box<dyn Action>,
|
||||
double_click_dispatch_action: Option<Box<dyn Action>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
@@ -1894,7 +1894,9 @@ impl Pane {
|
||||
}))
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
|
||||
if event.up.click_count == 2 {
|
||||
cx.dispatch_action(this.double_click_dispatch_action.boxed_clone())
|
||||
if let Some(action) = &this.double_click_dispatch_action {
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -136,6 +136,7 @@ actions!(
|
||||
ReloadActiveItem,
|
||||
SaveAs,
|
||||
SaveWithoutFormat,
|
||||
StartDebugger,
|
||||
ToggleBottomDock,
|
||||
ToggleCenteredLayout,
|
||||
ToggleLeftDock,
|
||||
@@ -855,7 +856,7 @@ impl Workspace {
|
||||
project.clone(),
|
||||
pane_history_timestamp.clone(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
Some(NewFile.boxed_clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -2368,7 +2369,7 @@ impl Workspace {
|
||||
self.project.clone(),
|
||||
self.pane_history_timestamp.clone(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
Some(NewFile.boxed_clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ collab_ui.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette.workspace = true
|
||||
copilot.workspace = true
|
||||
debugger_ui.workspace = true
|
||||
db.workspace = true
|
||||
diagnostics.workspace = true
|
||||
editor.workspace = true
|
||||
|
||||
@@ -428,6 +428,7 @@ fn main() {
|
||||
|
||||
zed::init(cx);
|
||||
project::Project::init(&client, cx);
|
||||
debugger_ui::init(cx);
|
||||
client::init(&client, cx);
|
||||
language::init(cx);
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
@@ -12,6 +12,7 @@ pub use app_menus::*;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
use client::ZED_URL_SCHEME;
|
||||
use collections::VecDeque;
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
|
||||
@@ -254,6 +255,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
workspace_handle.clone(),
|
||||
cx.clone(),
|
||||
);
|
||||
let debug_panel = DebugPanel::load(workspace_handle.clone(), cx.clone());
|
||||
|
||||
let (
|
||||
project_panel,
|
||||
@@ -263,6 +265,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
channels_panel,
|
||||
chat_panel,
|
||||
notification_panel,
|
||||
debug_panel,
|
||||
) = futures::try_join!(
|
||||
project_panel,
|
||||
outline_panel,
|
||||
@@ -271,6 +274,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
channels_panel,
|
||||
chat_panel,
|
||||
notification_panel,
|
||||
debug_panel
|
||||
)?;
|
||||
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
@@ -281,6 +285,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
workspace.add_panel(channels_panel, cx);
|
||||
workspace.add_panel(chat_panel, cx);
|
||||
workspace.add_panel(notification_panel, cx);
|
||||
workspace.add_panel(debug_panel, cx);
|
||||
cx.focus_self();
|
||||
})
|
||||
})
|
||||
@@ -3462,6 +3467,7 @@ mod tests {
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
tasks_ui::init(cx);
|
||||
debugger_ui::init(cx);
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
app_state
|
||||
})
|
||||
|
||||